unifare commited on
Commit
5fc700d
·
1 Parent(s): 5975319

Initial commit: ToolHub ASP.NET Core app

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +102 -0
  2. Controllers/AdminController.cs +109 -0
  3. Controllers/CategoryController.cs +202 -0
  4. Controllers/HomeController.cs +72 -0
  5. Controllers/TagController.cs +292 -0
  6. Controllers/ToolController.cs +345 -0
  7. Controllers/ToolStatisticsController.cs +153 -0
  8. Controllers/ToolsController.cs +27 -0
  9. Dockerfile +61 -0
  10. Models/Category.cs +34 -0
  11. Models/ErrorViewModel.cs +9 -0
  12. Models/Tag.cs +24 -0
  13. Models/Tool.cs +59 -0
  14. Models/ToolStatistics.cs +36 -0
  15. Models/ToolTag.cs +20 -0
  16. Models/User.cs +38 -0
  17. Models/UserFavorite.cs +20 -0
  18. Models/UserToolAccess.cs +40 -0
  19. Program.cs +150 -0
  20. Properties/launchSettings.json +23 -0
  21. README.md +124 -10
  22. README_HF_SPACES.md +147 -0
  23. Services/BaseToolService.cs +319 -0
  24. Services/IToolService.cs +26 -0
  25. Services/IUserService.cs +16 -0
  26. Services/ToolService.cs +192 -0
  27. Services/UserService.cs +152 -0
  28. ToolHub.csproj +15 -0
  29. ToolHub.csproj.user +6 -0
  30. Views/Admin/Categories.cshtml +387 -0
  31. Views/Admin/Index.cshtml +302 -0
  32. Views/Admin/Login.cshtml +103 -0
  33. Views/Admin/Tags.cshtml +349 -0
  34. Views/Admin/Tools.cshtml +934 -0
  35. Views/Category/Index.cshtml +343 -0
  36. Views/Home/Index.cshtml +184 -0
  37. Views/Home/Privacy.cshtml +6 -0
  38. Views/Home/Tools.cshtml +189 -0
  39. Views/Shared/Error.cshtml +25 -0
  40. Views/Shared/_AdminLayout.cshtml +286 -0
  41. Views/Shared/_Layout.cshtml +283 -0
  42. Views/Shared/_Layout.cshtml.css +48 -0
  43. Views/Shared/_ValidationScriptsPartial.cshtml +2 -0
  44. Views/Tag/Index.cshtml +349 -0
  45. Views/Tool/Index.cshtml +479 -0
  46. Views/ToolStatistics/Index.cshtml +493 -0
  47. Views/Tools/ImageCompressor.cshtml +479 -0
  48. Views/_ViewImports.cshtml +3 -0
  49. Views/_ViewStart.cshtml +3 -0
  50. appsettings.Development.json +8 -0
.dockerignore ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Git
2
+ .git
3
+ .gitignore
4
+ .gitattributes
5
+
6
+ # Visual Studio
7
+ .vs/
8
+ *.user
9
+ *.suo
10
+ *.userosscache
11
+ *.sln.docstates
12
+
13
+ # Build results
14
+ [Dd]ebug/
15
+ [Dd]ebugPublic/
16
+ [Rr]elease/
17
+ [Rr]eleases/
18
+ x64/
19
+ x86/
20
+ build/
21
+ bld/
22
+ [Bb]in/
23
+ [Oo]bj/
24
+ [Oo]ut/
25
+ msbuild.log
26
+ msbuild.err
27
+ msbuild.wrn
28
+
29
+ # NuGet
30
+ *.nupkg
31
+ *.snupkg
32
+ **/[Pp]ackages/*
33
+ !**/[Pp]ackages/build/
34
+ *.nuget.props
35
+ *.nuget.targets
36
+
37
+ # User-specific files
38
+ *.rsuser
39
+ *.suo
40
+ *.user
41
+ *.userosscache
42
+ *.sln.docstates
43
+
44
+ # Mono auto generated files
45
+ mono_crash.*
46
+
47
+ # Windows image file caches
48
+ Thumbs.db
49
+ ehthumbs.db
50
+
51
+ # Folder config file
52
+ [Dd]esktop.ini
53
+
54
+ # Recycle Bin used on file shares
55
+ $RECYCLE.BIN/
56
+
57
+ # Windows Installer files
58
+ *.cab
59
+ *.msi
60
+ *.msix
61
+ *.msm
62
+ *.msp
63
+
64
+ # Windows shortcuts
65
+ *.lnk
66
+
67
+ # JetBrains Rider
68
+ .idea/
69
+ *.sln.iml
70
+
71
+ # macOS
72
+ .DS_Store
73
+
74
+ # Linux
75
+ *~
76
+
77
+ # Temporary files
78
+ *.tmp
79
+ *.temp
80
+
81
+ # Logs
82
+ *.log
83
+
84
+ # Database files (will be created in container)
85
+ *.db
86
+ *.db-shm
87
+ *.db-wal
88
+
89
+ # Docker
90
+ Dockerfile*
91
+ docker-compose*
92
+ .dockerignore
93
+
94
+ # Documentation
95
+ README.md
96
+ *.md
97
+
98
+ # Test files
99
+ **/test/
100
+ **/tests/
101
+ **/*.Tests/
102
+ **/*.tests/
Controllers/AdminController.cs ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Mvc;
2
+ using Microsoft.AspNetCore.Authorization;
3
+ using Microsoft.AspNetCore.Authentication;
4
+ using Microsoft.AspNetCore.Authentication.Cookies;
5
+ using System.Security.Claims;
6
+ using ToolHub.Models;
7
+ using ToolHub.Services;
8
+
9
+ namespace ToolHub.Controllers;
10
+
11
+ public class AdminController : Controller
12
+ {
13
+ private readonly IUserService _userService;
14
+ private readonly IToolService _toolService;
15
+ private readonly IFreeSql _freeSql;
16
+
17
+ public AdminController(IUserService userService, IToolService toolService, IFreeSql freeSql)
18
+ {
19
+ _userService = userService;
20
+ _toolService = toolService;
21
+ _freeSql = freeSql;
22
+ }
23
+
24
+ [HttpGet]
25
+ public IActionResult Login()
26
+ {
27
+ if (User.Identity?.IsAuthenticated == true)
28
+ {
29
+ return RedirectToAction("Index");
30
+ }
31
+ return View();
32
+ }
33
+
34
+ [HttpPost]
35
+ public async Task<IActionResult> Login(string email, string password, bool rememberMe = false)
36
+ {
37
+ if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
38
+ {
39
+ ViewBag.Error = "请输入邮箱和密码";
40
+ return View();
41
+ }
42
+
43
+ var user = await _userService.GetUserByEmailAsync(email);
44
+ if (user == null || user.Role != "Admin" || !await _userService.VerifyPasswordAsync(email, password))
45
+ {
46
+ ViewBag.Error = "邮箱或密码错误,或您不是管理员";
47
+ return View();
48
+ }
49
+
50
+ var claims = new List<Claim>
51
+ {
52
+ new(ClaimTypes.NameIdentifier, user.Id.ToString()),
53
+ new(ClaimTypes.Name, user.UserName),
54
+ new(ClaimTypes.Email, user.Email),
55
+ new(ClaimTypes.Role, user.Role)
56
+ };
57
+
58
+ var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
59
+ var authProperties = new AuthenticationProperties
60
+ {
61
+ IsPersistent = rememberMe,
62
+ ExpiresUtc = rememberMe ? DateTimeOffset.UtcNow.AddDays(7) : DateTimeOffset.UtcNow.AddHours(1)
63
+ };
64
+
65
+ await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
66
+ new ClaimsPrincipal(claimsIdentity), authProperties);
67
+
68
+ return RedirectToAction("Index");
69
+ }
70
+
71
+ [HttpPost]
72
+ [Authorize(Roles = "Admin")]
73
+ public async Task<IActionResult> Logout()
74
+ {
75
+ await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
76
+ return RedirectToAction("Login");
77
+ }
78
+
79
+ [Authorize(Roles = "Admin")]
80
+ public async Task<IActionResult> Index()
81
+ {
82
+ // 统计数据
83
+ ViewBag.TotalTools = await _freeSql.Select<Tool>().Where(t => t.IsActive).CountAsync();
84
+ ViewBag.TotalCategories = await _freeSql.Select<Category>().Where(c => c.IsActive).CountAsync();
85
+ ViewBag.TotalUsers = await _freeSql.Select<User>().Where(u => u.IsActive).CountAsync();
86
+ ViewBag.TotalViews = await _freeSql.Select<Tool>().SumAsync(t => t.ViewCount);
87
+
88
+ // 最新工具
89
+ ViewBag.RecentTools = await _freeSql.Select<Tool>()
90
+ .Include(t => t.Category)
91
+ .Where(t => t.IsActive)
92
+ .OrderByDescending(t => t.CreatedAt)
93
+ .Take(5)
94
+ .ToListAsync();
95
+
96
+ return View();
97
+ }
98
+
99
+ [Authorize(Roles = "Admin")]
100
+ public async Task<IActionResult> Users(int page = 1)
101
+ {
102
+ var users = await _freeSql.Select<User>()
103
+ .Where(u => u.IsActive)
104
+ .OrderByDescending(u => u.CreatedAt)
105
+ .Page(page, 20)
106
+ .ToListAsync();
107
+ return View(users);
108
+ }
109
+ }
Controllers/CategoryController.cs ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Mvc;
2
+ using Microsoft.AspNetCore.Authorization;
3
+ using ToolHub.Models;
4
+
5
+ namespace ToolHub.Controllers;
6
+
7
+ [Authorize(Roles = "Admin")]
8
+ public class CategoryController : Controller
9
+ {
10
+ private readonly IFreeSql _freeSql;
11
+
12
+ public CategoryController(IFreeSql freeSql)
13
+ {
14
+ _freeSql = freeSql;
15
+ }
16
+
17
+ // 类别管理页面
18
+ public async Task<IActionResult> Index(int page = 1)
19
+ {
20
+ var pageSize = 20;
21
+ var categories = await _freeSql.Select<Category>()
22
+ .IncludeMany(c => c.Tools)
23
+ .Where(c => c.IsActive)
24
+ .OrderBy(c => c.SortOrder)
25
+ .Page(page, pageSize)
26
+ .ToListAsync();
27
+
28
+ // 获取总数用于分页
29
+ var totalCount = (int)await _freeSql.Select<Category>()
30
+ .Where(c => c.IsActive)
31
+ .CountAsync();
32
+ var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
33
+
34
+ ViewBag.CurrentPage = page;
35
+ ViewBag.TotalCount = totalCount;
36
+ ViewBag.TotalPages = totalPages;
37
+ ViewBag.PageSize = pageSize;
38
+
39
+ return View(categories);
40
+ }
41
+
42
+ // 获取分页分类列表(AJAX)
43
+ [HttpGet]
44
+ public async Task<IActionResult> GetCategories(int page = 1)
45
+ {
46
+ try
47
+ {
48
+ var pageSize = 20;
49
+ var categories = await _freeSql.Select<Category>()
50
+ .IncludeMany(c => c.Tools)
51
+ .Where(c => c.IsActive)
52
+ .OrderBy(c => c.SortOrder)
53
+ .Page(page, pageSize)
54
+ .ToListAsync();
55
+
56
+ var totalCount = (int)await _freeSql.Select<Category>()
57
+ .Where(c => c.IsActive)
58
+ .CountAsync();
59
+ var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
60
+
61
+ return Json(new
62
+ {
63
+ categories = categories,
64
+ pagination = new
65
+ {
66
+ currentPage = page,
67
+ totalPages = totalPages,
68
+ totalCount = totalCount,
69
+ pageSize = pageSize,
70
+ hasNext = page < totalPages,
71
+ hasPrev = page > 1
72
+ }
73
+ });
74
+ }
75
+ catch
76
+ {
77
+ return Json(new { success = false, message = "获取分类列表失败" });
78
+ }
79
+ }
80
+
81
+ // 保存类别(添加/编辑)
82
+ [HttpPost]
83
+ public async Task<IActionResult> Save([FromBody] CategoryDto categoryDto)
84
+ {
85
+ try
86
+ {
87
+ if (categoryDto.Id == 0)
88
+ {
89
+ // 添加新分类
90
+ var category = new Category
91
+ {
92
+ Name = categoryDto.Name,
93
+ Description = categoryDto.Description,
94
+ Icon = categoryDto.Icon,
95
+ Color = categoryDto.Color,
96
+ SortOrder = categoryDto.SortOrder,
97
+ IsActive = true
98
+ };
99
+ await _freeSql.Insert(category).ExecuteAffrowsAsync();
100
+ }
101
+ else
102
+ {
103
+ // 更新分类
104
+ await _freeSql.Update<Category>()
105
+ .Where(c => c.Id == categoryDto.Id)
106
+ .Set(c => c.Name, categoryDto.Name)
107
+ .Set(c => c.Description, categoryDto.Description)
108
+ .Set(c => c.Icon, categoryDto.Icon)
109
+ .Set(c => c.Color, categoryDto.Color)
110
+ .Set(c => c.SortOrder, categoryDto.SortOrder)
111
+ .Set(c => c.UpdatedAt, DateTime.Now)
112
+ .ExecuteAffrowsAsync();
113
+ }
114
+ return Json(new { success = true });
115
+ }
116
+ catch
117
+ {
118
+ return Json(new { success = false });
119
+ }
120
+ }
121
+
122
+ // 切换类别状态
123
+ [HttpPost]
124
+ public async Task<IActionResult> ToggleStatus([FromBody] ToggleCategoryStatusRequest request)
125
+ {
126
+ try
127
+ {
128
+ await _freeSql.Update<Category>()
129
+ .Where(c => c.Id == request.Id)
130
+ .Set(c => c.IsActive, request.IsActive)
131
+ .Set(c => c.UpdatedAt, DateTime.Now)
132
+ .ExecuteAffrowsAsync();
133
+ return Json(new { success = true });
134
+ }
135
+ catch
136
+ {
137
+ return Json(new { success = false });
138
+ }
139
+ }
140
+
141
+ // 删除类别
142
+ [HttpPost]
143
+ public async Task<IActionResult> Delete([FromBody] DeleteCategoryRequest request)
144
+ {
145
+ try
146
+ {
147
+ // 检查是否有工具使用此分类
148
+ var toolCount = await _freeSql.Select<Tool>().Where(t => t.CategoryId == request.Id).CountAsync();
149
+ if (toolCount > 0)
150
+ {
151
+ return Json(new { success = false, message = "该分类下还有工具,无法删除" });
152
+ }
153
+
154
+ await _freeSql.Delete<Category>().Where(c => c.Id == request.Id).ExecuteAffrowsAsync();
155
+ return Json(new { success = true });
156
+ }
157
+ catch
158
+ {
159
+ return Json(new { success = false });
160
+ }
161
+ }
162
+
163
+ // 获取类别列表(用于下拉选择)
164
+ [HttpGet]
165
+ public async Task<IActionResult> GetList()
166
+ {
167
+ try
168
+ {
169
+ var categories = await _freeSql.Select<Category>()
170
+ .Where(c => c.IsActive)
171
+ .OrderBy(c => c.SortOrder)
172
+ .ToListAsync();
173
+ return Json(categories);
174
+ }
175
+ catch
176
+ {
177
+ return Json(new List<Category>());
178
+ }
179
+ }
180
+ }
181
+
182
+ // DTOs
183
+ public class CategoryDto
184
+ {
185
+ public int Id { get; set; }
186
+ public string Name { get; set; } = string.Empty;
187
+ public string? Description { get; set; }
188
+ public string? Icon { get; set; }
189
+ public string? Color { get; set; }
190
+ public int SortOrder { get; set; }
191
+ }
192
+
193
+ public class ToggleCategoryStatusRequest
194
+ {
195
+ public int Id { get; set; }
196
+ public bool IsActive { get; set; }
197
+ }
198
+
199
+ public class DeleteCategoryRequest
200
+ {
201
+ public int Id { get; set; }
202
+ }
Controllers/HomeController.cs ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Diagnostics;
2
+ using Microsoft.AspNetCore.Mvc;
3
+ using ToolHub.Models;
4
+ using ToolHub.Services;
5
+
6
+ namespace ToolHub.Controllers
7
+ {
8
+ public class HomeController : Controller
9
+ {
10
+ private readonly ILogger<HomeController> _logger;
11
+ private readonly IToolService _toolService;
12
+
13
+ public HomeController(ILogger<HomeController> logger, IToolService toolService)
14
+ {
15
+ _logger = logger;
16
+ _toolService = toolService;
17
+ }
18
+
19
+ public async Task<IActionResult> Index()
20
+ {
21
+ ViewBag.Categories = await _toolService.GetCategoriesAsync();
22
+ ViewBag.HotTools = await _toolService.GetHotToolsAsync();
23
+ ViewBag.NewTools = await _toolService.GetNewToolsAsync();
24
+
25
+ return View();
26
+ }
27
+
28
+ public async Task<IActionResult> Tools(int categoryId = 0, int page = 1, string? search = null)
29
+ {
30
+ ViewBag.Categories = await _toolService.GetCategoriesAsync();
31
+ ViewBag.CurrentCategory = categoryId;
32
+ ViewBag.Search = search;
33
+
34
+ List<Tool> tools;
35
+ if (!string.IsNullOrEmpty(search))
36
+ {
37
+ tools = await _toolService.SearchToolsAsync(search, page);
38
+ }
39
+ else
40
+ {
41
+ tools = await _toolService.GetToolsByCategoryAsync(categoryId, page);
42
+ }
43
+
44
+ return View(tools);
45
+ }
46
+
47
+ public async Task<IActionResult> Tool(int id)
48
+ {
49
+ var tool = await _toolService.GetToolByIdAsync(id);
50
+ if (tool == null)
51
+ {
52
+ return NotFound();
53
+ }
54
+
55
+ // 增加浏览次数
56
+ await _toolService.IncrementViewCountAsync(id);
57
+
58
+ return View(tool);
59
+ }
60
+
61
+ public IActionResult Privacy()
62
+ {
63
+ return View();
64
+ }
65
+
66
+ [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
67
+ public IActionResult Error()
68
+ {
69
+ return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
70
+ }
71
+ }
72
+ }
Controllers/TagController.cs ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Mvc;
2
+ using Microsoft.AspNetCore.Authorization;
3
+ using ToolHub.Models;
4
+
5
+ namespace ToolHub.Controllers;
6
+
7
+ [Authorize(Roles = "Admin")]
8
+ public class TagController : Controller
9
+ {
10
+ private readonly IFreeSql _freeSql;
11
+
12
+ public TagController(IFreeSql freeSql)
13
+ {
14
+ _freeSql = freeSql;
15
+ }
16
+
17
+ // 标签管理页面
18
+ public async Task<IActionResult> Index(int page = 1)
19
+ {
20
+ var pageSize = 20;
21
+ var tags = await _freeSql.Select<Tag>()
22
+ .IncludeMany(t => t.ToolTags)
23
+ .Where(t => t.IsActive)
24
+ .OrderBy(t => t.Name)
25
+ .Page(page, pageSize)
26
+ .ToListAsync();
27
+
28
+ // 获取总数用于分页
29
+ var totalCount = (int)await _freeSql.Select<Tag>()
30
+ .Where(t => t.IsActive)
31
+ .CountAsync();
32
+ var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
33
+
34
+ ViewBag.CurrentPage = page;
35
+ ViewBag.TotalCount = totalCount;
36
+ ViewBag.TotalPages = totalPages;
37
+ ViewBag.PageSize = pageSize;
38
+
39
+ return View(tags);
40
+ }
41
+
42
+ // 获取分页标签列表(AJAX)
43
+ [HttpGet]
44
+ public async Task<IActionResult> GetTags(int page = 1)
45
+ {
46
+ try
47
+ {
48
+ var pageSize = 20;
49
+ var tags = await _freeSql.Select<Tag>()
50
+ .IncludeMany(t => t.ToolTags)
51
+ .Where(t => t.IsActive)
52
+ .OrderBy(t => t.Name)
53
+ .Page(page, pageSize)
54
+ .ToListAsync();
55
+
56
+ var totalCount = (int)await _freeSql.Select<Tag>()
57
+ .Where(t => t.IsActive)
58
+ .CountAsync();
59
+ var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
60
+
61
+ return Json(new
62
+ {
63
+ tags = tags,
64
+ pagination = new
65
+ {
66
+ currentPage = page,
67
+ totalPages = totalPages,
68
+ totalCount = totalCount,
69
+ pageSize = pageSize,
70
+ hasNext = page < totalPages,
71
+ hasPrev = page > 1
72
+ }
73
+ });
74
+ }
75
+ catch
76
+ {
77
+ return Json(new { success = false, message = "获取标签列表失败" });
78
+ }
79
+ }
80
+
81
+ // 获取单个标签信息
82
+ [HttpGet]
83
+ public async Task<IActionResult> Get(int id)
84
+ {
85
+ try
86
+ {
87
+ var tag = await _freeSql.Select<Tag>()
88
+ .Where(t => t.Id == id)
89
+ .FirstAsync();
90
+
91
+ if (tag == null)
92
+ return NotFound();
93
+
94
+ return Json(tag);
95
+ }
96
+ catch
97
+ {
98
+ return StatusCode(500);
99
+ }
100
+ }
101
+
102
+ // 保存标签(添加/编辑)
103
+ [HttpPost]
104
+ public async Task<IActionResult> Save([FromBody] TagDto tagDto)
105
+ {
106
+ try
107
+ {
108
+ if (tagDto.Id == 0)
109
+ {
110
+ // 添加新标签
111
+ var tag = new Tag
112
+ {
113
+ Name = tagDto.Name,
114
+ Color = tagDto.Color,
115
+ IsActive = true
116
+ };
117
+ await _freeSql.Insert(tag).ExecuteAffrowsAsync();
118
+ }
119
+ else
120
+ {
121
+ // 更新标签
122
+ await _freeSql.Update<Tag>()
123
+ .Where(t => t.Id == tagDto.Id)
124
+ .Set(t => t.Name, tagDto.Name)
125
+ .Set(t => t.Color, tagDto.Color)
126
+ .ExecuteAffrowsAsync();
127
+ }
128
+ return Json(new { success = true });
129
+ }
130
+ catch
131
+ {
132
+ return Json(new { success = false });
133
+ }
134
+ }
135
+
136
+ // 切换标签状态
137
+ [HttpPost]
138
+ public async Task<IActionResult> ToggleStatus([FromBody] ToggleTagStatusRequest request)
139
+ {
140
+ try
141
+ {
142
+ await _freeSql.Update<Tag>()
143
+ .Where(t => t.Id == request.Id)
144
+ .Set(t => t.IsActive, request.IsActive)
145
+ .ExecuteAffrowsAsync();
146
+ return Json(new { success = true });
147
+ }
148
+ catch
149
+ {
150
+ return Json(new { success = false });
151
+ }
152
+ }
153
+
154
+ // 删除标签
155
+ [HttpPost]
156
+ public async Task<IActionResult> Delete([FromBody] DeleteTagRequest request)
157
+ {
158
+ try
159
+ {
160
+ // 检查是否有工具使用此标签
161
+ var toolTagCount = await _freeSql.Select<ToolTag>().Where(tt => tt.TagId == request.Id).CountAsync();
162
+ if (toolTagCount > 0)
163
+ {
164
+ return Json(new { success = false, message = "该标签下还有工具,无法删除" });
165
+ }
166
+
167
+ await _freeSql.Delete<Tag>().Where(t => t.Id == request.Id).ExecuteAffrowsAsync();
168
+ return Json(new { success = true });
169
+ }
170
+ catch
171
+ {
172
+ return Json(new { success = false });
173
+ }
174
+ }
175
+
176
+ // 获取标签列表(用于下拉选择)
177
+ [HttpGet]
178
+ public async Task<IActionResult> GetList()
179
+ {
180
+ try
181
+ {
182
+ var tags = await _freeSql.Select<Tag>()
183
+ .Where(t => t.IsActive)
184
+ .OrderBy(t => t.Name)
185
+ .ToListAsync();
186
+ return Json(tags);
187
+ }
188
+ catch
189
+ {
190
+ return Json(new List<Tag>());
191
+ }
192
+ }
193
+
194
+ // 为工具添加标签
195
+ [HttpPost]
196
+ public async Task<IActionResult> AddToolTag([FromBody] AddToolTagRequest request)
197
+ {
198
+ try
199
+ {
200
+ // 检查是否已存在
201
+ var existing = await _freeSql.Select<ToolTag>()
202
+ .Where(tt => tt.ToolId == request.ToolId && tt.TagId == request.TagId)
203
+ .FirstAsync();
204
+
205
+ if (existing != null)
206
+ {
207
+ return Json(new { success = false, message = "该工具已添加此标签" });
208
+ }
209
+
210
+ var toolTag = new ToolTag
211
+ {
212
+ ToolId = request.ToolId,
213
+ TagId = request.TagId
214
+ };
215
+
216
+ await _freeSql.Insert(toolTag).ExecuteAffrowsAsync();
217
+ return Json(new { success = true });
218
+ }
219
+ catch
220
+ {
221
+ return Json(new { success = false });
222
+ }
223
+ }
224
+
225
+ // 移除工具标签
226
+ [HttpPost]
227
+ public async Task<IActionResult> RemoveToolTag([FromBody] RemoveToolTagRequest request)
228
+ {
229
+ try
230
+ {
231
+ await _freeSql.Delete<ToolTag>()
232
+ .Where(tt => tt.ToolId == request.ToolId && tt.TagId == request.TagId)
233
+ .ExecuteAffrowsAsync();
234
+ return Json(new { success = true });
235
+ }
236
+ catch
237
+ {
238
+ return Json(new { success = false });
239
+ }
240
+ }
241
+
242
+ // 获取工具的标签
243
+ [HttpGet]
244
+ public async Task<IActionResult> GetToolTags(int toolId)
245
+ {
246
+ try
247
+ {
248
+ var toolTags = await _freeSql.Select<ToolTag>()
249
+ .Include(tt => tt.Tag)
250
+ .Where(tt => tt.ToolId == toolId)
251
+ .ToListAsync();
252
+
253
+ var tags = toolTags.Select(tt => tt.Tag).ToList();
254
+ return Json(tags);
255
+ }
256
+ catch
257
+ {
258
+ return Json(new List<Tag>());
259
+ }
260
+ }
261
+ }
262
+
263
+ // DTOs
264
+ public class TagDto
265
+ {
266
+ public int Id { get; set; }
267
+ public string Name { get; set; } = string.Empty;
268
+ public string? Color { get; set; }
269
+ }
270
+
271
+ public class ToggleTagStatusRequest
272
+ {
273
+ public int Id { get; set; }
274
+ public bool IsActive { get; set; }
275
+ }
276
+
277
+ public class DeleteTagRequest
278
+ {
279
+ public int Id { get; set; }
280
+ }
281
+
282
+ public class AddToolTagRequest
283
+ {
284
+ public int ToolId { get; set; }
285
+ public int TagId { get; set; }
286
+ }
287
+
288
+ public class RemoveToolTagRequest
289
+ {
290
+ public int ToolId { get; set; }
291
+ public int TagId { get; set; }
292
+ }
Controllers/ToolController.cs ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Mvc;
2
+ using Microsoft.AspNetCore.Authorization;
3
+ using ToolHub.Models;
4
+ using ToolHub.Services;
5
+
6
+ namespace ToolHub.Controllers;
7
+
8
+ [Authorize(Roles = "Admin")]
9
+ public class ToolController : Controller
10
+ {
11
+ private readonly IToolService _toolService;
12
+ private readonly IFreeSql _freeSql;
13
+
14
+ public ToolController(IToolService toolService, IFreeSql freeSql)
15
+ {
16
+ _toolService = toolService;
17
+ _freeSql = freeSql;
18
+ }
19
+
20
+ // 工具管理页面
21
+ public async Task<IActionResult> Index(int page = 1, int categoryId = 0)
22
+ {
23
+ ViewBag.Categories = await _toolService.GetCategoriesAsync();
24
+ ViewBag.CurrentCategory = categoryId;
25
+ ViewBag.CurrentPage = page;
26
+
27
+ var pageSize = 20;
28
+ var tools = await _toolService.GetToolsByCategoryAsync(categoryId, page, pageSize,true);
29
+
30
+ // 获取总数用于分页
31
+ var totalCount = await GetToolsCountAsync(categoryId);
32
+ var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
33
+
34
+ ViewBag.TotalCount = totalCount;
35
+ ViewBag.TotalPages = totalPages;
36
+ ViewBag.PageSize = pageSize;
37
+
38
+ return View(tools);
39
+ }
40
+
41
+ // 获取工具总数
42
+ private async Task<int> GetToolsCountAsync(int categoryId = 0)
43
+ {
44
+ var query = _freeSql.Select<Tool>().Where(t => t.IsActive);
45
+
46
+ if (categoryId > 0)
47
+ {
48
+ query = query.Where(t => t.CategoryId == categoryId);
49
+ }
50
+
51
+ return (int)await query.CountAsync();
52
+ }
53
+
54
+ // 获取分页工具列表(AJAX)
55
+ [HttpGet]
56
+ public async Task<IActionResult> GetTools(int page = 1, int categoryId = 0, string search = "")
57
+ {
58
+ try
59
+ {
60
+ var pageSize = 20;
61
+ List<Tool> tools;
62
+ int totalCount;
63
+
64
+ if (!string.IsNullOrEmpty(search))
65
+ {
66
+ tools = await _toolService.SearchToolsAsync(search, page, pageSize);
67
+ totalCount = (int)await _freeSql.Select<Tool>()
68
+ .Where(t => t.IsActive && (t.Name.Contains(search) || t.Description!.Contains(search)))
69
+ .CountAsync();
70
+ }
71
+ else
72
+ {
73
+ tools = await _toolService.GetToolsByCategoryAsync(categoryId, page, pageSize);
74
+ totalCount = await GetToolsCountAsync(categoryId);
75
+ }
76
+
77
+ var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
78
+
79
+ return Json(new
80
+ {
81
+ tools = tools,
82
+ pagination = new
83
+ {
84
+ currentPage = page,
85
+ totalPages = totalPages,
86
+ totalCount = totalCount,
87
+ pageSize = pageSize,
88
+ hasNext = page < totalPages,
89
+ hasPrev = page > 1
90
+ }
91
+ });
92
+ }
93
+ catch
94
+ {
95
+ return Json(new { success = false, message = "获取工具列表失败" });
96
+ }
97
+ }
98
+
99
+ // 获取所有工具列表(用于统计页面)
100
+ [HttpGet]
101
+ public async Task<IActionResult> GetAllTools()
102
+ {
103
+ try
104
+ {
105
+ var tools = await _freeSql.Select<Tool>()
106
+ .Where(t => t.IsActive)
107
+ .OrderBy(t => t.Name)
108
+ .ToListAsync();
109
+
110
+ return Json(new { success = true, tools = tools });
111
+ }
112
+ catch
113
+ {
114
+ return Json(new { success = false, message = "获取工具列表失败" });
115
+ }
116
+ }
117
+
118
+ // 获取单个工具信息
119
+ [HttpGet]
120
+ public async Task<IActionResult> Get(int id)
121
+ {
122
+ try
123
+ {
124
+ var tool = await _freeSql.Select<Tool>()
125
+ .Where(t => t.Id == id)
126
+ .FirstAsync();
127
+
128
+ if (tool == null)
129
+ return NotFound();
130
+
131
+ return Json(tool);
132
+ }
133
+ catch
134
+ {
135
+ return StatusCode(500);
136
+ }
137
+ }
138
+
139
+ // 保存工具(添加/编辑)
140
+ [HttpPost]
141
+ public async Task<IActionResult> Save([FromBody] ToolDto toolDto)
142
+ {
143
+ try
144
+ {
145
+ if (toolDto.Id == 0)
146
+ {
147
+ // 添加新工具
148
+ var tool = new Tool
149
+ {
150
+ Name = toolDto.Name,
151
+ Description = toolDto.Description,
152
+ Icon = toolDto.Icon,
153
+ Image = toolDto.Image,
154
+ Url = toolDto.Url,
155
+ CategoryId = toolDto.CategoryId,
156
+ IsHot = toolDto.IsHot,
157
+ IsNew = toolDto.IsNew,
158
+ IsRecommended = toolDto.IsRecommended,
159
+ SortOrder = toolDto.SortOrder,
160
+ IsActive = true
161
+ };
162
+ await _freeSql.Insert(tool).ExecuteAffrowsAsync();
163
+ }
164
+ else
165
+ {
166
+ // 更新工具
167
+ await _freeSql.Update<Tool>()
168
+ .Where(t => t.Id == toolDto.Id)
169
+ .Set(t => t.Name, toolDto.Name)
170
+ .Set(t => t.Description, toolDto.Description)
171
+ .Set(t => t.Icon, toolDto.Icon)
172
+ .Set(t => t.Image, toolDto.Image)
173
+ .Set(t => t.Url, toolDto.Url)
174
+ .Set(t => t.CategoryId, toolDto.CategoryId)
175
+ .Set(t => t.IsHot, toolDto.IsHot)
176
+ .Set(t => t.IsNew, toolDto.IsNew)
177
+ .Set(t => t.IsRecommended, toolDto.IsRecommended)
178
+ .Set(t => t.SortOrder, toolDto.SortOrder)
179
+ .Set(t => t.UpdatedAt, DateTime.Now)
180
+ .ExecuteAffrowsAsync();
181
+ }
182
+ return Json(new { success = true });
183
+ }
184
+ catch
185
+ {
186
+ return Json(new { success = false });
187
+ }
188
+ }
189
+
190
+ // 删除工具
191
+ [HttpPost]
192
+ public async Task<IActionResult> Delete([FromBody] DeleteToolRequest request)
193
+ {
194
+ try
195
+ {
196
+ var result = await _toolService.DeleteToolAsync(request.Id);
197
+ return Json(new { success = result });
198
+ }
199
+ catch
200
+ {
201
+ return Json(new { success = false });
202
+ }
203
+ }
204
+
205
+ // 切换工具状态
206
+ [HttpPost]
207
+ public async Task<IActionResult> ToggleStatus([FromBody] ToggleToolStatusRequest request)
208
+ {
209
+ try
210
+ {
211
+ await _freeSql.Update<Tool>()
212
+ .Where(t => t.Id == request.Id)
213
+ .Set(t => t.IsActive, request.IsActive)
214
+ .Set(t => t.UpdatedAt, DateTime.Now)
215
+ .ExecuteAffrowsAsync();
216
+ return Json(new { success = true });
217
+ }
218
+ catch
219
+ {
220
+ return Json(new { success = false });
221
+ }
222
+ }
223
+
224
+ // 更新工具标志
225
+ [HttpPost]
226
+ public async Task<IActionResult> UpdateFlags([FromBody] UpdateToolFlagsRequest request)
227
+ {
228
+ try
229
+ {
230
+ await _freeSql.Update<Tool>()
231
+ .Where(t => t.Id == request.Id)
232
+ .Set(t => t.IsHot, request.IsHot)
233
+ .Set(t => t.IsNew, request.IsNew)
234
+ .Set(t => t.IsRecommended, request.IsRecommended)
235
+ .Set(t => t.UpdatedAt, DateTime.Now)
236
+ .ExecuteAffrowsAsync();
237
+ return Json(new { success = true });
238
+ }
239
+ catch
240
+ {
241
+ return Json(new { success = false });
242
+ }
243
+ }
244
+
245
+ // 初始化图片压缩工具
246
+ [HttpGet]
247
+ public async Task<IActionResult> InitImageCompressor()
248
+ {
249
+ try
250
+ {
251
+ // 检查是否已存在图片压缩工具
252
+ var existingTool = await _freeSql.Select<Tool>()
253
+ .Where(t => t.Name == "图片压缩工具" || t.Url!.Contains("/Tools/ImageCompressor"))
254
+ .FirstAsync();
255
+
256
+ if (existingTool != null)
257
+ {
258
+ return Json(new { success = false, message = "图片压缩工具已存在" });
259
+ }
260
+
261
+ // 查找或创建图像处理分类
262
+ var category = await _freeSql.Select<Category>()
263
+ .Where(c => c.Name == "图像处理" || c.Name == "图片工具")
264
+ .FirstAsync();
265
+
266
+ if (category == null)
267
+ {
268
+ // 创建图像处理分类
269
+ category = new Category
270
+ {
271
+ Name = "图像处理",
272
+ Description = "图片处理、编辑、转换等相关工具",
273
+ Icon = "fas fa-image",
274
+ Color = "#FF6B35",
275
+ SortOrder = 3,
276
+ IsActive = true,
277
+ CreatedAt = DateTime.Now
278
+ };
279
+ var categoryId = await _freeSql.Insert(category).ExecuteIdentityAsync();
280
+ category.Id = (int)categoryId;
281
+ }
282
+
283
+ // 添加图片压缩工具
284
+ var tool = new Tool
285
+ {
286
+ Name = "图片压缩工具",
287
+ Description = "快速压缩图片文件,支持JPG、PNG、WebP等格式,保持高质量的同时大幅减小文件体积",
288
+ Icon = "fas fa-compress-alt",
289
+ Image = "",
290
+ Url = "/Tools/ImageCompressor",
291
+ CategoryId = category.Id,
292
+ IsHot = true,
293
+ IsNew = true,
294
+ IsRecommended = true,
295
+ IsActive = true,
296
+ SortOrder = 1,
297
+ ViewCount = 0,
298
+ CreatedAt = DateTime.Now
299
+ };
300
+
301
+ await _freeSql.Insert(tool).ExecuteAffrowsAsync();
302
+
303
+ return Json(new { success = true, message = "图片压缩工具初始化成功" });
304
+ }
305
+ catch (Exception ex)
306
+ {
307
+ return Json(new { success = false, message = ex.Message });
308
+ }
309
+ }
310
+ }
311
+
312
+ // DTOs
313
+ public class ToolDto
314
+ {
315
+ public int Id { get; set; }
316
+ public string Name { get; set; } = string.Empty;
317
+ public string? Description { get; set; }
318
+ public string? Icon { get; set; }
319
+ public string? Image { get; set; }
320
+ public string? Url { get; set; }
321
+ public int CategoryId { get; set; }
322
+ public bool IsHot { get; set; }
323
+ public bool IsNew { get; set; }
324
+ public bool IsRecommended { get; set; }
325
+ public int SortOrder { get; set; }
326
+ }
327
+
328
+ public class DeleteToolRequest
329
+ {
330
+ public int Id { get; set; }
331
+ }
332
+
333
+ public class ToggleToolStatusRequest
334
+ {
335
+ public int Id { get; set; }
336
+ public bool IsActive { get; set; }
337
+ }
338
+
339
+ public class UpdateToolFlagsRequest
340
+ {
341
+ public int Id { get; set; }
342
+ public bool IsHot { get; set; }
343
+ public bool IsNew { get; set; }
344
+ public bool IsRecommended { get; set; }
345
+ }
Controllers/ToolStatisticsController.cs ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Mvc;
2
+ using Microsoft.AspNetCore.Authorization;
3
+ using ToolHub.Services;
4
+ using ToolHub.Models;
5
+
6
+ namespace ToolHub.Controllers;
7
+
8
+ [Authorize(Roles = "Admin")]
9
+ public class ToolStatisticsController : Controller
10
+ {
11
+ private readonly IToolService _toolService;
12
+
13
+ public ToolStatisticsController(IToolService toolService)
14
+ {
15
+ _toolService = toolService;
16
+ }
17
+
18
+ /// <summary>
19
+ /// 获取工具统计数据页面
20
+ /// </summary>
21
+ public async Task<IActionResult> Index(int toolId = 0)
22
+ {
23
+ ViewData["Title"] = "工具统计";
24
+
25
+ if (toolId > 0)
26
+ {
27
+ var tool = await _toolService.GetToolByIdAsync(toolId);
28
+ if (tool != null)
29
+ {
30
+ ViewData["ToolName"] = tool.Name;
31
+ ViewData["ToolId"] = toolId;
32
+ }
33
+ }
34
+
35
+ return View();
36
+ }
37
+
38
+ /// <summary>
39
+ /// 获取工具统计数据
40
+ /// </summary>
41
+ [HttpGet]
42
+ public async Task<IActionResult> GetToolStatistics(int toolId, DateTime? date = null)
43
+ {
44
+ try
45
+ {
46
+ var targetDate = date ?? DateTime.Today;
47
+ var stats = await _toolService.GetToolStatisticsAsync(toolId, targetDate);
48
+
49
+ return Json(new { success = true, data = stats });
50
+ }
51
+ catch (Exception ex)
52
+ {
53
+ return Json(new { success = false, message = ex.Message });
54
+ }
55
+ }
56
+
57
+ /// <summary>
58
+ /// 获取工具访问记录
59
+ /// </summary>
60
+ [HttpGet]
61
+ public async Task<IActionResult> GetToolAccessRecords(int toolId, DateTime? startDate = null, DateTime? endDate = null, int limit = 100)
62
+ {
63
+ try
64
+ {
65
+ var records = await _toolService.GetToolAccessRecordsAsync(toolId, startDate, endDate, limit);
66
+
67
+ return Json(new { success = true, data = records });
68
+ }
69
+ catch (Exception ex)
70
+ {
71
+ return Json(new { success = false, message = ex.Message });
72
+ }
73
+ }
74
+
75
+ /// <summary>
76
+ /// 获取用户访问记录
77
+ /// </summary>
78
+ [HttpGet]
79
+ public async Task<IActionResult> GetUserAccessRecords(int userId, DateTime? startDate = null, DateTime? endDate = null, int limit = 100)
80
+ {
81
+ try
82
+ {
83
+ var records = await _toolService.GetUserToolAccessRecordsAsync(userId, startDate, endDate, limit);
84
+
85
+ return Json(new { success = true, data = records });
86
+ }
87
+ catch (Exception ex)
88
+ {
89
+ return Json(new { success = false, message = ex.Message });
90
+ }
91
+ }
92
+
93
+ /// <summary>
94
+ /// 获取工具使用趋势
95
+ /// </summary>
96
+ [HttpGet]
97
+ public async Task<IActionResult> GetToolUsageTrend(int toolId, int days = 30)
98
+ {
99
+ try
100
+ {
101
+ var trend = await _toolService.GetToolUsageTrendAsync(toolId, days);
102
+
103
+ return Json(new { success = true, data = trend });
104
+ }
105
+ catch (Exception ex)
106
+ {
107
+ return Json(new { success = false, message = ex.Message });
108
+ }
109
+ }
110
+
111
+ /// <summary>
112
+ /// 获取热门工具
113
+ /// </summary>
114
+ [HttpGet]
115
+ public async Task<IActionResult> GetPopularTools(int count = 10, DateTime? startDate = null)
116
+ {
117
+ try
118
+ {
119
+ var tools = await _toolService.GetPopularToolsAsync(count, startDate);
120
+
121
+ return Json(new { success = true, data = tools });
122
+ }
123
+ catch (Exception ex)
124
+ {
125
+ return Json(new { success = false, message = ex.Message });
126
+ }
127
+ }
128
+
129
+ /// <summary>
130
+ /// 记录工具操作
131
+ /// </summary>
132
+ [HttpPost]
133
+ public async Task<IActionResult> RecordToolAction([FromBody] RecordToolActionRequest request)
134
+ {
135
+ try
136
+ {
137
+ await _toolService.RecordToolActionAsync(request.ToolId, request.ActionType, request.Duration);
138
+
139
+ return Json(new { success = true });
140
+ }
141
+ catch (Exception ex)
142
+ {
143
+ return Json(new { success = false, message = ex.Message });
144
+ }
145
+ }
146
+ }
147
+
148
+ public class RecordToolActionRequest
149
+ {
150
+ public int ToolId { get; set; }
151
+ public string ActionType { get; set; } = "view";
152
+ public int Duration { get; set; } = 0;
153
+ }
Controllers/ToolsController.cs ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Mvc;
2
+ using ToolHub.Services;
3
+
4
+ namespace ToolHub.Controllers;
5
+
6
+ public class ToolsController : Controller
7
+ {
8
+ private readonly IToolService _toolService;
9
+
10
+ public ToolsController(IToolService toolService)
11
+ {
12
+ _toolService = toolService;
13
+ }
14
+
15
+ // 图片压缩工具页面
16
+ public async Task<IActionResult> ImageCompressor()
17
+ {
18
+ // 增加访问计数
19
+ await _toolService.IncrementViewCountAsync( nameof(ImageCompressor));
20
+
21
+ ViewData["Title"] = "图片压缩工具";
22
+ ViewData["Description"] = "快速压缩图片,支持JPG、PNG、WebP等格式,保持高质量的同时大幅减小文件体积";
23
+ ViewData["Keywords"] = "图片压缩,在线压缩,图片优化,文件压缩";
24
+
25
+ return View();
26
+ }
27
+ }
Dockerfile ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用官方.NET 9.0运行时镜像作为基础镜像
2
+ FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
3
+ WORKDIR /app
4
+ EXPOSE 7860
5
+
6
+ # 安装supervisor和其他必要工具
7
+ RUN apt-get update && apt-get install -y \
8
+ supervisor \
9
+ curl \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # 创建supervisor配置目录
13
+ RUN mkdir -p /var/log/supervisor /etc/supervisor/conf.d
14
+
15
+ # 复制supervisor配置文件
16
+ COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
17
+
18
+ # 使用官方.NET 9.0 SDK镜像进行构建
19
+ FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
20
+ WORKDIR /src
21
+
22
+ # 复制项目文件
23
+ COPY ["ToolHub.csproj", "./"]
24
+ RUN dotnet restore "ToolHub.csproj"
25
+
26
+ # 复制所有源代码
27
+ COPY . .
28
+ WORKDIR "/src"
29
+
30
+ # 构建应用
31
+ RUN dotnet build "ToolHub.csproj" -c Release -o /app/build
32
+
33
+ # 发布应用
34
+ FROM build AS publish
35
+ RUN dotnet publish "ToolHub.csproj" -c Release -o /app/publish /p:UseAppHost=false
36
+
37
+ # 最终镜像
38
+ FROM base AS final
39
+ WORKDIR /app
40
+
41
+ # 复制发布的应用
42
+ COPY --from=publish /app/publish .
43
+
44
+ # 复制启动脚本
45
+ COPY start.sh /app/start.sh
46
+ RUN chmod +x /app/start.sh
47
+
48
+ # 创建数据目录
49
+ RUN mkdir -p /app/data
50
+
51
+ # 设置环境变量
52
+ ENV ASPNETCORE_URLS=http://+:7860
53
+ ENV ASPNETCORE_ENVIRONMENT=Production
54
+ ENV DOTNET_RUNNING_IN_CONTAINER=true
55
+
56
+ # 健康检查
57
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
58
+ CMD curl -f http://localhost:7860/ || exit 1
59
+
60
+ # 使用supervisor启动应用
61
+ CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
Models/Category.cs ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using FreeSql.DataAnnotations;
2
+
3
+ namespace ToolHub.Models;
4
+
5
+ [Table(Name = "Categories")]
6
+ public class Category
7
+ {
8
+ [Column(IsIdentity = true, IsPrimary = true)]
9
+ public int Id { get; set; }
10
+
11
+ [Column(StringLength = 100)]
12
+ public string Name { get; set; } = string.Empty;
13
+
14
+ [Column(StringLength = 500)]
15
+ public string? Description { get; set; }
16
+
17
+ [Column(StringLength = 50)]
18
+ public string? Icon { get; set; }
19
+
20
+ [Column(StringLength = 20)]
21
+ public string? Color { get; set; }
22
+
23
+ public int SortOrder { get; set; } = 0;
24
+
25
+ public bool IsActive { get; set; } = true;
26
+
27
+ public DateTime CreatedAt { get; set; } = DateTime.Now;
28
+
29
+ public DateTime? UpdatedAt { get; set; }
30
+
31
+ // 导航属性
32
+ [Navigate(nameof(Tool.CategoryId))]
33
+ public List<Tool> Tools { get; set; } = new();
34
+ }
Models/ErrorViewModel.cs ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ namespace ToolHub.Models
2
+ {
3
+ public class ErrorViewModel
4
+ {
5
+ public string? RequestId { get; set; }
6
+
7
+ public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
8
+ }
9
+ }
Models/Tag.cs ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using FreeSql.DataAnnotations;
2
+
3
+ namespace ToolHub.Models;
4
+
5
+ [Table(Name = "Tags")]
6
+ public class Tag
7
+ {
8
+ [Column(IsIdentity = true, IsPrimary = true)]
9
+ public int Id { get; set; }
10
+
11
+ [Column(StringLength = 50)]
12
+ public string Name { get; set; } = string.Empty;
13
+
14
+ [Column(StringLength = 20)]
15
+ public string? Color { get; set; }
16
+
17
+ public bool IsActive { get; set; } = true;
18
+
19
+ public DateTime CreatedAt { get; set; } = DateTime.Now;
20
+
21
+ // 导航属性
22
+ [Navigate(nameof(ToolTag.TagId))]
23
+ public List<ToolTag> ToolTags { get; set; } = new();
24
+ }
Models/Tool.cs ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using FreeSql.DataAnnotations;
2
+
3
+ namespace ToolHub.Models;
4
+
5
+ [Table(Name = "Tools")]
6
+ public class Tool
7
+ {
8
+ [Column(IsIdentity = true, IsPrimary = true)]
9
+ public int Id { get; set; }
10
+
11
+ [Column(StringLength = 200)]
12
+ public string Name { get; set; } = string.Empty;
13
+
14
+ [Column(StringLength = 1000)]
15
+ public string? Description { get; set; }
16
+
17
+ [Column(StringLength = 500)]
18
+ public string? Icon { get; set; }
19
+
20
+ [Column(StringLength = 500)]
21
+ public string? Image { get; set; }
22
+
23
+ [Column(StringLength = 500)]
24
+ public string? Url { get; set; }
25
+
26
+ public int CategoryId { get; set; }
27
+
28
+ public int ViewCount { get; set; } = 0;
29
+
30
+ public int FavoriteCount { get; set; } = 0;
31
+
32
+ public decimal Rating { get; set; } = 0;
33
+
34
+ public int RatingCount { get; set; } = 0;
35
+
36
+ public bool IsHot { get; set; } = false;
37
+
38
+ public bool IsNew { get; set; } = false;
39
+
40
+ public bool IsRecommended { get; set; } = false;
41
+
42
+ public bool IsActive { get; set; } = true;
43
+
44
+ public int SortOrder { get; set; } = 0;
45
+
46
+ public DateTime CreatedAt { get; set; } = DateTime.Now;
47
+
48
+ public DateTime? UpdatedAt { get; set; }
49
+
50
+ // 导航属性
51
+ [Navigate(nameof(CategoryId))]
52
+ public Category Category { get; set; } = null!;
53
+
54
+ [Navigate(nameof(UserFavorite.ToolId))]
55
+ public List<UserFavorite> UserFavorites { get; set; } = new();
56
+
57
+ [Navigate(nameof(ToolTag.ToolId))]
58
+ public List<ToolTag> ToolTags { get; set; } = new();
59
+ }
Models/ToolStatistics.cs ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using FreeSql.DataAnnotations;
2
+
3
+ namespace ToolHub.Models;
4
+
5
+ [Table(Name = "ToolStatistics")]
6
+ public class ToolStatistics
7
+ {
8
+ [Column(IsIdentity = true, IsPrimary = true)]
9
+ public int Id { get; set; }
10
+
11
+ public int ToolId { get; set; }
12
+
13
+ public DateTime Date { get; set; } // 统计日期
14
+
15
+ public int DailyViews { get; set; } = 0; // 日访问量
16
+
17
+ public int DailyUniqueViews { get; set; } = 0; // 日独立访问量
18
+
19
+ public int DailyFavorites { get; set; } = 0; // 日收藏数
20
+
21
+ public int DailyShares { get; set; } = 0; // 日分享数
22
+
23
+ public int DailyDownloads { get; set; } = 0; // 日下载数
24
+
25
+ public decimal AverageDuration { get; set; } = 0; // 平均停留时间(秒)
26
+
27
+ public int BounceCount { get; set; } = 0; // 跳出次数
28
+
29
+ public DateTime CreatedAt { get; set; } = DateTime.Now;
30
+
31
+ public DateTime? UpdatedAt { get; set; }
32
+
33
+ // 导航属性
34
+ [Navigate(nameof(ToolId))]
35
+ public Tool Tool { get; set; } = null!;
36
+ }
Models/ToolTag.cs ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using FreeSql.DataAnnotations;
2
+
3
+ namespace ToolHub.Models;
4
+
5
+ [Table(Name = "ToolTags")]
6
+ public class ToolTag
7
+ {
8
+ public int ToolId { get; set; }
9
+
10
+ public int TagId { get; set; }
11
+
12
+ public DateTime CreatedAt { get; set; } = DateTime.Now;
13
+
14
+ // 导航属性
15
+ [Navigate(nameof(ToolId))]
16
+ public Tool Tool { get; set; } = null!;
17
+
18
+ [Navigate(nameof(TagId))]
19
+ public Tag Tag { get; set; } = null!;
20
+ }
Models/User.cs ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using FreeSql.DataAnnotations;
2
+
3
+ namespace ToolHub.Models;
4
+
5
+ [Table(Name = "Users")]
6
+ public class User
7
+ {
8
+ [Column(IsIdentity = true, IsPrimary = true)]
9
+ public int Id { get; set; }
10
+
11
+ [Column(StringLength = 50)]
12
+ public string UserName { get; set; } = string.Empty;
13
+
14
+ [Column(StringLength = 100)]
15
+ public string Email { get; set; } = string.Empty;
16
+
17
+ [Column(StringLength = 255)]
18
+ public string PasswordHash { get; set; } = string.Empty;
19
+
20
+ [Column(StringLength = 20)]
21
+ public string? NickName { get; set; }
22
+
23
+ [Column(StringLength = 255)]
24
+ public string? Avatar { get; set; }
25
+
26
+ public DateTime CreatedAt { get; set; } = DateTime.Now;
27
+
28
+ public DateTime? UpdatedAt { get; set; }
29
+
30
+ public bool IsActive { get; set; } = true;
31
+
32
+ [Column(StringLength = 10)]
33
+ public string Role { get; set; } = "User"; // User, Admin
34
+
35
+ // 导航属性
36
+ [Navigate(nameof(UserFavorite.UserId))]
37
+ public List<UserFavorite> UserFavorites { get; set; } = new();
38
+ }
Models/UserFavorite.cs ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using FreeSql.DataAnnotations;
2
+
3
+ namespace ToolHub.Models;
4
+
5
+ [Table(Name = "UserFavorites")]
6
+ public class UserFavorite
7
+ {
8
+ public int UserId { get; set; }
9
+
10
+ public int ToolId { get; set; }
11
+
12
+ public DateTime CreatedAt { get; set; } = DateTime.Now;
13
+
14
+ // 导航属性
15
+ [Navigate(nameof(UserId))]
16
+ public User User { get; set; } = null!;
17
+
18
+ [Navigate(nameof(ToolId))]
19
+ public Tool Tool { get; set; } = null!;
20
+ }
Models/UserToolAccess.cs ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using FreeSql.DataAnnotations;
2
+
3
+ namespace ToolHub.Models;
4
+
5
+ [Table(Name = "UserToolAccesses")]
6
+ public class UserToolAccess
7
+ {
8
+ [Column(IsIdentity = true, IsPrimary = true)]
9
+ public int Id { get; set; }
10
+
11
+ public int? UserId { get; set; } // 可为空,支持匿名用户
12
+
13
+ public int ToolId { get; set; }
14
+
15
+ [Column(StringLength = 50)]
16
+ public string? SessionId { get; set; } // 会话ID,用于匿名用户跟踪
17
+
18
+ [Column(StringLength = 45)]
19
+ public string? IpAddress { get; set; } // IP地址
20
+
21
+ [Column(StringLength = 500)]
22
+ public string? UserAgent { get; set; } // 用户代理
23
+
24
+ [Column(StringLength = 100)]
25
+ public string? Referer { get; set; } // 来源页面
26
+
27
+ [Column(StringLength = 20)]
28
+ public string AccessType { get; set; } = "view"; // view, favorite, share, download
29
+
30
+ public int Duration { get; set; } = 0; // 停留时间(秒)
31
+
32
+ public DateTime CreatedAt { get; set; } = DateTime.Now;
33
+
34
+ // 导航属性
35
+ [Navigate(nameof(UserId))]
36
+ public User? User { get; set; }
37
+
38
+ [Navigate(nameof(ToolId))]
39
+ public Tool Tool { get; set; } = null!;
40
+ }
Program.cs ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using ToolHub.Services;
2
+ using Microsoft.AspNetCore.Authentication.Cookies;
3
+
4
+ var builder = WebApplication.CreateBuilder(args);
5
+
6
+ // Add services to the container.
7
+ builder.Services.AddControllersWithViews();
8
+
9
+ // 添加HttpContext访问器
10
+ builder.Services.AddHttpContextAccessor();
11
+
12
+ // 配置FreeSql
13
+ var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
14
+ ?? "Data Source=toolhub.db";
15
+
16
+ builder.Services.AddSingleton<IFreeSql>(provider =>
17
+ {
18
+ var freeSql = new FreeSql.FreeSqlBuilder()
19
+ .UseConnectionString(FreeSql.DataType.Sqlite, connectionString)
20
+ .UseAutoSyncStructure(true) // 自动同步数据库结构
21
+ .Build();
22
+
23
+ return freeSql;
24
+ });
25
+
26
+ // 注册服务
27
+ builder.Services.AddScoped<IToolService, ToolService>();
28
+ builder.Services.AddScoped<IUserService, UserService>();
29
+
30
+ // 配置身份验证
31
+ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
32
+ .AddCookie(options =>
33
+ {
34
+ options.LoginPath = "/Admin/Login";
35
+ options.LogoutPath = "/Admin/Logout";
36
+ options.ExpireTimeSpan = TimeSpan.FromDays(7);
37
+ });
38
+
39
+ var app = builder.Build();
40
+
41
+ // 初始化数据库和种子数据
42
+ using (var scope = app.Services.CreateScope())
43
+ {
44
+ var freeSql = scope.ServiceProvider.GetRequiredService<IFreeSql>();
45
+
46
+ // 确保数据库表已创建
47
+ freeSql.CodeFirst.SyncStructure<ToolHub.Models.User>();
48
+ freeSql.CodeFirst.SyncStructure<ToolHub.Models.Category>();
49
+ freeSql.CodeFirst.SyncStructure<ToolHub.Models.Tool>();
50
+ freeSql.CodeFirst.SyncStructure<ToolHub.Models.Tag>();
51
+ freeSql.CodeFirst.SyncStructure<ToolHub.Models.ToolTag>();
52
+ freeSql.CodeFirst.SyncStructure<ToolHub.Models.UserFavorite>();
53
+ freeSql.CodeFirst.SyncStructure<ToolHub.Models.UserToolAccess>();
54
+ freeSql.CodeFirst.SyncStructure<ToolHub.Models.ToolStatistics>();
55
+
56
+ // 添加初始数据
57
+ await SeedDataAsync(freeSql);
58
+ }
59
+
60
+ // Configure the HTTP request pipeline.
61
+ if (!app.Environment.IsDevelopment())
62
+ {
63
+ app.UseExceptionHandler("/Home/Error");
64
+ // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
65
+ app.UseHsts();
66
+ }
67
+
68
+ app.UseHttpsRedirection();
69
+ app.UseRouting();
70
+
71
+ app.UseAuthentication();
72
+ app.UseAuthorization();
73
+
74
+ app.MapStaticAssets();
75
+
76
+ app.MapControllerRoute(
77
+ name: "default",
78
+ pattern: "{controller=Home}/{action=Index}/{id?}")
79
+ .WithStaticAssets();
80
+
81
+
82
+ app.Run();
83
+
84
+ // 数据初始化方法
85
+ static async Task SeedDataAsync(IFreeSql freeSql)
86
+ {
87
+ // 检查是否已有数据
88
+ var categoryCount = await freeSql.Select<ToolHub.Models.Category>().CountAsync();
89
+ if (categoryCount > 0) return;
90
+
91
+ // 添加分类
92
+ var categories = new List<ToolHub.Models.Category>
93
+ {
94
+ new() { Name = "PDF工具", Description = "PDF文档处理相关工具", Icon = "fas fa-file-pdf", Color = "#dc3545", SortOrder = 1 },
95
+ new() { Name = "图片工具", Description = "图片处理和编辑工具", Icon = "fas fa-image", Color = "#165DFF", SortOrder = 2 },
96
+ new() { Name = "视频工具", Description = "视频处理和编辑工具", Icon = "fas fa-video", Color = "#FF7D00", SortOrder = 3 },
97
+ new() { Name = "文档转换", Description = "各种文档格式转换工具", Icon = "fas fa-file-alt", Color = "#52C41A", SortOrder = 4 },
98
+ new() { Name = "数据计算", Description = "数据计算和统计工具", Icon = "fas fa-calculator", Color = "#36CFC9", SortOrder = 5 },
99
+ new() { Name = "开发工具", Description = "程序开发相关工具", Icon = "fas fa-code", Color = "#722ED1", SortOrder = 6 },
100
+ new() { Name = "文本工具", Description = "文本处理和编辑工具", Icon = "fas fa-font", Color = "#13C2C2", SortOrder = 7 },
101
+ new() { Name = "生活娱乐", Description = "日常生活和娱乐工具", Icon = "fas fa-gamepad", Color = "#FA8C16", SortOrder = 8 }
102
+ };
103
+
104
+ await freeSql.Insert(categories).ExecuteAffrowsAsync();
105
+
106
+ // 添加示例工具
107
+ var tools = new List<ToolHub.Models.Tool>
108
+ {
109
+ new() { Name = "PDF转Word", Description = "高效将PDF文件转换为可编辑的Word文档,保留原始格式和排版",
110
+ Icon = "fas fa-file-word", Image = "https://design.gemcoder.com/staticResource/echoAiSystemImages/33769d8fd6cd6c3290700a9a1849a860.png",
111
+ CategoryId = 1, IsHot = true, Rating = 4.9m, RatingCount = 1250, ViewCount = 125000, SortOrder = 1 },
112
+
113
+ new() { Name = "图片压缩", Description = "减小图片文件大小,保持高质量,支持批量处理多种图片格式",
114
+ Icon = "fas fa-compress", Image = "https://design.gemcoder.com/staticResource/echoAiSystemImages/34a8fb1fdc206c992dddf5cda0318963.png",
115
+ CategoryId = 2, IsHot = true, Rating = 4.8m, RatingCount = 980, ViewCount = 98000, SortOrder = 2 },
116
+
117
+ new() { Name = "证件照生成", Description = "快速制作符���各国签证和证件要求的标准证件照,支持多种尺寸",
118
+ Icon = "fas fa-user-circle", Image = "https://design.gemcoder.com/staticResource/echoAiSystemImages/47b898b061a999bcbb5ee1defbdb6800.png",
119
+ CategoryId = 2, IsRecommended = true, Rating = 4.7m, RatingCount = 750, ViewCount = 75000, SortOrder = 3 },
120
+
121
+ new() { Name = "在线录屏", Description = "无需安装软件,直接在浏览器中录制屏幕、摄像头和音频,支持多种格式导出",
122
+ Icon = "fas fa-video", Image = "https://design.gemcoder.com/staticResource/echoAiSystemImages/930732ebbce6d0418fefe467d9f74ebc.png",
123
+ CategoryId = 3, IsNew = true, Rating = 4.6m, RatingCount = 520, ViewCount = 52000, SortOrder = 4 },
124
+
125
+ new() { Name = "PDF转Excel", Description = "将PDF表格数据转换为可编辑的Excel文件",
126
+ Icon = "fas fa-file-excel", CategoryId = 4, IsNew = true, Rating = 4.5m, RatingCount = 320, ViewCount = 32000, SortOrder = 5 },
127
+
128
+ new() { Name = "房贷利率计算器", Description = "计算不同贷款方式下的月供和总利息",
129
+ Icon = "fas fa-calculator", CategoryId = 5, Rating = 4.6m, RatingCount = 890, ViewCount = 89000, SortOrder = 6 }
130
+ };
131
+
132
+ await freeSql.Insert(tools).ExecuteAffrowsAsync();
133
+
134
+ // 添加默认管理员用户
135
+ var adminPassword = "admin123"; // 实际应用中应该使用更安全的密码
136
+ using var sha256 = System.Security.Cryptography.SHA256.Create();
137
+ var hashedBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(adminPassword + "ToolHub_Salt"));
138
+ var hashedPassword = Convert.ToBase64String(hashedBytes);
139
+
140
+ var admin = new ToolHub.Models.User
141
+ {
142
+ UserName = "admin",
143
+ Email = "admin@toolhub.com",
144
+ PasswordHash = hashedPassword,
145
+ NickName = "管理员",
146
+ Role = "Admin"
147
+ };
148
+
149
+ await freeSql.Insert(admin).ExecuteAffrowsAsync();
150
+ }
Properties/launchSettings.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json.schemastore.org/launchsettings.json",
3
+ "profiles": {
4
+ "http": {
5
+ "commandName": "Project",
6
+ "dotnetRunMessages": true,
7
+ "launchBrowser": true,
8
+ "applicationUrl": "http://localhost:5163",
9
+ "environmentVariables": {
10
+ "ASPNETCORE_ENVIRONMENT": "Development"
11
+ }
12
+ },
13
+ "https": {
14
+ "commandName": "Project",
15
+ "dotnetRunMessages": true,
16
+ "launchBrowser": true,
17
+ "applicationUrl": "https://localhost:7078;http://localhost:5163",
18
+ "environmentVariables": {
19
+ "ASPNETCORE_ENVIRONMENT": "Development"
20
+ }
21
+ }
22
+ }
23
+ }
README.md CHANGED
@@ -1,10 +1,124 @@
1
- ---
2
- title: Toolhub
3
- emoji: 📚
4
- colorFrom: blue
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ToolHub - 工具集合平台
2
+
3
+ ToolHub 是一个现代化的工具集合平台,提供各种实用工具的集中管理和访问。
4
+
5
+ ## 功能特性
6
+
7
+ ### 核心功能
8
+ - 🛠️ **工具管理**: 完整的工具CRUD操作,支持分类、标签、状态管理
9
+ - 📂 **分类管理**: 工具分类系统,支持层级分类和排序
10
+ - 🏷️ **标签管理**: 灵活的标签系统,支持颜色自定义
11
+ - 👥 **用户管理**: 用户注册、登录、收藏功能
12
+ - 📊 **数据统计**: 访问量统计、用户行为分析
13
+ - 🎨 **现代化UI**: 响应式设计,支持移动端访问
14
+
15
+ ### 管理后台功能
16
+ - 📈 **仪表板**: 数据概览和快速操作
17
+ - 🔧 **工具管理**:
18
+ - 支持分页显示(每页20条)
19
+ - 分类筛选和搜索
20
+ - 标签管理(为工具添加/移除标签)
21
+ - 状态切换(启用/禁用)
22
+ - 标志管理(热门/新品/推荐)
23
+ - 📂 **分类管理**:
24
+ - 支持分页显示
25
+ - 图标和颜色自定义
26
+ - 排序管理
27
+ - 🏷️ **标签管理**:
28
+ - 支持分页显示
29
+ - 颜色自定义
30
+ - 使用统计
31
+ - 👥 **用户管理**: 用户信息查看和管理
32
+
33
+ ### 技术特性
34
+ - 🚀 **.NET 9.0**: 最新的.NET框架
35
+ - 🗄️ **SQLite + FreeSql**: 轻量级数据库和ORM
36
+ - 🎨 **现代化UI**: CSS Grid、Flexbox布局
37
+ - 📱 **响应式设计**: 支持桌面和移动设备
38
+ - 🔒 **身份认证**: 基于角色的访问控制
39
+
40
+ ## 快速开始
41
+
42
+ ### 环境要求
43
+ - .NET 9.0 SDK
44
+ - Visual Studio 2022 或 VS Code
45
+
46
+ ### 安装步骤
47
+ 1. 克隆项目
48
+ ```bash
49
+ git clone [项目地址]
50
+ cd ToolHub
51
+ ```
52
+
53
+ 2. 运行项目
54
+ ```bash
55
+ dotnet run
56
+ ```
57
+
58
+ 3. 访问应用
59
+ - 前台网站: http://localhost:5000
60
+ - 管理后台: http://localhost:5000/Admin
61
+
62
+ ### 默认管理员账户
63
+ - 用户名: admin
64
+ - 密码: admin123
65
+
66
+ ## 数据库结构
67
+
68
+ ### 主要表结构
69
+ - **Tools**: 工具信息表
70
+ - **Categories**: 分类表
71
+ - **Tags**: 标签表
72
+ - **ToolTags**: 工具标签关联表
73
+ - **Users**: 用户表
74
+ - **UserFavorites**: 用户收藏表
75
+
76
+ ## 新增功能 (最新版本)
77
+
78
+ ### 分页功能
79
+ - ✅ 工具列表分页(每页20条)
80
+ - ✅ 分类列表分页(每页20条)
81
+ - ✅ 标签列表分页(每页20条)
82
+ - ✅ 智能分页控件,支持首页、末页、前后页跳转
83
+
84
+ ### 标签管理系统
85
+ - ✅ 完整的标签CRUD操作
86
+ - ✅ 标签颜色自定义
87
+ - ✅ 工具标签关联管理
88
+ - ✅ 标签使用统计
89
+ - ✅ 标签状态管理(启用/禁用)
90
+
91
+ ### 工具标签功能
92
+ - ✅ 为工具添加/移除标签
93
+ - ✅ 标签可视化显示
94
+ - ✅ 标签筛选和搜索
95
+ - ✅ 标签管理模态框
96
+
97
+ ## 开发指南
98
+
99
+ ### 项目结构
100
+ ```
101
+ ToolHub/
102
+ ├── Controllers/ # 控制器
103
+ ├── Models/ # 数据模型
104
+ ├── Services/ # 业务服务
105
+ ├── Views/ # 视图文件
106
+ │ ├── Admin/ # 管理后台视图
107
+ │ ├── Home/ # 前台页面视图
108
+ │ └── Shared/ # 共享布局
109
+ └── wwwroot/ # 静态资源
110
+ ```
111
+
112
+ ### 添加新功能
113
+ 1. 在 `Models` 文件夹中定义数据模型
114
+ 2. 在 `Controllers` 文件夹中创建控制器
115
+ 3. 在 `Services` 文件夹中实现业务逻辑
116
+ 4. 在 `Views` 文件夹中创建视图页面
117
+
118
+ ## 贡献指南
119
+
120
+ 欢迎提交 Issue 和 Pull Request 来改进项目。
121
+
122
+ ## 许可证
123
+
124
+ MIT License
README_HF_SPACES.md ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ToolHub - Hugging Face Spaces 部署指南
2
+
3
+ ## 概述
4
+
5
+ 这个项目已经配置为可以在 Hugging Face Spaces 上部署,使用 Docker 容器和 Supervisor 进行进程监控和自动重启。
6
+
7
+ ## 部署文件说明
8
+
9
+ ### 1. Dockerfile
10
+ - 基于 .NET 9.0 运行时镜像
11
+ - 多阶段构建,优化镜像大小
12
+ - 安装 Supervisor 用于进程监控
13
+ - 配置健康检查
14
+ - 暴露 7860 端口
15
+
16
+ ### 2. supervisord.conf
17
+ - 配置 Supervisor 监控 ASP.NET Core 应用
18
+ - 自动重启功能
19
+ - 日志轮转配置
20
+ - 健康检查程序
21
+
22
+ ### 3. start.sh
23
+ - 应用启动脚本
24
+ - 数据库初始化检查
25
+ - 环境变量配置
26
+
27
+ ### 4. appsettings.Production.json
28
+ - 生产环境配置
29
+ - 数据库连接字符串
30
+ - Kestrel 服务器配置
31
+
32
+ ## 在 Hugging Face Spaces 上部署
33
+
34
+ ### 步骤 1: 创建 Space
35
+ 1. 访问 [Hugging Face Spaces](https://huggingface.co/spaces)
36
+ 2. 点击 "Create new Space"
37
+ 3. 选择 "Docker" 作为 SDK
38
+ 4. 填写 Space 名称和描述
39
+
40
+ ### 步骤 2: 上传代码
41
+ 将以下文件上传到你的 Space 仓库:
42
+ - `Dockerfile`
43
+ - `supervisord.conf`
44
+ - `start.sh`
45
+ - `appsettings.Production.json`
46
+ - `.dockerignore`
47
+ - 所有项目源代码
48
+
49
+ ### 步骤 3: 配置环境变量(可选)
50
+ 在 Space 设置中可以配置以下环境变量:
51
+ - `ASPNETCORE_ENVIRONMENT`: 设置为 "Production"
52
+ - `ConnectionStrings__DefaultConnection`: 自定义数据库连接字符串
53
+
54
+ ### 步骤 4: 部署
55
+ 1. 提交代码到仓库
56
+ 2. Hugging Face 会自动构建 Docker 镜像
57
+ 3. 构建完成后应用会自动启动
58
+
59
+ ## 功能特性
60
+
61
+ ### Supervisor 监控
62
+ - **自动重启**: 应用崩溃时自动重启
63
+ - **日志管理**: 自动轮转日志文件
64
+ - **健康检查**: 定期检查应用状态
65
+ - **进程管理**: 统一管理所有后台进程
66
+
67
+ ### 数据库持久化
68
+ - SQLite 数据库存储在 `/app/data/` 目录
69
+ - 容器重启后数据不会丢失
70
+ - 自动创建数据库和表结构
71
+
72
+ ### 健康检查
73
+ - 每 30 秒检查一次应用状态
74
+ - 通过 HTTP 请求验证应用是否正常运行
75
+ - 失败时自动重启容器
76
+
77
+ ## 监控和日志
78
+
79
+ ### 查看日志
80
+ ```bash
81
+ # 查看应用日志
82
+ docker logs <container_id>
83
+
84
+ # 查看 Supervisor 日志
85
+ docker exec <container_id> cat /var/log/supervisor/supervisord.log
86
+
87
+ # 查看应用输出日志
88
+ docker exec <container_id> cat /var/log/supervisor/toolhub.log
89
+ ```
90
+
91
+ ### 监控状态
92
+ ```bash
93
+ # 查看 Supervisor 状态
94
+ docker exec <container_id> supervisorctl status
95
+
96
+ # 重启应用
97
+ docker exec <container_id> supervisorctl restart toolhub
98
+ ```
99
+
100
+ ## 故障排除
101
+
102
+ ### 常见问题
103
+
104
+ 1. **应用无法启动**
105
+ - 检查端口 7860 是否被占用
106
+ - 查看应用日志:`docker exec <container_id> cat /var/log/supervisor/toolhub_error.log`
107
+
108
+ 2. **数据库连接问题**
109
+ - 确保 `/app/data` 目录有写权限
110
+ - 检查数据库文件是否存在
111
+
112
+ 3. **Supervisor 问题**
113
+ - 查看 Supervisor 日志:`docker exec <container_id> cat /var/log/supervisor/supervisord.log`
114
+ - 重启 Supervisor:`docker exec <container_id> supervisorctl reload`
115
+
116
+ ### 性能优化
117
+ - 应用使用 .NET 9.0 运行时,性能优异
118
+ - SQLite 数据库适合中小型应用
119
+ - 容器化部署,资源隔离
120
+
121
+ ## 安全注意事项
122
+
123
+ 1. **默认管理员账户**
124
+ - 用户名:`admin`
125
+ - 密码:`admin123`
126
+ - **重要**: 部署后请立即修改默认密码
127
+
128
+ 2. **数据库安全**
129
+ - SQLite 数据库文件存储在容器内
130
+ - 建议定期备份数据库文件
131
+
132
+ 3. **网络安全**
133
+ - 应用运行在容器内,端口 7860
134
+ - Hugging Face Spaces 提供 HTTPS 访问
135
+
136
+ ## 更新部署
137
+
138
+ 1. 修改代码后提交到仓库
139
+ 2. Hugging Face 会自动重新构建镜像
140
+ 3. 新版本会自动部署并启动
141
+
142
+ ## 联系支持
143
+
144
+ 如果遇到部署问题,请检查:
145
+ 1. Dockerfile 语法是否正确
146
+ 2. 所有必需文件是否已上传
147
+ 3. 应用日志中是否有错误信息
Services/BaseToolService.cs ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using ToolHub.Models;
2
+ using Microsoft.AspNetCore.Http;
3
+ using System.Security.Claims;
4
+
5
+ namespace ToolHub.Services;
6
+
7
+ /// <summary>
8
+ /// 工具基类服务,提供统计数据和用户访问记录功能
9
+ /// </summary>
10
+ public abstract class BaseToolService
11
+ {
12
+ protected readonly IFreeSql _freeSql;
13
+ protected readonly IHttpContextAccessor _httpContextAccessor;
14
+
15
+ protected BaseToolService(IFreeSql freeSql, IHttpContextAccessor httpContextAccessor)
16
+ {
17
+ _freeSql = freeSql;
18
+ _httpContextAccessor = httpContextAccessor;
19
+ }
20
+
21
+ /// <summary>
22
+ /// 记录工具访问
23
+ /// </summary>
24
+ protected async Task RecordToolAccessAsync(int toolId, string accessType = "view", int duration = 0)
25
+ {
26
+ try
27
+ {
28
+ var httpContext = _httpContextAccessor.HttpContext;
29
+ if (httpContext == null) return;
30
+
31
+ var userId = GetCurrentUserId();
32
+ var sessionId = GetSessionId();
33
+ var ipAddress = GetClientIpAddress();
34
+ var userAgent = GetUserAgent();
35
+ var referer = GetReferer();
36
+
37
+ var access = new UserToolAccess
38
+ {
39
+ UserId = userId,
40
+ ToolId = toolId,
41
+ SessionId = sessionId,
42
+ IpAddress = ipAddress,
43
+ UserAgent = userAgent,
44
+ Referer = referer,
45
+ AccessType = accessType,
46
+ Duration = duration
47
+ };
48
+
49
+ await _freeSql.Insert(access).ExecuteAffrowsAsync();
50
+
51
+ // 更新工具总访问量
52
+ await _freeSql.Update<Tool>()
53
+ .Where(t => t.Id == toolId)
54
+ .Set(t => t.ViewCount + 1)
55
+ .ExecuteAffrowsAsync();
56
+
57
+ // 更新日统计数据
58
+ await UpdateDailyStatisticsAsync(toolId, accessType);
59
+ }
60
+ catch (Exception ex)
61
+ {
62
+ // 记录错误但不影响主流程
63
+ Console.WriteLine($"记录工具访问失败: {ex.Message}");
64
+ }
65
+ }
66
+
67
+ /// <summary>
68
+ /// 更新日统计数据
69
+ /// </summary>
70
+ protected async Task UpdateDailyStatisticsAsync(int toolId, string accessType)
71
+ {
72
+ var today = DateTime.Today;
73
+ var stats = await _freeSql.Select<ToolStatistics>()
74
+ .Where(s => s.ToolId == toolId && s.Date == today)
75
+ .FirstAsync();
76
+
77
+ if (stats == null)
78
+ {
79
+ // 创建新的日统计记录
80
+ stats = new ToolStatistics
81
+ {
82
+ ToolId = toolId,
83
+ Date = today
84
+ };
85
+ await _freeSql.Insert(stats).ExecuteAffrowsAsync();
86
+ }
87
+
88
+ // 更新统计数据
89
+ var updateQuery = _freeSql.Update<ToolStatistics>()
90
+ .Where(s => s.Id == stats.Id);
91
+
92
+ switch (accessType.ToLower())
93
+ {
94
+ case "view":
95
+ updateQuery.Set(s => s.DailyViews + 1);
96
+ break;
97
+ case "favorite":
98
+ updateQuery.Set(s => s.DailyFavorites + 1);
99
+ break;
100
+ case "share":
101
+ updateQuery.Set(s => s.DailyShares + 1);
102
+ break;
103
+ case "download":
104
+ updateQuery.Set(s => s.DailyDownloads + 1);
105
+ break;
106
+ }
107
+
108
+ updateQuery.Set(s => s.UpdatedAt, DateTime.Now);
109
+ await updateQuery.ExecuteAffrowsAsync();
110
+ }
111
+
112
+ /// <summary>
113
+ /// 获取工具统计数据
114
+ /// </summary>
115
+ protected async Task<ToolStatistics?> GetToolStatisticsAsync(int toolId, DateTime date)
116
+ {
117
+ return await _freeSql.Select<ToolStatistics>()
118
+ .Where(s => s.ToolId == toolId && s.Date == date)
119
+ .FirstAsync();
120
+ }
121
+
122
+ /// <summary>
123
+ /// 获取工具访问记录
124
+ /// </summary>
125
+ protected async Task<List<UserToolAccess>> GetToolAccessRecordsAsync(int toolId, DateTime? startDate = null, DateTime? endDate = null, int limit = 100)
126
+ {
127
+ var query = _freeSql.Select<UserToolAccess>()
128
+ .Include(ua => ua.User)
129
+ .Where(ua => ua.ToolId == toolId);
130
+
131
+ if (startDate.HasValue)
132
+ query = query.Where(ua => ua.CreatedAt >= startDate.Value);
133
+
134
+ if (endDate.HasValue)
135
+ query = query.Where(ua => ua.CreatedAt <= endDate.Value.AddDays(1));
136
+
137
+ return await query
138
+ .OrderByDescending(ua => ua.CreatedAt)
139
+ .Take(limit)
140
+ .ToListAsync();
141
+ }
142
+
143
+ /// <summary>
144
+ /// 获取用户访问的工具记录
145
+ /// </summary>
146
+ protected async Task<List<UserToolAccess>> GetUserToolAccessRecordsAsync(int userId, DateTime? startDate = null, DateTime? endDate = null, int limit = 100)
147
+ {
148
+ var query = _freeSql.Select<UserToolAccess>()
149
+ .Include(ua => ua.Tool)
150
+ .Where(ua => ua.UserId == userId);
151
+
152
+ if (startDate.HasValue)
153
+ query = query.Where(ua => ua.CreatedAt >= startDate.Value);
154
+
155
+ if (endDate.HasValue)
156
+ query = query.Where(ua => ua.CreatedAt <= endDate.Value);
157
+
158
+ return await query
159
+ .OrderByDescending(ua => ua.CreatedAt)
160
+ .Take(limit)
161
+ .ToListAsync();
162
+ }
163
+
164
+ /// <summary>
165
+ /// 获取热门工具(基于访问量)
166
+ /// </summary>
167
+ protected async Task<List<Tool>> GetPopularToolsAsync(int count = 10, DateTime? startDate = null)
168
+ {
169
+ var query = _freeSql.Select<Tool>()
170
+ .Include(t => t.Category)
171
+ .Where(t => t.IsActive);
172
+
173
+ if (startDate.HasValue)
174
+ {
175
+ // 基于指定日期后的访问量排序
176
+ var toolAccessData = _freeSql.Select<UserToolAccess>()
177
+ .Where(ua => ua.CreatedAt >= startDate.Value)
178
+ .GroupBy(ua => ua.ToolId)
179
+ .Select(g => new { ToolId = g.Key, Count = g.Count() })
180
+ .OrderByDescending(x => x.Count)
181
+ .Take(count)
182
+ .ToList();
183
+
184
+ var toolIds = toolAccessData.Select(x => x.ToolId).ToList();
185
+
186
+ if (toolIds.Any())
187
+ {
188
+ query = query.Where(t => toolIds.Contains(t.Id));
189
+ }
190
+ }
191
+ else
192
+ {
193
+ // 基于总访问量排序
194
+ query = query.OrderByDescending(t => t.ViewCount);
195
+ }
196
+
197
+ return await query.Take(count).ToListAsync();
198
+ }
199
+
200
+ /// <summary>
201
+ /// 获取当前用户ID
202
+ /// </summary>
203
+ protected int? GetCurrentUserId()
204
+ {
205
+ var httpContext = _httpContextAccessor.HttpContext;
206
+ if (httpContext?.User?.Identity?.IsAuthenticated == true)
207
+ {
208
+ var userIdClaim = httpContext.User.FindFirst(ClaimTypes.NameIdentifier);
209
+ if (userIdClaim != null && int.TryParse(userIdClaim.Value, out int userId))
210
+ {
211
+ return userId;
212
+ }
213
+ }
214
+ return null;
215
+ }
216
+
217
+ /// <summary>
218
+ /// 获取会话ID
219
+ /// </summary>
220
+ protected string? GetSessionId()
221
+ {
222
+ string sessionId = string.Empty;
223
+ try
224
+ {
225
+ var httpContext = _httpContextAccessor.HttpContext;
226
+ return httpContext?.Session?.Id;
227
+ }
228
+ catch (Exception ex)
229
+ {
230
+ Console.WriteLine($"获取会话ID失败: {ex.Message}");
231
+ return sessionId;
232
+ }
233
+ }
234
+
235
+ /// <summary>
236
+ /// 获取客户端IP地址
237
+ /// </summary>
238
+ protected string? GetClientIpAddress()
239
+ {
240
+ var httpContext = _httpContextAccessor.HttpContext;
241
+ if (httpContext == null) return null;
242
+
243
+ // 尝试从各种头部获取真实IP
244
+ var ip = httpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault() ??
245
+ httpContext.Request.Headers["X-Real-IP"].FirstOrDefault() ??
246
+ httpContext.Connection.RemoteIpAddress?.ToString();
247
+
248
+ return ip;
249
+ }
250
+
251
+ /// <summary>
252
+ /// 获取用户代理
253
+ /// </summary>
254
+ protected string? GetUserAgent()
255
+ {
256
+ var httpContext = _httpContextAccessor.HttpContext;
257
+ return httpContext?.Request.Headers["User-Agent"].FirstOrDefault();
258
+ }
259
+
260
+ /// <summary>
261
+ /// 获取来源页面
262
+ /// </summary>
263
+ protected string? GetReferer()
264
+ {
265
+ var httpContext = _httpContextAccessor.HttpContext;
266
+ return httpContext?.Request.Headers["Referer"].FirstOrDefault();
267
+ }
268
+
269
+ /// <summary>
270
+ /// 计算平均停留时间
271
+ /// </summary>
272
+ protected async Task UpdateAverageDurationAsync(int toolId)
273
+ {
274
+ var today = DateTime.Today;
275
+ var avgDuration = await _freeSql.Select<UserToolAccess>()
276
+ .Where(ua => ua.ToolId == toolId && ua.CreatedAt >= today && ua.Duration > 0)
277
+ .AvgAsync(ua => ua.Duration);
278
+
279
+ if (avgDuration > 0)
280
+ {
281
+ await _freeSql.Update<ToolStatistics>()
282
+ .Where(s => s.ToolId == toolId && s.Date == today)
283
+ .Set(s => s.AverageDuration, (decimal)avgDuration)
284
+ .ExecuteAffrowsAsync();
285
+ }
286
+ }
287
+
288
+ /// <summary>
289
+ /// 获取工具使用趋势数据
290
+ /// </summary>
291
+ protected async Task<List<object>> GetToolUsageTrendAsync(int toolId, int days = 30)
292
+ {
293
+ var endDate = DateTime.Today;
294
+ var startDate = endDate.AddDays(-days + 1);
295
+
296
+ var stats = await _freeSql.Select<ToolStatistics>()
297
+ .Where(s => s.ToolId == toolId && s.Date >= startDate && s.Date <= endDate)
298
+ .OrderBy(s => s.Date)
299
+ .ToListAsync();
300
+
301
+ var result = new List<object>();
302
+ for (var date = startDate; date <= endDate; date = date.AddDays(1))
303
+ {
304
+ var dayStats = stats.FirstOrDefault(s => s.Date == date);
305
+ result.Add(new
306
+ {
307
+ Date = date.ToString("yyyy-MM-dd"),
308
+ Views = dayStats?.DailyViews ?? 0,
309
+ UniqueViews = dayStats?.DailyUniqueViews ?? 0,
310
+ Favorites = dayStats?.DailyFavorites ?? 0,
311
+ Shares = dayStats?.DailyShares ?? 0,
312
+ Downloads = dayStats?.DailyDownloads ?? 0,
313
+ AverageDuration = dayStats?.AverageDuration ?? 0
314
+ });
315
+ }
316
+
317
+ return result;
318
+ }
319
+ }
Services/IToolService.cs ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using ToolHub.Models;
2
+
3
+ namespace ToolHub.Services;
4
+
5
+ public interface IToolService
6
+ {
7
+ Task<List<Category>> GetCategoriesAsync();
8
+ Task<List<Tool>> GetHotToolsAsync(int count = 8);
9
+ Task<List<Tool>> GetNewToolsAsync(int count = 12);
10
+ Task<List<Tool>> GetToolsByCategoryAsync(int categoryId, int page = 1, int pageSize = 20, bool isAll = false);
11
+ Task<Tool?> GetToolByIdAsync(int id);
12
+ Task<bool> AddToolAsync(Tool tool);
13
+ Task<bool> UpdateToolAsync(Tool tool);
14
+ Task<bool> DeleteToolAsync(int id);
15
+ Task<bool> IncrementViewCountAsync(int toolId);
16
+ Task<bool> IncrementViewCountAsync(string toolIdentifier);
17
+ Task<List<Tool>> SearchToolsAsync(string keyword, int page = 1, int pageSize = 20);
18
+
19
+ // 新增统计相关方法
20
+ Task<List<Tool>> GetPopularToolsAsync(int count = 10, DateTime? startDate = null);
21
+ Task<ToolStatistics?> GetToolStatisticsAsync(int toolId, DateTime date);
22
+ Task<List<UserToolAccess>> GetToolAccessRecordsAsync(int toolId, DateTime? startDate = null, DateTime? endDate = null, int limit = 100);
23
+ Task<List<UserToolAccess>> GetUserToolAccessRecordsAsync(int userId, DateTime? startDate = null, DateTime? endDate = null, int limit = 100);
24
+ Task<List<object>> GetToolUsageTrendAsync(int toolId, int days = 30);
25
+ Task RecordToolActionAsync(int toolId, string actionType, int duration = 0);
26
+ }
Services/IUserService.cs ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using ToolHub.Models;
2
+
3
+ namespace ToolHub.Services;
4
+
5
+ public interface IUserService
6
+ {
7
+ Task<User?> GetUserByIdAsync(int id);
8
+ Task<User?> GetUserByEmailAsync(string email);
9
+ Task<bool> CreateUserAsync(User user);
10
+ Task<bool> UpdateUserAsync(User user);
11
+ Task<bool> VerifyPasswordAsync(string email, string password);
12
+ Task<List<Tool>> GetUserFavoritesAsync(int userId);
13
+ Task<bool> AddFavoriteAsync(int userId, int toolId);
14
+ Task<bool> RemoveFavoriteAsync(int userId, int toolId);
15
+ Task<bool> IsFavoriteAsync(int userId, int toolId);
16
+ }
Services/ToolService.cs ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using ToolHub.Models;
2
+ using Microsoft.AspNetCore.Http;
3
+
4
+ namespace ToolHub.Services;
5
+
6
+ public class ToolService : BaseToolService, IToolService
7
+ {
8
+ public ToolService(IFreeSql freeSql, IHttpContextAccessor httpContextAccessor)
9
+ : base(freeSql, httpContextAccessor)
10
+ {
11
+ }
12
+
13
+ public async Task<List<Category>> GetCategoriesAsync()
14
+ {
15
+ return await _freeSql.Select<Category>()
16
+ .Where(c => c.IsActive)
17
+ .OrderBy(c => c.SortOrder)
18
+ .ToListAsync();
19
+ }
20
+
21
+ public async Task<List<Tool>> GetHotToolsAsync(int count = 8)
22
+ {
23
+ return await _freeSql.Select<Tool>()
24
+ .Include(t => t.Category)
25
+ .IncludeMany(t => t.ToolTags)
26
+ .Where(t => t.IsActive && t.IsHot)
27
+ .OrderByDescending(t => t.ViewCount)
28
+ .Take(count)
29
+ .ToListAsync();
30
+ }
31
+
32
+ public async Task<List<Tool>> GetNewToolsAsync(int count = 12)
33
+ {
34
+ return await _freeSql.Select<Tool>()
35
+ .Include(t => t.Category)
36
+ .IncludeMany(t => t.ToolTags)
37
+ .Where(t => t.IsActive)
38
+ .OrderByDescending(t => t.CreatedAt)
39
+ .Take(count)
40
+ .ToListAsync();
41
+ }
42
+
43
+ public async Task<List<Tool>> GetToolsByCategoryAsync(int categoryId, int page = 1, int pageSize = 20, bool isAll = false)
44
+ {
45
+ var query = _freeSql.Select<Tool>()
46
+ .Include(t => t.Category)
47
+ .IncludeMany(t => t.ToolTags);
48
+ if (!isAll) {
49
+ query.Where(t => t.IsActive);
50
+ }
51
+
52
+ if (categoryId > 0)
53
+ {
54
+ query = query.Where(t => t.CategoryId == categoryId);
55
+ }
56
+
57
+ return await query
58
+ .OrderBy(t => t.SortOrder)
59
+ .Page(page, pageSize)
60
+ .ToListAsync();
61
+ }
62
+
63
+ public async Task<Tool?> GetToolByIdAsync(int id)
64
+ {
65
+ return await _freeSql.Select<Tool>()
66
+ .Include(t => t.Category)
67
+ .IncludeMany(t => t.ToolTags)
68
+ .Where(t => t.Id == id && t.IsActive)
69
+ .FirstAsync();
70
+ }
71
+
72
+ public async Task<bool> AddToolAsync(Tool tool)
73
+ {
74
+ try
75
+ {
76
+ await _freeSql.Insert(tool).ExecuteAffrowsAsync();
77
+ return true;
78
+ }
79
+ catch
80
+ {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ public async Task<bool> UpdateToolAsync(Tool tool)
86
+ {
87
+ try
88
+ {
89
+ tool.UpdatedAt = DateTime.Now;
90
+ await _freeSql.Update<Tool>()
91
+ .SetSource(tool)
92
+ .ExecuteAffrowsAsync();
93
+ return true;
94
+ }
95
+ catch
96
+ {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ public async Task<bool> DeleteToolAsync(int id)
102
+ {
103
+ try
104
+ {
105
+ await _freeSql.Update<Tool>()
106
+ .Where(t => t.Id == id)
107
+ .Set(t => t.IsActive, false)
108
+ .Set(t => t.UpdatedAt, DateTime.Now)
109
+ .ExecuteAffrowsAsync();
110
+ return true;
111
+ }
112
+ catch
113
+ {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ public async Task<bool> IncrementViewCountAsync(int toolId)
119
+ {
120
+ try
121
+ {
122
+ await RecordToolAccessAsync(toolId, "view");
123
+ return true;
124
+ }
125
+ catch
126
+ {
127
+ return false;
128
+ }
129
+ }
130
+
131
+ public async Task<bool> IncrementViewCountAsync(string toolIdentifier)
132
+ {
133
+ try
134
+ {
135
+ var tool = await _freeSql.Select<Tool>()
136
+ .Where(t => t.Url!.Contains(toolIdentifier) || t.Name.Contains(toolIdentifier))
137
+ .FirstAsync();
138
+
139
+ if (tool != null)
140
+ {
141
+ await RecordToolAccessAsync(tool.Id, "view");
142
+ }
143
+ return true;
144
+ }
145
+ catch
146
+ {
147
+ return false;
148
+ }
149
+ }
150
+
151
+ public async Task<List<Tool>> SearchToolsAsync(string keyword, int page = 1, int pageSize = 20)
152
+ {
153
+ return await _freeSql.Select<Tool>()
154
+ .Include(t => t.Category)
155
+ .IncludeMany(t => t.ToolTags)
156
+ .Where(t => t.IsActive && (t.Name.Contains(keyword) || t.Description!.Contains(keyword)))
157
+ .OrderByDescending(t => t.ViewCount)
158
+ .Page(page, pageSize)
159
+ .ToListAsync();
160
+ }
161
+
162
+ // 新增统计相关方法
163
+ public new async Task<List<Tool>> GetPopularToolsAsync(int count = 10, DateTime? startDate = null)
164
+ {
165
+ return await base.GetPopularToolsAsync(count, startDate);
166
+ }
167
+
168
+ public new async Task<ToolStatistics?> GetToolStatisticsAsync(int toolId, DateTime date)
169
+ {
170
+ return await base.GetToolStatisticsAsync(toolId, date);
171
+ }
172
+
173
+ public new async Task<List<UserToolAccess>> GetToolAccessRecordsAsync(int toolId, DateTime? startDate = null, DateTime? endDate = null, int limit = 100)
174
+ {
175
+ return await base.GetToolAccessRecordsAsync(toolId, startDate, endDate, limit);
176
+ }
177
+
178
+ public new async Task<List<UserToolAccess>> GetUserToolAccessRecordsAsync(int userId, DateTime? startDate = null, DateTime? endDate = null, int limit = 100)
179
+ {
180
+ return await base.GetUserToolAccessRecordsAsync(userId, startDate, endDate, limit);
181
+ }
182
+
183
+ public new async Task<List<object>> GetToolUsageTrendAsync(int toolId, int days = 30)
184
+ {
185
+ return await base.GetToolUsageTrendAsync(toolId, days);
186
+ }
187
+
188
+ public async Task RecordToolActionAsync(int toolId, string actionType, int duration = 0)
189
+ {
190
+ await RecordToolAccessAsync(toolId, actionType, duration);
191
+ }
192
+ }
Services/UserService.cs ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using ToolHub.Models;
2
+ using System.Security.Cryptography;
3
+ using System.Text;
4
+
5
+ namespace ToolHub.Services;
6
+
7
+ public class UserService : IUserService
8
+ {
9
+ private readonly IFreeSql _freeSql;
10
+
11
+ public UserService(IFreeSql freeSql)
12
+ {
13
+ _freeSql = freeSql;
14
+ }
15
+
16
+ public async Task<User?> GetUserByIdAsync(int id)
17
+ {
18
+ return await _freeSql.Select<User>()
19
+ .Where(u => u.Id == id && u.IsActive)
20
+ .FirstAsync();
21
+ }
22
+
23
+ public async Task<User?> GetUserByEmailAsync(string email)
24
+ {
25
+ return await _freeSql.Select<User>()
26
+ .Where(u => u.Email == email && u.IsActive)
27
+ .FirstAsync();
28
+ }
29
+
30
+ public async Task<bool> CreateUserAsync(User user)
31
+ {
32
+ try
33
+ {
34
+ user.PasswordHash = HashPassword(user.PasswordHash);
35
+ await _freeSql.Insert(user).ExecuteAffrowsAsync();
36
+ return true;
37
+ }
38
+ catch
39
+ {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ public async Task<bool> UpdateUserAsync(User user)
45
+ {
46
+ try
47
+ {
48
+ user.UpdatedAt = DateTime.Now;
49
+ await _freeSql.Update<User>()
50
+ .SetSource(user)
51
+ .ExecuteAffrowsAsync();
52
+ return true;
53
+ }
54
+ catch
55
+ {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ public async Task<bool> VerifyPasswordAsync(string email, string password)
61
+ {
62
+ var user = await GetUserByEmailAsync(email);
63
+ if (user == null) return false;
64
+
65
+ return VerifyPassword(password, user.PasswordHash);
66
+ }
67
+
68
+ public async Task<List<Tool>> GetUserFavoritesAsync(int userId)
69
+ {
70
+ var favorites = await _freeSql.Select<UserFavorite>()
71
+ .Include(uf => uf.Tool.Category)
72
+ .Where(uf => uf.UserId == userId)
73
+ .OrderByDescending(uf => uf.CreatedAt)
74
+ .ToListAsync();
75
+
76
+ return favorites.Select(uf => uf.Tool).ToList();
77
+ }
78
+
79
+ public async Task<bool> AddFavoriteAsync(int userId, int toolId)
80
+ {
81
+ try
82
+ {
83
+ var existing = await _freeSql.Select<UserFavorite>()
84
+ .Where(uf => uf.UserId == userId && uf.ToolId == toolId)
85
+ .FirstAsync();
86
+
87
+ if (existing != null) return true;
88
+
89
+ await _freeSql.Insert(new UserFavorite
90
+ {
91
+ UserId = userId,
92
+ ToolId = toolId
93
+ }).ExecuteAffrowsAsync();
94
+
95
+ // 更新工具收藏数
96
+ await _freeSql.Update<Tool>()
97
+ .Where(t => t.Id == toolId)
98
+ .Set(t => t.FavoriteCount + 1)
99
+ .ExecuteAffrowsAsync();
100
+
101
+ return true;
102
+ }
103
+ catch
104
+ {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ public async Task<bool> RemoveFavoriteAsync(int userId, int toolId)
110
+ {
111
+ try
112
+ {
113
+ await _freeSql.Delete<UserFavorite>()
114
+ .Where(uf => uf.UserId == userId && uf.ToolId == toolId)
115
+ .ExecuteAffrowsAsync();
116
+
117
+ // 更新工具收藏数
118
+ await _freeSql.Update<Tool>()
119
+ .Where(t => t.Id == toolId)
120
+ .Set(t => t.FavoriteCount - 1)
121
+ .ExecuteAffrowsAsync();
122
+
123
+ return true;
124
+ }
125
+ catch
126
+ {
127
+ return false;
128
+ }
129
+ }
130
+
131
+ public async Task<bool> IsFavoriteAsync(int userId, int toolId)
132
+ {
133
+ var favorite = await _freeSql.Select<UserFavorite>()
134
+ .Where(uf => uf.UserId == userId && uf.ToolId == toolId)
135
+ .FirstAsync();
136
+
137
+ return favorite != null;
138
+ }
139
+
140
+ private string HashPassword(string password)
141
+ {
142
+ using var sha256 = SHA256.Create();
143
+ var hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password + "ToolHub_Salt"));
144
+ return Convert.ToBase64String(hashedBytes);
145
+ }
146
+
147
+ private bool VerifyPassword(string password, string hash)
148
+ {
149
+ var computedHash = HashPassword(password);
150
+ return computedHash == hash;
151
+ }
152
+ }
ToolHub.csproj ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <Project Sdk="Microsoft.NET.Sdk.Web">
2
+
3
+ <PropertyGroup>
4
+ <TargetFramework>net9.0</TargetFramework>
5
+ <Nullable>enable</Nullable>
6
+ <ImplicitUsings>enable</ImplicitUsings>
7
+ </PropertyGroup>
8
+
9
+ <ItemGroup>
10
+ <PackageReference Include="FreeSql" Version="3.5.207" />
11
+ <PackageReference Include="FreeSql.Provider.Sqlite" Version="3.5.207" />
12
+ <PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
13
+ </ItemGroup>
14
+
15
+ </Project>
ToolHub.csproj.user ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3
+ <PropertyGroup>
4
+ <ActiveDebugProfile>https</ActiveDebugProfile>
5
+ </PropertyGroup>
6
+ </Project>
Views/Admin/Categories.cshtml ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @model List<ToolHub.Models.Category>
2
+ @{
3
+ ViewData["Title"] = "工具分类管理";
4
+ Layout = "_AdminLayout";
5
+ var currentPage = ViewBag.CurrentPage as int? ?? 1;
6
+ var totalPages = ViewBag.TotalPages as int? ?? 1;
7
+ var totalCount = ViewBag.TotalCount as int? ?? 0;
8
+ var pageSize = ViewBag.PageSize as int? ?? 20;
9
+ }
10
+
11
+ <!-- 页面头部 -->
12
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
13
+ <div>
14
+ <h2 style="font-size: 1.5rem; font-weight: 700; margin: 0; color: var(--dark);">工具分类管理</h2>
15
+ <p style="color: var(--dark-2); margin: 0.5rem 0 0;">管理工具分类,组织和归类各种工具</p>
16
+ </div>
17
+ <button class="btn btn-primary" onclick="openCategoryModal()">
18
+ <i class="fas fa-plus"></i>
19
+ 添加分类
20
+ </button>
21
+ </div>
22
+
23
+ <!-- 分类列表 -->
24
+ <div class="data-table">
25
+ <div style="padding: 1rem 1.5rem; border-bottom: 1px solid var(--light-2); display: flex; justify-content: space-between; align-items: center;">
26
+ <span style="font-weight: 600;">分类列表 (@totalCount)</span>
27
+ </div>
28
+
29
+ <table>
30
+ <thead>
31
+ <tr>
32
+ <th>分类名称</th>
33
+ <th>描述</th>
34
+ <th>图标</th>
35
+ <th>颜色</th>
36
+ <th>排序</th>
37
+ <th>工具数量</th>
38
+ <th>状态</th>
39
+ <th>操作</th>
40
+ </tr>
41
+ </thead>
42
+ <tbody>
43
+ @foreach (var category in Model)
44
+ {
45
+ <tr>
46
+ <td>
47
+ <div style="display: flex; align-items: center;">
48
+ @if (!string.IsNullOrEmpty(category.Icon))
49
+ {
50
+ <i class="@category.Icon" style="margin-right: 0.75rem; color: @(category.Color ?? "var(--primary)"); font-size: 1.25rem;"></i>
51
+ }
52
+ <strong>@category.Name</strong>
53
+ </div>
54
+ </td>
55
+ <td style="max-width: 200px;">
56
+ <span style="color: var(--dark-2);">@(category.Description ?? "暂无描述")</span>
57
+ </td>
58
+ <td>
59
+ <code style="background: var(--light-1); padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem;">
60
+ @(category.Icon ?? "无")
61
+ </code>
62
+ </td>
63
+ <td>
64
+ @if (!string.IsNullOrEmpty(category.Color))
65
+ {
66
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
67
+ <div style="width: 20px; height: 20px; background: @category.Color; border-radius: 4px; border: 1px solid var(--light-2);"></div>
68
+ <code style="font-size: 0.75rem;">@category.Color</code>
69
+ </div>
70
+ }
71
+ else
72
+ {
73
+ <span style="color: var(--dark-2);">默认</span>
74
+ }
75
+ </td>
76
+ <td>
77
+ <span class="badge badge-secondary">@category.SortOrder</span>
78
+ </td>
79
+ <td>
80
+ <span class="badge badge-primary">@category.Tools?.Count</span>
81
+ </td>
82
+ <td>
83
+ @if (category.IsActive)
84
+ {
85
+ <span class="badge badge-success">启用</span>
86
+ }
87
+ else
88
+ {
89
+ <span class="badge badge-secondary">禁用</span>
90
+ }
91
+ </td>
92
+ <td>
93
+ <div style="display: flex; gap: 0.5rem;">
94
+ <button class="btn btn-outline btn-sm" onclick="editCategory(@category.Id, '@category.Name', '@category.Description', '@category.Icon', '@category.Color', @category.SortOrder)">
95
+ <i class="fas fa-edit"></i>
96
+ </button>
97
+ <button class="btn btn-outline btn-sm" onclick="toggleCategoryStatus(@category.Id, @category.IsActive.ToString().ToLower())" style="color: @(category.IsActive ? "var(--warning)" : "var(--success)");">
98
+ <i class="fas fa-@(category.IsActive ? "pause" : "play")"></i>
99
+ </button>
100
+ <button class="btn btn-outline btn-sm" onclick="deleteCategory(@category.Id, '@category.Name')" style="color: var(--danger);">
101
+ <i class="fas fa-trash"></i>
102
+ </button>
103
+ </div>
104
+ </td>
105
+ </tr>
106
+ }
107
+ </tbody>
108
+ </table>
109
+
110
+ @if (!Model.Any())
111
+ {
112
+ <div style="padding: 3rem; text-align: center; color: var(--dark-2);">
113
+ <i class="fas fa-tags" style="font-size: 3rem; margin-bottom: 1rem; display: block; opacity: 0.3;"></i>
114
+ <h3 style="margin-bottom: 0.5rem;">暂无分类</h3>
115
+ <p style="margin-bottom: 1.5rem;">还没有创建任何工具分类</p>
116
+ <button class="btn btn-primary" onclick="openCategoryModal()">
117
+ <i class="fas fa-plus"></i>
118
+ 创建第一个分类
119
+ </button>
120
+ </div>
121
+ }
122
+
123
+ <!-- 分页控件 -->
124
+ @if (totalPages > 1)
125
+ {
126
+ <div style="padding: 1.5rem; border-top: 1px solid var(--light-2); display: flex; justify-content: center; align-items: center; gap: 0.5rem;">
127
+ <button class="btn btn-outline btn-sm" onclick="changePage(1)" @(currentPage == 1 ? "disabled" : "")>
128
+ <i class="fas fa-angle-double-left"></i>
129
+ </button>
130
+ <button class="btn btn-outline btn-sm" onclick="changePage(@(currentPage - 1))" @(currentPage == 1 ? "disabled" : "")>
131
+ <i class="fas fa-angle-left"></i>
132
+ </button>
133
+
134
+ @{
135
+ var startPage = Math.Max(1, currentPage - 2);
136
+ var endPage = Math.Min(totalPages, currentPage + 2);
137
+ }
138
+
139
+ @for (int i = startPage; i <= endPage; i++)
140
+ {
141
+ <button class="btn @(i == currentPage ? "btn-primary" : "btn-outline") btn-sm" onclick="changePage(@i)">
142
+ @i
143
+ </button>
144
+ }
145
+
146
+ <button class="btn btn-outline btn-sm" onclick="changePage(@(currentPage + 1))" @(currentPage == totalPages ? "disabled" : "")>
147
+ <i class="fas fa-angle-right"></i>
148
+ </button>
149
+ <button class="btn btn-outline btn-sm" onclick="changePage(@totalPages)" @(currentPage == totalPages ? "disabled" : "")>
150
+ <i class="fas fa-angle-double-right"></i>
151
+ </button>
152
+ </div>
153
+ }
154
+ </div>
155
+
156
+ <!-- 分类模态框 -->
157
+ <div id="categoryModal" class="modal" style="display: none;">
158
+ <div class="modal-backdrop" onclick="closeCategoryModal()"></div>
159
+ <div class="modal-content" style="max-width: 500px;">
160
+ <div class="modal-header">
161
+ <h3 id="modalTitle">添加分类</h3>
162
+ <button onclick="closeCategoryModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer;">×</button>
163
+ </div>
164
+
165
+ <form id="categoryForm" onsubmit="submitCategory(event)">
166
+ <input type="hidden" id="categoryId" name="id" value="0" />
167
+
168
+ <div class="form-group" style="margin-bottom: 1.5rem;">
169
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">分类名称 *</label>
170
+ <input type="text" id="categoryName" name="name" required
171
+ style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);"
172
+ placeholder="请输入分类名称" />
173
+ </div>
174
+
175
+ <div class="form-group" style="margin-bottom: 1.5rem;">
176
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">描述</label>
177
+ <textarea id="categoryDescription" name="description" rows="3"
178
+ style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius); resize: vertical;"
179
+ placeholder="请输入分类描述"></textarea>
180
+ </div>
181
+
182
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem;">
183
+ <div class="form-group">
184
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">图标类名</label>
185
+ <input type="text" id="categoryIcon" name="icon"
186
+ style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);"
187
+ placeholder="如: fas fa-file-pdf" />
188
+ <small style="color: var(--dark-2); font-size: 0.75rem;">使用 Font Awesome 图标类名</small>
189
+ </div>
190
+
191
+ <div class="form-group">
192
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">颜色</label>
193
+ <input type="color" id="categoryColor" name="color" value="#165DFF"
194
+ style="width: 100%; padding: 0.5rem; border: 1px solid var(--light-2); border-radius: var(--border-radius); height: 3rem;" />
195
+ </div>
196
+ </div>
197
+
198
+ <div class="form-group" style="margin-bottom: 1.5rem;">
199
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">排序顺序</label>
200
+ <input type="number" id="categorySort" name="sortOrder" min="0" value="0"
201
+ style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);"
202
+ placeholder="数字越小排序越靠前" />
203
+ </div>
204
+
205
+ <div class="modal-footer">
206
+ <button type="button" class="btn btn-outline" onclick="closeCategoryModal()">取消</button>
207
+ <button type="submit" class="btn btn-primary">
208
+ <span id="submitText">保存</span>
209
+ </button>
210
+ </div>
211
+ </form>
212
+ </div>
213
+ </div>
214
+
215
+ <style>
216
+ .modal {
217
+ position: fixed;
218
+ top: 0;
219
+ left: 0;
220
+ right: 0;
221
+ bottom: 0;
222
+ z-index: 10000;
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ }
227
+
228
+ .modal-backdrop {
229
+ position: absolute;
230
+ top: 0;
231
+ left: 0;
232
+ right: 0;
233
+ bottom: 0;
234
+ background: rgba(0, 0, 0, 0.5);
235
+ }
236
+
237
+ .modal-content {
238
+ background: white;
239
+ border-radius: var(--border-radius);
240
+ position: relative;
241
+ z-index: 2;
242
+ max-height: 90vh;
243
+ overflow-y: auto;
244
+ width: 90%;
245
+ max-width: 600px;
246
+ }
247
+
248
+ .modal-header {
249
+ padding: 1.5rem;
250
+ border-bottom: 1px solid var(--light-2);
251
+ display: flex;
252
+ justify-content: space-between;
253
+ align-items: center;
254
+ }
255
+
256
+ .modal-header h3 {
257
+ margin: 0;
258
+ font-size: 1.25rem;
259
+ font-weight: 600;
260
+ }
261
+
262
+ .modal-footer {
263
+ padding: 1.5rem;
264
+ border-top: 1px solid var(--light-2);
265
+ display: flex;
266
+ justify-content: flex-end;
267
+ gap: 1rem;
268
+ }
269
+
270
+ .form-group input:focus,
271
+ .form-group textarea:focus {
272
+ border-color: var(--primary);
273
+ outline: none;
274
+ box-shadow: 0 0 0 3px rgba(22, 93, 255, 0.1);
275
+ }
276
+ </style>
277
+
278
+ <script>
279
+ let isEditing = false;
280
+
281
+ function changePage(page) {
282
+ window.location.href = `@Url.Action("Index", "Category")?page=${page}`;
283
+ }
284
+
285
+ function openCategoryModal(editMode = false) {
286
+ document.getElementById('categoryModal').style.display = 'flex';
287
+ document.getElementById('modalTitle').textContent = editMode ? '编辑分类' : '添加分类';
288
+ document.getElementById('submitText').textContent = editMode ? '更新' : '保存';
289
+ isEditing = editMode;
290
+
291
+ if (!editMode) {
292
+ document.getElementById('categoryForm').reset();
293
+ document.getElementById('categoryId').value = '0';
294
+ document.getElementById('categoryColor').value = '#165DFF';
295
+ }
296
+ }
297
+
298
+ function closeCategoryModal() {
299
+ document.getElementById('categoryModal').style.display = 'none';
300
+ }
301
+
302
+ function editCategory(id, name, description, icon, color, sortOrder) {
303
+ openCategoryModal(true);
304
+ document.getElementById('categoryId').value = id;
305
+ document.getElementById('categoryName').value = name;
306
+ document.getElementById('categoryDescription').value = description || '';
307
+ document.getElementById('categoryIcon').value = icon || '';
308
+ document.getElementById('categoryColor').value = color || '#165DFF';
309
+ document.getElementById('categorySort').value = sortOrder;
310
+ }
311
+
312
+ async function submitCategory(event) {
313
+ event.preventDefault();
314
+
315
+ const formData = new FormData(event.target);
316
+ const data = Object.fromEntries(formData.entries());
317
+
318
+ try {
319
+ const response = await fetch('/Category/Save', {
320
+ method: 'POST',
321
+ headers: {
322
+ 'Content-Type': 'application/json',
323
+ },
324
+ body: JSON.stringify(data)
325
+ });
326
+
327
+ if (response.ok) {
328
+ toolHub.showToast(isEditing ? '分类更新成功' : '分类添加成功', 'success');
329
+ closeCategoryModal();
330
+ setTimeout(() => location.reload(), 1000);
331
+ } else {
332
+ toolHub.showToast('操作失败,请重试', 'error');
333
+ }
334
+ } catch (error) {
335
+ console.error('提交失败:', error);
336
+ toolHub.showToast('操作失败,请重试', 'error');
337
+ }
338
+ }
339
+
340
+ async function toggleCategoryStatus(id, currentStatus) {
341
+ try {
342
+ const response = await fetch('/Category/ToggleStatus', {
343
+ method: 'POST',
344
+ headers: {
345
+ 'Content-Type': 'application/json',
346
+ },
347
+ body: JSON.stringify({ id: id, isActive: !currentStatus })
348
+ });
349
+
350
+ if (response.ok) {
351
+ toolHub.showToast('状态更新成功', 'success');
352
+ setTimeout(() => location.reload(), 1000);
353
+ } else {
354
+ toolHub.showToast('操作失败,请重试', 'error');
355
+ }
356
+ } catch (error) {
357
+ console.error('操作失败:', error);
358
+ toolHub.showToast('操作失败,请重试', 'error');
359
+ }
360
+ }
361
+
362
+ async function deleteCategory(id, name) {
363
+ if (!confirm(`确定要删除分类"${name}"吗?\n注意:删除分类会影响该分类下的所有工具。`)) {
364
+ return;
365
+ }
366
+
367
+ try {
368
+ const response = await fetch('/Category/Delete', {
369
+ method: 'POST',
370
+ headers: {
371
+ 'Content-Type': 'application/json',
372
+ },
373
+ body: JSON.stringify({ id: id })
374
+ });
375
+
376
+ if (response.ok) {
377
+ toolHub.showToast('分类删除成功', 'success');
378
+ setTimeout(() => location.reload(), 1000);
379
+ } else {
380
+ toolHub.showToast('删除失败,可能该分类下还有工具', 'error');
381
+ }
382
+ } catch (error) {
383
+ console.error('删除失败:', error);
384
+ toolHub.showToast('删除失败,请重试', 'error');
385
+ }
386
+ }
387
+ </script>
Views/Admin/Index.cshtml ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @{
2
+ ViewData["Title"] = "仪表板";
3
+ Layout = "_AdminLayout";
4
+ }
5
+
6
+ <!-- 统计卡片 -->
7
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 mb-6">
8
+ <div class="stats-card">
9
+ <div class="stats-number" style="color: var(--primary);">
10
+ @(ViewBag.TotalTools ?? 0)
11
+ </div>
12
+ <div class="stats-label">
13
+ <i class="fas fa-tools" style="margin-right: 0.5rem;"></i>
14
+ 工具总数
15
+ </div>
16
+ </div>
17
+
18
+ <div class="stats-card">
19
+ <div class="stats-number" style="color: var(--secondary);">
20
+ @(ViewBag.TotalCategories ?? 0)
21
+ </div>
22
+ <div class="stats-label">
23
+ <i class="fas fa-tags" style="margin-right: 0.5rem;"></i>
24
+ 分类总数
25
+ </div>
26
+ </div>
27
+
28
+ <div class="stats-card">
29
+ <div class="stats-number" style="color: var(--success);">
30
+ @(ViewBag.TotalUsers ?? 0)
31
+ </div>
32
+ <div class="stats-label">
33
+ <i class="fas fa-users" style="margin-right: 0.5rem;"></i>
34
+ 用户总数
35
+ </div>
36
+ </div>
37
+
38
+ <div class="stats-card">
39
+ <div class="stats-number" style="color: var(--warning);">
40
+ @{
41
+ var totalViews = ViewBag.TotalViews as long? ?? 0;
42
+ var viewsDisplay = totalViews > 1000000 ? $"{totalViews / 1000000:F1}M" :
43
+ totalViews > 1000 ? $"{totalViews / 1000:F0}K" :
44
+ totalViews.ToString();
45
+ }
46
+ @viewsDisplay
47
+ </div>
48
+ <div class="stats-label">
49
+ <i class="fas fa-eye" style="margin-right: 0.5rem;"></i>
50
+ 总访问量
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="grid grid-cols-1 lg:grid-cols-2">
56
+ <!-- 最新工具 -->
57
+ <div class="data-table" style="margin-bottom: 2rem;">
58
+ <div style="padding: 1.5rem; border-bottom: 1px solid var(--light-2);">
59
+ <h3 style="margin: 0; font-size: 1.1rem; font-weight: 600; color: var(--dark);">
60
+ <i class="fas fa-clock" style="margin-right: 0.5rem; color: var(--primary);"></i>
61
+ 最新工具
62
+ </h3>
63
+ </div>
64
+
65
+ @if (ViewBag.RecentTools is List<ToolHub.Models.Tool> recentTools && recentTools.Any())
66
+ {
67
+ <table>
68
+ <thead>
69
+ <tr>
70
+ <th>工具名称</th>
71
+ <th>分类</th>
72
+ <th>状态</th>
73
+ <th>创建时间</th>
74
+ </tr>
75
+ </thead>
76
+ <tbody>
77
+ @foreach (var tool in recentTools)
78
+ {
79
+ <tr>
80
+ <td>
81
+ <div style="display: flex; align-items: center;">
82
+ @if (!string.IsNullOrEmpty(tool.Icon))
83
+ {
84
+ <i class="@tool.Icon" style="margin-right: 0.75rem; color: var(--primary);"></i>
85
+ }
86
+ <div>
87
+ <strong>@tool.Name</strong>
88
+ @if (!string.IsNullOrEmpty(tool.Description) && tool.Description.Length > 30)
89
+ {
90
+ <br><small style="color: var(--dark-2);">@(tool.Description.Substring(0, 30))...</small>
91
+ }
92
+ </div>
93
+ </div>
94
+ </td>
95
+ <td>
96
+ <span class="badge badge-primary">@tool.Category?.Name</span>
97
+ </td>
98
+ <td>
99
+ @if (tool.IsHot)
100
+ {
101
+ <span class="badge badge-danger">热门</span>
102
+ }
103
+ @if (tool.IsNew)
104
+ {
105
+ <span class="badge badge-warning">新品</span>
106
+ }
107
+ @if (tool.IsRecommended)
108
+ {
109
+ <span class="badge badge-success">推荐</span>
110
+ }
111
+ @if (!tool.IsHot && !tool.IsNew && !tool.IsRecommended)
112
+ {
113
+ <span class="badge badge-secondary">普通</span>
114
+ }
115
+ </td>
116
+ <td style="color: var(--dark-2); font-size: 0.875rem;">
117
+ @tool.CreatedAt.ToString("MM-dd HH:mm")
118
+ </td>
119
+ </tr>
120
+ }
121
+ </tbody>
122
+ </table>
123
+ }
124
+ else
125
+ {
126
+ <div style="padding: 2rem; text-align: center; color: var(--dark-2);">
127
+ <i class="fas fa-inbox" style="font-size: 2rem; margin-bottom: 1rem; display: block;"></i>
128
+ 暂无工具数据
129
+ </div>
130
+ }
131
+
132
+ <div style="padding: 1rem; border-top: 1px solid var(--light-2); text-align: center;">
133
+ <a href="@Url.Action("Index", "Tool")" class="btn btn-outline btn-sm">
134
+ 查看所有工具
135
+ </a>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- 快速操作 -->
140
+ <div class="data-table">
141
+ <div style="padding: 1.5rem; border-bottom: 1px solid var(--light-2);">
142
+ <h3 style="margin: 0; font-size: 1.1rem; font-weight: 600; color: var(--dark);">
143
+ <i class="fas fa-bolt" style="margin-right: 0.5rem; color: var(--warning);"></i>
144
+ 快速操作
145
+ </h3>
146
+ </div>
147
+
148
+ <div style="padding: 1.5rem;">
149
+ <div style="display: grid; gap: 1rem;">
150
+ <a href="@Url.Action("Index", "Tool")"
151
+ style="display: flex; align-items: center; padding: 1rem; background: var(--light-1); border-radius: var(--border-radius); text-decoration: none; color: var(--dark); transition: var(--transition);"
152
+ onmouseover="this.style.background='var(--primary)'; this.style.color='white';"
153
+ onmouseout="this.style.background='var(--light-1)'; this.style.color='var(--dark)';">
154
+ <i class="fas fa-plus-circle" style="font-size: 1.5rem; margin-right: 1rem; color: var(--primary);"></i>
155
+ <div>
156
+ <strong>工具管理</strong>
157
+ <br><small style="opacity: 0.8;">管理平台上的工具</small>
158
+ </div>
159
+ </a>
160
+
161
+ <a href="@Url.Action("Index", "Category")"
162
+ style="display: flex; align-items: center; padding: 1rem; background: var(--light-1); border-radius: var(--border-radius); text-decoration: none; color: var(--dark); transition: var(--transition);"
163
+ onmouseover="this.style.background='var(--secondary)'; this.style.color='white';"
164
+ onmouseout="this.style.background='var(--light-1)'; this.style.color='var(--dark)';">
165
+ <i class="fas fa-tag" style="font-size: 1.5rem; margin-right: 1rem; color: var(--secondary);"></i>
166
+ <div>
167
+ <strong>分类管理</strong>
168
+ <br><small style="opacity: 0.8;">管理工具分类</small>
169
+ </div>
170
+ </a>
171
+
172
+ <a href="@Url.Action("Index", "Tag")"
173
+ style="display: flex; align-items: center; padding: 1rem; background: var(--light-1); border-radius: var(--border-radius); text-decoration: none; color: var(--dark); transition: var(--transition);"
174
+ onmouseover="this.style.background='var(--accent)'; this.style.color='white';"
175
+ onmouseout="this.style.background='var(--light-1)'; this.style.color='var(--dark)';">
176
+ <i class="fas fa-tags" style="font-size: 1.5rem; margin-right: 1rem; color: var(--accent);"></i>
177
+ <div>
178
+ <strong>标签管理</strong>
179
+ <br><small style="opacity: 0.8;">管理工具标签</small>
180
+ </div>
181
+ </a>
182
+
183
+ <a href="@Url.Action("Users", "Admin")"
184
+ style="display: flex; align-items: center; padding: 1rem; background: var(--light-1); border-radius: var(--border-radius); text-decoration: none; color: var(--dark); transition: var(--transition);"
185
+ onmouseover="this.style.background='var(--success)'; this.style.color='white';"
186
+ onmouseout="this.style.background='var(--light-1)'; this.style.color='var(--dark)';">
187
+ <i class="fas fa-user-cog" style="font-size: 1.5rem; margin-right: 1rem; color: var(--success);"></i>
188
+ <div>
189
+ <strong>用户管理</strong>
190
+ <br><small style="opacity: 0.8;">管理注册用户</small>
191
+ </div>
192
+ </a>
193
+
194
+ <a href="@Url.Action("Index", "Home")" target="_blank"
195
+ style="display: flex; align-items: center; padding: 1rem; background: var(--light-1); border-radius: var(--border-radius); text-decoration: none; color: var(--dark); transition: var(--transition);"
196
+ onmouseover="this.style.background='var(--accent)'; this.style.color='white';"
197
+ onmouseout="this.style.background='var(--light-1)'; this.style.color='var(--dark)';">
198
+ <i class="fas fa-external-link-alt" style="font-size: 1.5rem; margin-right: 1rem; color: var(--accent);"></i>
199
+ <div>
200
+ <strong>访问网站</strong>
201
+ <br><small style="opacity: 0.8;">查看前台网站</small>
202
+ </div>
203
+ </a>
204
+
205
+ <button onclick="initImageCompressor()"
206
+ style="display: flex; align-items: center; padding: 1rem; background: var(--light-1); border-radius: var(--border-radius); border: none; color: var(--dark); transition: var(--transition); cursor: pointer; width: 100%;"
207
+ onmouseover="this.style.background='var(--warning)'; this.style.color='white';"
208
+ onmouseout="this.style.background='var(--light-1)'; this.style.color='var(--dark)';">
209
+ <i class="fas fa-compress-alt" style="font-size: 1.5rem; margin-right: 1rem; color: var(--warning);"></i>
210
+ <div style="text-align: left;">
211
+ <strong>初始化图片压缩工具</strong>
212
+ <br><small style="opacity: 0.8;">添加图片压缩工具到数据库</small>
213
+ </div>
214
+ </button>
215
+ </div>
216
+ </div>
217
+ </div>
218
+ </div>
219
+
220
+ <!-- 系统信息 -->
221
+ <div class="data-table mt-6">
222
+ <div style="padding: 1.5rem; border-bottom: 1px solid var(--light-2);">
223
+ <h3 style="margin: 0; font-size: 1.1rem; font-weight: 600; color: var(--dark);">
224
+ <i class="fas fa-info-circle" style="margin-right: 0.5rem; color: var(--primary);"></i>
225
+ 系统信息
226
+ </h3>
227
+ </div>
228
+
229
+ <div style="padding: 1.5rem;">
230
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
231
+ <div>
232
+ <strong>系统版本</strong>
233
+ <p style="color: var(--dark-2); margin: 0.25rem 0 0;">ToolHub v1.0.0</p>
234
+ </div>
235
+ <div>
236
+ <strong>运行环境</strong>
237
+ <p style="color: var(--dark-2); margin: 0.25rem 0 0;">.NET 9.0</p>
238
+ </div>
239
+ <div>
240
+ <strong>数据库</strong>
241
+ <p style="color: var(--dark-2); margin: 0.25rem 0 0;">SQLite + FreeSql</p>
242
+ </div>
243
+ <div>
244
+ <strong>服务器时间</strong>
245
+ <p style="color: var(--dark-2); margin: 0.25rem 0 0;">@DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")</p>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ </div>
250
+
251
+ <script>
252
+ async function initImageCompressor() {
253
+ try {
254
+ const button = event.target.closest('button');
255
+ const originalContent = button.innerHTML;
256
+
257
+ // 显示加载状态
258
+ button.innerHTML = `
259
+ <i class="fas fa-spinner fa-spin" style="font-size: 1.5rem; margin-right: 1rem; color: var(--warning);"></i>
260
+ <div style="text-align: left;">
261
+ <strong>初始化中...</strong>
262
+ <br><small style="opacity: 0.8;">请稍候</small>
263
+ </div>
264
+ `;
265
+ button.disabled = true;
266
+
267
+ const response = await fetch('/Tool/InitImageCompressor');
268
+ const result = await response.json();
269
+
270
+ if (result.success) {
271
+ toolHub.showToast('图片压缩工具初始化成功!', 'success');
272
+
273
+ // 恢复按钮状态
274
+ button.innerHTML = `
275
+ <i class="fas fa-check-circle" style="font-size: 1.5rem; margin-right: 1rem; color: var(--success);"></i>
276
+ <div style="text-align: left;">
277
+ <strong>初始化完成</strong>
278
+ <br><small style="opacity: 0.8;">图片压缩工具已添加</small>
279
+ </div>
280
+ `;
281
+
282
+ // 3秒后恢复原状态
283
+ setTimeout(() => {
284
+ button.innerHTML = originalContent;
285
+ button.disabled = false;
286
+ }, 3000);
287
+ } else {
288
+ toolHub.showToast(result.message || '初始化失败', 'error');
289
+ button.innerHTML = originalContent;
290
+ button.disabled = false;
291
+ }
292
+ } catch (error) {
293
+ console.error('初始化失败:', error);
294
+ toolHub.showToast('初始化失败,请重试', 'error');
295
+
296
+ // 恢复按钮状态
297
+ const button = event.target.closest('button');
298
+ button.innerHTML = originalContent;
299
+ button.disabled = false;
300
+ }
301
+ }
302
+ </script>
Views/Admin/Login.cshtml ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @{
2
+ ViewData["Title"] = "管理员登录";
3
+ Layout = null;
4
+ }
5
+
6
+ <!DOCTYPE html>
7
+ <html lang="zh-CN">
8
+ <head>
9
+ <meta charset="utf-8" />
10
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
11
+ <title>@ViewData["Title"] - ToolHub</title>
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
13
+ <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet" />
14
+ <link rel="stylesheet" href="~/css/toolhub.css" asp-append-version="true" />
15
+ </head>
16
+ <body style="background: linear-gradient(135deg, var(--primary), var(--secondary)); min-height: 100vh; display: flex; align-items: center; justify-content: center;">
17
+ <div class="container" style="max-width: 400px;">
18
+ <div class="card" style="padding: 2rem; text-align: center;">
19
+ <!-- Logo -->
20
+ <div style="margin-bottom: 2rem;">
21
+ <div class="brand-icon" style="margin: 0 auto 1rem;">
22
+ <i class="fas fa-wrench"></i>
23
+ </div>
24
+ <h1 style="font-size: 1.5rem; font-weight: 700; margin: 0;">
25
+ <span class="text-gradient">ToolHub</span> 管理后台
26
+ </h1>
27
+ <p style="color: var(--dark-2); margin-top: 0.5rem;">请登录您的管理员账户</p>
28
+ </div>
29
+
30
+ <!-- 错误消息 -->
31
+ @if (ViewBag.Error != null)
32
+ {
33
+ <div style="background: rgba(255, 77, 79, 0.1); color: var(--danger); padding: 0.75rem; border-radius: var(--border-radius); margin-bottom: 1.5rem; border-left: 4px solid var(--danger);">
34
+ <i class="fas fa-exclamation-circle" style="margin-right: 0.5rem;"></i>
35
+ @ViewBag.Error
36
+ </div>
37
+ }
38
+
39
+ <!-- 登录表单 -->
40
+ <form method="post" action="@Url.Action("Login", "Admin")">
41
+ <div style="margin-bottom: 1.25rem;">
42
+ <input type="email"
43
+ name="email"
44
+ placeholder="管理员邮箱"
45
+ required
46
+ style="width: 100%; padding: 0.875rem; border: 1px solid var(--light-2); border-radius: var(--border-radius); font-size: 0.875rem; transition: var(--transition);"
47
+ onfocus="this.style.borderColor='var(--primary)'; this.style.boxShadow='0 0 0 3px rgba(22, 93, 255, 0.1)'"
48
+ onblur="this.style.borderColor='var(--light-2)'; this.style.boxShadow='none'" />
49
+ </div>
50
+
51
+ <div style="margin-bottom: 1.25rem;">
52
+ <input type="password"
53
+ name="password"
54
+ placeholder="密码"
55
+ required
56
+ style="width: 100%; padding: 0.875rem; border: 1px solid var(--light-2); border-radius: var(--border-radius); font-size: 0.875rem; transition: var(--transition);"
57
+ onfocus="this.style.borderColor='var(--primary)'; this.style.boxShadow='0 0 0 3px rgba(22, 93, 255, 0.1)'"
58
+ onblur="this.style.borderColor='var(--light-2)'; this.style.boxShadow='none'" />
59
+ </div>
60
+
61
+ <div style="margin-bottom: 1.5rem; display: flex; align-items: center; gap: 0.5rem;">
62
+ <input type="checkbox" name="rememberMe" id="rememberMe" value="true" />
63
+ <label for="rememberMe" style="font-size: 0.875rem; color: var(--dark-2); cursor: pointer;">
64
+ 记住我
65
+ </label>
66
+ </div>
67
+
68
+ <button type="submit" class="btn btn-primary" style="width: 100%; padding: 0.875rem; font-weight: 600;">
69
+ <i class="fas fa-sign-in-alt" style="margin-right: 0.5rem;"></i>
70
+ 登录
71
+ </button>
72
+ </form>
73
+
74
+ <!-- 说明文字 -->
75
+ <div style="margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--light-2);">
76
+ <p style="font-size: 0.75rem; color: var(--dark-2); margin: 0;">
77
+ <i class="fas fa-shield-alt" style="margin-right: 0.25rem;"></i>
78
+ 默认管理员账户:admin@toolhub.com / admin123
79
+ </p>
80
+ <p style="font-size: 0.75rem; color: var(--dark-2); margin: 0.5rem 0 0;">
81
+ <a href="@Url.Action("Index", "Home")" style="color: var(--primary); text-decoration: none;">
82
+ <i class="fas fa-arrow-left" style="margin-right: 0.25rem;"></i>
83
+ 返回首页
84
+ </a>
85
+ </p>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <script>
91
+ // 简单的表单验证
92
+ document.querySelector('form').addEventListener('submit', function(e) {
93
+ const email = this.email.value.trim();
94
+ const password = this.password.value.trim();
95
+
96
+ if (!email || !password) {
97
+ e.preventDefault();
98
+ alert('请填写完整的登录信息');
99
+ }
100
+ });
101
+ </script>
102
+ </body>
103
+ </html>
Views/Admin/Tags.cshtml ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @model List<ToolHub.Models.Tag>
2
+ @{
3
+ ViewData["Title"] = "标签管理";
4
+ Layout = "_AdminLayout";
5
+ var currentPage = ViewBag.CurrentPage as int? ?? 1;
6
+ var totalPages = ViewBag.TotalPages as int? ?? 1;
7
+ var totalCount = ViewBag.TotalCount as int? ?? 0;
8
+ var pageSize = ViewBag.PageSize as int? ?? 20;
9
+ }
10
+
11
+ <!-- 页面头部 -->
12
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
13
+ <div>
14
+ <h2 style="font-size: 1.5rem; font-weight: 700; margin: 0; color: var(--dark);">标签管理</h2>
15
+ <p style="color: var(--dark-2); margin: 0.5rem 0 0;">管理工具标签,为工具添加分类标识</p>
16
+ </div>
17
+ <button class="btn btn-primary" onclick="openTagModal()">
18
+ <i class="fas fa-plus"></i>
19
+ 添加标签
20
+ </button>
21
+ </div>
22
+
23
+ <!-- 标签列表 -->
24
+ <div class="data-table">
25
+ <div style="padding: 1rem 1.5rem; border-bottom: 1px solid var(--light-2); display: flex; justify-content: space-between; align-items: center;">
26
+ <span style="font-weight: 600;">标签列表 (@totalCount)</span>
27
+ </div>
28
+
29
+ <table>
30
+ <thead>
31
+ <tr>
32
+ <th>标签名称</th>
33
+ <th>颜色</th>
34
+ <th>使用次数</th>
35
+ <th>创建时间</th>
36
+ <th>状态</th>
37
+ <th>操作</th>
38
+ </tr>
39
+ </thead>
40
+ <tbody>
41
+ @foreach (var tag in Model)
42
+ {
43
+ <tr>
44
+ <td>
45
+ <div style="display: flex; align-items: center;">
46
+ <span class="badge" style="background: @(tag.Color ?? "var(--primary)"); color: white; margin-right: 0.75rem;">
47
+ @tag.Name
48
+ </span>
49
+ <strong>@tag.Name</strong>
50
+ </div>
51
+ </td>
52
+ <td>
53
+ @if (!string.IsNullOrEmpty(tag.Color))
54
+ {
55
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
56
+ <div style="width: 20px; height: 20px; background: @tag.Color; border-radius: 4px; border: 1px solid var(--light-2);"></div>
57
+ <code style="font-size: 0.75rem;">@tag.Color</code>
58
+ </div>
59
+ }
60
+ else
61
+ {
62
+ <span style="color: var(--dark-2);">默认</span>
63
+ }
64
+ </td>
65
+ <td>
66
+ <span class="badge badge-primary">@tag.ToolTags?.Count</span>
67
+ </td>
68
+ <td style="color: var(--dark-2); font-size: 0.875rem;">
69
+ @tag.CreatedAt.ToString("yyyy-MM-dd")
70
+ </td>
71
+ <td>
72
+ @if (tag.IsActive)
73
+ {
74
+ <span class="badge badge-success">启用</span>
75
+ }
76
+ else
77
+ {
78
+ <span class="badge badge-secondary">禁用</span>
79
+ }
80
+ </td>
81
+ <td>
82
+ <div style="display: flex; gap: 0.5rem;">
83
+ <button class="btn btn-outline btn-sm" onclick="editTag(@tag.Id, '@tag.Name', '@tag.Color')">
84
+ <i class="fas fa-edit"></i>
85
+ </button>
86
+ <button class="btn btn-outline btn-sm" onclick="toggleTagStatus(@tag.Id, @tag.IsActive.ToString().ToLower())" style="color: @(tag.IsActive ? "var(--warning)" : "var(--success)");">
87
+ <i class="fas fa-@(tag.IsActive ? "pause" : "play")"></i>
88
+ </button>
89
+ <button class="btn btn-outline btn-sm" onclick="deleteTag(@tag.Id, '@tag.Name')" style="color: var(--danger);">
90
+ <i class="fas fa-trash"></i>
91
+ </button>
92
+ </div>
93
+ </td>
94
+ </tr>
95
+ }
96
+ </tbody>
97
+ </table>
98
+
99
+ @if (!Model.Any())
100
+ {
101
+ <div style="padding: 3rem; text-align: center; color: var(--dark-2);">
102
+ <i class="fas fa-tags" style="font-size: 3rem; margin-bottom: 1rem; display: block; opacity: 0.3;"></i>
103
+ <h3 style="margin-bottom: 0.5rem;">暂无标签</h3>
104
+ <p style="margin-bottom: 1.5rem;">还没有创建任何标签</p>
105
+ <button class="btn btn-primary" onclick="openTagModal()">
106
+ <i class="fas fa-plus"></i>
107
+ 创建第一个标签
108
+ </button>
109
+ </div>
110
+ }
111
+
112
+ <!-- 分页控件 -->
113
+ @if (totalPages > 1)
114
+ {
115
+ <div style="padding: 1.5rem; border-top: 1px solid var(--light-2); display: flex; justify-content: center; align-items: center; gap: 0.5rem;">
116
+ <button class="btn btn-outline btn-sm" onclick="changePage(1)" @(currentPage == 1 ? "disabled" : "")>
117
+ <i class="fas fa-angle-double-left"></i>
118
+ </button>
119
+ <button class="btn btn-outline btn-sm" onclick="changePage(@(currentPage - 1))" @(currentPage == 1 ? "disabled" : "")>
120
+ <i class="fas fa-angle-left"></i>
121
+ </button>
122
+
123
+ @{
124
+ var startPage = Math.Max(1, currentPage - 2);
125
+ var endPage = Math.Min(totalPages, currentPage + 2);
126
+ }
127
+
128
+ @for (int i = startPage; i <= endPage; i++)
129
+ {
130
+ <button class="btn @(i == currentPage ? "btn-primary" : "btn-outline") btn-sm" onclick="changePage(@i)">
131
+ @i
132
+ </button>
133
+ }
134
+
135
+ <button class="btn btn-outline btn-sm" onclick="changePage(@(currentPage + 1))" @(currentPage == totalPages ? "disabled" : "")>
136
+ <i class="fas fa-angle-right"></i>
137
+ </button>
138
+ <button class="btn btn-outline btn-sm" onclick="changePage(@totalPages)" @(currentPage == totalPages ? "disabled" : "")>
139
+ <i class="fas fa-angle-double-right"></i>
140
+ </button>
141
+ </div>
142
+ }
143
+ </div>
144
+
145
+ <!-- 标签模态框 -->
146
+ <div id="tagModal" class="modal" style="display: none;">
147
+ <div class="modal-backdrop" onclick="closeTagModal()"></div>
148
+ <div class="modal-content" style="max-width: 400px;">
149
+ <div class="modal-header">
150
+ <h3 id="modalTitle">添加标签</h3>
151
+ <button onclick="closeTagModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer;">×</button>
152
+ </div>
153
+
154
+ <form id="tagForm" onsubmit="submitTag(event)">
155
+ <input type="hidden" id="tagId" name="id" value="0" />
156
+
157
+ <div class="form-group" style="margin-bottom: 1.5rem;">
158
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">标签名称 *</label>
159
+ <input type="text" id="tagName" name="name" required
160
+ style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);"
161
+ placeholder="请输入标签名称" />
162
+ </div>
163
+
164
+ <div class="form-group" style="margin-bottom: 1.5rem;">
165
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">标签颜色</label>
166
+ <input type="color" id="tagColor" name="color" value="#165DFF"
167
+ style="width: 100%; padding: 0.5rem; border: 1px solid var(--light-2); border-radius: var(--border-radius); height: 3rem;" />
168
+ <small style="color: var(--dark-2); font-size: 0.75rem;">选择标签的显示颜色</small>
169
+ </div>
170
+
171
+ <div class="modal-footer">
172
+ <button type="button" class="btn btn-outline" onclick="closeTagModal()">取消</button>
173
+ <button type="submit" class="btn btn-primary">
174
+ <span id="submitText">保存</span>
175
+ </button>
176
+ </div>
177
+ </form>
178
+ </div>
179
+ </div>
180
+
181
+ <style>
182
+ .modal {
183
+ position: fixed;
184
+ top: 0;
185
+ left: 0;
186
+ right: 0;
187
+ bottom: 0;
188
+ z-index: 10000;
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ }
193
+
194
+ .modal-backdrop {
195
+ position: absolute;
196
+ top: 0;
197
+ left: 0;
198
+ right: 0;
199
+ bottom: 0;
200
+ background: rgba(0, 0, 0, 0.5);
201
+ }
202
+
203
+ .modal-content {
204
+ background: white;
205
+ border-radius: var(--border-radius);
206
+ position: relative;
207
+ z-index: 2;
208
+ max-height: 90vh;
209
+ overflow-y: auto;
210
+ width: 90%;
211
+ max-width: 600px;
212
+ }
213
+
214
+ .modal-header {
215
+ padding: 1.5rem;
216
+ border-bottom: 1px solid var(--light-2);
217
+ display: flex;
218
+ justify-content: space-between;
219
+ align-items: center;
220
+ }
221
+
222
+ .modal-header h3 {
223
+ margin: 0;
224
+ font-size: 1.25rem;
225
+ font-weight: 600;
226
+ }
227
+
228
+ .modal-footer {
229
+ padding: 1.5rem;
230
+ border-top: 1px solid var(--light-2);
231
+ display: flex;
232
+ justify-content: flex-end;
233
+ gap: 1rem;
234
+ }
235
+
236
+ .form-group input:focus {
237
+ border-color: var(--primary);
238
+ outline: none;
239
+ box-shadow: 0 0 0 3px rgba(22, 93, 255, 0.1);
240
+ }
241
+ </style>
242
+
243
+ <script>
244
+ let isEditing = false;
245
+
246
+ function changePage(page) {
247
+ window.location.href = `@Url.Action("Index", "Tag")?page=${page}`;
248
+ }
249
+
250
+ function openTagModal(editMode = false) {
251
+ document.getElementById('tagModal').style.display = 'flex';
252
+ document.getElementById('modalTitle').textContent = editMode ? '编辑标签' : '添加标签';
253
+ document.getElementById('submitText').textContent = editMode ? '更新' : '保存';
254
+ isEditing = editMode;
255
+
256
+ if (!editMode) {
257
+ document.getElementById('tagForm').reset();
258
+ document.getElementById('tagId').value = '0';
259
+ document.getElementById('tagColor').value = '#165DFF';
260
+ }
261
+ }
262
+
263
+ function closeTagModal() {
264
+ document.getElementById('tagModal').style.display = 'none';
265
+ }
266
+
267
+ function editTag(id, name, color) {
268
+ openTagModal(true);
269
+ document.getElementById('tagId').value = id;
270
+ document.getElementById('tagName').value = name;
271
+ document.getElementById('tagColor').value = color || '#165DFF';
272
+ }
273
+
274
+ async function submitTag(event) {
275
+ event.preventDefault();
276
+
277
+ const formData = new FormData(event.target);
278
+ const data = Object.fromEntries(formData.entries());
279
+
280
+ try {
281
+ const response = await fetch('/Tag/Save', {
282
+ method: 'POST',
283
+ headers: {
284
+ 'Content-Type': 'application/json',
285
+ },
286
+ body: JSON.stringify(data)
287
+ });
288
+
289
+ if (response.ok) {
290
+ toolHub.showToast(isEditing ? '标签更新成功' : '标签添加成功', 'success');
291
+ closeTagModal();
292
+ setTimeout(() => location.reload(), 1000);
293
+ } else {
294
+ toolHub.showToast('操作失败,请重试', 'error');
295
+ }
296
+ } catch (error) {
297
+ console.error('提交失败:', error);
298
+ toolHub.showToast('操作失败,请重试', 'error');
299
+ }
300
+ }
301
+
302
+ async function toggleTagStatus(id, currentStatus) {
303
+ try {
304
+ const response = await fetch('/Tag/ToggleStatus', {
305
+ method: 'POST',
306
+ headers: {
307
+ 'Content-Type': 'application/json',
308
+ },
309
+ body: JSON.stringify({ id: id, isActive: !currentStatus })
310
+ });
311
+
312
+ if (response.ok) {
313
+ toolHub.showToast('状态更新成功', 'success');
314
+ setTimeout(() => location.reload(), 1000);
315
+ } else {
316
+ toolHub.showToast('操作失败,请重试', 'error');
317
+ }
318
+ } catch (error) {
319
+ console.error('操作失败:', error);
320
+ toolHub.showToast('操作失败,请重试', 'error');
321
+ }
322
+ }
323
+
324
+ async function deleteTag(id, name) {
325
+ if (!confirm(`确定要删除标签"${name}"吗?\n注意:删除标签会影响使用该标签的工具。`)) {
326
+ return;
327
+ }
328
+
329
+ try {
330
+ const response = await fetch('/Tag/Delete', {
331
+ method: 'POST',
332
+ headers: {
333
+ 'Content-Type': 'application/json',
334
+ },
335
+ body: JSON.stringify({ id: id })
336
+ });
337
+
338
+ if (response.ok) {
339
+ toolHub.showToast('标签删除成功', 'success');
340
+ setTimeout(() => location.reload(), 1000);
341
+ } else {
342
+ toolHub.showToast('删除失败,可能该标签下还有工具', 'error');
343
+ }
344
+ } catch (error) {
345
+ console.error('删除失败:', error);
346
+ toolHub.showToast('删除失败,请重试', 'error');
347
+ }
348
+ }
349
+ </script>
Views/Admin/Tools.cshtml ADDED
@@ -0,0 +1,934 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @model List<ToolHub.Models.Tool>
2
+ @{
3
+ ViewData["Title"] = "工具管理";
4
+ Layout = "_AdminLayout";
5
+ var categories = ViewBag.Categories as List<ToolHub.Models.Category> ?? new List<ToolHub.Models.Category>();
6
+ var currentCategory = ViewBag.CurrentCategory as int? ?? 0;
7
+ var currentPage = ViewBag.CurrentPage as int? ?? 1;
8
+ var totalPages = ViewBag.TotalPages as int? ?? 1;
9
+ var totalCount = ViewBag.TotalCount as int? ?? 0;
10
+ var pageSize = ViewBag.PageSize as int? ?? 20;
11
+ }
12
+
13
+ <!-- 页面头部 -->
14
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
15
+ <div>
16
+ <h2 style="font-size: 1.5rem; font-weight: 700; margin: 0; color: var(--dark);">工具管理</h2>
17
+ <p style="color: var(--dark-2); margin: 0.5rem 0 0;">管理平台上的所有工具,添加、编辑或删除工具</p>
18
+ </div>
19
+ <div style="display: flex; gap: 1rem;">
20
+ <a href="@Url.Action("Index", "Tag")" class="btn btn-secondary">
21
+ <i class="fas fa-tags"></i>
22
+ 标签管理
23
+ </a>
24
+ <button class="btn btn-primary" onclick="openToolModal()">
25
+ <i class="fas fa-plus"></i>
26
+ 添加工具
27
+ </button>
28
+ </div>
29
+ </div>
30
+
31
+ <!-- 筛选和搜索 -->
32
+ <div style="background: white; padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--shadow-sm); margin-bottom: 2rem;">
33
+ <div style="display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 1rem; align-items: end;">
34
+ <div>
35
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.875rem;">分类筛选</label>
36
+ <select id="categoryFilter" onchange="filterTools()" style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);">
37
+ <option value="0">全部分类</option>
38
+ @foreach (var category in categories)
39
+ {
40
+ <option value="@category.Id" selected="@(currentCategory == category.Id)">@category.Name</option>
41
+ }
42
+ </select>
43
+ </div>
44
+
45
+ <div>
46
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.875rem;">状态筛选</label>
47
+ <select id="statusFilter" onchange="filterTools()" style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);">
48
+ <option value="">全部状态</option>
49
+ <option value="active">启用</option>
50
+ <option value="inactive">禁用</option>
51
+ <option value="hot">热门</option>
52
+ <option value="new">新品</option>
53
+ <option value="recommended">推荐</option>
54
+ </select>
55
+ </div>
56
+
57
+ <div>
58
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.875rem;">搜索工具</label>
59
+ <input type="text" id="searchInput" placeholder="输入工具名称..." onkeyup="filterTools()"
60
+ style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);" />
61
+ </div>
62
+
63
+ <button class="btn btn-outline" onclick="clearFilters()">
64
+ <i class="fas fa-undo"></i>
65
+ 重置
66
+ </button>
67
+ </div>
68
+ </div>
69
+
70
+ <!-- 工具列表 -->
71
+ <div class="data-table">
72
+ <div style="padding: 1rem 1.5rem; border-bottom: 1px solid var(--light-2); display: flex; justify-content: space-between; align-items: center;">
73
+ <span style="font-weight: 600;">工具列表 (@totalCount)</span>
74
+ <div style="display: flex; gap: 0.5rem;">
75
+ <button class="btn btn-outline btn-sm" onclick="toggleView('table')" id="tableViewBtn">
76
+ <i class="fas fa-list"></i> 表格
77
+ </button>
78
+ <button class="btn btn-outline btn-sm" onclick="toggleView('grid')" id="gridViewBtn">
79
+ <i class="fas fa-th"></i> 网格
80
+ </button>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- 表格视图 -->
85
+ <div id="tableView">
86
+ <table>
87
+ <thead>
88
+ <tr>
89
+ <th>工具信息</th>
90
+ <th>分类</th>
91
+ <th>统计</th>
92
+ <th>标签</th>
93
+ <th>状态</th>
94
+ <th>创建时间</th>
95
+ <th>操作</th>
96
+ </tr>
97
+ </thead>
98
+ <tbody id="toolsTableBody">
99
+ @foreach (var tool in Model)
100
+ {
101
+ <tr data-category="@tool.CategoryId" data-status="@(tool.IsActive ? "active" : "inactive")"
102
+ data-hot="@tool.IsHot.ToString().ToLower()" data-new="@tool.IsNew.ToString().ToLower()" data-recommended="@tool.IsRecommended.ToString().ToLower()">
103
+ <td>
104
+ <div style="display: flex; align-items: center;">
105
+ @if (!string.IsNullOrEmpty(tool.Image))
106
+ {
107
+ <img src="@tool.Image" alt="@tool.Name" style="width: 40px; height: 40px; border-radius: var(--border-radius); margin-right: 0.75rem; object-fit: cover;" />
108
+ }
109
+ else
110
+ {
111
+ <div style="width: 40px; height: 40px; background: linear-gradient(135deg, var(--primary), var(--secondary)); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; margin-right: 0.75rem;">
112
+ <i class="@tool.Icon" style="color: white;"></i>
113
+ </div>
114
+ }
115
+ <div>
116
+ <strong class="tool-name">@tool.Name</strong>
117
+ @if (!string.IsNullOrEmpty(tool.Description))
118
+ {
119
+ <br><small style="color: var(--dark-2);">@(tool.Description.Length > 50 ? tool.Description.Substring(0, 50) + "..." : tool.Description)</small>
120
+ }
121
+ </div>
122
+ </div>
123
+ </td>
124
+ <td>
125
+ <span class="badge badge-primary">@tool.Category?.Name</span>
126
+ </td>
127
+ <td>
128
+ <div style="font-size: 0.875rem;">
129
+ <div style="margin-bottom: 0.25rem;">
130
+ <i class="fas fa-eye" style="color: var(--primary); margin-right: 0.25rem;"></i>
131
+ @(tool.ViewCount > 1000 ? (tool.ViewCount / 1000).ToString("F0") + "K" : tool.ViewCount.ToString())
132
+ </div>
133
+ @if (tool.Rating > 0)
134
+ {
135
+ <div>
136
+ <i class="fas fa-star" style="color: var(--warning); margin-right: 0.25rem;"></i>
137
+ @tool.Rating.ToString("F1")
138
+ </div>
139
+ }
140
+ </div>
141
+ </td>
142
+ <td>
143
+ <div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
144
+ @if (tool.IsHot)
145
+ {
146
+ <span class="badge badge-danger">热门</span>
147
+ }
148
+ @if (tool.IsNew)
149
+ {
150
+ <span class="badge badge-warning">新品</span>
151
+ }
152
+ @if (tool.IsRecommended)
153
+ {
154
+ <span class="badge badge-success">推荐</span>
155
+ }
156
+ <button class="btn btn-outline btn-xs" onclick="manageToolTags(@tool.Id, '@tool.Name')" title="管理标签">
157
+ <i class="fas fa-tags"></i>
158
+ </button>
159
+ </div>
160
+ </td>
161
+ <td>
162
+ @if (tool.IsActive)
163
+ {
164
+ <span class="badge badge-success">启用</span>
165
+ }
166
+ else
167
+ {
168
+ <span class="badge badge-secondary">禁用</span>
169
+ }
170
+ </td>
171
+ <td style="color: var(--dark-2); font-size: 0.875rem;">
172
+ @tool.CreatedAt.ToString("yyyy-MM-dd")
173
+ </td>
174
+ <td>
175
+ <div style="display: flex; gap: 0.5rem;">
176
+ <button class="btn btn-outline btn-sm" onclick="editTool(@tool.Id)" title="编辑">
177
+ <i class="fas fa-edit"></i>
178
+ </button>
179
+ <a href="@Url.Action("Index", "ToolStatistics", new { toolId = tool.Id })" class="btn btn-outline btn-sm" title="统计">
180
+ <i class="fas fa-chart-bar"></i>
181
+ </a>
182
+ <button class="btn btn-outline btn-sm" onclick="toggleToolStatus(@tool.Id, @tool.IsActive.ToString().ToLower())"
183
+ style="color: @(tool.IsActive ? "var(--warning)" : "var(--success)");" title="@(tool.IsActive ? "禁用" : "启用")">
184
+ <i class="fas fa-@(tool.IsActive ? "pause" : "play")"></i>
185
+ </button>
186
+ <button class="btn btn-outline btn-sm" onclick="deleteTool(@tool.Id, '@tool.Name')"
187
+ style="color: var(--danger);" title="删除">
188
+ <i class="fas fa-trash"></i>
189
+ </button>
190
+ </div>
191
+ </td>
192
+ </tr>
193
+ }
194
+ </tbody>
195
+ </table>
196
+ </div>
197
+
198
+ @if (!Model.Any())
199
+ {
200
+ <div style="padding: 3rem; text-align: center; color: var(--dark-2);">
201
+ <i class="fas fa-tools" style="font-size: 3rem; margin-bottom: 1rem; display: block; opacity: 0.3;"></i>
202
+ <h3 style="margin-bottom: 0.5rem;">暂无工具</h3>
203
+ <p style="margin-bottom: 1.5rem;">还没有添加任何工具</p>
204
+ <button class="btn btn-primary" onclick="openToolModal()">
205
+ <i class="fas fa-plus"></i>
206
+ 添加第一个工具
207
+ </button>
208
+ </div>
209
+ }
210
+
211
+ <!-- 分页控件 -->
212
+ @if (totalPages > 1)
213
+ {
214
+ <div style="padding: 1.5rem; border-top: 1px solid var(--light-2); display: flex; justify-content: center; align-items: center; gap: 0.5rem;">
215
+ <button class="btn btn-outline btn-sm" onclick="changePage(1)" @(currentPage == 1 ? "disabled" : "")>
216
+ <i class="fas fa-angle-double-left"></i>
217
+ </button>
218
+ <button class="btn btn-outline btn-sm" onclick="changePage(@(currentPage - 1))" @(currentPage == 1 ? "disabled" : "")>
219
+ <i class="fas fa-angle-left"></i>
220
+ </button>
221
+
222
+ @{
223
+ var startPage = Math.Max(1, currentPage - 2);
224
+ var endPage = Math.Min(totalPages, currentPage + 2);
225
+ }
226
+
227
+ @for (int i = startPage; i <= endPage; i++)
228
+ {
229
+ <button class="btn @(i == currentPage ? "btn-primary" : "btn-outline") btn-sm" onclick="changePage(@i)">
230
+ @i
231
+ </button>
232
+ }
233
+
234
+ <button class="btn btn-outline btn-sm" onclick="changePage(@(currentPage + 1))" @(currentPage == totalPages ? "disabled" : "")>
235
+ <i class="fas fa-angle-right"></i>
236
+ </button>
237
+ <button class="btn btn-outline btn-sm" onclick="changePage(@totalPages)" @(currentPage == totalPages ? "disabled" : "")>
238
+ <i class="fas fa-angle-double-right"></i>
239
+ </button>
240
+ </div>
241
+ }
242
+ </div>
243
+
244
+ <!-- 工具模态框 -->
245
+ <div id="toolModal" class="modal" style="display: none;">
246
+ <div class="modal-backdrop" onclick="closeToolModal()"></div>
247
+ <div class="modal-content" style="max-width: 600px;">
248
+ <div class="modal-header">
249
+ <h3 id="toolModalTitle">添加工具</h3>
250
+ <button onclick="closeToolModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer;">×</button>
251
+ </div>
252
+
253
+ <form id="toolForm" onsubmit="submitTool(event)">
254
+ <div class="modal-body">
255
+ <input type="hidden" id="toolId" name="id" value="0" />
256
+
257
+ <div class="form-group">
258
+ <label>工具名称 *</label>
259
+ <input type="text" id="toolName" name="name" required placeholder="请输入工具名称" />
260
+ </div>
261
+
262
+ <div class="form-group">
263
+ <label>工具描述</label>
264
+ <textarea id="toolDescription" name="description" rows="3" placeholder="请输入工具描述"></textarea>
265
+ </div>
266
+
267
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
268
+ <div class="form-group">
269
+ <label>所属分类 *</label>
270
+ <select id="toolCategory" name="categoryId" required>
271
+ <option value="">请选择分类</option>
272
+ @foreach (var category in categories)
273
+ {
274
+ <option value="@category.Id">@category.Name</option>
275
+ }
276
+ </select>
277
+ </div>
278
+
279
+ <div class="form-group">
280
+ <label>图标选择</label>
281
+ <div style="display: flex; gap: 0.5rem;">
282
+ <input type="text" id="toolIcon" name="icon" placeholder="如: fas fa-file-pdf" readonly style="flex: 1;" />
283
+ <button type="button" class="btn btn-outline" onclick="openIconSelector()">
284
+ <i class="fas fa-icons"></i>
285
+ 选择图标
286
+ </button>
287
+ </div>
288
+ </div>
289
+ </div>
290
+
291
+ <div class="form-group">
292
+ <label>工具链接</label>
293
+ <input type="url" id="toolUrl" name="url" placeholder="https://example.com" />
294
+ </div>
295
+
296
+ <div class="form-group">
297
+ <label>工具图片链接</label>
298
+ <input type="url" id="toolImage" name="image" placeholder="https://example.com/image.jpg" />
299
+ </div>
300
+
301
+ <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem;">
302
+ <div class="form-group">
303
+ <label style="display: flex; align-items: center; gap: 0.5rem;">
304
+ <input type="checkbox" id="toolIsHot" name="isHot" />
305
+ <span>热门工具</span>
306
+ </label>
307
+ </div>
308
+
309
+ <div class="form-group">
310
+ <label style="display: flex; align-items: center; gap: 0.5rem;">
311
+ <input type="checkbox" id="toolIsNew" name="isNew" />
312
+ <span>新品工具</span>
313
+ </label>
314
+ </div>
315
+
316
+ <div class="form-group">
317
+ <label style="display: flex; align-items: center; gap: 0.5rem;">
318
+ <input type="checkbox" id="toolIsRecommended" name="isRecommended" />
319
+ <span>推荐工具</span>
320
+ </label>
321
+ </div>
322
+ </div>
323
+
324
+ <div class="form-group">
325
+ <label>排序顺序</label>
326
+ <input type="number" id="toolSort" name="sortOrder" min="0" value="0" placeholder="数字越小排序越靠前" />
327
+ </div>
328
+ </div>
329
+
330
+ <div class="modal-footer">
331
+ <button type="button" class="btn btn-outline" onclick="closeToolModal()">取消</button>
332
+ <button type="submit" class="btn btn-primary">
333
+ <span id="toolSubmitText">保存</span>
334
+ </button>
335
+ </div>
336
+ </form>
337
+ </div>
338
+ </div>
339
+
340
+ <!-- 标签管理模态框 -->
341
+ <div id="tagModal" class="modal" style="display: none;">
342
+ <div class="modal-backdrop" onclick="closeTagModal()"></div>
343
+ <div class="modal-content" style="max-width: 500px;">
344
+ <div class="modal-header">
345
+ <h3 id="tagModalTitle">管理工具标签</h3>
346
+ <button onclick="closeTagModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer;">×</button>
347
+ </div>
348
+
349
+ <div class="modal-body">
350
+ <div style="margin-bottom: 1rem;">
351
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">当前标签</label>
352
+ <div id="currentTags" style="display: flex; flex-wrap: wrap; gap: 0.5rem; min-height: 2rem; padding: 0.5rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);">
353
+ <span style="color: var(--dark-2); font-style: italic;">加载中...</span>
354
+ </div>
355
+ </div>
356
+
357
+ <div>
358
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">添加标签</label>
359
+ <div style="display: flex; gap: 0.5rem;">
360
+ <select id="tagSelect" style="flex: 1; padding: 0.5rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);">
361
+ <option value="">选择标签...</option>
362
+ </select>
363
+ <button class="btn btn-primary btn-sm" onclick="addToolTag()">
364
+ <i class="fas fa-plus"></i>
365
+ </button>
366
+ </div>
367
+ </div>
368
+ </div>
369
+
370
+ <div class="modal-footer">
371
+ <button type="button" class="btn btn-outline" onclick="closeTagModal()">关闭</button>
372
+ </div>
373
+ </div>
374
+ </div>
375
+
376
+ <!-- 图标选择器模态框 -->
377
+ <div id="iconSelectorModal" class="modal" style="display: none;">
378
+ <div class="modal-backdrop" onclick="closeIconSelector()"></div>
379
+ <div class="modal-content" style="max-width: 800px; max-height: 80vh;">
380
+ <div class="modal-header">
381
+ <h3>选择图标</h3>
382
+ <button onclick="closeIconSelector()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer;">×</button>
383
+ </div>
384
+
385
+ <div class="modal-body" style="max-height: 60vh; overflow-y: auto;">
386
+ <div style="margin-bottom: 1rem;">
387
+ <input type="text" id="iconSearch" placeholder="搜索图标..."
388
+ style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);"
389
+ onkeyup="filterIcons()" />
390
+ </div>
391
+
392
+ <div id="iconGrid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1rem;">
393
+ <!-- 图标将通过JavaScript动态加载 -->
394
+ </div>
395
+ </div>
396
+
397
+ <div class="modal-footer">
398
+ <button type="button" class="btn btn-outline" onclick="closeIconSelector()">取消</button>
399
+ </div>
400
+ </div>
401
+ </div>
402
+
403
+ <script>
404
+ let currentView = 'table';
405
+ let isEditingTool = false;
406
+ let currentToolId = 0;
407
+ let currentToolName = '';
408
+
409
+ function changePage(page) {
410
+ const categoryFilter = document.getElementById('categoryFilter').value;
411
+ const searchInput = document.getElementById('searchInput').value;
412
+
413
+ let url = `@Url.Action("Index", "Tool")?page=${page}`;
414
+ if (categoryFilter !== '0') {
415
+ url += `&categoryId=${categoryFilter}`;
416
+ }
417
+ if (searchInput) {
418
+ url += `&search=${encodeURIComponent(searchInput)}`;
419
+ }
420
+
421
+ window.location.href = url;
422
+ }
423
+
424
+ function toggleView(view) {
425
+ currentView = view;
426
+ document.getElementById('tableView').style.display = view === 'table' ? 'block' : 'none';
427
+
428
+ document.getElementById('tableViewBtn').classList.toggle('btn-primary', view === 'table');
429
+ document.getElementById('tableViewBtn').classList.toggle('btn-outline', view !== 'table');
430
+ }
431
+
432
+ function filterTools() {
433
+ const categoryFilter = document.getElementById('categoryFilter').value;
434
+ const statusFilter = document.getElementById('statusFilter').value;
435
+ const searchInput = document.getElementById('searchInput').value.toLowerCase();
436
+
437
+ const tableRows = document.querySelectorAll('#toolsTableBody tr');
438
+
439
+ tableRows.forEach(row => {
440
+ const category = row.getAttribute('data-category');
441
+ const status = row.getAttribute('data-status');
442
+ const isHot = row.getAttribute('data-hot') === 'true';
443
+ const isNew = row.getAttribute('data-new') === 'true';
444
+ const isRecommended = row.getAttribute('data-recommended') === 'true';
445
+
446
+ const toolName = row.querySelector('.tool-name').textContent.toLowerCase();
447
+
448
+ let showRow = true;
449
+
450
+ // 分类筛选
451
+ if (categoryFilter !== '0' && category !== categoryFilter) {
452
+ showRow = false;
453
+ }
454
+
455
+ // 状态筛选
456
+ if (statusFilter) {
457
+ switch(statusFilter) {
458
+ case 'active':
459
+ if (status !== 'active') showRow = false;
460
+ break;
461
+ case 'inactive':
462
+ if (status !== 'inactive') showRow = false;
463
+ break;
464
+ case 'hot':
465
+ if (!isHot) showRow = false;
466
+ break;
467
+ case 'new':
468
+ if (!isNew) showRow = false;
469
+ break;
470
+ case 'recommended':
471
+ if (!isRecommended) showRow = false;
472
+ break;
473
+ }
474
+ }
475
+
476
+ // 搜索筛选
477
+ if (searchInput && !toolName.includes(searchInput)) {
478
+ showRow = false;
479
+ }
480
+
481
+ row.style.display = showRow ? '' : 'none';
482
+ });
483
+ }
484
+
485
+ function clearFilters() {
486
+ document.getElementById('categoryFilter').value = '0';
487
+ document.getElementById('statusFilter').value = '';
488
+ document.getElementById('searchInput').value = '';
489
+ filterTools();
490
+ }
491
+
492
+ function openToolModal(editMode = false) {
493
+ document.getElementById('toolModal').style.display = 'flex';
494
+ document.getElementById('toolModalTitle').textContent = editMode ? '编辑工具' : '添加工具';
495
+ document.getElementById('toolSubmitText').textContent = editMode ? '更新' : '保存';
496
+ isEditingTool = editMode;
497
+
498
+ if (!editMode) {
499
+ document.getElementById('toolForm').reset();
500
+ document.getElementById('toolId').value = '0';
501
+ }
502
+ }
503
+
504
+ function closeToolModal() {
505
+ document.getElementById('toolModal').style.display = 'none';
506
+ }
507
+
508
+ async function editTool(id) {
509
+ try {
510
+ const response = await fetch(`/Tool/Get?id=${id}`);
511
+ if (response.ok) {
512
+ const tool = await response.json();
513
+ openToolModal(true);
514
+
515
+ document.getElementById('toolId').value = tool.id;
516
+ document.getElementById('toolName').value = tool.name;
517
+ document.getElementById('toolDescription').value = tool.description || '';
518
+ document.getElementById('toolCategory').value = tool.categoryId;
519
+ document.getElementById('toolIcon').value = tool.icon || '';
520
+ document.getElementById('toolUrl').value = tool.url || '';
521
+ document.getElementById('toolImage').value = tool.image || '';
522
+ document.getElementById('toolIsHot').checked = tool.isHot;
523
+ document.getElementById('toolIsNew').checked = tool.isNew;
524
+ document.getElementById('toolIsRecommended').checked = tool.isRecommended;
525
+ document.getElementById('toolSort').value = tool.sortOrder;
526
+ }
527
+ } catch (error) {
528
+ console.error('获取工具信息失败:', error);
529
+ toolHub.showToast('获取工具信息失败', 'error');
530
+ }
531
+ }
532
+
533
+ async function submitTool(event) {
534
+ event.preventDefault();
535
+
536
+ const formData = new FormData(event.target);
537
+ const data = Object.fromEntries(formData.entries());
538
+
539
+ // 处理复选框
540
+ data.isHot = document.getElementById('toolIsHot').checked;
541
+ data.isNew = document.getElementById('toolIsNew').checked;
542
+ data.isRecommended = document.getElementById('toolIsRecommended').checked;
543
+
544
+ try {
545
+ const response = await fetch('/Tool/Save', {
546
+ method: 'POST',
547
+ headers: {
548
+ 'Content-Type': 'application/json',
549
+ },
550
+ body: JSON.stringify(data)
551
+ });
552
+
553
+ if (response.ok) {
554
+ toolHub.showToast(isEditingTool ? '工具更新成功' : '工具添加成功', 'success');
555
+ closeToolModal();
556
+ setTimeout(() => location.reload(), 1000);
557
+ } else {
558
+ toolHub.showToast('操作失败,请重试', 'error');
559
+ }
560
+ } catch (error) {
561
+ console.error('提交失败:', error);
562
+ toolHub.showToast('操作失败,请重试', 'error');
563
+ }
564
+ }
565
+
566
+ async function toggleToolStatus(id, currentStatus) {
567
+ try {
568
+ const response = await fetch('/Tool/ToggleStatus', {
569
+ method: 'POST',
570
+ headers: {
571
+ 'Content-Type': 'application/json',
572
+ },
573
+ body: JSON.stringify({ id: id, isActive: !currentStatus })
574
+ });
575
+
576
+ if (response.ok) {
577
+ toolHub.showToast('状态更新成功', 'success');
578
+ setTimeout(() => location.reload(), 1000);
579
+ } else {
580
+ toolHub.showToast('操作失败,请重试', 'error');
581
+ }
582
+ } catch (error) {
583
+ console.error('操作失败:', error);
584
+ toolHub.showToast('操作失败,请重试', 'error');
585
+ }
586
+ }
587
+
588
+ async function deleteTool(id, name) {
589
+ if (!confirm(`确定要删除工具"${name}"吗?此操作不可恢复。`)) {
590
+ return;
591
+ }
592
+
593
+ try {
594
+ const response = await fetch('/Tool/Delete', {
595
+ method: 'POST',
596
+ headers: {
597
+ 'Content-Type': 'application/json',
598
+ },
599
+ body: JSON.stringify({ id: id })
600
+ });
601
+
602
+ if (response.ok) {
603
+ toolHub.showToast('工具删除成功', 'success');
604
+ setTimeout(() => location.reload(), 1000);
605
+ } else {
606
+ toolHub.showToast('删除失败,请重试', 'error');
607
+ }
608
+ } catch (error) {
609
+ console.error('删除失败:', error);
610
+ toolHub.showToast('删除失败,请重试', 'error');
611
+ }
612
+ }
613
+
614
+ // 标签管理功能
615
+ async function manageToolTags(toolId, toolName) {
616
+ currentToolId = toolId;
617
+ currentToolName = toolName;
618
+
619
+ document.getElementById('tagModal').style.display = 'flex';
620
+ document.getElementById('tagModalTitle').textContent = `管理工具标签 - ${toolName}`;
621
+
622
+ await loadToolTags();
623
+ await loadAvailableTags();
624
+ }
625
+
626
+ function closeTagModal() {
627
+ document.getElementById('tagModal').style.display = 'none';
628
+ }
629
+
630
+ async function loadToolTags() {
631
+ try {
632
+ const response = await fetch(`/Tag/GetToolTags?toolId=${currentToolId}`);
633
+ const tags = await response.json();
634
+
635
+ const currentTagsDiv = document.getElementById('currentTags');
636
+ if (tags.length === 0) {
637
+ currentTagsDiv.innerHTML = '<span style="color: var(--dark-2); font-style: italic;">暂无标签</span>';
638
+ } else {
639
+ currentTagsDiv.innerHTML = tags.map(tag => `
640
+ <span class="badge" style="background: ${tag.color || 'var(--primary)'}; color: white; display: flex; align-items: center; gap: 0.25rem;">
641
+ ${tag.name}
642
+ <button onclick="removeToolTag(${tag.id})" style="background: none; border: none; color: white; cursor: pointer; font-size: 0.75rem;">
643
+ <i class="fas fa-times"></i>
644
+ </button>
645
+ </span>
646
+ `).join('');
647
+ }
648
+ } catch (error) {
649
+ console.error('加载工具标签失败:', error);
650
+ toolHub.showToast('加载工具标签失败', 'error');
651
+ }
652
+ }
653
+
654
+ async function loadAvailableTags() {
655
+ try {
656
+ const response = await fetch('/Tag/GetList');
657
+ const tags = await response.json();
658
+
659
+ const tagSelect = document.getElementById('tagSelect');
660
+ tagSelect.innerHTML = '<option value="">选择标签...</option>';
661
+
662
+ tags.forEach(tag => {
663
+ const option = document.createElement('option');
664
+ option.value = tag.id;
665
+ option.textContent = tag.name;
666
+ tagSelect.appendChild(option);
667
+ });
668
+ } catch (error) {
669
+ console.error('加载可用标签失败:', error);
670
+ toolHub.showToast('加载可用标签失败', 'error');
671
+ }
672
+ }
673
+
674
+ async function addToolTag() {
675
+ const tagId = document.getElementById('tagSelect').value;
676
+ if (!tagId) {
677
+ toolHub.showToast('请选择要添加的标签', 'warning');
678
+ return;
679
+ }
680
+
681
+ try {
682
+ const response = await fetch('/Tag/AddToolTag', {
683
+ method: 'POST',
684
+ headers: {
685
+ 'Content-Type': 'application/json',
686
+ },
687
+ body: JSON.stringify({ toolId: currentToolId, tagId: parseInt(tagId) })
688
+ });
689
+
690
+ const result = await response.json();
691
+ if (result.success) {
692
+ toolHub.showToast('标签添加成功', 'success');
693
+ document.getElementById('tagSelect').value = '';
694
+ await loadToolTags();
695
+ } else {
696
+ toolHub.showToast(result.message || '添加失败', 'error');
697
+ }
698
+ } catch (error) {
699
+ console.error('添加标签失败:', error);
700
+ toolHub.showToast('添加标签失败', 'error');
701
+ }
702
+ }
703
+
704
+ async function removeToolTag(tagId) {
705
+ if (!confirm('确定要移除这个标签吗?')) {
706
+ return;
707
+ }
708
+
709
+ try {
710
+ const response = await fetch('/Tag/RemoveToolTag', {
711
+ method: 'POST',
712
+ headers: {
713
+ 'Content-Type': 'application/json',
714
+ },
715
+ body: JSON.stringify({ toolId: currentToolId, tagId: tagId })
716
+ });
717
+
718
+ const result = await response.json();
719
+ if (result.success) {
720
+ toolHub.showToast('标签移除成功', 'success');
721
+ await loadToolTags();
722
+ } else {
723
+ toolHub.showToast('移除失败', 'error');
724
+ }
725
+ } catch (error) {
726
+ console.error('移除标签失败:', error);
727
+ toolHub.showToast('移除标签失败', 'error');
728
+ }
729
+ }
730
+
731
+ // 图标选择器功能
732
+ const iconList = [
733
+ // 文件类型图标
734
+ { name: 'PDF文件', class: 'fas fa-file-pdf', category: '文件' },
735
+ { name: 'Word文档', class: 'fas fa-file-word', category: '文件' },
736
+ { name: 'Excel表格', class: 'fas fa-file-excel', category: '文件' },
737
+ { name: 'PowerPoint', class: 'fas fa-file-powerpoint', category: '文件' },
738
+ { name: '文本文件', class: 'fas fa-file-alt', category: '文件' },
739
+ { name: '图片文件', class: 'fas fa-file-image', category: '文件' },
740
+ { name: '压缩文件', class: 'fas fa-file-archive', category: '文件' },
741
+ { name: '代码文件', class: 'fas fa-file-code', category: '文件' },
742
+ { name: '音频文件', class: 'fas fa-file-audio', category: '文件' },
743
+ { name: '视频文件', class: 'fas fa-file-video', category: '文件' },
744
+
745
+ // 工具类图标
746
+ { name: '工具', class: 'fas fa-tools', category: '工具' },
747
+ { name: '扳手', class: 'fas fa-wrench', category: '工具' },
748
+ { name: '锤子', class: 'fas fa-hammer', category: '工具' },
749
+ { name: '螺丝刀', class: 'fas fa-screwdriver', category: '工具' },
750
+ { name: '齿轮', class: 'fas fa-cog', category: '工具' },
751
+ { name: '设置', class: 'fas fa-cogs', category: '工具' },
752
+
753
+ // 媒体图标
754
+ { name: '图片', class: 'fas fa-image', category: '媒体' },
755
+ { name: '视频', class: 'fas fa-video', category: '媒体' },
756
+ { name: '音乐', class: 'fas fa-music', category: '媒体' },
757
+ { name: '相机', class: 'fas fa-camera', category: '媒体' },
758
+ { name: '麦克风', class: 'fas fa-microphone', category: '媒体' },
759
+ { name: '播放', class: 'fas fa-play', category: '媒体' },
760
+ { name: '���停', class: 'fas fa-pause', category: '媒体' },
761
+ { name: '停止', class: 'fas fa-stop', category: '媒体' },
762
+
763
+ // 网络图标
764
+ { name: '网络', class: 'fas fa-network-wired', category: '网络' },
765
+ { name: 'WiFi', class: 'fas fa-wifi', category: '网络' },
766
+ { name: '链接', class: 'fas fa-link', category: '网络' },
767
+ { name: '下载', class: 'fas fa-download', category: '网络' },
768
+ { name: '上传', class: 'fas fa-upload', category: '网络' },
769
+ { name: '云存储', class: 'fas fa-cloud', category: '网络' },
770
+ { name: '服务器', class: 'fas fa-server', category: '网络' },
771
+ { name: '数据库', class: 'fas fa-database', category: '网络' },
772
+
773
+ // 设计图标
774
+ { name: '画笔', class: 'fas fa-paint-brush', category: '设计' },
775
+ { name: '调色板', class: 'fas fa-palette', category: '设计' },
776
+ { name: '设计', class: 'fas fa-drafting-compass', category: '设计' },
777
+ { name: '图层', class: 'fas fa-layer-group', category: '设计' },
778
+ { name: '裁剪', class: 'fas fa-crop', category: '设计' },
779
+ { name: '滤镜', class: 'fas fa-magic', category: '设计' },
780
+
781
+ // 开发图标
782
+ { name: '代码', class: 'fas fa-code', category: '开发' },
783
+ { name: '终端', class: 'fas fa-terminal', category: '开发' },
784
+ { name: '调试', class: 'fas fa-bug', category: '开发' },
785
+ { name: 'Git', class: 'fab fa-git-alt', category: '开发' },
786
+ { name: 'GitHub', class: 'fab fa-github', category: '开发' },
787
+ { name: 'HTML', class: 'fab fa-html5', category: '开发' },
788
+ { name: 'CSS', class: 'fab fa-css3-alt', category: '开发' },
789
+ { name: 'JavaScript', class: 'fab fa-js-square', category: '开发' },
790
+ { name: 'React', class: 'fab fa-react', category: '开发' },
791
+ { name: 'Node.js', class: 'fab fa-node-js', category: '开发' },
792
+ { name: 'Python', class: 'fab fa-python', category: '开发' },
793
+ { name: 'Java', class: 'fab fa-java', category: '开发' },
794
+ { name: 'PHP', class: 'fab fa-php', category: '开发' },
795
+
796
+ // 通用图标
797
+ { name: '主页', class: 'fas fa-home', category: '通用' },
798
+ { name: '用户', class: 'fas fa-user', category: '通用' },
799
+ { name: '用户组', class: 'fas fa-users', category: '通用' },
800
+ { name: '设置', class: 'fas fa-cog', category: '通用' },
801
+ { name: '搜索', class: 'fas fa-search', category: '通用' },
802
+ { name: '编辑', class: 'fas fa-edit', category: '通用' },
803
+ { name: '删除', class: 'fas fa-trash', category: '通用' },
804
+ { name: '添加', class: 'fas fa-plus', category: '通用' },
805
+ { name: '保存', class: 'fas fa-save', category: '通用' },
806
+ { name: '关闭', class: 'fas fa-times', category: '通用' },
807
+ { name: '检查', class: 'fas fa-check', category: '通用' },
808
+ { name: '警告', class: 'fas fa-exclamation-triangle', category: '通用' },
809
+ { name: '信息', class: 'fas fa-info-circle', category: '通用' },
810
+ { name: '问号', class: 'fas fa-question-circle', category: '通用' },
811
+ { name: '星标', class: 'fas fa-star', category: '通用' },
812
+ { name: '心形', class: 'fas fa-heart', category: '通用' },
813
+ { name: '点赞', class: 'fas fa-thumbs-up', category: '通用' },
814
+ { name: '分享', class: 'fas fa-share', category: '通用' },
815
+ { name: '邮件', class: 'fas fa-envelope', category: '通用' },
816
+ { name: '电话', class: 'fas fa-phone', category: '通用' },
817
+ { name: '日历', class: 'fas fa-calendar', category: '通用' },
818
+ { name: '时钟', class: 'fas fa-clock', category: '通用' },
819
+ { name: '地图', class: 'fas fa-map-marker-alt', category: '通用' },
820
+ { name: '购物车', class: 'fas fa-shopping-cart', category: '通用' },
821
+ { name: '钱包', class: 'fas fa-wallet', category: '通用' },
822
+ { name: '信用卡', class: 'fas fa-credit-card', category: '通用' },
823
+ { name: '锁', class: 'fas fa-lock', category: '通用' },
824
+ { name: '钥匙', class: 'fas fa-key', category: '通用' },
825
+ { name: '盾牌', class: 'fas fa-shield-alt', category: '通用' },
826
+ { name: '眼睛', class: 'fas fa-eye', category: '通用' },
827
+ { name: '眼睛斜杠', class: 'fas fa-eye-slash', category: '通用' },
828
+ { name: '打印', class: 'fas fa-print', category: '通用' },
829
+ { name: '扫描', class: 'fas fa-scanner', category: '通用' },
830
+ { name: '传真', class: 'fas fa-fax', category: '通用' },
831
+ { name: '计算器', class: 'fas fa-calculator', category: '通用' },
832
+ { name: '图表', class: 'fas fa-chart-bar', category: '通用' },
833
+ { name: '饼图', class: 'fas fa-chart-pie', category: '通用' },
834
+ { name: '折线图', class: 'fas fa-chart-line', category: '通用' },
835
+ { name: '表格', class: 'fas fa-table', category: '通用' },
836
+ { name: '列表', class: 'fas fa-list', category: '通用' },
837
+ { name: '网格', class: 'fas fa-th', category: '通用' },
838
+ { name: '标签', class: 'fas fa-tags', category: '通用' },
839
+ { name: '书签', class: 'fas fa-bookmark', category: '通用' },
840
+ { name: '文件夹', class: 'fas fa-folder', category: '通用' },
841
+ { name: '文件', class: 'fas fa-file', category: '通用' },
842
+ { name: '剪贴板', class: 'fas fa-clipboard', category: '通用' },
843
+ { name: '复制', class: 'fas fa-copy', category: '通用' },
844
+ { name: '粘贴', class: 'fas fa-paste', category: '通用' },
845
+ { name: '剪切', class: 'fas fa-cut', category: '通用' },
846
+ { name: '撤销', class: 'fas fa-undo', category: '通用' },
847
+ { name: '重做', class: 'fas fa-redo', category: '通用' },
848
+ { name: '刷新', class: 'fas fa-sync', category: '通用' },
849
+ { name: '旋转', class: 'fas fa-sync-alt', category: '通用' },
850
+ { name: '箭头', class: 'fas fa-arrow-right', category: '通用' },
851
+ { name: '左箭头', class: 'fas fa-arrow-left', category: '通用' },
852
+ { name: '上箭头', class: 'fas fa-arrow-up', category: '通用' },
853
+ { name: '下箭头', class: 'fas fa-arrow-down', category: '通用' },
854
+ { name: '外部链接', class: 'fas fa-external-link-alt', category: '通用' },
855
+ { name: '返回', class: 'fas fa-arrow-circle-left', category: '通用' },
856
+ { name: '前进', class: 'fas fa-arrow-circle-right', category: '通用' },
857
+ { name: '展开', class: 'fas fa-chevron-down', category: '通用' },
858
+ { name: '收起', class: 'fas fa-chevron-up', category: '通用' },
859
+ { name: '菜单', class: 'fas fa-bars', category: '通用' },
860
+ { name: '汉堡菜单', class: 'fas fa-hamburger', category: '通用' },
861
+ { name: '更多', class: 'fas fa-ellipsis-h', category: '通用' },
862
+ { name: '垂直更多', class: 'fas fa-ellipsis-v', category: '通用' }
863
+ ];
864
+
865
+ function openIconSelector() {
866
+ document.getElementById('iconSelectorModal').style.display = 'flex';
867
+ loadIconGrid();
868
+ }
869
+
870
+ function closeIconSelector() {
871
+ document.getElementById('iconSelectorModal').style.display = 'none';
872
+ document.getElementById('iconSearch').value = '';
873
+ }
874
+
875
+ function loadIconGrid() {
876
+ const iconGrid = document.getElementById('iconGrid');
877
+ iconGrid.innerHTML = '';
878
+
879
+ iconList.forEach(icon => {
880
+ const iconDiv = document.createElement('div');
881
+ iconDiv.className = 'icon-item';
882
+ iconDiv.style.cssText = `
883
+ display: flex;
884
+ flex-direction: column;
885
+ align-items: center;
886
+ padding: 1rem;
887
+ border: 1px solid var(--light-2);
888
+ border-radius: var(--border-radius);
889
+ cursor: pointer;
890
+ transition: all 0.2s;
891
+ background: white;
892
+ `;
893
+ iconDiv.onmouseover = () => {
894
+ iconDiv.style.borderColor = 'var(--primary)';
895
+ iconDiv.style.backgroundColor = 'var(--light-1)';
896
+ };
897
+ iconDiv.onmouseout = () => {
898
+ iconDiv.style.borderColor = 'var(--light-2)';
899
+ iconDiv.style.backgroundColor = 'white';
900
+ };
901
+ iconDiv.onclick = () => selectIcon(icon.class);
902
+
903
+ iconDiv.innerHTML = `
904
+ <i class="${icon.class}" style="font-size: 2rem; color: var(--primary); margin-bottom: 0.5rem;"></i>
905
+ <div style="font-size: 0.75rem; text-align: center; color: var(--dark-2);">${icon.name}</div>
906
+ <div style="font-size: 0.625rem; text-align: center; color: var(--dark-3); margin-top: 0.25rem;">${icon.class}</div>
907
+ `;
908
+
909
+ iconGrid.appendChild(iconDiv);
910
+ });
911
+ }
912
+
913
+ function selectIcon(iconClass) {
914
+ document.getElementById('toolIcon').value = iconClass;
915
+ closeIconSelector();
916
+ toolHub.showToast('图标已选择', 'success');
917
+ }
918
+
919
+ function filterIcons() {
920
+ const searchTerm = document.getElementById('iconSearch').value.toLowerCase();
921
+ const iconItems = document.querySelectorAll('.icon-item');
922
+
923
+ iconItems.forEach(item => {
924
+ const iconName = item.querySelector('div').textContent.toLowerCase();
925
+ const iconClass = item.querySelector('div:last-child').textContent.toLowerCase();
926
+
927
+ if (iconName.includes(searchTerm) || iconClass.includes(searchTerm)) {
928
+ item.style.display = 'flex';
929
+ } else {
930
+ item.style.display = 'none';
931
+ }
932
+ });
933
+ }
934
+ </script>
Views/Category/Index.cshtml ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @model List<ToolHub.Models.Category>
2
+ @{
3
+ ViewData["Title"] = "工具分类管理";
4
+ Layout = "_AdminLayout";
5
+ }
6
+
7
+ <!-- 页面头部 -->
8
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
9
+ <div>
10
+ <h2 style="font-size: 1.5rem; font-weight: 700; margin: 0; color: var(--dark);">工具分类管理</h2>
11
+ <p style="color: var(--dark-2); margin: 0.5rem 0 0;">管理工具分类,组织和归类各种工具</p>
12
+ </div>
13
+ <button class="btn btn-primary" onclick="openCategoryModal()">
14
+ <i class="fas fa-plus"></i>
15
+ 添加分类
16
+ </button>
17
+ </div>
18
+
19
+ <!-- 分类列表 -->
20
+ <div class="data-table">
21
+ <table>
22
+ <thead>
23
+ <tr>
24
+ <th>分类名称</th>
25
+ <th>描述</th>
26
+ <th>图标</th>
27
+ <th>颜色</th>
28
+ <th>排序</th>
29
+ <th>工具数量</th>
30
+ <th>状态</th>
31
+ <th>操作</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody>
35
+ @foreach (var category in Model)
36
+ {
37
+ <tr>
38
+ <td>
39
+ <div style="display: flex; align-items: center;">
40
+ @if (!string.IsNullOrEmpty(category.Icon))
41
+ {
42
+ <i class="@category.Icon" style="margin-right: 0.75rem; color: @(category.Color ?? "var(--primary)"); font-size: 1.25rem;"></i>
43
+ }
44
+ <strong>@category.Name</strong>
45
+ </div>
46
+ </td>
47
+ <td style="max-width: 200px;">
48
+ <span style="color: var(--dark-2);">@(category.Description ?? "暂无描述")</span>
49
+ </td>
50
+ <td>
51
+ <code style="background: var(--light-1); padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem;">
52
+ @(category.Icon ?? "无")
53
+ </code>
54
+ </td>
55
+ <td>
56
+ @if (!string.IsNullOrEmpty(category.Color))
57
+ {
58
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
59
+ <div style="width: 20px; height: 20px; background: @category.Color; border-radius: 4px; border: 1px solid var(--light-2);"></div>
60
+ <code style="font-size: 0.75rem;">@category.Color</code>
61
+ </div>
62
+ }
63
+ else
64
+ {
65
+ <span style="color: var(--dark-2);">默认</span>
66
+ }
67
+ </td>
68
+ <td>
69
+ <span class="badge badge-secondary">@category.SortOrder</span>
70
+ </td>
71
+ <td>
72
+ <span class="badge badge-primary">@category.Tools?.Count</span>
73
+ </td>
74
+ <td>
75
+ @if (category.IsActive)
76
+ {
77
+ <span class="badge badge-success">启用</span>
78
+ }
79
+ else
80
+ {
81
+ <span class="badge badge-secondary">禁用</span>
82
+ }
83
+ </td>
84
+ <td>
85
+ <div style="display: flex; gap: 0.5rem;">
86
+ <button class="btn btn-outline btn-sm" onclick="editCategory(@category.Id, '@category.Name', '@category.Description', '@category.Icon', '@category.Color', @category.SortOrder)">
87
+ <i class="fas fa-edit"></i>
88
+ </button>
89
+ <button class="btn btn-outline btn-sm" onclick="toggleCategoryStatus(@category.Id, @category.IsActive.ToString().ToLower())" style="color: @(category.IsActive ? "var(--warning)" : "var(--success)");">
90
+ <i class="fas fa-@(category.IsActive ? "pause" : "play")"></i>
91
+ </button>
92
+ <button class="btn btn-outline btn-sm" onclick="deleteCategory(@category.Id, '@category.Name')" style="color: var(--danger);">
93
+ <i class="fas fa-trash"></i>
94
+ </button>
95
+ </div>
96
+ </td>
97
+ </tr>
98
+ }
99
+ </tbody>
100
+ </table>
101
+
102
+ @if (!Model.Any())
103
+ {
104
+ <div style="padding: 3rem; text-align: center; color: var(--dark-2);">
105
+ <i class="fas fa-tags" style="font-size: 3rem; margin-bottom: 1rem; display: block; opacity: 0.3;"></i>
106
+ <h3 style="margin-bottom: 0.5rem;">暂无分类</h3>
107
+ <p style="margin-bottom: 1.5rem;">还没有创建任何工具分类</p>
108
+ <button class="btn btn-primary" onclick="openCategoryModal()">
109
+ <i class="fas fa-plus"></i>
110
+ 创建第一个分类
111
+ </button>
112
+ </div>
113
+ }
114
+ </div>
115
+
116
+ <!-- 分类模态框 -->
117
+ <div id="categoryModal" class="modal" style="display: none;">
118
+ <div class="modal-backdrop" onclick="closeCategoryModal()"></div>
119
+ <div class="modal-content" style="max-width: 500px;">
120
+ <div class="modal-header">
121
+ <h3 id="modalTitle">添加分类</h3>
122
+ <button onclick="closeCategoryModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer;">×</button>
123
+ </div>
124
+
125
+ <form id="categoryForm" onsubmit="submitCategory(event)">
126
+ <input type="hidden" id="categoryId" name="id" value="0" />
127
+
128
+ <div class="form-group" style="margin-bottom: 1.5rem;">
129
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">分类名称 *</label>
130
+ <input type="text" id="categoryName" name="name" required
131
+ style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);"
132
+ placeholder="请输入分类名称" />
133
+ </div>
134
+
135
+ <div class="form-group" style="margin-bottom: 1.5rem;">
136
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">描述</label>
137
+ <textarea id="categoryDescription" name="description" rows="3"
138
+ style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius); resize: vertical;"
139
+ placeholder="请输入分类描述"></textarea>
140
+ </div>
141
+
142
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem;">
143
+ <div class="form-group">
144
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">图标类名</label>
145
+ <input type="text" id="categoryIcon" name="icon"
146
+ style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);"
147
+ placeholder="如: fas fa-file-pdf" />
148
+ <small style="color: var(--dark-2); font-size: 0.75rem;">使用 Font Awesome 图标类名</small>
149
+ </div>
150
+
151
+ <div class="form-group">
152
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">颜色</label>
153
+ <input type="color" id="categoryColor" name="color" value="#165DFF"
154
+ style="width: 100%; padding: 0.5rem; border: 1px solid var(--light-2); border-radius: var(--border-radius); height: 3rem;" />
155
+ </div>
156
+ </div>
157
+
158
+ <div class="form-group" style="margin-bottom: 1.5rem;">
159
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">排序顺序</label>
160
+ <input type="number" id="categorySort" name="sortOrder" min="0" value="0"
161
+ style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);"
162
+ placeholder="数字越小排序越靠前" />
163
+ </div>
164
+
165
+ <div class="modal-footer">
166
+ <button type="button" class="btn btn-outline" onclick="closeCategoryModal()">取消</button>
167
+ <button type="submit" class="btn btn-primary">
168
+ <span id="submitText">保存</span>
169
+ </button>
170
+ </div>
171
+ </form>
172
+ </div>
173
+ </div>
174
+
175
+ <style>
176
+ .modal {
177
+ position: fixed;
178
+ top: 0;
179
+ left: 0;
180
+ right: 0;
181
+ bottom: 0;
182
+ z-index: 10000;
183
+ display: flex;
184
+ align-items: center;
185
+ justify-content: center;
186
+ }
187
+
188
+ .modal-backdrop {
189
+ position: absolute;
190
+ top: 0;
191
+ left: 0;
192
+ right: 0;
193
+ bottom: 0;
194
+ background: rgba(0, 0, 0, 0.5);
195
+ }
196
+
197
+ .modal-content {
198
+ background: white;
199
+ border-radius: var(--border-radius);
200
+ position: relative;
201
+ z-index: 2;
202
+ max-height: 90vh;
203
+ overflow-y: auto;
204
+ width: 90%;
205
+ max-width: 600px;
206
+ }
207
+
208
+ .modal-header {
209
+ padding: 1.5rem;
210
+ border-bottom: 1px solid var(--light-2);
211
+ display: flex;
212
+ justify-content: space-between;
213
+ align-items: center;
214
+ }
215
+
216
+ .modal-header h3 {
217
+ margin: 0;
218
+ font-size: 1.25rem;
219
+ font-weight: 600;
220
+ }
221
+
222
+ .modal-footer {
223
+ padding: 1.5rem;
224
+ border-top: 1px solid var(--light-2);
225
+ display: flex;
226
+ justify-content: flex-end;
227
+ gap: 1rem;
228
+ }
229
+
230
+ .form-group input:focus,
231
+ .form-group textarea:focus {
232
+ border-color: var(--primary);
233
+ outline: none;
234
+ box-shadow: 0 0 0 3px rgba(22, 93, 255, 0.1);
235
+ }
236
+ </style>
237
+
238
+ <script>
239
+ let isEditing = false;
240
+
241
+ function openCategoryModal(editMode = false) {
242
+ document.getElementById('categoryModal').style.display = 'flex';
243
+ document.getElementById('modalTitle').textContent = editMode ? '编辑分类' : '添加分类';
244
+ document.getElementById('submitText').textContent = editMode ? '更新' : '保存';
245
+ isEditing = editMode;
246
+
247
+ if (!editMode) {
248
+ document.getElementById('categoryForm').reset();
249
+ document.getElementById('categoryId').value = '0';
250
+ document.getElementById('categoryColor').value = '#165DFF';
251
+ }
252
+ }
253
+
254
+ function closeCategoryModal() {
255
+ document.getElementById('categoryModal').style.display = 'none';
256
+ }
257
+
258
+ function editCategory(id, name, description, icon, color, sortOrder) {
259
+ openCategoryModal(true);
260
+ document.getElementById('categoryId').value = id;
261
+ document.getElementById('categoryName').value = name;
262
+ document.getElementById('categoryDescription').value = description || '';
263
+ document.getElementById('categoryIcon').value = icon || '';
264
+ document.getElementById('categoryColor').value = color || '#165DFF';
265
+ document.getElementById('categorySort').value = sortOrder;
266
+ }
267
+
268
+ async function submitCategory(event) {
269
+ event.preventDefault();
270
+
271
+ const formData = new FormData(event.target);
272
+ const data = Object.fromEntries(formData.entries());
273
+
274
+ try {
275
+ const response = await fetch('/Category/Save', {
276
+ method: 'POST',
277
+ headers: {
278
+ 'Content-Type': 'application/json',
279
+ },
280
+ body: JSON.stringify(data)
281
+ });
282
+
283
+ if (response.ok) {
284
+ toolHub.showToast(isEditing ? '分类更新成功' : '分类添加成功', 'success');
285
+ closeCategoryModal();
286
+ setTimeout(() => location.reload(), 1000);
287
+ } else {
288
+ toolHub.showToast('操作失败,请重试', 'error');
289
+ }
290
+ } catch (error) {
291
+ console.error('提交失败:', error);
292
+ toolHub.showToast('操作失败,请重试', 'error');
293
+ }
294
+ }
295
+
296
+ async function toggleCategoryStatus(id, currentStatus) {
297
+ try {
298
+ const response = await fetch('/Category/ToggleStatus', {
299
+ method: 'POST',
300
+ headers: {
301
+ 'Content-Type': 'application/json',
302
+ },
303
+ body: JSON.stringify({ id: id, isActive: !currentStatus })
304
+ });
305
+
306
+ if (response.ok) {
307
+ toolHub.showToast('状态更新成功', 'success');
308
+ setTimeout(() => location.reload(), 1000);
309
+ } else {
310
+ toolHub.showToast('操作失败,请重试', 'error');
311
+ }
312
+ } catch (error) {
313
+ console.error('操作失败:', error);
314
+ toolHub.showToast('操作失败,请重试', 'error');
315
+ }
316
+ }
317
+
318
+ async function deleteCategory(id, name) {
319
+ if (!confirm(`确定要删除分类"${name}"吗?\n注意:删除分类会影响该分类下的所有工具。`)) {
320
+ return;
321
+ }
322
+
323
+ try {
324
+ const response = await fetch('/Category/Delete', {
325
+ method: 'POST',
326
+ headers: {
327
+ 'Content-Type': 'application/json',
328
+ },
329
+ body: JSON.stringify({ id: id })
330
+ });
331
+
332
+ if (response.ok) {
333
+ toolHub.showToast('分类删除成功', 'success');
334
+ setTimeout(() => location.reload(), 1000);
335
+ } else {
336
+ toolHub.showToast('删除失败,可能该分类下还有工具', 'error');
337
+ }
338
+ } catch (error) {
339
+ console.error('删除失败:', error);
340
+ toolHub.showToast('删除失败,请重试', 'error');
341
+ }
342
+ }
343
+ </script>
Views/Home/Index.cshtml ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @{
2
+ ViewData["Title"] = "首页";
3
+ var categories = ViewBag.Categories as List<ToolHub.Models.Category> ?? new List<ToolHub.Models.Category>();
4
+ var hotTools = ViewBag.HotTools as List<ToolHub.Models.Tool> ?? new List<ToolHub.Models.Tool>();
5
+ var newTools = ViewBag.NewTools as List<ToolHub.Models.Tool> ?? new List<ToolHub.Models.Tool>();
6
+ }
7
+
8
+ <!-- 页面头部 -->
9
+ <div class="container" style="padding: 2rem 0 1rem;">
10
+ <div style="text-align: center; margin-bottom: 2rem;">
11
+ <h1 style="font-size: 2rem; font-weight: 700; margin-bottom: 0.5rem; color: var(--dark);">
12
+ <span class="text-gradient">ToolHub</span> 工具平台
13
+ </h1>
14
+ <p style="color: var(--dark-2); font-size: 1rem;">发现和使用最好的在线工具</p>
15
+ </div>
16
+
17
+ <!-- 快速工具入口 -->
18
+ <div style="display: flex; justify-content: center; flex-wrap: wrap; gap: 1rem; margin-bottom: 2rem;">
19
+ <a href="@Url.Action("Tools", "Home", new { categoryId = 1 })" class="quick-tool">
20
+ <i class="fas fa-file-pdf" style="color: var(--danger);"></i>
21
+ <span>PDF转换</span>
22
+ </a>
23
+ <a href="@Url.Action("Tools", "Home", new { categoryId = 2 })" class="quick-tool">
24
+ <i class="fas fa-image" style="color: var(--primary);"></i>
25
+ <span>图片工具</span>
26
+ </a>
27
+ <a href="@Url.Action("Tools", "Home", new { categoryId = 5 })" class="quick-tool">
28
+ <i class="fas fa-calculator" style="color: var(--success);"></i>
29
+ <span>数据换算</span>
30
+ </a>
31
+ <a href="@Url.Action("Tools", "Home")" class="quick-tool">
32
+ <i class="fas fa-ellipsis-h" style="color: var(--dark-2);"></i>
33
+ <span>更多工具</span>
34
+ </a>
35
+ </div>
36
+ </div>
37
+
38
+ <!-- 用户最近使用工具 -->
39
+ @if (User.Identity?.IsAuthenticated == true)
40
+ {
41
+ <section class="container mb-8">
42
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
43
+ <h3 style="font-size: 1.25rem; font-weight: 700; color: var(--dark);">
44
+ <i class="fas fa-clock" style="color: var(--warning); margin-right: 0.5rem;"></i>
45
+ 最近使用
46
+ </h3>
47
+ <a href="@Url.Action("Tools", "Home")" class="btn btn-outline btn-sm">
48
+ 查看更多
49
+ </a>
50
+ </div>
51
+
52
+ <div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6">
53
+ <!-- 这里将通过AJAX动态加载最近使用的工具 -->
54
+ <div id="recent-tools-container">
55
+ <div style="grid-column: 1 / -1; text-align: center; padding: 2rem; color: var(--dark-2);">
56
+ <i class="fas fa-clock" style="font-size: 2rem; margin-bottom: 1rem; display: block; opacity: 0.5;"></i>
57
+ 暂无最近使用的工具
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </section>
62
+
63
+ <!-- 用户收藏工具 -->
64
+ <section class="container mb-8">
65
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
66
+ <h3 style="font-size: 1.25rem; font-weight: 700; color: var(--dark);">
67
+ <i class="fas fa-heart" style="color: var(--danger); margin-right: 0.5rem;"></i>
68
+ 我的收藏
69
+ </h3>
70
+ <a href="@Url.Action("Tools", "Home")" class="btn btn-outline btn-sm">
71
+ 查看更多
72
+ </a>
73
+ </div>
74
+
75
+ <div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6">
76
+ <!-- 这里将通过AJAX动态加载收藏的工具 -->
77
+ <div id="favorite-tools-container">
78
+ <div style="grid-column: 1 / -1; text-align: center; padding: 2rem; color: var(--dark-2);">
79
+ <i class="fas fa-heart" style="font-size: 2rem; margin-bottom: 1rem; display: block; opacity: 0.5;"></i>
80
+ 暂无收藏的工具
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </section>
85
+ }
86
+
87
+ <!-- 最新工具区域 -->
88
+ @if (newTools.Any())
89
+ {
90
+ <section class="container mb-8">
91
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
92
+ <h3 style="font-size: 1.25rem; font-weight: 700; color: var(--dark);">
93
+ <i class="fas fa-sparkles" style="color: var(--accent); margin-right: 0.5rem;"></i>
94
+ 最新工具
95
+ </h3>
96
+ <a href="@Url.Action("Tools", "Home")" class="btn btn-outline btn-sm">
97
+ 查看更多
98
+ </a>
99
+ </div>
100
+
101
+ <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6">
102
+ @foreach (var tool in newTools.Take(6))
103
+ {
104
+ <div class="card" style="padding: 1.5rem; text-align: center;" data-tool-id="@tool.Id">
105
+ <div style="width: 3.5rem; height: 3.5rem; border-radius: 50%; background: var(--light-1); display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; color: var(--primary); transition: var(--transition);">
106
+ <i class="@tool.Icon" style="font-size: 1.5rem;"></i>
107
+ </div>
108
+ <h4 style="font-weight: 600; margin-bottom: 0.5rem; font-size: 0.9rem;">@tool.Name</h4>
109
+ <p style="font-size: 0.75rem; color: var(--dark-2); line-height: 1.4; margin: 0;">
110
+ @(tool.Description?.Length > 30 ? tool.Description.Substring(0, 30) + "..." : tool.Description)
111
+ </p>
112
+ @if (tool.IsNew)
113
+ {
114
+ <span class="badge" style="background: var(--accent); color: white; font-size: 0.65rem; margin-top: 0.5rem; display: inline-block;">新品</span>
115
+ }
116
+ </div>
117
+ }
118
+ </div>
119
+ </section>
120
+ }
121
+
122
+ <!-- 热门工具区域 -->
123
+ @if (hotTools.Any())
124
+ {
125
+ <section class="container mb-8">
126
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
127
+ <h3 style="font-size: 1.25rem; font-weight: 700; color: var(--dark);">
128
+ <i class="fas fa-fire" style="color: var(--danger); margin-right: 0.5rem;"></i>
129
+ 热门工具
130
+ </h3>
131
+ <a href="@Url.Action("Tools", "Home")" class="btn btn-outline btn-sm">
132
+ 查看更多
133
+ </a>
134
+ </div>
135
+
136
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
137
+ @foreach (var tool in hotTools.Take(4))
138
+ {
139
+ <div class="card tool-card" data-tool-id="@tool.Id">
140
+ @if (!string.IsNullOrEmpty(tool.Image))
141
+ {
142
+ <img src="@tool.Image" alt="@tool.Name" class="card-img" />
143
+ }
144
+ else
145
+ {
146
+ <div class="card-img" style="background: linear-gradient(135deg, var(--primary), var(--secondary)); display: flex; align-items: center; justify-content: center;">
147
+ <i class="@tool.Icon" style="font-size: 3rem; color: white;"></i>
148
+ </div>
149
+ }
150
+
151
+ @if (tool.IsHot)
152
+ {
153
+ <div class="tool-badge hot">热门</div>
154
+ }
155
+
156
+ <div class="card-body">
157
+ <h4 class="card-title">@tool.Name</h4>
158
+ <p class="card-text">@(tool.Description ?? "")</p>
159
+
160
+ <div class="card-footer">
161
+ <div class="tool-stats">
162
+ @if (tool.Rating > 0)
163
+ {
164
+ <div class="tool-rating">
165
+ <i class="fas fa-star rating-stars"></i>
166
+ <span>@tool.Rating.ToString("F1")</span>
167
+ </div>
168
+ }
169
+ <span>@(tool.ViewCount > 1000 ? (tool.ViewCount / 1000).ToString("F0") + "K" : tool.ViewCount.ToString()) 次使用</span>
170
+ </div>
171
+ <a href="@(tool.Url ?? "#")" class="btn btn-primary btn-sm" target="_blank">
172
+ 使用
173
+ </a>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ }
178
+ </div>
179
+ </section>
180
+ }
181
+
182
+ @section Scripts {
183
+ <script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.4.3/echarts.min.js"></script>
184
+ }
Views/Home/Privacy.cshtml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ @{
2
+ ViewData["Title"] = "Privacy Policy";
3
+ }
4
+ <h1>@ViewData["Title"]</h1>
5
+
6
+ <p>Use this page to detail your site's privacy policy.</p>
Views/Home/Tools.cshtml ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @model List<ToolHub.Models.Tool>
2
+ @{
3
+ ViewData["Title"] = "工具列表";
4
+ var categories = ViewBag.Categories as List<ToolHub.Models.Category> ?? new List<ToolHub.Models.Category>();
5
+ var currentCategory = ViewBag.CurrentCategory as int? ?? 0;
6
+ var search = ViewBag.Search as string;
7
+ }
8
+
9
+ <div class="container">
10
+ <!-- 页面标题 -->
11
+ <div class="text-center mb-6" style="padding: 2rem 0;">
12
+ <h1 style="font-size: 2rem; font-weight: 700; margin-bottom: 1rem; color: var(--dark);">
13
+ @if (!string.IsNullOrEmpty(search))
14
+ {
15
+ <span>搜索结果: "@search"</span>
16
+ }
17
+ else if (currentCategory > 0)
18
+ {
19
+ var categoryName = categories.FirstOrDefault(c => c.Id == currentCategory)?.Name ?? "工具";
20
+ <span>@categoryName</span>
21
+ }
22
+ else
23
+ {
24
+ <span>所有工具</span>
25
+ }
26
+ </h1>
27
+ <p style="color: var(--dark-2);">发现更多实用工具,提升您的工作效率</p>
28
+ </div>
29
+
30
+ <!-- 分类标签 -->
31
+ @if (string.IsNullOrEmpty(search))
32
+ {
33
+ <div class="category-tabs mb-6">
34
+ <a href="@Url.Action("Tools", "Home")" class="category-tab @(currentCategory == 0 ? "active" : "")">
35
+ 全部工具
36
+ </a>
37
+ @foreach (var category in categories)
38
+ {
39
+ <a href="@Url.Action("Tools", "Home", new { categoryId = category.Id })"
40
+ class="category-tab @(currentCategory == category.Id ? "active" : "")">
41
+ <i class="@category.Icon" style="margin-right: 0.5rem;"></i>
42
+ @category.Name
43
+ </a>
44
+ }
45
+ </div>
46
+ }
47
+
48
+ <!-- 工具网格 -->
49
+ @if (Model.Any())
50
+ {
51
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
52
+ @foreach (var tool in Model)
53
+ {
54
+ <div class="card tool-card" data-tool-id="@tool.Id">
55
+ @if (!string.IsNullOrEmpty(tool.Image))
56
+ {
57
+ <img src="@tool.Image" alt="@tool.Name" class="card-img" />
58
+ }
59
+ else
60
+ {
61
+ <div class="card-img" style="background: linear-gradient(135deg, var(--primary), var(--secondary)); display: flex; align-items: center; justify-content: center;">
62
+ <i class="@tool.Icon" style="font-size: 3rem; color: white;"></i>
63
+ </div>
64
+ }
65
+
66
+ <!-- 工具标签 -->
67
+ @if (tool.IsHot)
68
+ {
69
+ <div class="tool-badge hot">热门</div>
70
+ }
71
+ @if (tool.IsNew)
72
+ {
73
+ <div class="tool-badge new">新品</div>
74
+ }
75
+ @if (tool.IsRecommended)
76
+ {
77
+ <div class="tool-badge recommended">推荐</div>
78
+ }
79
+
80
+ <div class="card-body">
81
+ <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.75rem;">
82
+ <div style="flex: 1;">
83
+ <h4 class="card-title">@tool.Name</h4>
84
+ <p class="card-text">@(tool.Description ?? "")</p>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="card-footer">
89
+ <div class="tool-stats">
90
+ @if (tool.Rating > 0)
91
+ {
92
+ <div class="tool-rating">
93
+ <i class="fas fa-star rating-stars"></i>
94
+ <span>@tool.Rating.ToString("F1")</span>
95
+ </div>
96
+ }
97
+ <span>@(tool.ViewCount > 1000 ? (tool.ViewCount / 1000).ToString("F0") + "K" : tool.ViewCount.ToString()) 次使用</span>
98
+ </div>
99
+ <a href="@(tool.Url ?? "#")" class="btn btn-primary btn-sm" target="_blank">
100
+ 使用
101
+ </a>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ }
106
+ </div>
107
+
108
+ <!-- 分页 (如果需要的话) -->
109
+ <div class="text-center mt-8">
110
+ <button class="btn btn-outline" id="loadMore" style="display: none;">
111
+ <span>加载更多</span>
112
+ <i class="fas fa-chevron-down"></i>
113
+ </button>
114
+ </div>
115
+ }
116
+ else
117
+ {
118
+ <!-- 空状态 -->
119
+ <div class="text-center" style="padding: 4rem 2rem;">
120
+ <div style="font-size: 4rem; color: var(--light-3); margin-bottom: 1rem;">
121
+ <i class="fas fa-search"></i>
122
+ </div>
123
+ <h3 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--dark);">
124
+ @if (!string.IsNullOrEmpty(search))
125
+ {
126
+ <span>未找到相关工具</span>
127
+ }
128
+ else
129
+ {
130
+ <span>暂无工具</span>
131
+ }
132
+ </h3>
133
+ <p style="color: var(--dark-2); margin-bottom: 2rem;">
134
+ @if (!string.IsNullOrEmpty(search))
135
+ {
136
+ <span>试试其他关键词,或浏览其他分类的工具</span>
137
+ }
138
+ else
139
+ {
140
+ <span>该分类下暂时没有工具,敬请期待</span>
141
+ }
142
+ </p>
143
+ <a href="@Url.Action("Tools", "Home")" class="btn btn-primary">
144
+ <i class="fas fa-th-large" style="margin-right: 0.5rem;"></i>
145
+ <span>浏览所有工具</span>
146
+ </a>
147
+ </div>
148
+ }
149
+ </div>
150
+
151
+ <style>
152
+ /* 工具页面特定样式 */
153
+ .category-tabs {
154
+ scrollbar-width: none;
155
+ -ms-overflow-style: none;
156
+ }
157
+
158
+ .category-tabs::-webkit-scrollbar {
159
+ display: none;
160
+ }
161
+
162
+ @@media (max-width: 767px) {
163
+ .category-tabs {
164
+ padding-bottom: 1rem;
165
+ }
166
+
167
+ .grid {
168
+ grid-template-columns: repeat(1, 1fr);
169
+ }
170
+ }
171
+
172
+ @@media (min-width: 768px) and (max-width: 1023px) {
173
+ .grid {
174
+ grid-template-columns: repeat(2, 1fr);
175
+ }
176
+ }
177
+
178
+ @@media (min-width: 1024px) and (max-width: 1279px) {
179
+ .grid {
180
+ grid-template-columns: repeat(3, 1fr);
181
+ }
182
+ }
183
+
184
+ @@media (min-width: 1280px) {
185
+ .grid {
186
+ grid-template-columns: repeat(4, 1fr);
187
+ }
188
+ }
189
+ </style>
Views/Shared/Error.cshtml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @model ErrorViewModel
2
+ @{
3
+ ViewData["Title"] = "Error";
4
+ }
5
+
6
+ <h1 class="text-danger">Error.</h1>
7
+ <h2 class="text-danger">An error occurred while processing your request.</h2>
8
+
9
+ @if (Model.ShowRequestId)
10
+ {
11
+ <p>
12
+ <strong>Request ID:</strong> <code>@Model.RequestId</code>
13
+ </p>
14
+ }
15
+
16
+ <h3>Development Mode</h3>
17
+ <p>
18
+ Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
19
+ </p>
20
+ <p>
21
+ <strong>The Development environment shouldn't be enabled for deployed applications.</strong>
22
+ It can result in displaying sensitive information from exceptions to end users.
23
+ For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
24
+ and restarting the app.
25
+ </p>
Views/Shared/_AdminLayout.cshtml ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>@ViewData["Title"] - ToolHub 管理后台</title>
7
+
8
+ <!-- 外部字体和图标 -->
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
10
+ <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet" />
11
+
12
+ <!-- 样式文件 -->
13
+ <link rel="stylesheet" href="~/css/toolhub.css" asp-append-version="true" />
14
+
15
+ <style>
16
+ .admin-sidebar {
17
+ width: 250px;
18
+ background: white;
19
+ box-shadow: var(--shadow);
20
+ position: fixed;
21
+ top: 0;
22
+ left: 0;
23
+ height: 100vh;
24
+ z-index: 1000;
25
+ overflow-y: auto;
26
+ }
27
+
28
+ .admin-main {
29
+ margin-left: 250px;
30
+ min-height: 100vh;
31
+ background: #fafafa;
32
+ }
33
+
34
+ .admin-header {
35
+ background: white;
36
+ padding: 1rem 2rem;
37
+ box-shadow: var(--shadow-sm);
38
+ display: flex;
39
+ justify-content: space-between;
40
+ align-items: center;
41
+ }
42
+
43
+ .admin-content {
44
+ padding: 2rem;
45
+ }
46
+
47
+ .sidebar-brand {
48
+ padding: 1.5rem;
49
+ border-bottom: 1px solid var(--light-2);
50
+ text-align: center;
51
+ }
52
+
53
+ .sidebar-nav {
54
+ padding: 1rem 0;
55
+ }
56
+
57
+ .sidebar-nav-item {
58
+ display: block;
59
+ padding: 0.75rem 1.5rem;
60
+ color: var(--dark-2);
61
+ text-decoration: none;
62
+ transition: var(--transition);
63
+ border-left: 3px solid transparent;
64
+ }
65
+
66
+ .sidebar-nav-item:hover,
67
+ .sidebar-nav-item.active {
68
+ background: var(--light-1);
69
+ color: var(--primary);
70
+ border-left-color: var(--primary);
71
+ text-decoration: none;
72
+ }
73
+
74
+ .sidebar-nav-item i {
75
+ width: 1.5rem;
76
+ margin-right: 0.75rem;
77
+ }
78
+
79
+ .stats-card {
80
+ background: white;
81
+ padding: 1.5rem;
82
+ border-radius: var(--border-radius);
83
+ box-shadow: var(--shadow-sm);
84
+ transition: var(--transition);
85
+ }
86
+
87
+ .stats-card:hover {
88
+ transform: translateY(-2px);
89
+ box-shadow: var(--shadow);
90
+ }
91
+
92
+ .stats-number {
93
+ font-size: 2rem;
94
+ font-weight: 700;
95
+ margin-bottom: 0.5rem;
96
+ }
97
+
98
+ .stats-label {
99
+ color: var(--dark-2);
100
+ font-size: 0.875rem;
101
+ }
102
+
103
+ .data-table {
104
+ background: white;
105
+ border-radius: var(--border-radius);
106
+ box-shadow: var(--shadow-sm);
107
+ overflow: hidden;
108
+ }
109
+
110
+ .data-table table {
111
+ width: 100%;
112
+ border-collapse: collapse;
113
+ }
114
+
115
+ .data-table th,
116
+ .data-table td {
117
+ padding: 0.75rem 1rem;
118
+ text-align: left;
119
+ border-bottom: 1px solid var(--light-2);
120
+ }
121
+
122
+ .data-table th {
123
+ background: var(--light-1);
124
+ font-weight: 600;
125
+ color: var(--dark);
126
+ }
127
+
128
+ .data-table tbody tr:hover {
129
+ background: var(--light-1);
130
+ }
131
+
132
+ .badge {
133
+ display: inline-block;
134
+ padding: 0.25rem 0.75rem;
135
+ font-size: 0.75rem;
136
+ font-weight: 500;
137
+ border-radius: 50px;
138
+ color: white;
139
+ }
140
+
141
+ .badge-primary { background: var(--primary); }
142
+ .badge-success { background: var(--success); }
143
+ .badge-warning { background: var(--warning); }
144
+ .badge-danger { background: var(--danger); }
145
+ .badge-secondary { background: var(--dark-2); }
146
+
147
+ @@media (max-width: 768px) {
148
+ .admin-sidebar {
149
+ transform: translateX(-100%);
150
+ transition: transform 0.3s ease;
151
+ }
152
+
153
+ .admin-sidebar.show {
154
+ transform: translateX(0);
155
+ }
156
+
157
+ .admin-main {
158
+ margin-left: 0;
159
+ }
160
+ }
161
+ </style>
162
+
163
+ @await RenderSectionAsync("Styles", required: false)
164
+ </head>
165
+ <body class="admin-page">
166
+ <!-- 侧边栏 -->
167
+ <div class="admin-sidebar">
168
+ <div class="sidebar-brand">
169
+ <div class="brand-icon" style="margin-bottom: 0.5rem;">
170
+ <i class="fas fa-wrench"></i>
171
+ </div>
172
+ <h5 style="margin: 0; font-weight: 700;">
173
+ <span class="text-gradient">ToolHub</span>
174
+ </h5>
175
+ <p style="font-size: 0.75rem; color: var(--dark-2); margin: 0.25rem 0 0;">管理后台</p>
176
+ </div>
177
+
178
+ <nav class="sidebar-nav">
179
+ <a href="@Url.Action("Index", "Admin")" class="sidebar-nav-item @(ViewContext.RouteData.Values["action"]?.ToString() == "Index" ? "active" : "")">
180
+ <i class="fas fa-chart-line"></i>
181
+ 仪表板
182
+ </a>
183
+ <a href="@Url.Action("Index", "Category")" class="sidebar-nav-item @(ViewContext.RouteData.Values["controller"]?.ToString() == "Category" ? "active" : "")">
184
+ <i class="fas fa-tags"></i>
185
+ 工具分类
186
+ </a>
187
+ <a href="@Url.Action("Index", "Tag")" class="sidebar-nav-item @(ViewContext.RouteData.Values["controller"]?.ToString() == "Tag" ? "active" : "")">
188
+ <i class="fas fa-tags"></i>
189
+ 标签管理
190
+ </a>
191
+ <a href="@Url.Action("Index", "Tool")" class="sidebar-nav-item @(ViewContext.RouteData.Values["controller"]?.ToString() == "Tool" ? "active" : "")">
192
+ <i class="fas fa-tools"></i>
193
+ 工具管理
194
+ </a>
195
+ <a href="@Url.Action("Index", "ToolStatistics")" class="sidebar-nav-item @(ViewContext.RouteData.Values["controller"]?.ToString() == "ToolStatistics" ? "active" : "")">
196
+ <i class="fas fa-chart-bar"></i>
197
+ 工具统计
198
+ </a>
199
+ <a href="@Url.Action("Users", "Admin")" class="sidebar-nav-item @(ViewContext.RouteData.Values["action"]?.ToString() == "Users" ? "active" : "")">
200
+ <i class="fas fa-users"></i>
201
+ 用户管理
202
+ </a>
203
+ <a href="@Url.Action("Index", "Home")" class="sidebar-nav-item" target="_blank">
204
+ <i class="fas fa-external-link-alt"></i>
205
+ 查看网站
206
+ </a>
207
+ </nav>
208
+ </div>
209
+
210
+ <!-- 主内容区 -->
211
+ <div class="admin-main">
212
+ <!-- 顶部导航 -->
213
+ <header class="admin-header">
214
+ <div style="display: flex; align-items: center;">
215
+ <button class="btn btn-outline btn-sm mobile-sidebar-toggle" style="display: none; margin-right: 1rem;">
216
+ <i class="fas fa-bars"></i>
217
+ </button>
218
+ <h1 style="font-size: 1.25rem; font-weight: 600; margin: 0; color: var(--dark);">
219
+ @ViewData["Title"]
220
+ </h1>
221
+ </div>
222
+
223
+ <div style="display: flex; align-items: center; gap: 1rem;">
224
+ <span style="font-size: 0.875rem; color: var(--dark-2);">
225
+ 欢迎,<strong>@User.Identity?.Name</strong>
226
+ </span>
227
+ <form method="post" action="@Url.Action("Logout", "Admin")" style="margin: 0;">
228
+ <button type="submit" class="btn btn-outline btn-sm">
229
+ <i class="fas fa-sign-out-alt"></i>
230
+ 退出
231
+ </button>
232
+ </form>
233
+ </div>
234
+ </header>
235
+
236
+ <!-- 内容区 -->
237
+ <main class="admin-content">
238
+ @RenderBody()
239
+ </main>
240
+ </div>
241
+
242
+ <!-- JavaScript -->
243
+ <script src="~/js/toolhub.js" asp-append-version="true"></script>
244
+
245
+ <script>
246
+ // 移动端侧边栏切换
247
+ document.addEventListener('DOMContentLoaded', function() {
248
+ const toggleBtn = document.querySelector('.mobile-sidebar-toggle');
249
+ const sidebar = document.querySelector('.admin-sidebar');
250
+
251
+ if (toggleBtn && sidebar) {
252
+ // 显示移动端按钮
253
+ if (window.innerWidth <= 768) {
254
+ toggleBtn.style.display = 'inline-flex';
255
+ }
256
+
257
+ // 切换侧边栏
258
+ toggleBtn.addEventListener('click', function() {
259
+ sidebar.classList.toggle('show');
260
+ });
261
+
262
+ // 点击外部关闭
263
+ document.addEventListener('click', function(e) {
264
+ if (window.innerWidth <= 768 &&
265
+ !sidebar.contains(e.target) &&
266
+ !toggleBtn.contains(e.target)) {
267
+ sidebar.classList.remove('show');
268
+ }
269
+ });
270
+
271
+ // 响应式处理
272
+ window.addEventListener('resize', function() {
273
+ if (window.innerWidth <= 768) {
274
+ toggleBtn.style.display = 'inline-flex';
275
+ } else {
276
+ toggleBtn.style.display = 'none';
277
+ sidebar.classList.remove('show');
278
+ }
279
+ });
280
+ }
281
+ });
282
+ </script>
283
+
284
+ @await RenderSectionAsync("Scripts", required: false)
285
+ </body>
286
+ </html>
Views/Shared/_Layout.cshtml ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>@ViewData["Title"] - ToolHub</title>
7
+ <meta name="description" content="ToolHub - 一站式在线工具平台,集成图片处理、文档转换、数据计算等多种实用工具" />
8
+ <meta name="keywords" content="在线工具,PDF转换,图片处理,文档转换,数据计算" />
9
+
10
+ <!-- Favicon -->
11
+ <link rel="icon" type="image/x-icon" href="~/favicon.ico" />
12
+
13
+ <!-- 外部字体和图标 -->
14
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
15
+ <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet" />
16
+
17
+ <!-- 样式文件 -->
18
+ <link rel="stylesheet" href="~/css/toolhub.css" asp-append-version="true" />
19
+
20
+ @await RenderSectionAsync("Styles", required: false)
21
+ </head>
22
+ <body>
23
+ <!-- 导航栏 -->
24
+ <header class="navbar">
25
+ <div class="container">
26
+ <div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
27
+ <!-- Logo -->
28
+ <a href="@Url.Action("Index", "Home")" class="navbar-brand">
29
+ <div class="brand-icon">
30
+ <i class="fas fa-wrench"></i>
31
+ </div>
32
+ <span class="text-gradient">ToolHub</span>
33
+ </a>
34
+
35
+ <!-- 搜索框 - 桌面端 -->
36
+ <div class="search-container" style="display: none;">
37
+ <form class="search-form" action="@Url.Action("Tools", "Home")" method="get">
38
+ <input type="text" name="search" class="search-input" placeholder="搜索工具、文档或教程..." value="@ViewBag.Search" />
39
+ <i class="fas fa-search search-icon"></i>
40
+ </form>
41
+ </div>
42
+
43
+ <!-- 导航链接和按钮 -->
44
+ <div style="display: flex; align-items: center; gap: 1rem;">
45
+ <!-- 桌面端链接 -->
46
+ <nav style="display: none;">
47
+ <a href="@Url.Action("Tools", "Home")" class="btn btn-outline btn-sm">
48
+ <i class="fas fa-th-large"></i>
49
+ <span>所有工具</span>
50
+ </a>
51
+ <a href="@Url.Action("Privacy", "Home")" class="btn btn-outline btn-sm">
52
+ <i class="fas fa-shield-alt"></i>
53
+ <span>隐私政策</span>
54
+ </a>
55
+ </nav>
56
+
57
+ <!-- 移动端菜单按钮 -->
58
+ <button class="mobile-menu-toggle btn btn-outline btn-sm">
59
+ <i class="fas fa-bars"></i>
60
+ </button>
61
+ </div>
62
+ </div>
63
+
64
+ <!-- 移动端搜索容器 -->
65
+ <div class="mobile-search-container" style="margin-top: 1rem; display: none;">
66
+ <form class="search-form" action="@Url.Action("Tools", "Home")" method="get">
67
+ <input type="text" name="search" class="search-input" placeholder="搜索工具..." value="@ViewBag.Search" />
68
+ <i class="fas fa-search search-icon"></i>
69
+ </form>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- 移动端菜单 -->
74
+ <div class="mobile-menu" style="display: none;">
75
+ <div class="container">
76
+ <div style="padding: 1rem 0; border-top: 1px solid var(--light-2);">
77
+ <a href="@Url.Action("Tools", "Home")" style="display: flex; align-items: center; padding: 0.75rem; text-decoration: none; color: var(--dark);">
78
+ <i class="fas fa-th-large" style="width: 1.5rem; margin-right: 0.75rem;"></i>
79
+ <span>所有工具</span>
80
+ </a>
81
+ <a href="@Url.Action("Privacy", "Home")" style="display: flex; align-items: center; padding: 0.75rem; text-decoration: none; color: var(--dark);">
82
+ <i class="fas fa-shield-alt" style="width: 1.5rem; margin-right: 0.75rem;"></i>
83
+ <span>隐私政策</span>
84
+ </a>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </header>
89
+
90
+ <!-- 主内容区 -->
91
+ <main style="min-height: calc(100vh - 200px); padding-top: 5rem;">
92
+ @RenderBody()
93
+ </main>
94
+
95
+ <!-- 页脚 -->
96
+ <footer class="footer">
97
+ <div class="container">
98
+ <div class="footer-grid">
99
+ <!-- 品牌信息 -->
100
+ <div class="footer-section">
101
+ <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem;">
102
+ <div class="brand-icon">
103
+ <i class="fas fa-wrench"></i>
104
+ </div>
105
+ <span style="font-size: 1.25rem; font-weight: 700; color: white;">ToolHub</span>
106
+ </div>
107
+ <p style="margin-bottom: 1.5rem; max-width: 300px;">
108
+ 一站式在线工具平台,集成图片处理、文档转换、数据计算等多种实用工具,满足您的日常工作和学习需求。
109
+ </p>
110
+ <div class="social-links">
111
+ <a href="#" class="social-link">
112
+ <i class="fab fa-weixin"></i>
113
+ </a>
114
+ <a href="#" class="social-link">
115
+ <i class="fab fa-weibo"></i>
116
+ </a>
117
+ <a href="#" class="social-link">
118
+ <i class="fab fa-qq"></i>
119
+ </a>
120
+ <a href="#" class="social-link">
121
+ <i class="fab fa-github"></i>
122
+ </a>
123
+ </div>
124
+ </div>
125
+
126
+ <!-- 快速链接 -->
127
+ <div class="footer-section">
128
+ <h4>快速链接</h4>
129
+ <ul>
130
+ <li><a href="@Url.Action("Index", "Home")">首页</a></li>
131
+ <li><a href="@Url.Action("Tools", "Home")">全部工具</a></li>
132
+ <li><a href="#">关于我们</a></li>
133
+ <li><a href="#">联系我们</a></li>
134
+ </ul>
135
+ </div>
136
+
137
+ <!-- 支持 -->
138
+ <div class="footer-section">
139
+ <h4>支持</h4>
140
+ <ul>
141
+ <li><a href="#">帮助中心</a></li>
142
+ <li><a href="#">使用教程</a></li>
143
+ <li><a href="#">常见问题</a></li>
144
+ <li><a href="#">反馈建议</a></li>
145
+ </ul>
146
+ </div>
147
+
148
+ <!-- 法律 -->
149
+ <div class="footer-section">
150
+ <h4>法律</h4>
151
+ <ul>
152
+ <li><a href="@Url.Action("Privacy", "Home")">隐私政策</a></li>
153
+ <li><a href="#">服务条款</a></li>
154
+ <li><a href="#">版权声明</a></li>
155
+ <li><a href="#">免责声明</a></li>
156
+ </ul>
157
+ </div>
158
+ </div>
159
+
160
+ <div class="footer-bottom">
161
+ <p style="color: var(--light-3); font-size: 0.875rem;">
162
+ © 2025 ToolHub. 保留所有权利。
163
+ </p>
164
+ <div style="display: flex; gap: 1.5rem;">
165
+ <a href="@Url.Action("Privacy", "Home")" style="color: var(--light-3); font-size: 0.875rem; text-decoration: none;">隐私政策</a>
166
+ <a href="#" style="color: var(--light-3); font-size: 0.875rem; text-decoration: none;">服务条款</a>
167
+ <a href="#" style="color: var(--light-3); font-size: 0.875rem; text-decoration: none;">Cookies</a>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </footer>
172
+
173
+ <!-- 返回顶部按钮 -->
174
+ <button class="back-to-top">
175
+ <i class="fas fa-chevron-up"></i>
176
+ </button>
177
+
178
+ <!-- 提示消息容器 -->
179
+ <div id="toast-container" style="position: fixed; top: 2rem; right: 2rem; z-index: 10000;"></div>
180
+
181
+ <!-- JavaScript -->
182
+ <script src="~/js/toolhub.js" asp-append-version="true"></script>
183
+
184
+ @await RenderSectionAsync("Scripts", required: false)
185
+
186
+ <style>
187
+ /* 移动端响应式 */
188
+ @@media (min-width: 768px) {
189
+ .search-container {
190
+ display: block !important;
191
+ }
192
+
193
+ nav {
194
+ display: flex !important;
195
+ gap: 0.5rem;
196
+ }
197
+
198
+ .mobile-menu-toggle {
199
+ display: none !important;
200
+ }
201
+ }
202
+
203
+ /* 提示消息样式 */
204
+ .toast {
205
+ position: relative;
206
+ background: white;
207
+ border-radius: var(--border-radius);
208
+ box-shadow: var(--shadow-lg);
209
+ padding: 1rem;
210
+ margin-bottom: 0.5rem;
211
+ transform: translateX(100%);
212
+ transition: transform 0.3s ease;
213
+ border-left: 4px solid var(--primary);
214
+ }
215
+
216
+ .toast.show {
217
+ transform: translateX(0);
218
+ }
219
+
220
+ .toast.toast-success {
221
+ border-left-color: var(--success);
222
+ }
223
+
224
+ .toast.toast-error {
225
+ border-left-color: var(--danger);
226
+ }
227
+
228
+ .toast.toast-warning {
229
+ border-left-color: var(--warning);
230
+ }
231
+
232
+ .toast-content {
233
+ display: flex;
234
+ align-items: center;
235
+ gap: 0.75rem;
236
+ }
237
+
238
+ .toast-content i {
239
+ font-size: 1.25rem;
240
+ }
241
+
242
+ .toast-success .toast-content i {
243
+ color: var(--success);
244
+ }
245
+
246
+ .toast-error .toast-content i {
247
+ color: var(--danger);
248
+ }
249
+
250
+ .toast-warning .toast-content i {
251
+ color: var(--warning);
252
+ }
253
+
254
+ .toast-info .toast-content i {
255
+ color: var(--primary);
256
+ }
257
+
258
+ /* 移动端菜单动画 */
259
+ .mobile-menu {
260
+ background: white;
261
+ border-top: 1px solid var(--light-2);
262
+ box-shadow: var(--shadow);
263
+ max-height: 0;
264
+ overflow: hidden;
265
+ transition: max-height 0.3s ease;
266
+ }
267
+
268
+ .mobile-menu.show {
269
+ max-height: 300px;
270
+ }
271
+
272
+ .mobile-search-container {
273
+ max-height: 0;
274
+ overflow: hidden;
275
+ transition: max-height 0.3s ease;
276
+ }
277
+
278
+ .mobile-search-container.show {
279
+ max-height: 100px;
280
+ }
281
+ </style>
282
+ </body>
283
+ </html>
Views/Shared/_Layout.cshtml.css ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
2
+ for details on configuring this project to bundle and minify static web assets. */
3
+
4
+ a.navbar-brand {
5
+ white-space: normal;
6
+ text-align: center;
7
+ word-break: break-all;
8
+ }
9
+
10
+ a {
11
+ color: #0077cc;
12
+ }
13
+
14
+ .btn-primary {
15
+ color: #fff;
16
+ background-color: #1b6ec2;
17
+ border-color: #1861ac;
18
+ }
19
+
20
+ .nav-pills .nav-link.active, .nav-pills .show > .nav-link {
21
+ color: #fff;
22
+ background-color: #1b6ec2;
23
+ border-color: #1861ac;
24
+ }
25
+
26
+ .border-top {
27
+ border-top: 1px solid #e5e5e5;
28
+ }
29
+ .border-bottom {
30
+ border-bottom: 1px solid #e5e5e5;
31
+ }
32
+
33
+ .box-shadow {
34
+ box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
35
+ }
36
+
37
+ button.accept-policy {
38
+ font-size: 1rem;
39
+ line-height: inherit;
40
+ }
41
+
42
+ .footer {
43
+ position: absolute;
44
+ bottom: 0;
45
+ width: 100%;
46
+ white-space: nowrap;
47
+ line-height: 60px;
48
+ }
Views/Shared/_ValidationScriptsPartial.cshtml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ <script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
2
+ <script src="~/lib/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js"></script>
Views/Tag/Index.cshtml ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @model List<ToolHub.Models.Tag>
2
+ @{
3
+ ViewData["Title"] = "标签管理";
4
+ Layout = "_AdminLayout";
5
+ var currentPage = ViewBag.CurrentPage as int? ?? 1;
6
+ var totalPages = ViewBag.TotalPages as int? ?? 1;
7
+ var totalCount = ViewBag.TotalCount as int? ?? 0;
8
+ var pageSize = ViewBag.PageSize as int? ?? 20;
9
+ }
10
+
11
+ <!-- 页面头部 -->
12
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
13
+ <div>
14
+ <h2 style="font-size: 1.5rem; font-weight: 700; margin: 0; color: var(--dark);">标签管理</h2>
15
+ <p style="color: var(--dark-2); margin: 0.5rem 0 0;">管理工具标签,为工具添加分类标识</p>
16
+ </div>
17
+ <button class="btn btn-primary" onclick="openTagModal()">
18
+ <i class="fas fa-plus"></i>
19
+ 添加标签
20
+ </button>
21
+ </div>
22
+
23
+ <!-- 标签列表 -->
24
+ <div class="data-table">
25
+ <div style="padding: 1rem 1.5rem; border-bottom: 1px solid var(--light-2); display: flex; justify-content: space-between; align-items: center;">
26
+ <span style="font-weight: 600;">标签列表 (@totalCount)</span>
27
+ </div>
28
+
29
+ <table>
30
+ <thead>
31
+ <tr>
32
+ <th>标签名称</th>
33
+ <th>颜色</th>
34
+ <th>使用次数</th>
35
+ <th>创建时间</th>
36
+ <th>状态</th>
37
+ <th>操作</th>
38
+ </tr>
39
+ </thead>
40
+ <tbody>
41
+ @foreach (var tag in Model)
42
+ {
43
+ <tr>
44
+ <td>
45
+ <div style="display: flex; align-items: center;">
46
+ <span class="badge" style="background: @(tag.Color ?? "var(--primary)"); color: white; margin-right: 0.75rem;">
47
+ @tag.Name
48
+ </span>
49
+ <strong>@tag.Name</strong>
50
+ </div>
51
+ </td>
52
+ <td>
53
+ @if (!string.IsNullOrEmpty(tag.Color))
54
+ {
55
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
56
+ <div style="width: 20px; height: 20px; background: @tag.Color; border-radius: 4px; border: 1px solid var(--light-2);"></div>
57
+ <code style="font-size: 0.75rem;">@tag.Color</code>
58
+ </div>
59
+ }
60
+ else
61
+ {
62
+ <span style="color: var(--dark-2);">默认</span>
63
+ }
64
+ </td>
65
+ <td>
66
+ <span class="badge badge-primary">@tag.ToolTags?.Count</span>
67
+ </td>
68
+ <td style="color: var(--dark-2); font-size: 0.875rem;">
69
+ @tag.CreatedAt.ToString("yyyy-MM-dd")
70
+ </td>
71
+ <td>
72
+ @if (tag.IsActive)
73
+ {
74
+ <span class="badge badge-success">启用</span>
75
+ }
76
+ else
77
+ {
78
+ <span class="badge badge-secondary">禁用</span>
79
+ }
80
+ </td>
81
+ <td>
82
+ <div style="display: flex; gap: 0.5rem;">
83
+ <button class="btn btn-outline btn-sm" onclick="editTag(@tag.Id, '@tag.Name', '@tag.Color')">
84
+ <i class="fas fa-edit"></i>
85
+ </button>
86
+ <button class="btn btn-outline btn-sm" onclick="toggleTagStatus(@tag.Id, @tag.IsActive.ToString().ToLower())" style="color: @(tag.IsActive ? "var(--warning)" : "var(--success)");">
87
+ <i class="fas fa-@(tag.IsActive ? "pause" : "play")"></i>
88
+ </button>
89
+ <button class="btn btn-outline btn-sm" onclick="deleteTag(@tag.Id, '@tag.Name')" style="color: var(--danger);">
90
+ <i class="fas fa-trash"></i>
91
+ </button>
92
+ </div>
93
+ </td>
94
+ </tr>
95
+ }
96
+ </tbody>
97
+ </table>
98
+
99
+ @if (!Model.Any())
100
+ {
101
+ <div style="padding: 3rem; text-align: center; color: var(--dark-2);">
102
+ <i class="fas fa-tags" style="font-size: 3rem; margin-bottom: 1rem; display: block; opacity: 0.3;"></i>
103
+ <h3 style="margin-bottom: 0.5rem;">暂无标签</h3>
104
+ <p style="margin-bottom: 1.5rem;">还没有创建任何标签</p>
105
+ <button class="btn btn-primary" onclick="openTagModal()">
106
+ <i class="fas fa-plus"></i>
107
+ 创建第一个标签
108
+ </button>
109
+ </div>
110
+ }
111
+
112
+ <!-- 分页控件 -->
113
+ @if (totalPages > 1)
114
+ {
115
+ <div style="padding: 1.5rem; border-top: 1px solid var(--light-2); display: flex; justify-content: center; align-items: center; gap: 0.5rem;">
116
+ <button class="btn btn-outline btn-sm" onclick="changePage(1)" @(currentPage == 1 ? "disabled" : "")>
117
+ <i class="fas fa-angle-double-left"></i>
118
+ </button>
119
+ <button class="btn btn-outline btn-sm" onclick="changePage(@(currentPage - 1))" @(currentPage == 1 ? "disabled" : "")>
120
+ <i class="fas fa-angle-left"></i>
121
+ </button>
122
+
123
+ @{
124
+ var startPage = Math.Max(1, currentPage - 2);
125
+ var endPage = Math.Min(totalPages, currentPage + 2);
126
+ }
127
+
128
+ @for (int i = startPage; i <= endPage; i++)
129
+ {
130
+ <button class="btn @(i == currentPage ? "btn-primary" : "btn-outline") btn-sm" onclick="changePage(@i)">
131
+ @i
132
+ </button>
133
+ }
134
+
135
+ <button class="btn btn-outline btn-sm" onclick="changePage(@(currentPage + 1))" @(currentPage == totalPages ? "disabled" : "")>
136
+ <i class="fas fa-angle-right"></i>
137
+ </button>
138
+ <button class="btn btn-outline btn-sm" onclick="changePage(@totalPages)" @(currentPage == totalPages ? "disabled" : "")>
139
+ <i class="fas fa-angle-double-right"></i>
140
+ </button>
141
+ </div>
142
+ }
143
+ </div>
144
+
145
+ <!-- 标签模态框 -->
146
+ <div id="tagModal" class="modal" style="display: none;">
147
+ <div class="modal-backdrop" onclick="closeTagModal()"></div>
148
+ <div class="modal-content" style="max-width: 400px;">
149
+ <div class="modal-header">
150
+ <h3 id="modalTitle">添加标签</h3>
151
+ <button onclick="closeTagModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer;">×</button>
152
+ </div>
153
+
154
+ <form id="tagForm" onsubmit="submitTag(event)">
155
+ <input type="hidden" id="tagId" name="id" value="0" />
156
+
157
+ <div class="form-group" style="margin-bottom: 1.5rem;">
158
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">标签名称 *</label>
159
+ <input type="text" id="tagName" name="name" required
160
+ style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);"
161
+ placeholder="请输入标签名称" />
162
+ </div>
163
+
164
+ <div class="form-group" style="margin-bottom: 1.5rem;">
165
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">标签颜色</label>
166
+ <input type="color" id="tagColor" name="color" value="#165DFF"
167
+ style="width: 100%; padding: 0.5rem; border: 1px solid var(--light-2); border-radius: var(--border-radius); height: 3rem;" />
168
+ <small style="color: var(--dark-2); font-size: 0.75rem;">选择标签的显示颜色</small>
169
+ </div>
170
+
171
+ <div class="modal-footer">
172
+ <button type="button" class="btn btn-outline" onclick="closeTagModal()">取消</button>
173
+ <button type="submit" class="btn btn-primary">
174
+ <span id="submitText">保存</span>
175
+ </button>
176
+ </div>
177
+ </form>
178
+ </div>
179
+ </div>
180
+
181
+ <style>
182
+ .modal {
183
+ position: fixed;
184
+ top: 0;
185
+ left: 0;
186
+ right: 0;
187
+ bottom: 0;
188
+ z-index: 10000;
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ }
193
+
194
+ .modal-backdrop {
195
+ position: absolute;
196
+ top: 0;
197
+ left: 0;
198
+ right: 0;
199
+ bottom: 0;
200
+ background: rgba(0, 0, 0, 0.5);
201
+ }
202
+
203
+ .modal-content {
204
+ background: white;
205
+ border-radius: var(--border-radius);
206
+ position: relative;
207
+ z-index: 2;
208
+ max-height: 90vh;
209
+ overflow-y: auto;
210
+ width: 90%;
211
+ max-width: 600px;
212
+ }
213
+
214
+ .modal-header {
215
+ padding: 1.5rem;
216
+ border-bottom: 1px solid var(--light-2);
217
+ display: flex;
218
+ justify-content: space-between;
219
+ align-items: center;
220
+ }
221
+
222
+ .modal-header h3 {
223
+ margin: 0;
224
+ font-size: 1.25rem;
225
+ font-weight: 600;
226
+ }
227
+
228
+ .modal-footer {
229
+ padding: 1.5rem;
230
+ border-top: 1px solid var(--light-2);
231
+ display: flex;
232
+ justify-content: flex-end;
233
+ gap: 1rem;
234
+ }
235
+
236
+ .form-group input:focus {
237
+ border-color: var(--primary);
238
+ outline: none;
239
+ box-shadow: 0 0 0 3px rgba(22, 93, 255, 0.1);
240
+ }
241
+ </style>
242
+
243
+ <script>
244
+ let isEditing = false;
245
+
246
+ function changePage(page) {
247
+ window.location.href = `@Url.Action("Index", "Tag")?page=${page}`;
248
+ }
249
+
250
+ function openTagModal(editMode = false) {
251
+ document.getElementById('tagModal').style.display = 'flex';
252
+ document.getElementById('modalTitle').textContent = editMode ? '编辑标签' : '添加标签';
253
+ document.getElementById('submitText').textContent = editMode ? '更新' : '保存';
254
+ isEditing = editMode;
255
+
256
+ if (!editMode) {
257
+ document.getElementById('tagForm').reset();
258
+ document.getElementById('tagId').value = '0';
259
+ document.getElementById('tagColor').value = '#165DFF';
260
+ }
261
+ }
262
+
263
+ function closeTagModal() {
264
+ document.getElementById('tagModal').style.display = 'none';
265
+ }
266
+
267
+ function editTag(id, name, color) {
268
+ openTagModal(true);
269
+ document.getElementById('tagId').value = id;
270
+ document.getElementById('tagName').value = name;
271
+ document.getElementById('tagColor').value = color || '#165DFF';
272
+ }
273
+
274
+ async function submitTag(event) {
275
+ event.preventDefault();
276
+
277
+ const formData = new FormData(event.target);
278
+ const data = Object.fromEntries(formData.entries());
279
+
280
+ try {
281
+ const response = await fetch('/Tag/Save', {
282
+ method: 'POST',
283
+ headers: {
284
+ 'Content-Type': 'application/json',
285
+ },
286
+ body: JSON.stringify(data)
287
+ });
288
+
289
+ if (response.ok) {
290
+ toolHub.showToast(isEditing ? '标签更新成功' : '标签添加成功', 'success');
291
+ closeTagModal();
292
+ setTimeout(() => location.reload(), 1000);
293
+ } else {
294
+ toolHub.showToast('操作失败,请重试', 'error');
295
+ }
296
+ } catch (error) {
297
+ console.error('提交失败:', error);
298
+ toolHub.showToast('操作失败,请重试', 'error');
299
+ }
300
+ }
301
+
302
+ async function toggleTagStatus(id, currentStatus) {
303
+ try {
304
+ const response = await fetch('/Tag/ToggleStatus', {
305
+ method: 'POST',
306
+ headers: {
307
+ 'Content-Type': 'application/json',
308
+ },
309
+ body: JSON.stringify({ id: id, isActive: !currentStatus })
310
+ });
311
+
312
+ if (response.ok) {
313
+ toolHub.showToast('状态更新成功', 'success');
314
+ setTimeout(() => location.reload(), 1000);
315
+ } else {
316
+ toolHub.showToast('操作失败,请重试', 'error');
317
+ }
318
+ } catch (error) {
319
+ console.error('操作失败:', error);
320
+ toolHub.showToast('操作失败,请重试', 'error');
321
+ }
322
+ }
323
+
324
+ async function deleteTag(id, name) {
325
+ if (!confirm(`确定要删除标签"${name}"吗?\n注意:删除标签会影响使用该标签的工具。`)) {
326
+ return;
327
+ }
328
+
329
+ try {
330
+ const response = await fetch('/Tag/Delete', {
331
+ method: 'POST',
332
+ headers: {
333
+ 'Content-Type': 'application/json',
334
+ },
335
+ body: JSON.stringify({ id: id })
336
+ });
337
+
338
+ if (response.ok) {
339
+ toolHub.showToast('标签删除成功', 'success');
340
+ setTimeout(() => location.reload(), 1000);
341
+ } else {
342
+ toolHub.showToast('删除失败,可能该标签下还有工具', 'error');
343
+ }
344
+ } catch (error) {
345
+ console.error('删除失败:', error);
346
+ toolHub.showToast('删除失败,请重试', 'error');
347
+ }
348
+ }
349
+ </script>
Views/Tool/Index.cshtml ADDED
@@ -0,0 +1,479 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @model List<ToolHub.Models.Tool>
2
+ @{
3
+ ViewData["Title"] = "工具管理";
4
+ Layout = "_AdminLayout";
5
+ var categories = ViewBag.Categories as List<ToolHub.Models.Category> ?? new List<ToolHub.Models.Category>();
6
+ var currentCategory = ViewBag.CurrentCategory as int? ?? 0;
7
+ }
8
+
9
+ <!-- 页面头部 -->
10
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
11
+ <div>
12
+ <h2 style="font-size: 1.5rem; font-weight: 700; margin: 0; color: var(--dark);">工具管理</h2>
13
+ <p style="color: var(--dark-2); margin: 0.5rem 0 0;">管理平台上的所有工具,添加、编辑或删除工具</p>
14
+ </div>
15
+ <button class="btn btn-primary" onclick="openToolModal()">
16
+ <i class="fas fa-plus"></i>
17
+ 添加工具
18
+ </button>
19
+ </div>
20
+
21
+ <!-- 筛选和搜索 -->
22
+ <div style="background: white; padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--shadow-sm); margin-bottom: 2rem;">
23
+ <div style="display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 1rem; align-items: end;">
24
+ <div>
25
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.875rem;">分类筛选</label>
26
+ <select id="categoryFilter" onchange="filterTools()" style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);">
27
+ <option value="0">全部分类</option>
28
+ @foreach (var category in categories)
29
+ {
30
+ <option value="@category.Id" selected="@(currentCategory == category.Id)">@category.Name</option>
31
+ }
32
+ </select>
33
+ </div>
34
+
35
+ <div>
36
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.875rem;">状态筛选</label>
37
+ <select id="statusFilter" onchange="filterTools()" style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);">
38
+ <option value="">全部状态</option>
39
+ <option value="active">启用</option>
40
+ <option value="inactive">禁用</option>
41
+ <option value="hot">热门</option>
42
+ <option value="new">新品</option>
43
+ <option value="recommended">推荐</option>
44
+ </select>
45
+ </div>
46
+
47
+ <div>
48
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.875rem;">搜索工具</label>
49
+ <input type="text" id="searchInput" placeholder="输入工具名称..." onkeyup="filterTools()"
50
+ style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);" />
51
+ </div>
52
+
53
+ <button class="btn btn-outline" onclick="clearFilters()">
54
+ <i class="fas fa-undo"></i>
55
+ 重置
56
+ </button>
57
+ </div>
58
+ </div>
59
+
60
+ <!-- 工具列表 -->
61
+ <div class="data-table">
62
+ <div style="padding: 1rem 1.5rem; border-bottom: 1px solid var(--light-2); display: flex; justify-content: space-between; align-items: center;">
63
+ <span style="font-weight: 600;">工具列表 (@Model.Count)</span>
64
+ <div style="display: flex; gap: 0.5rem;">
65
+ <button class="btn btn-outline btn-sm" onclick="toggleView('table')" id="tableViewBtn">
66
+ <i class="fas fa-list"></i> 表格
67
+ </button>
68
+ <button class="btn btn-outline btn-sm" onclick="toggleView('grid')" id="gridViewBtn">
69
+ <i class="fas fa-th"></i> 网格
70
+ </button>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- 表格视图 -->
75
+ <div id="tableView">
76
+ <table>
77
+ <thead>
78
+ <tr>
79
+ <th>工具信息</th>
80
+ <th>分类</th>
81
+ <th>统计</th>
82
+ <th>标签</th>
83
+ <th>状态</th>
84
+ <th>创建时间</th>
85
+ <th>操作</th>
86
+ </tr>
87
+ </thead>
88
+ <tbody id="toolsTableBody">
89
+ @foreach (var tool in Model)
90
+ {
91
+ <tr data-category="@tool.CategoryId" data-status="@(tool.IsActive ? "active" : "inactive")"
92
+ data-hot="@tool.IsHot.ToString().ToLower()" data-new="@tool.IsNew.ToString().ToLower()" data-recommended="@tool.IsRecommended.ToString().ToLower()">
93
+ <td>
94
+ <div style="display: flex; align-items: center;">
95
+ @if (!string.IsNullOrEmpty(tool.Image))
96
+ {
97
+ <img src="@tool.Image" alt="@tool.Name" style="width: 40px; height: 40px; border-radius: var(--border-radius); margin-right: 0.75rem; object-fit: cover;" />
98
+ }
99
+ else
100
+ {
101
+ <div style="width: 40px; height: 40px; background: linear-gradient(135deg, var(--primary), var(--secondary)); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; margin-right: 0.75rem;">
102
+ <i class="@tool.Icon" style="color: white;"></i>
103
+ </div>
104
+ }
105
+ <div>
106
+ <strong class="tool-name">@tool.Name</strong>
107
+ @if (!string.IsNullOrEmpty(tool.Description))
108
+ {
109
+ <br><small style="color: var(--dark-2);">@(tool.Description.Length > 50 ? tool.Description.Substring(0, 50) + "..." : tool.Description)</small>
110
+ }
111
+ </div>
112
+ </div>
113
+ </td>
114
+ <td>
115
+ <span class="badge badge-primary">@tool.Category?.Name</span>
116
+ </td>
117
+ <td>
118
+ <div style="font-size: 0.875rem;">
119
+ <div style="margin-bottom: 0.25rem;">
120
+ <i class="fas fa-eye" style="color: var(--primary); margin-right: 0.25rem;"></i>
121
+ @(tool.ViewCount > 1000 ? (tool.ViewCount / 1000).ToString("F0") + "K" : tool.ViewCount.ToString())
122
+ </div>
123
+ @if (tool.Rating > 0)
124
+ {
125
+ <div>
126
+ <i class="fas fa-star" style="color: var(--warning); margin-right: 0.25rem;"></i>
127
+ @tool.Rating.ToString("F1")
128
+ </div>
129
+ }
130
+ </div>
131
+ </td>
132
+ <td>
133
+ <div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
134
+ @if (tool.IsHot)
135
+ {
136
+ <span class="badge badge-danger">热门</span>
137
+ }
138
+ @if (tool.IsNew)
139
+ {
140
+ <span class="badge badge-warning">新品</span>
141
+ }
142
+ @if (tool.IsRecommended)
143
+ {
144
+ <span class="badge badge-success">推荐</span>
145
+ }
146
+ </div>
147
+ </td>
148
+ <td>
149
+ @if (tool.IsActive)
150
+ {
151
+ <span class="badge badge-success">启用</span>
152
+ }
153
+ else
154
+ {
155
+ <span class="badge badge-secondary">禁用</span>
156
+ }
157
+ </td>
158
+ <td style="color: var(--dark-2); font-size: 0.875rem;">
159
+ @tool.CreatedAt.ToString("yyyy-MM-dd")
160
+ </td>
161
+ <td>
162
+ <div style="display: flex; gap: 0.5rem;">
163
+ <button class="btn btn-outline btn-sm" onclick="editTool(@tool.Id)" title="编辑">
164
+ <i class="fas fa-edit"></i>
165
+ </button>
166
+ <button class="btn btn-outline btn-sm" onclick="toggleToolStatus(@tool.Id, @tool.IsActive.ToString().ToLower())"
167
+ style="color: @(tool.IsActive ? "var(--warning)" : "var(--success)");" title="@(tool.IsActive ? "禁用" : "启用")">
168
+ <i class="fas fa-@(tool.IsActive ? "pause" : "play")"></i>
169
+ </button>
170
+ <button class="btn btn-outline btn-sm" onclick="deleteTool(@tool.Id, '@tool.Name')"
171
+ style="color: var(--danger);" title="删除">
172
+ <i class="fas fa-trash"></i>
173
+ </button>
174
+ </div>
175
+ </td>
176
+ </tr>
177
+ }
178
+ </tbody>
179
+ </table>
180
+ </div>
181
+
182
+ @if (!Model.Any())
183
+ {
184
+ <div style="padding: 3rem; text-align: center; color: var(--dark-2);">
185
+ <i class="fas fa-tools" style="font-size: 3rem; margin-bottom: 1rem; display: block; opacity: 0.3;"></i>
186
+ <h3 style="margin-bottom: 0.5rem;">暂无工具</h3>
187
+ <p style="margin-bottom: 1.5rem;">还没有添加任何工具</p>
188
+ <button class="btn btn-primary" onclick="openToolModal()">
189
+ <i class="fas fa-plus"></i>
190
+ 添加第一个工具
191
+ </button>
192
+ </div>
193
+ }
194
+ </div>
195
+
196
+ <!-- 工具模态框 -->
197
+ <div id="toolModal" class="modal" style="display: none;">
198
+ <div class="modal-backdrop" onclick="closeToolModal()"></div>
199
+ <div class="modal-content" style="max-width: 600px;">
200
+ <div class="modal-header">
201
+ <h3 id="toolModalTitle">添加工具</h3>
202
+ <button onclick="closeToolModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer;">×</button>
203
+ </div>
204
+
205
+ <form id="toolForm" onsubmit="submitTool(event)">
206
+ <div class="modal-body">
207
+ <input type="hidden" id="toolId" name="id" value="0" />
208
+
209
+ <div class="form-group">
210
+ <label>工具名称 *</label>
211
+ <input type="text" id="toolName" name="name" required placeholder="请输入工具名称" />
212
+ </div>
213
+
214
+ <div class="form-group">
215
+ <label>工具描述</label>
216
+ <textarea id="toolDescription" name="description" rows="3" placeholder="请输入工具描述"></textarea>
217
+ </div>
218
+
219
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
220
+ <div class="form-group">
221
+ <label>所属分类 *</label>
222
+ <select id="toolCategory" name="categoryId" required>
223
+ <option value="">请选择分类</option>
224
+ @foreach (var category in categories)
225
+ {
226
+ <option value="@category.Id">@category.Name</option>
227
+ }
228
+ </select>
229
+ </div>
230
+
231
+ <div class="form-group">
232
+ <label>图标类名</label>
233
+ <input type="text" id="toolIcon" name="icon" placeholder="如: fas fa-file-pdf" />
234
+ </div>
235
+ </div>
236
+
237
+ <div class="form-group">
238
+ <label>工具链接</label>
239
+ <input type="url" id="toolUrl" name="url" placeholder="https://example.com" />
240
+ </div>
241
+
242
+ <div class="form-group">
243
+ <label>工具图片链接</label>
244
+ <input type="url" id="toolImage" name="image" placeholder="https://example.com/image.jpg" />
245
+ </div>
246
+
247
+ <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem;">
248
+ <div class="form-group">
249
+ <label style="display: flex; align-items: center; gap: 0.5rem;">
250
+ <input type="checkbox" id="toolIsHot" name="isHot" />
251
+ <span>热门工具</span>
252
+ </label>
253
+ </div>
254
+
255
+ <div class="form-group">
256
+ <label style="display: flex; align-items: center; gap: 0.5rem;">
257
+ <input type="checkbox" id="toolIsNew" name="isNew" />
258
+ <span>新品工具</span>
259
+ </label>
260
+ </div>
261
+
262
+ <div class="form-group">
263
+ <label style="display: flex; align-items: center; gap: 0.5rem;">
264
+ <input type="checkbox" id="toolIsRecommended" name="isRecommended" />
265
+ <span>推荐工具</span>
266
+ </label>
267
+ </div>
268
+ </div>
269
+
270
+ <div class="form-group">
271
+ <label>排序顺序</label>
272
+ <input type="number" id="toolSort" name="sortOrder" min="0" value="0" placeholder="数字越小排序越靠前" />
273
+ </div>
274
+ </div>
275
+
276
+ <div class="modal-footer">
277
+ <button type="button" class="btn btn-outline" onclick="closeToolModal()">取消</button>
278
+ <button type="submit" class="btn btn-primary">
279
+ <span id="toolSubmitText">保存</span>
280
+ </button>
281
+ </div>
282
+ </form>
283
+ </div>
284
+ </div>
285
+
286
+ <script>
287
+ let currentView = 'table';
288
+ let isEditingTool = false;
289
+
290
+ function toggleView(view) {
291
+ currentView = view;
292
+ document.getElementById('tableView').style.display = view === 'table' ? 'block' : 'none';
293
+
294
+ document.getElementById('tableViewBtn').classList.toggle('btn-primary', view === 'table');
295
+ document.getElementById('tableViewBtn').classList.toggle('btn-outline', view !== 'table');
296
+ }
297
+
298
+ function filterTools() {
299
+ const categoryFilter = document.getElementById('categoryFilter').value;
300
+ const statusFilter = document.getElementById('statusFilter').value;
301
+ const searchInput = document.getElementById('searchInput').value.toLowerCase();
302
+
303
+ const tableRows = document.querySelectorAll('#toolsTableBody tr');
304
+
305
+ tableRows.forEach(row => {
306
+ const category = row.getAttribute('data-category');
307
+ const status = row.getAttribute('data-status');
308
+ const isHot = row.getAttribute('data-hot') === 'true';
309
+ const isNew = row.getAttribute('data-new') === 'true';
310
+ const isRecommended = row.getAttribute('data-recommended') === 'true';
311
+
312
+ const toolName = row.querySelector('.tool-name').textContent.toLowerCase();
313
+
314
+ let showRow = true;
315
+
316
+ // 分类筛选
317
+ if (categoryFilter !== '0' && category !== categoryFilter) {
318
+ showRow = false;
319
+ }
320
+
321
+ // 状态筛选
322
+ if (statusFilter) {
323
+ switch(statusFilter) {
324
+ case 'active':
325
+ if (status !== 'active') showRow = false;
326
+ break;
327
+ case 'inactive':
328
+ if (status !== 'inactive') showRow = false;
329
+ break;
330
+ case 'hot':
331
+ if (!isHot) showRow = false;
332
+ break;
333
+ case 'new':
334
+ if (!isNew) showRow = false;
335
+ break;
336
+ case 'recommended':
337
+ if (!isRecommended) showRow = false;
338
+ break;
339
+ }
340
+ }
341
+
342
+ // 搜索筛选
343
+ if (searchInput && !toolName.includes(searchInput)) {
344
+ showRow = false;
345
+ }
346
+
347
+ row.style.display = showRow ? '' : 'none';
348
+ });
349
+ }
350
+
351
+ function clearFilters() {
352
+ document.getElementById('categoryFilter').value = '0';
353
+ document.getElementById('statusFilter').value = '';
354
+ document.getElementById('searchInput').value = '';
355
+ filterTools();
356
+ }
357
+
358
+ function openToolModal(editMode = false) {
359
+ document.getElementById('toolModal').style.display = 'flex';
360
+ document.getElementById('toolModalTitle').textContent = editMode ? '编辑工具' : '添加工具';
361
+ document.getElementById('toolSubmitText').textContent = editMode ? '更新' : '保存';
362
+ isEditingTool = editMode;
363
+
364
+ if (!editMode) {
365
+ document.getElementById('toolForm').reset();
366
+ document.getElementById('toolId').value = '0';
367
+ }
368
+ }
369
+
370
+ function closeToolModal() {
371
+ document.getElementById('toolModal').style.display = 'none';
372
+ }
373
+
374
+ async function editTool(id) {
375
+ try {
376
+ const response = await fetch(`/Tool/Get?id=${id}`);
377
+ if (response.ok) {
378
+ const tool = await response.json();
379
+ openToolModal(true);
380
+
381
+ document.getElementById('toolId').value = tool.id;
382
+ document.getElementById('toolName').value = tool.name;
383
+ document.getElementById('toolDescription').value = tool.description || '';
384
+ document.getElementById('toolCategory').value = tool.categoryId;
385
+ document.getElementById('toolIcon').value = tool.icon || '';
386
+ document.getElementById('toolUrl').value = tool.url || '';
387
+ document.getElementById('toolImage').value = tool.image || '';
388
+ document.getElementById('toolIsHot').checked = tool.isHot;
389
+ document.getElementById('toolIsNew').checked = tool.isNew;
390
+ document.getElementById('toolIsRecommended').checked = tool.isRecommended;
391
+ document.getElementById('toolSort').value = tool.sortOrder;
392
+ }
393
+ } catch (error) {
394
+ console.error('获取工具信息失败:', error);
395
+ toolHub.showToast('获取工具信息失败', 'error');
396
+ }
397
+ }
398
+
399
+ async function submitTool(event) {
400
+ event.preventDefault();
401
+
402
+ const formData = new FormData(event.target);
403
+ const data = Object.fromEntries(formData.entries());
404
+
405
+ // 处理复选框
406
+ data.isHot = document.getElementById('toolIsHot').checked;
407
+ data.isNew = document.getElementById('toolIsNew').checked;
408
+ data.isRecommended = document.getElementById('toolIsRecommended').checked;
409
+
410
+ try {
411
+ const response = await fetch('/Tool/Save', {
412
+ method: 'POST',
413
+ headers: {
414
+ 'Content-Type': 'application/json',
415
+ },
416
+ body: JSON.stringify(data)
417
+ });
418
+
419
+ if (response.ok) {
420
+ toolHub.showToast(isEditingTool ? '工具更新成功' : '工具添加成功', 'success');
421
+ closeToolModal();
422
+ setTimeout(() => location.reload(), 1000);
423
+ } else {
424
+ toolHub.showToast('操作失败,请重试', 'error');
425
+ }
426
+ } catch (error) {
427
+ console.error('提交失败:', error);
428
+ toolHub.showToast('操作失败,请重试', 'error');
429
+ }
430
+ }
431
+
432
+ async function toggleToolStatus(id, currentStatus) {
433
+ try {
434
+ const response = await fetch('/Tool/ToggleStatus', {
435
+ method: 'POST',
436
+ headers: {
437
+ 'Content-Type': 'application/json',
438
+ },
439
+ body: JSON.stringify({ id: id, isActive: !currentStatus })
440
+ });
441
+
442
+ if (response.ok) {
443
+ toolHub.showToast('状态更新成功', 'success');
444
+ setTimeout(() => location.reload(), 1000);
445
+ } else {
446
+ toolHub.showToast('操作失败,请重试', 'error');
447
+ }
448
+ } catch (error) {
449
+ console.error('操作失败:', error);
450
+ toolHub.showToast('操作失败,请重试', 'error');
451
+ }
452
+ }
453
+
454
+ async function deleteTool(id, name) {
455
+ if (!confirm(`确定要删除工具"${name}"吗?此操作不可恢复。`)) {
456
+ return;
457
+ }
458
+
459
+ try {
460
+ const response = await fetch('/Tool/Delete', {
461
+ method: 'POST',
462
+ headers: {
463
+ 'Content-Type': 'application/json',
464
+ },
465
+ body: JSON.stringify({ id: id })
466
+ });
467
+
468
+ if (response.ok) {
469
+ toolHub.showToast('工具删除成功', 'success');
470
+ setTimeout(() => location.reload(), 1000);
471
+ } else {
472
+ toolHub.showToast('删除失败,请重试', 'error');
473
+ }
474
+ } catch (error) {
475
+ console.error('删除失败:', error);
476
+ toolHub.showToast('删除失败,请重试', 'error');
477
+ }
478
+ }
479
+ </script>
Views/ToolStatistics/Index.cshtml ADDED
@@ -0,0 +1,493 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @{
2
+ ViewData["Title"] = "工具统计";
3
+ Layout = "_AdminLayout";
4
+ var toolId = ViewData["ToolId"] as int? ?? 0;
5
+ var toolName = ViewData["ToolName"] as string ?? "所有工具";
6
+ }
7
+
8
+ @section Scripts {
9
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
10
+ <script>
11
+ // 确保Chart.js加载完成
12
+ window.addEventListener('load', function() {
13
+ if (typeof Chart === 'undefined') {
14
+ console.error('Chart.js 未能正确加载');
15
+ } else {
16
+ console.log('Chart.js 加载成功');
17
+ }
18
+ });
19
+ </script>
20
+ }
21
+
22
+ <!-- 页面头部 -->
23
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
24
+ <div>
25
+ <h2 style="font-size: 1.5rem; font-weight: 700; margin: 0; color: var(--dark);">工具统计</h2>
26
+ <p style="color: var(--dark-2); margin: 0.5rem 0 0;">查看工具的详细使用统计和用户访问记录</p>
27
+ </div>
28
+ <div style="display: flex; gap: 1rem;">
29
+ <a href="@Url.Action("Index", "Tool")" class="btn btn-secondary">
30
+ <i class="fas fa-arrow-left"></i>
31
+ 返回工具管理
32
+ </a>
33
+ </div>
34
+ </div>
35
+
36
+ <!-- 工具选择 -->
37
+ <div style="background: white; padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--shadow-sm); margin-bottom: 2rem;">
38
+ <div style="display: grid; grid-template-columns: 1fr auto; gap: 1rem; align-items: end;">
39
+ <div>
40
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.875rem;">选择工具</label>
41
+ <select id="toolSelector" onchange="loadToolStatistics()" style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);">
42
+ <option value="0">所有工具</option>
43
+ </select>
44
+ </div>
45
+ <div>
46
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.875rem;">统计日期</label>
47
+ <input type="date" id="statDate" onchange="loadToolStatistics()"
48
+ value="@DateTime.Today.ToString("yyyy-MM-dd")"
49
+ style="padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);" />
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <!-- 统计概览 -->
55
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
56
+ <div style="background: white; padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--shadow-sm);">
57
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
58
+ <h3 style="font-size: 1rem; font-weight: 600; margin: 0; color: var(--dark);">今日访问量</h3>
59
+ <i class="fas fa-eye" style="color: var(--primary); font-size: 1.25rem;"></i>
60
+ </div>
61
+ <div id="todayViews" style="font-size: 2rem; font-weight: 700; color: var(--primary);">-</div>
62
+ <div style="font-size: 0.875rem; color: var(--dark-2); margin-top: 0.5rem;">总访问量: <span id="totalViews">-</span></div>
63
+ </div>
64
+
65
+ <div style="background: white; padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--shadow-sm);">
66
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
67
+ <h3 style="font-size: 1rem; font-weight: 600; margin: 0; color: var(--dark);">今日收藏</h3>
68
+ <i class="fas fa-heart" style="color: var(--danger); font-size: 1.25rem;"></i>
69
+ </div>
70
+ <div id="todayFavorites" style="font-size: 2rem; font-weight: 700; color: var(--danger);">-</div>
71
+ <div style="font-size: 0.875rem; color: var(--dark-2); margin-top: 0.5rem;">总收藏数: <span id="totalFavorites">-</span></div>
72
+ </div>
73
+
74
+ <div style="background: white; padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--shadow-sm);">
75
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
76
+ <h3 style="font-size: 1rem; font-weight: 600; margin: 0; color: var(--dark);">今日分享</h3>
77
+ <i class="fas fa-share" style="color: var(--success); font-size: 1.25rem;"></i>
78
+ </div>
79
+ <div id="todayShares" style="font-size: 2rem; font-weight: 700; color: var(--success);">-</div>
80
+ <div style="font-size: 0.875rem; color: var(--dark-2); margin-top: 0.5rem;">平均停留时间: <span id="avgDuration">-</span>秒</div>
81
+ </div>
82
+
83
+ <div style="background: white; padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--shadow-sm);">
84
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
85
+ <h3 style="font-size: 1rem; font-weight: 600; margin: 0; color: var(--dark);">今日下载</h3>
86
+ <i class="fas fa-download" style="color: var(--warning); font-size: 1.25rem;"></i>
87
+ </div>
88
+ <div id="todayDownloads" style="font-size: 2rem; font-weight: 700; color: var(--warning);">-</div>
89
+ <div style="font-size: 0.875rem; color: var(--dark-2); margin-top: 0.5rem;">跳出率: <span id="bounceRate">-</span>%</div>
90
+ </div>
91
+ </div>
92
+
93
+ <!-- 使用趋势图表 -->
94
+ <div style="background: white; padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--shadow-sm); margin-bottom: 2rem;">
95
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
96
+ <h3 style="font-size: 1.25rem; font-weight: 600; margin: 0; color: var(--dark);">使用趋势 (最近30天)</h3>
97
+ <div style="display: flex; gap: 0.5rem;">
98
+ <button class="btn btn-outline btn-sm" onclick="loadUsageTrend(7)">7天</button>
99
+ <button class="btn btn-outline btn-sm" onclick="loadUsageTrend(30)">30天</button>
100
+ <button class="btn btn-outline btn-sm" onclick="loadUsageTrend(90)">90天</button>
101
+ </div>
102
+ </div>
103
+ <div id="trendChart" style="height: 300px; display: flex; align-items: center; justify-content: center; color: var(--dark-2);">
104
+ <div style="text-align: center;">
105
+ <i class="fas fa-chart-line" style="font-size: 3rem; margin-bottom: 1rem; display: block; opacity: 0.3;"></i>
106
+ <p>请选择工具查看使用趋势</p>
107
+ </div>
108
+ </div>
109
+ </div>
110
+
111
+ <!-- 访问记录 -->
112
+ <div style="background: white; padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--shadow-sm);">
113
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
114
+ <h3 style="font-size: 1.25rem; font-weight: 600; margin: 0; color: var(--dark);">最近访问记录</h3>
115
+ <div style="display: flex; gap: 0.5rem;">
116
+ <input type="date" id="startDate" onchange="loadAccessRecords()"
117
+ value="@DateTime.Today.AddDays(-7).ToString("yyyy-MM-dd")"
118
+ style="padding: 0.5rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);" />
119
+ <input type="date" id="endDate" onchange="loadAccessRecords()"
120
+ value="@DateTime.Today.ToString("yyyy-MM-dd")"
121
+ style="padding: 0.5rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);" />
122
+ </div>
123
+ </div>
124
+ <div id="accessRecords" style="min-height: 200px; display: flex; align-items: center; justify-content: center; color: var(--dark-2);">
125
+ <div style="text-align: center;">
126
+ <i class="fas fa-list" style="font-size: 3rem; margin-bottom: 1rem; display: block; opacity: 0.3;"></i>
127
+ <p>请选择工具查看访问记录</p>
128
+ </div>
129
+ </div>
130
+ </div>
131
+
132
+ <script>
133
+ let currentToolId = @toolId;
134
+ let currentDays = 30;
135
+
136
+ // 页面加载时初始化
137
+ document.addEventListener('DOMContentLoaded', function() {
138
+ loadTools();
139
+ // 等待Chart.js加载完成
140
+ setTimeout(() => {
141
+ if (currentToolId > 0) {
142
+ loadToolStatistics();
143
+ loadUsageTrend(currentDays);
144
+ loadAccessRecords();
145
+ }
146
+ }, 500);
147
+ });
148
+
149
+ // 加载工具列表
150
+ async function loadTools() {
151
+ try {
152
+ const response = await fetch('/Tool/GetAllTools');
153
+ const result = await response.json();
154
+
155
+ if (result.success && result.tools) {
156
+ const toolSelector = document.getElementById('toolSelector');
157
+ toolSelector.innerHTML = '<option value="0">所有工具</option>';
158
+
159
+ result.tools.forEach(tool => {
160
+ const option = document.createElement('option');
161
+ option.value = tool.id;
162
+ option.textContent = tool.name;
163
+ if (tool.id === currentToolId) {
164
+ option.selected = true;
165
+ }
166
+ toolSelector.appendChild(option);
167
+ });
168
+ }
169
+ } catch (error) {
170
+ console.error('加载工具列表失败:', error);
171
+ }
172
+ }
173
+
174
+ // 加载工具统计
175
+ async function loadToolStatistics() {
176
+ const toolId = document.getElementById('toolSelector').value;
177
+ const date = document.getElementById('statDate').value;
178
+
179
+ if (toolId === '0') {
180
+ resetStatistics();
181
+ return;
182
+ }
183
+
184
+ try {
185
+ const response = await fetch(`/ToolStatistics/GetToolStatistics?toolId=${toolId}&date=${date}`);
186
+ const result = await response.json();
187
+
188
+ if (result.success && result.data) {
189
+ updateStatistics(result.data);
190
+ }
191
+ else{
192
+ resetStatistics();
193
+ }
194
+ currentToolId = toolId;
195
+ loadUsageTrend(currentDays);
196
+ loadAccessRecords();
197
+ } catch (error) {
198
+ console.error('加载统计数据失败:', error);
199
+ }
200
+ }
201
+
202
+ // 更新统计数据
203
+ function updateStatistics(stats) {
204
+ document.getElementById('todayViews').textContent = stats.dailyViews || 0;
205
+ document.getElementById('todayFavorites').textContent = stats.dailyFavorites || 0;
206
+ document.getElementById('todayShares').textContent = stats.dailyShares || 0;
207
+ document.getElementById('todayDownloads').textContent = stats.dailyDownloads || 0;
208
+ document.getElementById('avgDuration').textContent = Math.round(stats.averageDuration || 0);
209
+
210
+ // 这里可以添加获取总数据的逻辑
211
+ document.getElementById('totalViews').textContent = '-';
212
+ document.getElementById('totalFavorites').textContent = '-';
213
+ document.getElementById('bounceRate').textContent = '-';
214
+ }
215
+
216
+ // 重置统计数据
217
+ function resetStatistics() {
218
+ document.getElementById('todayViews').textContent = '-';
219
+ document.getElementById('todayFavorites').textContent = '-';
220
+ document.getElementById('todayShares').textContent = '-';
221
+ document.getElementById('todayDownloads').textContent = '-';
222
+ document.getElementById('avgDuration').textContent = '-';
223
+ document.getElementById('totalViews').textContent = '-';
224
+ document.getElementById('totalFavorites').textContent = '-';
225
+ document.getElementById('bounceRate').textContent = '-';
226
+ }
227
+
228
+ // 加载使用趋势
229
+ async function loadUsageTrend(days) {
230
+ const toolId = document.getElementById('toolSelector').value;
231
+ currentDays = days;
232
+
233
+ if (toolId === '0') {
234
+ document.getElementById('trendChart').innerHTML = `
235
+ <div style="text-align: center;">
236
+ <i class="fas fa-chart-line" style="font-size: 3rem; margin-bottom: 1rem; display: block; opacity: 0.3;"></i>
237
+ <p>请选择工具查看使用趋势</p>
238
+ </div>
239
+ `;
240
+ return;
241
+ }
242
+
243
+ try {
244
+ const response = await fetch(`/ToolStatistics/GetToolUsageTrend?toolId=${toolId}&days=${days}`);
245
+ const result = await response.json();
246
+
247
+ if (result.success && result.data) {
248
+ renderTrendChart(result.data);
249
+ }
250
+ } catch (error) {
251
+ console.error('加载使用趋势失败:', error);
252
+ }
253
+ }
254
+
255
+ // 渲染趋势图表
256
+ function renderTrendChart(data) {
257
+ const chartDiv = document.getElementById('trendChart');
258
+
259
+ // 清空容器
260
+ chartDiv.innerHTML = '<canvas id="trendCanvas" style="width: 100%; height: 100%;"></canvas>';
261
+
262
+ // 准备数据
263
+ const labels = data.map(item => item.date);
264
+ const viewsData = data.map(item => item.views);
265
+ const favoritesData = data.map(item => item.favorites);
266
+ const sharesData = data.map(item => item.shares);
267
+ const downloadsData = data.map(item => item.downloads);
268
+
269
+ // 创建图表
270
+ const ctx = document.getElementById('trendCanvas').getContext('2d');
271
+
272
+ // 销毁现有图表
273
+ if (window.trendChart && typeof window.trendChart.destroy === 'function') {
274
+ window.trendChart.destroy();
275
+ }
276
+
277
+ // 检查Chart.js是否可用
278
+ if (typeof Chart === 'undefined') {
279
+ chartDiv.innerHTML = `
280
+ <div style="text-align: center; padding: 2rem;">
281
+ <i class="fas fa-exclamation-triangle" style="font-size: 2rem; margin-bottom: 1rem; display: block; color: var(--warning);"></i>
282
+ <p style="color: var(--dark-2); margin: 0;">Chart.js 库加载失败,请刷新页面重试</p>
283
+ </div>
284
+ `;
285
+ return;
286
+ }
287
+
288
+ try {
289
+ window.trendChart = new Chart(ctx, {
290
+ type: 'line',
291
+ data: {
292
+ labels: labels,
293
+ datasets: [
294
+ {
295
+ label: '访问量',
296
+ data: viewsData,
297
+ borderColor: '#165DFF',
298
+ backgroundColor: 'rgba(22, 93, 255, 0.1)',
299
+ borderWidth: 2,
300
+ fill: true,
301
+ tension: 0.4
302
+ },
303
+ {
304
+ label: '收藏',
305
+ data: favoritesData,
306
+ borderColor: '#FF4D4F',
307
+ backgroundColor: 'rgba(255, 77, 79, 0.1)',
308
+ borderWidth: 2,
309
+ fill: true,
310
+ tension: 0.4
311
+ },
312
+ {
313
+ label: '分享',
314
+ data: sharesData,
315
+ borderColor: '#52C41A',
316
+ backgroundColor: 'rgba(82, 196, 26, 0.1)',
317
+ borderWidth: 2,
318
+ fill: true,
319
+ tension: 0.4
320
+ },
321
+ {
322
+ label: '下载',
323
+ data: downloadsData,
324
+ borderColor: '#FAAD14',
325
+ backgroundColor: 'rgba(250, 173, 20, 0.1)',
326
+ borderWidth: 2,
327
+ fill: true,
328
+ tension: 0.4
329
+ }
330
+ ]
331
+ },
332
+ options: {
333
+ responsive: true,
334
+ maintainAspectRatio: false,
335
+ plugins: {
336
+ legend: {
337
+ position: 'top',
338
+ labels: {
339
+ usePointStyle: true,
340
+ padding: 20,
341
+ font: {
342
+ size: 12
343
+ }
344
+ }
345
+ },
346
+ tooltip: {
347
+ mode: 'index',
348
+ intersect: false,
349
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
350
+ titleColor: '#1D2129',
351
+ bodyColor: '#4E5969',
352
+ borderColor: '#E5E6EB',
353
+ borderWidth: 1,
354
+ cornerRadius: 8,
355
+ displayColors: true
356
+ }
357
+ },
358
+ scales: {
359
+ x: {
360
+ grid: {
361
+ color: '#F5F5F5'
362
+ },
363
+ ticks: {
364
+ color: '#4E5969',
365
+ maxRotation: 45
366
+ }
367
+ },
368
+ y: {
369
+ beginAtZero: true,
370
+ grid: {
371
+ color: '#F5F5F5'
372
+ },
373
+ ticks: {
374
+ color: '#4E5969'
375
+ }
376
+ }
377
+ },
378
+ interaction: {
379
+ mode: 'nearest',
380
+ axis: 'x',
381
+ intersect: false
382
+ }
383
+ }
384
+ });
385
+ } catch (error) {
386
+ console.error('创建图表失败:', error);
387
+ chartDiv.innerHTML = `
388
+ <div style="text-align: center; padding: 2rem;">
389
+ <i class="fas fa-exclamation-triangle" style="font-size: 2rem; margin-bottom: 1rem; display: block; color: var(--warning);"></i>
390
+ <p style="color: var(--dark-2); margin: 0;">图表渲染失败,请刷新页面重试</p>
391
+ <small style="color: var(--dark-3);">错误信息: ${error.message}</small>
392
+ </div>
393
+ `;
394
+ }
395
+ }
396
+
397
+ // 加载访问记录
398
+ async function loadAccessRecords() {
399
+ const toolId = document.getElementById('toolSelector').value;
400
+ const startDate = document.getElementById('startDate').value;
401
+ const endDate = document.getElementById('endDate').value;
402
+
403
+ if (toolId === '0') {
404
+ document.getElementById('accessRecords').innerHTML = `
405
+ <div style="text-align: center;">
406
+ <i class="fas fa-list" style="font-size: 3rem; margin-bottom: 1rem; display: block; opacity: 0.3;"></i>
407
+ <p>请选择工具查看访问记录</p>
408
+ </div>
409
+ `;
410
+ return;
411
+ }
412
+
413
+ try {
414
+ const response = await fetch(`/ToolStatistics/GetToolAccessRecords?toolId=${toolId}&startDate=${startDate}&endDate=${endDate}&limit=50`);
415
+ const result = await response.json();
416
+
417
+ if (result.success && result.data) {
418
+ renderAccessRecords(result.data);
419
+ }
420
+ } catch (error) {
421
+ console.error('加载访问记录失败:', error);
422
+ }
423
+ }
424
+
425
+ // 渲染访问记录
426
+ function renderAccessRecords(records) {
427
+ const recordsDiv = document.getElementById('accessRecords');
428
+
429
+ if (records.length === 0) {
430
+ recordsDiv.innerHTML = `
431
+ <div style="text-align: center; padding: 3rem;">
432
+ <i class="fas fa-inbox" style="font-size: 3rem; margin-bottom: 1rem; display: block; opacity: 0.3; color: var(--dark-2);"></i>
433
+ <p style="color: var(--dark-2); margin: 0;">暂无访问记录</p>
434
+ </div>
435
+ `;
436
+ return;
437
+ }
438
+
439
+ let html = '<div style="overflow-x: auto;">';
440
+ html += '<table style="width: 100%; border-collapse: collapse; font-size: 0.875rem;">';
441
+ html += '<thead><tr style="background: var(--light-1);">';
442
+ html += '<th style="padding: 0.75rem; border: 1px solid var(--light-2); text-align: left; font-weight: 600;">时间</th>';
443
+ html += '<th style="padding: 0.75rem; border: 1px solid var(--light-2); text-align: left; font-weight: 600;">用户</th>';
444
+ html += '<th style="padding: 0.75rem; border: 1px solid var(--light-2); text-align: left; font-weight: 600;">IP地址</th>';
445
+ html += '<th style="padding: 0.75rem; border: 1px solid var(--light-2); text-align: left; font-weight: 600;">操作类型</th>';
446
+ html += '<th style="padding: 0.75rem; border: 1px solid var(--light-2); text-align: left; font-weight: 600;">停留时间</th>';
447
+ html += '<th style="padding: 0.75rem; border: 1px solid var(--light-2); text-align: left; font-weight: 600;">用户代理</th>';
448
+ html += '</tr></thead>';
449
+ html += '<tbody>';
450
+
451
+ records.forEach((record, index) => {
452
+ const user = record.user ? record.user.nickName || record.user.userName : '匿名用户';
453
+ const duration = record.duration > 0 ? `${record.duration}秒` : '-';
454
+ const userAgent = record.userAgent ? record.userAgent.substring(0, 50) + (record.userAgent.length > 50 ? '...' : '') : '-';
455
+ const rowStyle = index % 2 === 0 ? 'background: white;' : 'background: var(--light-1);';
456
+
457
+ html += `<tr style="${rowStyle}">`;
458
+ html += `<td style="padding: 0.75rem; border: 1px solid var(--light-2);">${new Date(record.createdAt).toLocaleString()}</td>`;
459
+ html += `<td style="padding: 0.75rem; border: 1px solid var(--light-2);">${user}</td>`;
460
+ html += `<td style="padding: 0.75rem; border: 1px solid var(--light-2); font-family: monospace;">${record.ipAddress || '-'}</td>`;
461
+ html += `<td style="padding: 0.75rem; border: 1px solid var(--light-2);">${getAccessTypeBadge(record.accessType)}</td>`;
462
+ html += `<td style="padding: 0.75rem; border: 1px solid var(--light-2);">${duration}</td>`;
463
+ html += `<td style="padding: 0.75rem; border: 1px solid var(--light-2); font-size: 0.75rem; color: var(--dark-2);" title="${record.userAgent || ''}">${userAgent}</td>`;
464
+ html += '</tr>';
465
+ });
466
+
467
+ html += '</tbody></table>';
468
+ html += '</div>';
469
+ recordsDiv.innerHTML = html;
470
+ }
471
+
472
+ // 获取操作类型徽章
473
+ function getAccessTypeBadge(accessType) {
474
+ const badges = {
475
+ 'view': '<span style="background: #E6F7FF; color: #1890FF; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem;">查看</span>',
476
+ 'favorite': '<span style="background: #FFF2E8; color: #FA541C; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem;">收藏</span>',
477
+ 'share': '<span style="background: #F6FFED; color: #52C41A; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem;">分享</span>',
478
+ 'download': '<span style="background: #FFF7E6; color: #FA8C16; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem;">下载</span>'
479
+ };
480
+ return badges[accessType] || `<span style="background: #F5F5F5; color: #666; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem;">${accessType}</span>`;
481
+ }
482
+
483
+ // 获取操作类型文本
484
+ function getAccessTypeText(accessType) {
485
+ const types = {
486
+ 'view': '查看',
487
+ 'favorite': '收藏',
488
+ 'share': '分享',
489
+ 'download': '下载'
490
+ };
491
+ return types[accessType] || accessType;
492
+ }
493
+ </script>
Views/Tools/ImageCompressor.cshtml ADDED
@@ -0,0 +1,479 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @{
2
+ ViewData["Title"] = "图片压缩工具";
3
+ Layout = "_Layout";
4
+ }
5
+
6
+ <div class="container" style="padding-top: 6rem; padding-bottom: 4rem;">
7
+ <!-- 页面头部 -->
8
+ <div class="text-center mb-8">
9
+ <div class="brand-icon" style="width: 4rem; height: 4rem; margin: 0 auto 1.5rem; font-size: 1.8rem;">
10
+ <i class="fas fa-compress-alt"></i>
11
+ </div>
12
+ <h1 class="hero-title">图片压缩工具</h1>
13
+ <p class="hero-description">快速压缩图片文件,支持JPG、PNG、WebP等格式<br>
14
+ 保持高质量的同时大幅减小文件体积,提升网站加载速度</p>
15
+ </div>
16
+
17
+ <!-- 工具主体 -->
18
+ <div class="card" style="max-width: 800px; margin: 0 auto;">
19
+ <div class="card-body" style="padding: 2rem;">
20
+ <!-- 上传区域 -->
21
+ <div id="uploadArea" class="upload-area">
22
+ <div class="upload-content">
23
+ <i class="fas fa-cloud-upload-alt" style="font-size: 3rem; color: var(--primary); margin-bottom: 1rem;"></i>
24
+ <h3>拖拽图片到此处或点击选择</h3>
25
+ <p style="color: var(--dark-2); margin-bottom: 1.5rem;">支持 JPG、PNG、WebP、GIF 格式,单文件最大 10MB</p>
26
+ <button type="button" class="btn btn-primary" onclick="selectFiles()">
27
+ <i class="fas fa-upload"></i>
28
+ 选择图片
29
+ </button>
30
+ </div>
31
+ <input type="file" id="fileInput" multiple accept="image/*" style="display: none;" onchange="handleFiles(this.files)">
32
+ </div>
33
+
34
+ <!-- 压缩设置 -->
35
+ <div id="settingsPanel" class="settings-panel" style="display: none;">
36
+ <h4 style="margin-bottom: 1rem;">压缩设置</h4>
37
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
38
+ <div>
39
+ <label>压缩质量</label>
40
+ <div style="display: flex; align-items: center; gap: 1rem;">
41
+ <input type="range" id="qualitySlider" min="0.1" max="1" step="0.1" value="0.8"
42
+ style="flex: 1;" oninput="updateQualityDisplay(this.value)">
43
+ <span id="qualityDisplay" style="font-weight: 600; min-width: 50px;">80%</span>
44
+ </div>
45
+ </div>
46
+ <div>
47
+ <label>输出格式</label>
48
+ <select id="formatSelect" style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);">
49
+ <option value="original">保持原格式</option>
50
+ <option value="jpeg">JPEG</option>
51
+ <option value="png">PNG</option>
52
+ <option value="webp">WebP</option>
53
+ </select>
54
+ </div>
55
+ </div>
56
+ <button type="button" class="btn btn-primary" onclick="compressAllImages()" id="compressBtn">
57
+ <i class="fas fa-compress"></i>
58
+ 开始压缩
59
+ </button>
60
+ </div>
61
+
62
+ <!-- 文件列表 -->
63
+ <div id="fileList" class="file-list" style="display: none;">
64
+ <h4 style="margin-bottom: 1rem;">图片列表</h4>
65
+ <div id="imageItems"></div>
66
+ </div>
67
+
68
+ <!-- 压缩结果 -->
69
+ <div id="resultsPanel" class="results-panel" style="display: none;">
70
+ <h4 style="margin-bottom: 1rem;">压缩完成</h4>
71
+ <div id="compressResults"></div>
72
+ <div style="margin-top: 1.5rem; text-align: center;">
73
+ <button type="button" class="btn btn-primary" onclick="downloadAll()">
74
+ <i class="fas fa-download"></i>
75
+ 下载全部
76
+ </button>
77
+ <button type="button" class="btn btn-outline" onclick="resetTool()" style="margin-left: 1rem;">
78
+ <i class="fas fa-redo"></i>
79
+ 重新开始
80
+ </button>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ <!-- 使用说明 -->
87
+ <div class="card mt-8">
88
+ <div class="card-body">
89
+ <h3 style="margin-bottom: 1.5rem; text-align: center;">使用说明</h3>
90
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem;">
91
+ <div style="text-align: center;">
92
+ <div style="width: 3rem; height: 3rem; background: linear-gradient(135deg, var(--primary), var(--secondary)); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; color: white; font-size: 1.2rem;">1</div>
93
+ <h4>选择图片</h4>
94
+ <p style="color: var(--dark-2); font-size: 0.875rem;">支持拖拽上传或点击选择,可同时处理多张图片</p>
95
+ </div>
96
+ <div style="text-align: center;">
97
+ <div style="width: 3rem; height: 3rem; background: linear-gradient(135deg, var(--primary), var(--secondary)); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; color: white; font-size: 1.2rem;">2</div>
98
+ <h4>调整设置</h4>
99
+ <p style="color: var(--dark-2); font-size: 0.875rem;">设置压缩质量和输出格式,质量越低文件越小</p>
100
+ </div>
101
+ <div style="text-align: center;">
102
+ <div style="width: 3rem; height: 3rem; background: linear-gradient(135deg, var(--primary), var(--secondary)); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; color: white; font-size: 1.2rem;">3</div>
103
+ <h4>下载结果</h4>
104
+ <p style="color: var(--dark-2); font-size: 0.875rem;">压缩完成后可单独下载或批量下载所有图片</p>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+
111
+ <style>
112
+ .upload-area {
113
+ border: 2px dashed var(--light-2);
114
+ border-radius: var(--border-radius-lg);
115
+ padding: 3rem 2rem;
116
+ text-align: center;
117
+ background: var(--light-1);
118
+ transition: var(--transition);
119
+ cursor: pointer;
120
+ }
121
+
122
+ .upload-area:hover {
123
+ border-color: var(--primary);
124
+ background: rgba(22, 93, 255, 0.05);
125
+ }
126
+
127
+ .upload-area.dragover {
128
+ border-color: var(--primary);
129
+ background: rgba(22, 93, 255, 0.1);
130
+ }
131
+
132
+ .settings-panel, .file-list, .results-panel {
133
+ margin-top: 2rem;
134
+ padding-top: 2rem;
135
+ border-top: 1px solid var(--light-2);
136
+ }
137
+
138
+ .image-item {
139
+ display: flex;
140
+ align-items: center;
141
+ padding: 1rem;
142
+ border: 1px solid var(--light-2);
143
+ border-radius: var(--border-radius);
144
+ margin-bottom: 1rem;
145
+ background: white;
146
+ }
147
+
148
+ .image-preview {
149
+ width: 60px;
150
+ height: 60px;
151
+ object-fit: cover;
152
+ border-radius: var(--border-radius);
153
+ margin-right: 1rem;
154
+ }
155
+
156
+ .image-info {
157
+ flex: 1;
158
+ }
159
+
160
+ .image-actions {
161
+ display: flex;
162
+ gap: 0.5rem;
163
+ }
164
+
165
+ .progress-bar {
166
+ width: 100%;
167
+ height: 6px;
168
+ background: var(--light-2);
169
+ border-radius: 3px;
170
+ overflow: hidden;
171
+ margin-top: 0.5rem;
172
+ }
173
+
174
+ .progress-fill {
175
+ height: 100%;
176
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
177
+ transition: width 0.3s ease;
178
+ width: 0%;
179
+ }
180
+
181
+ .compression-stats {
182
+ display: grid;
183
+ grid-template-columns: 1fr 1fr 1fr;
184
+ gap: 1rem;
185
+ text-align: center;
186
+ padding: 1rem;
187
+ background: var(--light-1);
188
+ border-radius: var(--border-radius);
189
+ margin-bottom: 1rem;
190
+ }
191
+
192
+ .stat-item h5 {
193
+ margin: 0 0 0.25rem;
194
+ font-size: 1.2rem;
195
+ font-weight: 700;
196
+ color: var(--primary);
197
+ }
198
+
199
+ .stat-item p {
200
+ margin: 0;
201
+ font-size: 0.875rem;
202
+ color: var(--dark-2);
203
+ }
204
+
205
+ /* 响应式调整 */
206
+ @@media (max-width: 768px) {
207
+ .upload-area {
208
+ padding: 2rem 1rem;
209
+ }
210
+
211
+ .compression-stats {
212
+ grid-template-columns: 1fr;
213
+ gap: 0.5rem;
214
+ }
215
+ }
216
+ </style>
217
+
218
+ <script>
219
+ let selectedFiles = [];
220
+ let compressedFiles = [];
221
+
222
+ // 文件选择
223
+ function selectFiles() {
224
+ document.getElementById('fileInput').click();
225
+ }
226
+
227
+ // 处理文件选择
228
+ function handleFiles(files) {
229
+ if (files.length === 0) return;
230
+
231
+ selectedFiles = Array.from(files).filter(file => {
232
+ return file.type.startsWith('image/') && file.size <= 10 * 1024 * 1024; // 10MB限制
233
+ });
234
+
235
+ if (selectedFiles.length === 0) {
236
+ toolHub.showToast('请选择有效的图片文件(小于10MB)', 'error');
237
+ return;
238
+ }
239
+
240
+ displaySelectedFiles();
241
+ document.getElementById('settingsPanel').style.display = 'block';
242
+ document.getElementById('fileList').style.display = 'block';
243
+ }
244
+
245
+ // 显示选中的文件
246
+ function displaySelectedFiles() {
247
+ const container = document.getElementById('imageItems');
248
+ container.innerHTML = '';
249
+
250
+ selectedFiles.forEach((file, index) => {
251
+ const item = document.createElement('div');
252
+ item.className = 'image-item';
253
+ item.innerHTML = `
254
+ <img class="image-preview" src="${URL.createObjectURL(file)}" alt="${file.name}">
255
+ <div class="image-info">
256
+ <div style="font-weight: 600; margin-bottom: 0.25rem;">${file.name}</div>
257
+ <div style="font-size: 0.875rem; color: var(--dark-2);">
258
+ ${formatFileSize(file.size)} • ${file.type}
259
+ </div>
260
+ <div class="progress-bar" id="progress-${index}" style="display: none;">
261
+ <div class="progress-fill" id="progress-fill-${index}"></div>
262
+ </div>
263
+ </div>
264
+ <div class="image-actions">
265
+ <button class="btn btn-outline btn-sm" onclick="removeFile(${index})">
266
+ <i class="fas fa-times"></i>
267
+ </button>
268
+ </div>
269
+ `;
270
+ container.appendChild(item);
271
+ });
272
+ }
273
+
274
+ // 移除文件
275
+ function removeFile(index) {
276
+ selectedFiles.splice(index, 1);
277
+ if (selectedFiles.length === 0) {
278
+ document.getElementById('settingsPanel').style.display = 'none';
279
+ document.getElementById('fileList').style.display = 'none';
280
+ } else {
281
+ displaySelectedFiles();
282
+ }
283
+ }
284
+
285
+ // 更新质量显示
286
+ function updateQualityDisplay(value) {
287
+ document.getElementById('qualityDisplay').textContent = Math.round(value * 100) + '%';
288
+ }
289
+
290
+ // 压缩所有图片
291
+ async function compressAllImages() {
292
+ const quality = parseFloat(document.getElementById('qualitySlider').value);
293
+ const format = document.getElementById('formatSelect').value;
294
+
295
+ compressedFiles = [];
296
+ document.getElementById('compressBtn').disabled = true;
297
+ document.getElementById('compressBtn').innerHTML = '<i class="fas fa-spinner fa-spin"></i> 压缩中...';
298
+
299
+ for (let i = 0; i < selectedFiles.length; i++) {
300
+ const file = selectedFiles[i];
301
+ document.getElementById(`progress-${i}`).style.display = 'block';
302
+
303
+ try {
304
+ const compressedFile = await compressImage(file, quality, format, (progress) => {
305
+ document.getElementById(`progress-fill-${i}`).style.width = progress + '%';
306
+ });
307
+ compressedFiles.push(compressedFile);
308
+ } catch (error) {
309
+ console.error('压缩失败:', error);
310
+ toolHub.showToast(`压缩 ${file.name} 失败`, 'error');
311
+ }
312
+ }
313
+
314
+ showResults();
315
+ document.getElementById('compressBtn').disabled = false;
316
+ document.getElementById('compressBtn').innerHTML = '<i class="fas fa-compress"></i> 开始压缩';
317
+ }
318
+
319
+ // 压缩单个图片
320
+ function compressImage(file, quality, format, onProgress) {
321
+ return new Promise((resolve, reject) => {
322
+ const canvas = document.createElement('canvas');
323
+ const ctx = canvas.getContext('2d');
324
+ const img = new Image();
325
+
326
+ img.onload = function() {
327
+ canvas.width = img.width;
328
+ canvas.height = img.height;
329
+
330
+ ctx.drawImage(img, 0, 0);
331
+
332
+ const outputFormat = format === 'original' ? file.type : `image/${format}`;
333
+
334
+ canvas.toBlob((blob) => {
335
+ if (blob) {
336
+ const compressedFile = new File([blob],
337
+ getCompressedFileName(file.name, format),
338
+ { type: outputFormat }
339
+ );
340
+ onProgress(100);
341
+ resolve({
342
+ original: file,
343
+ compressed: compressedFile,
344
+ compressionRatio: ((file.size - blob.size) / file.size * 100).toFixed(1)
345
+ });
346
+ } else {
347
+ reject(new Error('压缩失败'));
348
+ }
349
+ }, outputFormat, quality);
350
+ };
351
+
352
+ img.onerror = () => reject(new Error('图片加载失败'));
353
+ img.src = URL.createObjectURL(file);
354
+
355
+ // 模拟进度
356
+ let progress = 0;
357
+ const progressInterval = setInterval(() => {
358
+ progress += 10;
359
+ if (progress >= 90) {
360
+ clearInterval(progressInterval);
361
+ } else {
362
+ onProgress(progress);
363
+ }
364
+ }, 50);
365
+ });
366
+ }
367
+
368
+ // 获取压缩后的文件名
369
+ function getCompressedFileName(originalName, format) {
370
+ if (format === 'original') return originalName;
371
+
372
+ const nameWithoutExt = originalName.substring(0, originalName.lastIndexOf('.'));
373
+ return `${nameWithoutExt}_compressed.${format === 'jpeg' ? 'jpg' : format}`;
374
+ }
375
+
376
+ // 显示压缩结果
377
+ function showResults() {
378
+ const totalOriginalSize = compressedFiles.reduce((sum, item) => sum + item.original.size, 0);
379
+ const totalCompressedSize = compressedFiles.reduce((sum, item) => sum + item.compressed.size, 0);
380
+ const totalSavings = ((totalOriginalSize - totalCompressedSize) / totalOriginalSize * 100).toFixed(1);
381
+
382
+ document.getElementById('compressResults').innerHTML = `
383
+ <div class="compression-stats">
384
+ <div class="stat-item">
385
+ <h5>${formatFileSize(totalOriginalSize)}</h5>
386
+ <p>原始大小</p>
387
+ </div>
388
+ <div class="stat-item">
389
+ <h5>${formatFileSize(totalCompressedSize)}</h5>
390
+ <p>压缩后大小</p>
391
+ </div>
392
+ <div class="stat-item">
393
+ <h5>${totalSavings}%</h5>
394
+ <p>压缩率</p>
395
+ </div>
396
+ </div>
397
+ ${compressedFiles.map((item, index) => `
398
+ <div class="image-item">
399
+ <img class="image-preview" src="${URL.createObjectURL(item.compressed)}" alt="${item.compressed.name}">
400
+ <div class="image-info">
401
+ <div style="font-weight: 600; margin-bottom: 0.25rem;">${item.compressed.name}</div>
402
+ <div style="font-size: 0.875rem; color: var(--dark-2);">
403
+ ${formatFileSize(item.original.size)} → ${formatFileSize(item.compressed.size)} (节省 ${item.compressionRatio}%)
404
+ </div>
405
+ </div>
406
+ <div class="image-actions">
407
+ <button class="btn btn-primary btn-sm" onclick="downloadFile(${index})">
408
+ <i class="fas fa-download"></i> 下载
409
+ </button>
410
+ </div>
411
+ </div>
412
+ `).join('')}
413
+ `;
414
+
415
+ document.getElementById('resultsPanel').style.display = 'block';
416
+ }
417
+
418
+ // 下载单个文件
419
+ function downloadFile(index) {
420
+ const item = compressedFiles[index];
421
+ const url = URL.createObjectURL(item.compressed);
422
+ const a = document.createElement('a');
423
+ a.href = url;
424
+ a.download = item.compressed.name;
425
+ a.click();
426
+ URL.revokeObjectURL(url);
427
+ }
428
+
429
+ // 下载所有文件
430
+ function downloadAll() {
431
+ compressedFiles.forEach((item, index) => {
432
+ setTimeout(() => downloadFile(index), index * 200);
433
+ });
434
+ }
435
+
436
+ // 重置工具
437
+ function resetTool() {
438
+ selectedFiles = [];
439
+ compressedFiles = [];
440
+ document.getElementById('fileInput').value = '';
441
+ document.getElementById('settingsPanel').style.display = 'none';
442
+ document.getElementById('fileList').style.display = 'none';
443
+ document.getElementById('resultsPanel').style.display = 'none';
444
+ document.getElementById('imageItems').innerHTML = '';
445
+ document.getElementById('compressResults').innerHTML = '';
446
+ }
447
+
448
+ // 格式化文件大小
449
+ function formatFileSize(bytes) {
450
+ if (bytes === 0) return '0 B';
451
+ const k = 1024;
452
+ const sizes = ['B', 'KB', 'MB', 'GB'];
453
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
454
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
455
+ }
456
+
457
+ // 拖拽上传
458
+ document.addEventListener('DOMContentLoaded', function() {
459
+ const uploadArea = document.getElementById('uploadArea');
460
+
461
+ uploadArea.addEventListener('dragover', function(e) {
462
+ e.preventDefault();
463
+ uploadArea.classList.add('dragover');
464
+ });
465
+
466
+ uploadArea.addEventListener('dragleave', function(e) {
467
+ e.preventDefault();
468
+ uploadArea.classList.remove('dragover');
469
+ });
470
+
471
+ uploadArea.addEventListener('drop', function(e) {
472
+ e.preventDefault();
473
+ uploadArea.classList.remove('dragover');
474
+ handleFiles(e.dataTransfer.files);
475
+ });
476
+
477
+ uploadArea.addEventListener('click', selectFiles);
478
+ });
479
+ </script>
Views/_ViewImports.cshtml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ @using ToolHub
2
+ @using ToolHub.Models
3
+ @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Views/_ViewStart.cshtml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ @{
2
+ Layout = "_Layout";
3
+ }
appsettings.Development.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "Logging": {
3
+ "LogLevel": {
4
+ "Default": "Information",
5
+ "Microsoft.AspNetCore": "Warning"
6
+ }
7
+ }
8
+ }