| |
| |
| |
| |
|
|
| (function() { |
| 'use strict'; |
|
|
| |
| |
|
|
| |
| let adminPaginator = null; |
| let container = null; |
|
|
| |
| const SeedQuestionManagement = { |
| init: function(containerElement) { |
| container = containerElement; |
| this.initPaginators(); |
| this.initEventListeners(); |
|
|
| |
| const loadData = () => { |
| this.loadAllQuestions(); |
| }; |
|
|
| |
| const checkI18nReady = () => { |
| if (window.i18next && window.i18next.t) { |
| try { |
| const test = window.i18next.t('common.loading'); |
| |
| if (test && test !== 'common.loading') { |
| loadData(); |
| return true; |
| } |
| } catch (e) { |
| |
| } |
| } |
| return false; |
| }; |
|
|
| |
| if (checkI18nReady()) { |
| return; |
| } |
|
|
| |
| const checkI18n = setInterval(() => { |
| if (checkI18nReady()) { |
| clearInterval(checkI18n); |
| } |
| }, 100); |
|
|
| |
| setTimeout(() => { |
| clearInterval(checkI18n); |
| console.warn('i18next initialization timeout or check failed, loading data anyway'); |
| loadData(); |
| }, 10000); |
| }, |
|
|
| initPaginators: function() { |
| |
| adminPaginator = createPaginator('admin', { |
| initialPage: 1, |
| initialPageSize: 20, |
| onPageChange: (page, pageSize) => { |
| this.loadAllQuestions(); |
| }, |
| onPageSizeChange: (pageSize) => { |
| this.loadAllQuestions(); |
| } |
| }); |
| }, |
|
|
| initEventListeners: function() { |
| |
| const adminSearchInput = container.querySelector('#adminSearchInput'); |
| if (adminSearchInput) { |
| let searchTimeout; |
| adminSearchInput.addEventListener('input', (e) => { |
| clearTimeout(searchTimeout); |
| searchTimeout = setTimeout(() => { |
| this.loadAllQuestions(); |
| }, 500); |
| }); |
| } |
|
|
| |
| const manageTypesBtn = container.querySelector('#manageTypesBtn'); |
| if (manageTypesBtn) { |
| manageTypesBtn.addEventListener('click', () => { |
| this.showManageTypesModal(); |
| }); |
| } |
|
|
| |
| const refreshAdminBtn = container.querySelector('#refreshAdminBtn'); |
| if (refreshAdminBtn) { |
| refreshAdminBtn.addEventListener('click', () => { |
| this.loadAllQuestions(); |
| }); |
| } |
|
|
| |
| const exportBtn = container.querySelector('#exportBtn'); |
| if (exportBtn) { |
| exportBtn.addEventListener('click', () => { |
| this.exportQuestions(); |
| }); |
| } |
| }, |
|
|
| loadAllQuestions: async function() { |
| if (!adminPaginator) return; |
|
|
| const tbody = container.querySelector('#adminTableBody'); |
| if (!tbody) return; |
|
|
| try { |
| tbody.innerHTML = '<tr><td colspan="10" class="loading">加载中...</td></tr>'; |
|
|
| const skip = adminPaginator.getSkip(); |
| const limit = adminPaginator.getLimit(); |
|
|
| const search = container.querySelector('#adminSearchInput')?.value.trim() || null; |
|
|
| const params = new URLSearchParams({ |
| skip: skip.toString(), |
| limit: limit.toString() |
| }); |
| if (search) { |
| params.append('search', search); |
| } |
|
|
| const questions = await apiGet(`/seed-questions/admin/all?${params.toString()}`); |
|
|
| if (questions.length === 0) { |
| tbody.innerHTML = `<tr><td colspan="10" class="empty-state"><div class="empty-state-icon">📝</div><div class="empty-state-text">${t('common.noData')}</div></td></tr>`; |
| const paginationContainer = container.querySelector('#adminPaginationContainer'); |
| if (paginationContainer) paginationContainer.classList.add('hidden'); |
| return; |
| } |
|
|
| tbody.innerHTML = questions.map(q => ` |
| <tr> |
| <td>${q.id}</td> |
| <td>${escapeHtml(q.question)}</td> |
| <td>${escapeHtml(q.type)}</td> |
| <td>${escapeHtml(q.subtype)}</td> |
| <td>${escapeHtml(q.species_or_domain || '-')}</td> |
| <td>${escapeHtml(q.model || '-')}</td> |
| <td>${q.date ? this.formatDate(q.date) : '-'}</td> |
| <td>${q.is_verified ? '✓' : '-'}</td> |
| <td>${escapeHtml(q.creator_full_name || `用户ID:${q.creator_id}`)}</td> |
| <td>${this.formatDateTime(q.created_at)}</td> |
| </tr> |
| `).join(''); |
|
|
| |
| const total = await this.getAllQuestionsCount(search); |
| adminPaginator.setTotalCount(total); |
| adminPaginator.render(); |
| const paginationContainer = container.querySelector('#adminPaginationContainer'); |
| if (paginationContainer) paginationContainer.classList.remove('hidden'); |
|
|
| } catch (error) { |
| console.error('加载问题列表失败:', error); |
| tbody.innerHTML = '<tr><td colspan="10" class="loading">加载失败,请刷新重试</td></tr>'; |
| } |
| }, |
|
|
| getAllQuestionsCount: async function(search) { |
| try { |
| const result = await apiGet(`/seed-questions/admin/all?skip=0&limit=1000${search ? '&search=' + encodeURIComponent(search) : ''}`); |
| return result.length; |
| } catch (error) { |
| return 0; |
| } |
| }, |
|
|
| exportQuestions: async function() { |
| try { |
| const exportBtn = container.querySelector('#exportBtn'); |
| const originalText = exportBtn ? exportBtn.textContent : '导出CSV'; |
| if (exportBtn) { |
| exportBtn.disabled = true; |
| exportBtn.textContent = '导出中...'; |
| } |
|
|
| const response = await fetch('/api/seed-questions/admin/export', { |
| method: 'GET', |
| headers: getAuthHeaders() |
| }); |
|
|
| if (!response.ok) { |
| throw new Error('导出失败'); |
| } |
|
|
| const blob = await response.blob(); |
| const url = window.URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = `seed_questions_export_${new Date().toISOString().split('T')[0]}.csv`; |
| document.body.appendChild(a); |
| a.click(); |
| document.body.removeChild(a); |
| window.URL.revokeObjectURL(url); |
|
|
| showSuccess('导出成功!'); |
|
|
| } catch (error) { |
| console.error('导出失败:', error); |
| showError('导出失败,请重试'); |
| } finally { |
| const exportBtn = container.querySelector('#exportBtn'); |
| if (exportBtn) { |
| exportBtn.disabled = false; |
| exportBtn.textContent = '导出CSV'; |
| } |
| } |
| }, |
|
|
| formatDate: function(dateString) { |
| if (!dateString) return '-'; |
| const date = new Date(dateString); |
| return date.toLocaleDateString('zh-CN'); |
| }, |
|
|
| formatDateTime: function(dateString) { |
| if (!dateString) return '-'; |
| const date = new Date(dateString); |
| return date.toLocaleString('zh-CN'); |
| }, |
|
|
| showManageTypesModal: async function() { |
| try { |
| |
| const types = await apiGet('/seed-questions/admin/types?limit=1000'); |
|
|
| |
| const grouped = {}; |
| types.forEach(t => { |
| if (!grouped[t.type]) { |
| grouped[t.type] = []; |
| } |
| grouped[t.type].push(t); |
| }); |
|
|
| |
| let content = ` |
| <div style="max-height: 60vh; overflow-y: auto;"> |
| <div style="margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center;"> |
| <h4 style="margin: 0;">类型和亚类列表</h4> |
| <button class="btn btn-primary btn-sm" id="addTypeBtn">添加类型/亚类</button> |
| </div> |
| <div id="typesListContainer"> |
| `; |
|
|
| |
| const sortedTypes = Object.keys(grouped).sort(); |
| if (sortedTypes.length === 0) { |
| content += '<div style="text-align: center; padding: 40px; color: #999;">暂无类型和亚类</div>'; |
| } else { |
| sortedTypes.forEach(typeName => { |
| const subtypes = grouped[typeName].sort((a, b) => (a.order || 0) - (b.order || 0)); |
| content += ` |
| <div style="margin-bottom: 16px; border: 1px solid #e0e0e0; border-radius: 8px; padding: 12px;"> |
| <div style="font-weight: bold; font-size: 16px; margin-bottom: 8px; color: #667eea;"> |
| ${escapeHtml(typeName)} |
| </div> |
| <div style="display: flex; flex-direction: column; gap: 8px;"> |
| `; |
| subtypes.forEach(subtype => { |
| content += ` |
| <div style="display: flex; justify-content: space-between; align-items: center; padding: 8px; background: #f8f9fa; border-radius: 4px;"> |
| <div> |
| <span style="font-weight: 500;">${escapeHtml(subtype.subtype)}</span> |
| <span style="color: #999; font-size: 12px; margin-left: 8px;">(ID: ${subtype.id}, 顺序: ${subtype.order || 0})</span> |
| </div> |
| <div style="display: flex; gap: 8px;"> |
| <button class="btn btn-sm btn-secondary" onclick="window.SeedQuestionManagement.editType(${subtype.id})">编辑</button> |
| <button class="btn btn-sm btn-danger" onclick="window.SeedQuestionManagement.deleteType(${subtype.id})">删除</button> |
| </div> |
| </div> |
| `; |
| }); |
| content += ` |
| </div> |
| </div> |
| `; |
| }); |
| } |
|
|
| content += ` |
| </div> |
| </div> |
| `; |
|
|
| openModal('管理类型和亚类', content, null, true); |
|
|
| |
| const addTypeBtn = document.getElementById('addTypeBtn'); |
| if (addTypeBtn) { |
| addTypeBtn.addEventListener('click', () => { |
| this.showAddTypeModal(); |
| }); |
| } |
|
|
| } catch (error) { |
| console.error('加载类型列表失败:', error); |
| showError('加载类型列表失败: ' + (error.message || '未知错误')); |
| } |
| }, |
|
|
| showAddTypeModal: function() { |
| const title = '添加类型/亚类'; |
| const content = ` |
| <form id="addTypeForm"> |
| <div class="form-group"> |
| <label>类型名称 *</label> |
| <input type="text" id="typeName" required minlength="1" maxlength="100" placeholder="例如: 事实性"> |
| </div> |
| <div class="form-group"> |
| <label>亚类名称 *</label> |
| <input type="text" id="subtypeName" required minlength="1" maxlength="100" placeholder="例如: 定义"> |
| </div> |
| <div class="form-group"> |
| <label>显示顺序</label> |
| <input type="number" id="typeOrder" value="0" min="0"> |
| <small style="color: #666; display: block; margin-top: 4px;"> |
| 数字越小越靠前显示 |
| </small> |
| </div> |
| </form> |
| `; |
|
|
| openModal(title, content, async () => { |
| await this.saveType(); |
| }); |
| }, |
|
|
| saveType: async function() { |
| const typeName = document.getElementById('typeName').value.trim(); |
| const subtypeName = document.getElementById('subtypeName').value.trim(); |
| const order = parseInt(document.getElementById('typeOrder').value) || 0; |
|
|
| if (!typeName || !subtypeName) { |
| showError('类型名称和亚类名称不能为空'); |
| return; |
| } |
|
|
| try { |
| await apiPost('/seed-questions/admin/types', { |
| type: typeName, |
| subtype: subtypeName, |
| order: order |
| }); |
| showSuccess('类型/亚类添加成功'); |
| closeModal(); |
| |
| this.showManageTypesModal(); |
| } catch (error) { |
| console.error('添加类型失败:', error); |
| showError('添加类型失败: ' + (error.message || '未知错误')); |
| } |
| }, |
|
|
| editType: async function(typeId) { |
| try { |
| const types = await apiGet('/seed-questions/admin/types?limit=1000'); |
| const type = types.find(t => t.id === typeId); |
| if (!type) { |
| showError('类型不存在'); |
| return; |
| } |
|
|
| const title = '编辑类型/亚类'; |
| const content = ` |
| <form id="editTypeForm"> |
| <div class="form-group"> |
| <label>类型名称 *</label> |
| <input type="text" id="editTypeName" value="${escapeHtml(type.type)}" required minlength="1" maxlength="100"> |
| </div> |
| <div class="form-group"> |
| <label>亚类名称 *</label> |
| <input type="text" id="editSubtypeName" value="${escapeHtml(type.subtype)}" required minlength="1" maxlength="100"> |
| </div> |
| <div class="form-group"> |
| <label>显示顺序</label> |
| <input type="number" id="editTypeOrder" value="${type.order || 0}" min="0"> |
| <small style="color: #666; display: block; margin-top: 4px;"> |
| 数字越小越靠前显示 |
| </small> |
| </div> |
| </form> |
| `; |
|
|
| openModal(title, content, async () => { |
| await this.updateType(typeId); |
| }); |
| } catch (error) { |
| console.error('加载类型信息失败:', error); |
| showError('加载类型信息失败: ' + (error.message || '未知错误')); |
| } |
| }, |
|
|
| updateType: async function(typeId) { |
| const typeName = document.getElementById('editTypeName').value.trim(); |
| const subtypeName = document.getElementById('editSubtypeName').value.trim(); |
| const order = parseInt(document.getElementById('editTypeOrder').value) || 0; |
|
|
| if (!typeName || !subtypeName) { |
| showError('类型名称和亚类名称不能为空'); |
| return; |
| } |
|
|
| try { |
| await apiPut(`/seed-questions/admin/types/${typeId}`, { |
| type: typeName, |
| subtype: subtypeName, |
| order: order |
| }); |
| showSuccess('类型/亚类更新成功'); |
| closeModal(); |
| |
| this.showManageTypesModal(); |
| } catch (error) { |
| console.error('更新类型失败:', error); |
| showError('更新类型失败: ' + (error.message || '未知错误')); |
| } |
| }, |
|
|
| deleteType: async function(typeId) { |
| if (!confirm('确定要删除这个类型/亚类吗?删除后无法恢复。')) { |
| return; |
| } |
|
|
| try { |
| await apiDelete(`/seed-questions/admin/types/${typeId}`); |
| showSuccess('类型/亚类删除成功'); |
| |
| this.showManageTypesModal(); |
| } catch (error) { |
| console.error('删除类型失败:', error); |
| showError('删除类型失败: ' + (error.message || '未知错误')); |
| } |
| } |
| }; |
|
|
| |
| window.SeedQuestionManagement = SeedQuestionManagement; |
| })(); |
|
|