preview_bookmarks.html 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. {% extends "base.html" %}
  2. {% block title %}逐鹿导航 - 导入预览{% endblock %}
  3. {% block head %}
  4. {{ super() }}
  5. <!-- 确保 jQuery 和 Bootstrap JS 正确加载 -->
  6. <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  7. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
  8. {% endblock %}
  9. {% block content %}
  10. <div class="row">
  11. <div class="col-md-12">
  12. <div class="card">
  13. <div class="card-header d-flex justify-content-between align-items-center">
  14. <h4>书签导入预览</h4>
  15. <div>
  16. <span class="badge bg-primary">总计: {{ preview_data.total_count }} 个书签</span>
  17. </div>
  18. </div>
  19. <div class="card-body">
  20. <!-- 统计信息 -->
  21. <div class="row mb-4">
  22. <div class="col-md-3">
  23. <div class="card text-white bg-success">
  24. <div class="card-body text-center">
  25. <h4>{{ preview_data.folders | sum(attribute='new_count') }}</h4>
  26. <p>新书签</p>
  27. </div>
  28. </div>
  29. </div>
  30. <div class="col-md-3">
  31. <div class="card text-white bg-warning">
  32. <div class="card-body text-center">
  33. <h4>{{ preview_data.folders | sum(attribute='duplicate_count') }}</h4>
  34. <p>重复书签</p>
  35. </div>
  36. </div>
  37. </div>
  38. <div class="col-md-3">
  39. <div class="card text-white bg-info">
  40. <div class="card-body text-center">
  41. <h4>{{ preview_data.folders | length }}</h4>
  42. <p>分类数量</p>
  43. </div>
  44. </div>
  45. </div>
  46. <div class="col-md-3">
  47. <div class="card">
  48. <div class="card-body text-center">
  49. <div class="form-check form-switch">
  50. <input class="form-check-input" type="checkbox" id="selectAll" checked>
  51. <label class="form-check-label" for="selectAll">全选</label>
  52. </div>
  53. </div>
  54. </div>
  55. </div>
  56. </div>
  57. <!-- 书签预览 -->
  58. <form id="previewForm">
  59. {% for folder in preview_data.folders %}
  60. <div class="card mb-3">
  61. <div class="card-header">
  62. <h5 class="mb-0">
  63. <span class="badge bg-secondary me-2">{{ folder.bookmarks | length }}</span>
  64. {{ folder.name }}
  65. <small class="text-muted">
  66. (新: {{ folder.new_count }}, 重复: {{ folder.duplicate_count }})
  67. </small>
  68. </h5>
  69. </div>
  70. <div class="card-body">
  71. <div class="table-responsive">
  72. <table class="table table-hover">
  73. <thead>
  74. <tr>
  75. <th width="30">
  76. <input type="checkbox" class="folder-select"
  77. data-folder="{{ folder.name }}" checked>
  78. </th>
  79. <th width="40%">名称</th>
  80. <th width="40%">网址</th>
  81. <th width="20%">分类</th>
  82. <th width="80">状态</th>
  83. </tr>
  84. </thead>
  85. <tbody>
  86. {% for bookmark in folder.bookmarks %}
  87. <tr class="{% if bookmark.is_duplicate %}table-warning{% endif %}">
  88. <td>
  89. <input type="checkbox" name="selected_bookmarks"
  90. value="{{ loop.index0 }}"
  91. data-folder="{{ folder.name }}"
  92. {% if bookmark.selected %}checked{% endif %}
  93. class="bookmark-check">
  94. </td>
  95. <td>
  96. <input type="text" class="form-control form-control-sm bookmark-name"
  97. value="{{ bookmark.name }}"
  98. data-original="{{ bookmark.name }}">
  99. </td>
  100. <td>
  101. <input type="text" class="form-control form-control-sm bookmark-url"
  102. value="{{ bookmark.url }}"
  103. data-original="{{ bookmark.url }}">
  104. {% if bookmark.description %}
  105. <small class="text-muted">{{ bookmark.description }}</small>
  106. {% endif %}
  107. </td>
  108. <td>
  109. <select class="form-select form-select-sm bookmark-category">
  110. <option value="{{ folder.name }}">{{ folder.name }}</option>
  111. {% for category in user_categories %}
  112. <option value="{{ category.name }}">{{ category.name }}</option>
  113. {% endfor %}
  114. <option value="_custom">自定义...</option>
  115. </select>
  116. <input type="text" class="form-control form-control-sm mt-1 custom-category"
  117. placeholder="输入分类名称" style="display: none;">
  118. </td>
  119. <td>
  120. {% if bookmark.is_duplicate %}
  121. <span class="badge bg-warning">重复</span>
  122. {% else %}
  123. <span class="badge bg-success">新</span>
  124. {% endif %}
  125. </td>
  126. </tr>
  127. {% endfor %}
  128. </tbody>
  129. </table>
  130. </div>
  131. </div>
  132. </div>
  133. {% endfor %}
  134. </form>
  135. <!-- 操作按钮 -->
  136. <div class="d-flex justify-content-between mt-4">
  137. <a href="{{ url_for('import_bookmarks') }}" class="btn btn-secondary">
  138. <i class="fas fa-arrow-left"></i> 重新上传
  139. </a>
  140. <button type="button" id="startImport" class="btn btn-success">
  141. <i class="fas fa-download"></i> 开始导入
  142. </button>
  143. </div>
  144. </div>
  145. </div>
  146. </div>
  147. </div>
  148. <!-- 导入进度模态框 -->
  149. <div class="modal fade" id="importProgressModal" tabindex="-1">
  150. <div class="modal-dialog">
  151. <div class="modal-content">
  152. <div class="modal-header">
  153. <h5 class="modal-title">正在导入书签</h5>
  154. </div>
  155. <div class="modal-body">
  156. <div class="progress mb-3">
  157. <div class="progress-bar progress-bar-striped progress-bar-animated"
  158. role="progressbar" style="width: 0%"></div>
  159. </div>
  160. <div class="import-status">
  161. <p>准备开始导入...</p>
  162. </div>
  163. <div class="import-results" style="display: none;">
  164. <h6>导入结果:</h6>
  165. <ul class="list-unstyled"></ul>
  166. </div>
  167. </div>
  168. <div class="modal-footer">
  169. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="cancelBtn" style="display: none;">取消</button>
  170. <button type="button" class="btn btn-primary" data-bs-dismiss="modal" id="completeBtn" style="display: none;">完成</button>
  171. </div>
  172. </div>
  173. </div>
  174. </div>
  175. {% endblock %}
  176. {% block scripts %}
  177. <script>
  178. // 检查 jQuery 和 Bootstrap 是否已加载
  179. function checkDependencies() {
  180. if (typeof jQuery === 'undefined') {
  181. console.error('jQuery 未加载,正在动态加载...');
  182. // 动态加载 jQuery
  183. var script = document.createElement('script');
  184. script.src = 'https://code.jquery.com/jquery-3.6.0.min.js';
  185. script.onload = function() {
  186. console.log('jQuery 动态加载完成');
  187. // 加载 Bootstrap JS
  188. loadBootstrap();
  189. };
  190. document.head.appendChild(script);
  191. } else {
  192. console.log('jQuery 已加载');
  193. // 检查 Bootstrap
  194. if (typeof bootstrap === 'undefined') {
  195. loadBootstrap();
  196. } else {
  197. console.log('Bootstrap 已加载');
  198. initializePage();
  199. }
  200. }
  201. }
  202. function loadBootstrap() {
  203. console.log('正在加载 Bootstrap JS...');
  204. var script = document.createElement('script');
  205. script.src = 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js';
  206. script.onload = function() {
  207. console.log('Bootstrap JS 动态加载完成');
  208. initializePage();
  209. };
  210. script.onerror = function() {
  211. console.error('Bootstrap JS 加载失败');
  212. // 即使 Bootstrap 加载失败,也尝试初始化页面
  213. initializePage();
  214. };
  215. document.head.appendChild(script);
  216. }
  217. function initializePage() {
  218. console.log('页面初始化开始');
  219. // 检查 Bootstrap modal 是否可用
  220. if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
  221. console.log('Bootstrap Modal 可用');
  222. } else {
  223. console.warn('Bootstrap Modal 不可用,将使用备用方案');
  224. }
  225. // 全选/全不选
  226. $('#selectAll').change(function() {
  227. $('.bookmark-check').prop('checked', this.checked);
  228. updateSelectionCount();
  229. });
  230. // 文件夹选择
  231. $('.folder-select').change(function() {
  232. const folder = $(this).data('folder');
  233. $(`.bookmark-check[data-folder="${folder}"]`).prop('checked', this.checked);
  234. updateSelectionCount();
  235. });
  236. // 单个书签选择
  237. $('.bookmark-check').change(updateSelectionCount);
  238. // 分类选择
  239. $('.bookmark-category').change(function() {
  240. const customInput = $(this).closest('td').find('.custom-category');
  241. if ($(this).val() === '_custom') {
  242. customInput.show();
  243. } else {
  244. customInput.hide();
  245. }
  246. });
  247. // 开始导入
  248. $('#startImport').click(function() {
  249. console.log('开始导入按钮被点击');
  250. const selectedBookmarks = prepareImportData();
  251. console.log('选中的书签:', selectedBookmarks);
  252. if (selectedBookmarks.length === 0) {
  253. alert('请至少选择一个书签进行导入');
  254. return;
  255. }
  256. // 使用 Bootstrap Modal 或备用方案
  257. try {
  258. // 尝试使用 Bootstrap Modal
  259. if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
  260. var modalElement = document.getElementById('importProgressModal');
  261. var modal = new bootstrap.Modal(modalElement);
  262. modal.show();
  263. } else {
  264. // 备用方案:直接显示模态框
  265. $('#importProgressModal').show();
  266. $('body').addClass('modal-open');
  267. }
  268. } catch (error) {
  269. console.error('显示模态框失败:', error);
  270. // 备用方案:直接显示模态框
  271. $('#importProgressModal').show();
  272. $('body').addClass('modal-open');
  273. }
  274. startImport(selectedBookmarks);
  275. });
  276. function updateSelectionCount() {
  277. const selectedCount = $('.bookmark-check:checked').length;
  278. const totalCount = $('.bookmark-check').length;
  279. $('#selectAll').prop('checked', selectedCount === totalCount);
  280. console.log('已选择书签数量:', selectedCount, '/', totalCount);
  281. }
  282. function prepareImportData() {
  283. const selectedBookmarks = [];
  284. $('.bookmark-check:checked').each(function() {
  285. const row = $(this).closest('tr');
  286. const folder = $(this).data('folder');
  287. const name = row.find('.bookmark-name').val();
  288. const url = row.find('.bookmark-url').val();
  289. const categorySelect = row.find('.bookmark-category');
  290. let category = categorySelect.val();
  291. if (category === '_custom') {
  292. category = row.find('.custom-category').val() || folder;
  293. }
  294. selectedBookmarks.push({
  295. name: name,
  296. url: url,
  297. folder: folder,
  298. custom_category: category !== folder ? category : null
  299. });
  300. });
  301. return selectedBookmarks;
  302. }
  303. function startImport(bookmarks) {
  304. const importData = {
  305. selected_bookmarks: bookmarks,
  306. import_as_public: {{ preview_data.import_as_public | tojson }}
  307. };
  308. console.log('开始导入,数据:', importData);
  309. // 开始导入
  310. $.ajax({
  311. url: '/api/import-bookmarks',
  312. method: 'POST',
  313. contentType: 'application/json',
  314. data: JSON.stringify(importData),
  315. success: function(response) {
  316. console.log('导入启动响应:', response);
  317. if (response.status === 'started') {
  318. monitorProgress(response.import_id);
  319. } else {
  320. console.error('导入启动失败:', response);
  321. updateProgress('导入启动失败: ' + (response.error || '未知错误'), 0, true);
  322. }
  323. },
  324. error: function(xhr, status, error) {
  325. console.error('AJAX错误:', status, error, xhr.responseText);
  326. updateProgress('导入启动失败: ' + error, 0, true);
  327. }
  328. });
  329. }
  330. function monitorProgress(importId) {
  331. console.log('开始监控进度:', importId);
  332. const progressBar = $('.progress-bar');
  333. const statusText = $('.import-status');
  334. const resultsDiv = $('.import-results');
  335. const resultsList = $('.import-results ul');
  336. const cancelBtn = $('#cancelBtn');
  337. const completeBtn = $('#completeBtn');
  338. let checkCount = 0;
  339. const maxChecks = 300;
  340. function checkProgress() {
  341. if (checkCount >= maxChecks) {
  342. console.log('进度检查超时');
  343. updateProgress('导入超时', 0, true);
  344. return;
  345. }
  346. const progressUrl = '/api/import-progress/' + importId;
  347. console.log('查询进度:', progressUrl);
  348. $.get(progressUrl)
  349. .done(function(data) {
  350. console.log('进度响应:', data);
  351. if (data.error) {
  352. console.error('进度查询错误:', data.error);
  353. updateProgress('导入任务不存在: ' + data.error, 0, true);
  354. return;
  355. }
  356. const percent = data.total > 0 ? Math.round((data.processed / data.total) * 100) : 0;
  357. progressBar.css('width', percent + '%');
  358. progressBar.text(percent + '%');
  359. statusText.html(`
  360. <p>进度: ${data.processed}/${data.total} (${percent}%)</p>
  361. <p>状态: ${data.status}</p>
  362. <p>成功: ${data.results.success} | 失败: ${data.results.failed}</p>
  363. `);
  364. if (data.status === 'completed' || data.status === 'failed') {
  365. console.log('导入完成,状态:', data.status);
  366. // 显示结果
  367. resultsDiv.show();
  368. resultsList.empty();
  369. if (data.results.errors && data.results.errors.length > 0) {
  370. data.results.errors.forEach(error => {
  371. resultsList.append(`<li class="text-danger">${error}</li>`);
  372. });
  373. }
  374. if (data.results.success > 0) {
  375. resultsList.prepend(`<li class="text-success">成功导入 ${data.results.success} 个书签</li>`);
  376. }
  377. cancelBtn.hide();
  378. completeBtn.show();
  379. progressBar.removeClass('progress-bar-animated');
  380. if (data.status === 'completed') {
  381. progressBar.removeClass('bg-warning').addClass('bg-success');
  382. // 3秒后自动跳转
  383. setTimeout(function() {
  384. window.location.href = "/dashboard";
  385. }, 3000);
  386. } else {
  387. progressBar.removeClass('bg-success').addClass('bg-danger');
  388. }
  389. } else {
  390. // 继续检查进度
  391. setTimeout(checkProgress, 1000);
  392. checkCount++;
  393. }
  394. })
  395. .fail(function(xhr, status, error) {
  396. console.error('进度查询失败:', status, error, xhr.responseText);
  397. updateProgress('进度查询失败: ' + error, 0, true);
  398. });
  399. }
  400. cancelBtn.show().off('click').click(function() {
  401. console.log('用户取消导入');
  402. try {
  403. if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
  404. var modalElement = document.getElementById('importProgressModal');
  405. var modal = bootstrap.Modal.getInstance(modalElement);
  406. modal.hide();
  407. } else {
  408. $('#importProgressModal').hide();
  409. $('body').removeClass('modal-open');
  410. }
  411. } catch (error) {
  412. $('#importProgressModal').hide();
  413. $('body').removeClass('modal-open');
  414. }
  415. });
  416. completeBtn.click(function() {
  417. console.log('用户确认完成');
  418. window.location.href = "/dashboard";
  419. });
  420. checkProgress();
  421. }
  422. function updateProgress(message, percent, isError) {
  423. const progressBar = $('.progress-bar');
  424. const statusText = $('.import-status');
  425. progressBar.css('width', percent + '%').text(percent + '%');
  426. statusText.html(`<p class="${isError ? 'text-danger' : ''}">${message}</p>`);
  427. if (isError) {
  428. progressBar.removeClass('bg-success progress-bar-animated').addClass('bg-danger');
  429. $('#completeBtn').show();
  430. $('#cancelBtn').hide();
  431. }
  432. }
  433. updateSelectionCount();
  434. console.log('页面初始化完成');
  435. }
  436. // 页面加载完成后检查依赖并初始化
  437. if (document.readyState === 'loading') {
  438. document.addEventListener('DOMContentLoaded', checkDependencies);
  439. } else {
  440. checkDependencies();
  441. }
  442. </script>
  443. {% endblock %}