dashboard.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. {% extends "base.html" %}
  2. {% block title %}逐鹿导航 - 我的导航{% endblock %}
  3. {% block scripts %}
  4. <script>
  5. // 按创建时间升序排序站点(旧站点在前,新站点在后)
  6. function sortSitesByCreatedAt(sites) {
  7. return sites.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
  8. }
  9. function showCategoryModal(action, categoryId = null) {
  10. const modal = new bootstrap.Modal(document.getElementById('categoryModal'));
  11. const form = document.getElementById('categoryForm');
  12. const modalTitle = document.getElementById('categoryModalLabel');
  13. if (action === 'add') {
  14. modalTitle.textContent = '新增分类';
  15. form.reset();
  16. document.getElementById('categoryId').value = '';
  17. } else if (action === 'edit') {
  18. modalTitle.textContent = '编辑分类';
  19. fetch(`/api/category/${categoryId}`)
  20. .then(response => response.json())
  21. .then(data => {
  22. document.getElementById('categoryId').value = data.id;
  23. document.getElementById('categoryName').value = data.name;
  24. document.getElementById('isPublic').checked = data.is_public;
  25. });
  26. }
  27. modal.show();
  28. }
  29. function submitCategoryForm() {
  30. const form = document.getElementById('categoryForm');
  31. const formData = new FormData(form);
  32. const categoryId = formData.get('category_id');
  33. const url = categoryId ? `/api/category/${categoryId}` : '/api/category';
  34. const method = categoryId ? 'PUT' : 'POST';
  35. // 将 FormData 转换为 JSON 对象
  36. const jsonData = {
  37. name: formData.get('name'),
  38. is_public: formData.get('is_public') === 'on' // 将复选框值转换为布尔值
  39. };
  40. if (categoryId) {
  41. jsonData.id = categoryId; // 添加分类 ID(仅用于编辑)
  42. }
  43. fetch(url, {
  44. method: method,
  45. headers: {
  46. 'Content-Type': 'application/json',
  47. 'Accept': 'application/json'
  48. },
  49. body: JSON.stringify(jsonData)
  50. })
  51. .then(response => response.json())
  52. .then(data => {
  53. if (data.success) {
  54. location.reload();
  55. } else {
  56. alert(data.message || '操作失败');
  57. }
  58. })
  59. .catch(error => {
  60. console.error('Error:', error);
  61. alert('操作失败');
  62. });
  63. }
  64. </script>
  65. {% endblock %}
  66. {% block content %}
  67. <h2>我的导航</h2>
  68. <div class="row mb-4">
  69. <div class="col-md-12">
  70. <a href="{{ url_for('add_site') }}" class="btn btn-primary me-2">添加站点</a>
  71. <button type="button" class="btn btn-secondary me-2" onclick="showCategoryModal('add')">添加分类</button>
  72. <button type="button" class="btn btn-info me-2" data-bs-toggle="modal" data-bs-target="#manageCategoriesModal">
  73. 管理分类
  74. </button>
  75. <a href="{{ url_for('import_bookmarks') }}" class="btn btn-success">导入书签</a>
  76. </div>
  77. </div>
  78. <!-- 分类管理弹窗 -->
  79. <div class="modal fade" id="manageCategoriesModal" tabindex="-1" aria-labelledby="manageCategoriesModalLabel" aria-hidden="true">
  80. <div class="modal-dialog modal-lg">
  81. <div class="modal-content">
  82. <div class="modal-header">
  83. <h5 class="modal-title" id="manageCategoriesModalLabel">管理分类</h5>
  84. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  85. </div>
  86. <div class="modal-body">
  87. {% if categories %}
  88. <div class="list-group">
  89. {% for category in categories %}
  90. <div class="d-flex justify-content-between align-items-center mb-2 border rounded p-2">
  91. <div>
  92. <h6 class="mb-1">{{ category.name }}</h6>
  93. <small class="text-muted">{{ category.sites|length }} 个站点</small>
  94. </div>
  95. <div>
  96. <button type="button" class="btn btn-sm btn-outline-primary me-1" onclick="showCategoryModal('edit', {{ category.id }})">编辑</button>
  97. <form method="POST" action="{{ url_for('delete_category', category_id=category.id) }}"
  98. class="d-inline"
  99. onsubmit="return confirm('确定要删除分类 {{ category.name }} 吗?这将同时删除该分类下的所有站点。');">
  100. <button type="submit" class="btn btn-sm btn-outline-danger">删除</button>
  101. </form>
  102. </div>
  103. </div>
  104. {% endfor %}
  105. </div>
  106. {% else %}
  107. <p class="text-muted text-center py-3">您还没有创建任何分类,点击上方"添加分类"按钮创建第一个分类吧!</p>
  108. {% endif %}
  109. </div>
  110. <div class="modal-footer">
  111. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
  112. </div>
  113. </div>
  114. </div>
  115. </div>
  116. <!-- 编辑/新增分类弹窗(放到 body 直接下方,脱离父 modal) -->
  117. <div class="modal fade" id="categoryModal" tabindex="-1" aria-labelledby="categoryModalLabel" aria-hidden="true">
  118. <div class="modal-dialog">
  119. <div class="modal-content">
  120. <div class="modal-header">
  121. <h5 class="modal-title" id="categoryModalLabel">编辑分类</h5>
  122. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  123. </div>
  124. <div class="modal-body">
  125. <form id="categoryForm">
  126. <input type="hidden" id="categoryId" name="category_id">
  127. <div class="mb-3">
  128. <label for="categoryName" class="form-label">分类名称</label>
  129. <input type="text" class="form-control" id="categoryName" name="name" required>
  130. </div>
  131. <div class="mb-3 form-check">
  132. <input type="checkbox" class="form-check-input" id="isPublic" name="is_public">
  133. <label class="form-check-label" for="isPublic">公开分类</label>
  134. </div>
  135. </form>
  136. </div>
  137. <div class="modal-footer">
  138. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
  139. <button type="button" class="btn btn-primary" onclick="submitCategoryForm()">保存</button>
  140. </div>
  141. </div>
  142. </div>
  143. </div>
  144. <!-- 按分类显示站点(保持原样) -->
  145. <div class="card">
  146. <div class="card-header">
  147. <h5>我的站点</h5>
  148. </div>
  149. <div class="card-body">
  150. {% if sites %}
  151. {% set categorized_sites = {} %}
  152. {% for site in sites %}
  153. {% if site.category %}
  154. {% if site.category.id not in categorized_sites %}
  155. {% set _ = categorized_sites.update({site.category.id: {'category': site.category, 'sites': [site]}}) %}
  156. {% else %}
  157. {% set _ = categorized_sites[site.category.id].sites.append(site) %}
  158. {% endif %}
  159. {% endif %}
  160. {% endfor %}
  161. {% for category_id, data in categorized_sites.items() %}
  162. <div class="mb-4">
  163. <h6 class="text-primary fw-bold border-bottom pb-2 mb-3">
  164. 📁 {{ data.category.name }} ({{ data.sites|length }})
  165. </h6>
  166. <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-3">
  167. {% for site in data.sites %}
  168. <div class="col">
  169. <div class="card h-100">
  170. <div class="card-body py-2">
  171. <div class="row align-items-center">
  172. <div class="col-auto">
  173. {% if site.custom_icon %}
  174. <img src="{{ site.custom_icon }}" alt="图标"
  175. style="width:40px; height:40px; border-radius:8px; margin-right:8px;"
  176. onerror="this.src='{{ url_for('static', filename='images/default-icon.png') }}'">
  177. {% elif site.icon %}
  178. <img src="{{ site.icon }}" alt="图标"
  179. style="width:40px; height:40px; border-radius:8px; margin-right:8px;"
  180. onerror="this.src='{{ url_for('static', filename='images/default-icon.png') }}'">
  181. {% else %}
  182. <img src="{{ url_for('static', filename='images/default-icon.png') }}" alt="图标"
  183. style="width:40px; height:40px; border-radius:8px; margin-right:8px;">
  184. {% endif %}
  185. </div>
  186. <div class="col">
  187. <div class="d-flex justify-content-between align-items-center">
  188. <div>
  189. <h6 class="mb-1">
  190. <a href="{{ url_for('site_detail', site_id=site.id) }}"
  191. class="text-decoration-none text-dark">{{ site.name }}</a>
  192. </h6>
  193. <small class="text-muted">{{ site.url }}</small>
  194. </div>
  195. <div>
  196. <span class="badge bg-secondary bg-opacity-75 small">
  197. {{ '公开' if site.is_public else '私有' }}
  198. </span>
  199. </div>
  200. </div>
  201. {% if site.description %}
  202. <small class="text-muted d-block mt-1">{{ site.description }}</small>
  203. {% endif %}
  204. <div class="mt-1">
  205. <a href="{{ url_for('edit_site', site_id=site.id) }}" class="btn btn-sm btn-outline-primary me-1">编辑</a>
  206. <a href="{{ url_for('site_detail', site_id=site.id) }}" class="btn btn-sm btn-outline-secondary me-1">查看</a>
  207. <form method="POST" action="{{ url_for('delete_site', site_id=site.id) }}"
  208. class="d-inline"
  209. onsubmit="return confirm('确定要删除站点 {{ site.name }} 吗?');">
  210. <button type="submit" class="btn btn-sm btn-outline-danger">删除</button>
  211. </form>
  212. </div>
  213. </div>
  214. </div>
  215. </div>
  216. </div>
  217. </div>
  218. {% endfor %}
  219. </div>
  220. </div>
  221. {% endfor %}
  222. {% set uncategorized_sites = sites | selectattr('category', 'equalto', none) | list %}
  223. {% if uncategorized_sites %}
  224. <div class="mb-4">
  225. <h6 class="text-muted fw-bold border-bottom pb-2 mb-3">
  226. 📋 未分类 ({{ uncategorized_sites|length }})
  227. </h6>
  228. <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-3">
  229. {% for site in uncategorized_sites %}
  230. <div class="col">
  231. <div class="card h-100">
  232. <div class="card-body py-2">
  233. <div class="row align-items-center">
  234. <div class="col-auto">
  235. {% if site.custom_icon %}
  236. <img src="{{ site.custom_icon }}" alt="图标"
  237. style="width:40px; height:40px; border-radius:8px; margin-right:8px;"
  238. onerror="this.src='{{ url_for('static', filename='images/default-icon.png') }}'">
  239. {% elif site.icon %}
  240. <img src="{{ site.icon }}" alt="图标"
  241. style="width:40px; height:40px; border-radius:8px; margin-right:8px;"
  242. onerror="this.src='{{ url_for('static', filename='images/default-icon.png') }}'">
  243. {% else %}
  244. <img src="{{ url_for('static', filename='images/default-icon.png') }}" alt="图标"
  245. style="width:40px; height:40px; border-radius:8px; margin-right:8px;">
  246. {% endif %}
  247. </div>
  248. <div class="col">
  249. <div class="d-flex justify-content-between align-items-center">
  250. <div>
  251. <h6 class="mb-1">
  252. <a href="{{ url_for('site_detail', site_id=site.id) }}"
  253. class="text-decoration-none text-dark">{{ site.name }}</a>
  254. </h6>
  255. <small class="text-muted">{{ site.url }}</small>
  256. </div>
  257. <div>
  258. <span class="badge bg-secondary bg-opacity-75 small">
  259. {{ '公开' if site.is_public else '私有' }}
  260. </span>
  261. </div>
  262. </div>
  263. {% if site.description %}
  264. <small class="text-muted d-block mt-1">{{ site.description }}</small>
  265. {% endif %}
  266. <div class="mt-1">
  267. <a href="{{ url_for('edit_site', site_id=site.id) }}" class="btn btn-sm btn-outline-primary me-1">编辑</a>
  268. <a href="{{ url_for('site_detail', site_id=site.id) }}" class="btn btn-sm btn-outline-secondary me-1">查看</a>
  269. <form method="POST" action="{{ url_for('delete_site', site_id=site.id) }}"
  270. class="d-inline"
  271. onsubmit="return confirm('确定要删除站点 {{ site.name }} 吗?');">
  272. <button type="submit" class="btn btn-sm btn-outline-danger">删除</button>
  273. </form>
  274. </div>
  275. </div>
  276. </div>
  277. </div>
  278. </div>
  279. </div>
  280. {% endfor %}
  281. </div>
  282. </div>
  283. {% endif %}
  284. {% else %}
  285. <p class="text-muted">您还没有添加任何站点,点击上方按钮添加第一个站点吧!</p>
  286. {% endif %}
  287. </div>
  288. </div>
  289. {% endblock %}