新增Q&A页面
This commit is contained in:
parent
2d1f54218f
commit
d744e00c66
Binary file not shown.
BIN
XNSimHtml/assets/icons/png/question.png
Normal file
BIN
XNSimHtml/assets/icons/png/question.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
BIN
XNSimHtml/assets/icons/png/question_b.png
Normal file
BIN
XNSimHtml/assets/icons/png/question_b.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
@ -96,6 +96,9 @@ class ContentArea extends HTMLElement {
|
||||
case 'help':
|
||||
contentElement = document.createElement('help-component');
|
||||
break;
|
||||
case 'qa':
|
||||
contentElement = document.createElement('qa-component');
|
||||
break;
|
||||
case 'run-env-config':
|
||||
contentElement = document.createElement('run-env-config');
|
||||
break;
|
||||
|
846
XNSimHtml/components/qa-component.js
Normal file
846
XNSimHtml/components/qa-component.js
Normal file
@ -0,0 +1,846 @@
|
||||
class QAComponent extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.accessLevel = 0;
|
||||
this.questions = [];
|
||||
this.currentPage = 1;
|
||||
this.pageSize = 5; // 每页显示5个问题
|
||||
this.totalPages = 1;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.checkUserAccess();
|
||||
this.loadQuestions();
|
||||
this.render();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
async checkUserAccess() {
|
||||
try {
|
||||
const userInfo = localStorage.getItem('userInfo');
|
||||
if (userInfo) {
|
||||
const user = JSON.parse(userInfo);
|
||||
this.accessLevel = user.access_level || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户权限失败:', error);
|
||||
this.accessLevel = 0;
|
||||
}
|
||||
}
|
||||
|
||||
async loadQuestions() {
|
||||
try {
|
||||
console.log('Loading page:', this.currentPage);
|
||||
const response = await fetch(`/api/qa/questions?sort=desc`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
this.questions = data.questions;
|
||||
this.renderQuestions();
|
||||
} else {
|
||||
console.error('加载问题失败:', data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载问题失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background-color: #f5f7fa;
|
||||
--primary-color: #1890ff;
|
||||
--primary-hover: #40a9ff;
|
||||
--danger-color: #ff4d4f;
|
||||
--danger-hover: #ff7875;
|
||||
--success-color: #52c41a;
|
||||
--success-hover: #73d13d;
|
||||
}
|
||||
|
||||
.qa-container {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
padding: 24px;
|
||||
min-height: calc(100% - 40px);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.qa-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.ask-question-btn {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.ask-question-btn:hover {
|
||||
background-color: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
.qa-item {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #eef2f7;
|
||||
}
|
||||
|
||||
.qa-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.qa-question {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 12px;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.qa-answer {
|
||||
color: #4a5568;
|
||||
line-height: 1.7;
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #edf2f7;
|
||||
}
|
||||
|
||||
.answer-btn {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-top: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.answer-btn:hover {
|
||||
background-color: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background-color: var(--danger-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background-color: var(--danger-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.question-meta {
|
||||
font-size: 13px;
|
||||
color: #718096;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
min-height: 120px;
|
||||
max-height: 300px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background-color: var(--success-color);
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background-color: var(--success-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: var(--danger-color);
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background-color: var(--danger-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.qa-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.qa-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.question-form {
|
||||
display: none;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.question-form.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.form-actions button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-actions .submit-btn {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-actions .submit-btn:hover {
|
||||
background-color: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.form-actions .cancel-btn {
|
||||
background-color: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-actions .cancel-btn:hover {
|
||||
background-color: var(--danger-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.answer-form {
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.answer-form.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.answer-btn.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: #718096;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
line-height: 1;
|
||||
transition: color 0.3s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.question-form {
|
||||
display: block;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #4a5568;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.confirm-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1001;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.confirm-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.confirm-content {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
position: relative;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.confirm-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.confirm-message {
|
||||
color: #4a5568;
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.confirm-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.confirm-actions button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.confirm-actions .confirm-btn {
|
||||
background-color: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.confirm-actions .confirm-btn:hover {
|
||||
background-color: var(--danger-hover);
|
||||
}
|
||||
|
||||
.confirm-actions .cancel-btn {
|
||||
background-color: #e2e8f0;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.confirm-actions .cancel-btn:hover {
|
||||
background-color: #cbd5e0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 30px;
|
||||
gap: 15px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
color: #2d3748;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
background-color: #f8f9fa;
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
background-color: #f8f9fa;
|
||||
color: #a0aec0;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
color: #4a5568;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
<div class="qa-container">
|
||||
<div class="qa-header">
|
||||
<button class="ask-question-btn">
|
||||
<img src="assets/icons/png/plus.png" alt="提问" width="16" height="16">
|
||||
提问
|
||||
</button>
|
||||
</div>
|
||||
<div class="qa-content">
|
||||
<div class="qa-list" id="qaList">
|
||||
<!-- 问题列表将通过JavaScript动态添加 -->
|
||||
</div>
|
||||
<div class="pagination" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript动态添加 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="questionModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">提问</div>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="question-form">
|
||||
<div class="form-group">
|
||||
<label for="questionTitle">问题标题</label>
|
||||
<input type="text" id="questionTitle" placeholder="请输入问题标题">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="questionContent">问题内容</label>
|
||||
<textarea id="questionContent" placeholder="请详细描述您的问题"></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="cancel-btn">取消</button>
|
||||
<button class="submit-btn">提交问题</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="confirm-modal" id="confirmModal">
|
||||
<div class="confirm-content">
|
||||
<div class="confirm-title">确认删除</div>
|
||||
<div class="confirm-message">确定要删除这个内容吗?此操作不可恢复。</div>
|
||||
<div class="confirm-actions">
|
||||
<button class="cancel-btn">取消</button>
|
||||
<button class="confirm-btn">确认删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const askQuestionBtn = this.shadowRoot.querySelector('.ask-question-btn');
|
||||
const modal = this.shadowRoot.querySelector('.modal-overlay');
|
||||
const modalClose = this.shadowRoot.querySelector('.modal-close');
|
||||
const cancelBtn = this.shadowRoot.querySelector('.cancel-btn');
|
||||
const submitBtn = this.shadowRoot.querySelector('.submit-btn');
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.remove('active');
|
||||
this.shadowRoot.getElementById('questionTitle').value = '';
|
||||
this.shadowRoot.getElementById('questionContent').value = '';
|
||||
};
|
||||
|
||||
askQuestionBtn.addEventListener('click', () => {
|
||||
modal.classList.add('active');
|
||||
});
|
||||
|
||||
modalClose.addEventListener('click', closeModal);
|
||||
cancelBtn.addEventListener('click', closeModal);
|
||||
|
||||
// 点击模态框外部关闭
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
submitBtn.addEventListener('click', async () => {
|
||||
const title = this.shadowRoot.getElementById('questionTitle').value.trim();
|
||||
const content = this.shadowRoot.getElementById('questionContent').value.trim();
|
||||
|
||||
if (!title || !content) {
|
||||
alert('请填写完整的问题信息');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
const response = await fetch('/api/qa/questions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
content,
|
||||
userInfo
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
closeModal();
|
||||
await this.loadQuestions();
|
||||
} else {
|
||||
alert(data.message || '创建问题失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建问题失败:', error);
|
||||
alert('创建问题失败,请稍后重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async addAnswer(questionId, content) {
|
||||
try {
|
||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
const response = await fetch(`/api/qa/questions/${questionId}/answers`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
userInfo
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
await this.loadQuestions();
|
||||
} else {
|
||||
alert(data.message || '添加回答失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加回答失败:', error);
|
||||
alert('添加回答失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteQuestion(questionId) {
|
||||
try {
|
||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
const response = await fetch(`/api/qa/questions/${questionId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ userInfo })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
await this.loadQuestions();
|
||||
} else {
|
||||
alert(data.message || '删除问题失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除问题失败:', error);
|
||||
alert('删除问题失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAnswer(answerId) {
|
||||
try {
|
||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
const response = await fetch(`/api/qa/answers/${answerId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ userInfo })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
await this.loadQuestions();
|
||||
} else {
|
||||
alert(data.message || '删除回答失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除回答失败:', error);
|
||||
alert('删除回答失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
showConfirmDialog(type, id) {
|
||||
const modal = this.shadowRoot.querySelector('.confirm-modal');
|
||||
const confirmBtn = modal.querySelector('.confirm-btn');
|
||||
const cancelBtn = modal.querySelector('.cancel-btn');
|
||||
const message = modal.querySelector('.confirm-message');
|
||||
|
||||
message.textContent = type === 'question' ?
|
||||
'确定要删除这个问题吗?此操作不可恢复。' :
|
||||
'确定要删除这个回答吗?此操作不可恢复。';
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.remove('active');
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (type === 'question') {
|
||||
await this.deleteQuestion(id);
|
||||
} else {
|
||||
await this.deleteAnswer(id);
|
||||
}
|
||||
closeModal();
|
||||
};
|
||||
|
||||
// 移除旧的事件监听器
|
||||
const newConfirmBtn = confirmBtn.cloneNode(true);
|
||||
const newCancelBtn = cancelBtn.cloneNode(true);
|
||||
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
|
||||
cancelBtn.parentNode.replaceChild(newCancelBtn, cancelBtn);
|
||||
|
||||
// 添加新的事件监听器
|
||||
newConfirmBtn.addEventListener('click', handleConfirm);
|
||||
newCancelBtn.addEventListener('click', closeModal);
|
||||
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
renderQuestions() {
|
||||
const qaList = this.shadowRoot.getElementById('qaList');
|
||||
|
||||
if (this.questions.length === 0) {
|
||||
qaList.innerHTML = '<div class="error">暂无问题</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.totalPages = Math.ceil(this.questions.length / this.pageSize);
|
||||
const startIndex = (this.currentPage - 1) * this.pageSize;
|
||||
const endIndex = Math.min(startIndex + this.pageSize, this.questions.length);
|
||||
const currentPageQuestions = this.questions.slice(startIndex, endIndex);
|
||||
|
||||
qaList.innerHTML = currentPageQuestions.map(question => `
|
||||
<div class="qa-item">
|
||||
<div class="qa-question">
|
||||
<span>${question.title}</span>
|
||||
<button class="delete-btn ${this.accessLevel >= 3 ? '' : 'hidden'}"
|
||||
onclick="this.getRootNode().host.showConfirmDialog('question', ${question.id})">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
<div class="question-meta">
|
||||
提问者:${question.author} | 时间:${question.created_at}
|
||||
</div>
|
||||
<div class="qa-content">${question.content}</div>
|
||||
${question.answers.map(answer => `
|
||||
<div class="qa-answer">
|
||||
<div class="answer-content">${answer.content}</div>
|
||||
<div class="question-meta">
|
||||
<span>回答者:${answer.author} | 时间:${answer.created_at}</span>
|
||||
<button class="delete-btn ${this.accessLevel >= 3 ? '' : 'hidden'}"
|
||||
onclick="this.getRootNode().host.showConfirmDialog('answer', ${answer.id})">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
<button class="answer-btn ${this.accessLevel >= 2 ? '' : 'hidden'}" data-question-id="${question.id}">
|
||||
回答
|
||||
</button>
|
||||
<div class="answer-form" data-question-id="${question.id}">
|
||||
<div class="form-group">
|
||||
<textarea placeholder="请输入您的回答"></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="cancel-btn">取消</button>
|
||||
<button class="submit-btn">提交回答</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// 添加分页
|
||||
const pagination = this.shadowRoot.getElementById('pagination');
|
||||
pagination.innerHTML = `
|
||||
<button class="pagination-btn" id="prevPage" ${this.currentPage === 1 ? 'disabled' : ''}>
|
||||
上一页
|
||||
</button>
|
||||
<span class="page-info">第 ${this.currentPage} 页 / 共 ${this.totalPages} 页</span>
|
||||
<button class="pagination-btn" id="nextPage" ${this.currentPage === this.totalPages ? 'disabled' : ''}>
|
||||
下一页
|
||||
</button>
|
||||
`;
|
||||
|
||||
// 添加分页按钮事件监听
|
||||
this.addPaginationListeners();
|
||||
|
||||
// 重新绑定回答按钮的事件监听器
|
||||
const answerBtns = qaList.querySelectorAll('.answer-btn');
|
||||
answerBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const questionId = parseInt(btn.dataset.questionId);
|
||||
const answerForm = qaList.querySelector(`.answer-form[data-question-id="${questionId}"]`);
|
||||
answerForm.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// 绑定回答表单的事件监听器
|
||||
const answerForms = qaList.querySelectorAll('.answer-form');
|
||||
answerForms.forEach(form => {
|
||||
const questionId = parseInt(form.dataset.questionId);
|
||||
const cancelBtn = form.querySelector('.cancel-btn');
|
||||
const submitBtn = form.querySelector('.submit-btn');
|
||||
const textarea = form.querySelector('textarea');
|
||||
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
form.classList.remove('active');
|
||||
textarea.value = '';
|
||||
});
|
||||
|
||||
submitBtn.addEventListener('click', () => {
|
||||
const content = textarea.value.trim();
|
||||
if (!content) {
|
||||
alert('请输入回答内容');
|
||||
return;
|
||||
}
|
||||
this.addAnswer(questionId, content);
|
||||
form.classList.remove('active');
|
||||
textarea.value = '';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 添加分页按钮事件监听
|
||||
addPaginationListeners() {
|
||||
const prevButton = this.shadowRoot.getElementById('prevPage');
|
||||
const nextButton = this.shadowRoot.getElementById('nextPage');
|
||||
|
||||
if (prevButton) {
|
||||
prevButton.addEventListener('click', () => {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.renderQuestions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (nextButton) {
|
||||
nextButton.addEventListener('click', () => {
|
||||
if (this.currentPage < this.totalPages) {
|
||||
this.currentPage++;
|
||||
this.renderQuestions();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('qa-component', QAComponent);
|
@ -166,6 +166,10 @@ class SubToolbar extends HTMLElement {
|
||||
<img src="assets/icons/png/help.png" alt="帮助" class="icon">
|
||||
帮助
|
||||
</div>
|
||||
<div class="sub-item" data-icon="question">
|
||||
<img src="assets/icons/png/question.png" alt="Q&A" class="icon">
|
||||
Q&A
|
||||
</div>
|
||||
</div>
|
||||
<!-- 配置子菜单 -->
|
||||
<div class="sub-menu" data-parent="config">
|
||||
|
@ -206,6 +206,7 @@
|
||||
</div>
|
||||
|
||||
<script src="components/header-tools.js"></script>
|
||||
<script src="components/qa-component.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const tabsContainer = document.querySelector('tabs-container');
|
||||
@ -329,6 +330,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理Q&A标签页
|
||||
if (title === 'Q&A') {
|
||||
const id = 'qa';
|
||||
tabsContainer.createTab(id, title, icon, parentText, parentTool);
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理运行环境配置标签页
|
||||
if (title === '运行环境配置') {
|
||||
const id = 'run-env-config';
|
||||
@ -440,6 +448,7 @@
|
||||
'运行日志': 'file',
|
||||
'系统信息': 'server',
|
||||
'帮助': 'help',
|
||||
'Q&A': 'question',
|
||||
'运行环境配置': 'chip',
|
||||
'模型配置': 'cube',
|
||||
'服务配置': 'settings',
|
||||
|
102
XNSimHtml/routes/qa.js
Normal file
102
XNSimHtml/routes/qa.js
Normal file
@ -0,0 +1,102 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
getQuestions,
|
||||
createQuestion,
|
||||
addAnswer,
|
||||
deleteQuestion,
|
||||
deleteAnswer
|
||||
} = require('../utils/db-utils');
|
||||
|
||||
// 获取所有问题
|
||||
router.get('/questions', (req, res) => {
|
||||
try {
|
||||
const questions = getQuestions();
|
||||
res.json({ success: true, questions });
|
||||
} catch (error) {
|
||||
console.error('获取问题列表失败:', error);
|
||||
res.status(500).json({ success: false, message: '获取问题列表失败', error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建新问题
|
||||
router.post('/questions', (req, res) => {
|
||||
try {
|
||||
const { title, content } = req.body;
|
||||
const userInfo = req.body.userInfo || {};
|
||||
|
||||
if (!title || !content) {
|
||||
return res.status(400).json({ success: false, message: '标题和内容不能为空' });
|
||||
}
|
||||
|
||||
const result = createQuestion(title, content, userInfo.username);
|
||||
res.status(201).json(result);
|
||||
} catch (error) {
|
||||
console.error('创建问题失败:', error);
|
||||
res.status(500).json({ success: false, message: '创建问题失败', error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 添加回答
|
||||
router.post('/questions/:questionId/answers', (req, res) => {
|
||||
try {
|
||||
const { questionId } = req.params;
|
||||
const { content } = req.body;
|
||||
const userInfo = req.body.userInfo || {};
|
||||
|
||||
if (!content) {
|
||||
return res.status(400).json({ success: false, message: '回答内容不能为空' });
|
||||
}
|
||||
|
||||
// 检查用户权限
|
||||
if (!userInfo.access_level || userInfo.access_level < 2) {
|
||||
return res.status(403).json({ success: false, message: '权限不足,需要权限级别大于等于2' });
|
||||
}
|
||||
|
||||
const result = addAnswer(questionId, content, userInfo.username);
|
||||
res.status(201).json(result);
|
||||
} catch (error) {
|
||||
console.error('添加回答失败:', error);
|
||||
res.status(500).json({ success: false, message: '添加回答失败', error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除问题
|
||||
router.delete('/questions/:questionId', (req, res) => {
|
||||
try {
|
||||
const { questionId } = req.params;
|
||||
const userInfo = req.body.userInfo || {};
|
||||
|
||||
// 检查用户权限
|
||||
if (!userInfo.access_level || userInfo.access_level < 3) {
|
||||
return res.status(403).json({ success: false, message: '权限不足,需要权限级别大于等于3' });
|
||||
}
|
||||
|
||||
const result = deleteQuestion(questionId);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('删除问题失败:', error);
|
||||
res.status(500).json({ success: false, message: '删除问题失败', error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除回答
|
||||
router.delete('/answers/:answerId', (req, res) => {
|
||||
try {
|
||||
const { answerId } = req.params;
|
||||
const userInfo = req.body.userInfo || {};
|
||||
|
||||
// 检查用户权限
|
||||
if (!userInfo.access_level || userInfo.access_level < 3) {
|
||||
return res.status(403).json({ success: false, message: '权限不足,需要权限级别大于等于3' });
|
||||
}
|
||||
|
||||
const result = deleteAnswer(answerId);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('删除回答失败:', error);
|
||||
res.status(500).json({ success: false, message: '删除回答失败', error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
@ -21,6 +21,7 @@ const udpMonitorRoutes = require('./routes/udp-monitor');
|
||||
const productsRoutes = require('./routes/products');
|
||||
const interfaceRoutes = require('./routes/interface-config');
|
||||
const icdImportRoutes = require('./routes/icd-import');
|
||||
const qaRoutes = require('./routes/qa');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
@ -33,7 +34,8 @@ if (!xnCorePath) {
|
||||
|
||||
// 中间件
|
||||
app.use(bodyParser.json());
|
||||
app.use(express.static(path.join(__dirname, '.')));
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(express.static(__dirname));
|
||||
|
||||
// 监听进程退出事件
|
||||
process.on('exit', performCleanup);
|
||||
@ -72,6 +74,7 @@ app.use('/api/udp-monitor', udpMonitorRoutes);
|
||||
app.use('/api', productsRoutes);
|
||||
app.use('/api/interface', interfaceRoutes);
|
||||
app.use('/api/icd', icdImportRoutes);
|
||||
app.use('/api/qa', qaRoutes);
|
||||
|
||||
// 主页路由
|
||||
app.get('/', (req, res) => {
|
||||
|
@ -984,6 +984,206 @@ function getDataInterfaceStructs(systemName = 'XNSim', productName, ataName) {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有问题
|
||||
function getQuestions() {
|
||||
try {
|
||||
const xnCorePath = getXNCorePath();
|
||||
if (!xnCorePath) {
|
||||
throw new Error('XNCore环境变量未设置,无法获取数据库路径');
|
||||
}
|
||||
|
||||
const dbPath = xnCorePath + '/database/XNSim.db';
|
||||
if (!dbPath) {
|
||||
throw new Error('无法找到数据库文件');
|
||||
}
|
||||
|
||||
// 打开数据库连接
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
|
||||
const questions = db.prepare(`
|
||||
SELECT q.*,
|
||||
COUNT(a.id) as answer_count,
|
||||
MAX(a.created_at) as last_answer_time
|
||||
FROM questions q
|
||||
LEFT JOIN answers a ON q.id = a.question_id
|
||||
GROUP BY q.id
|
||||
ORDER BY q.created_at DESC
|
||||
`).all();
|
||||
|
||||
// 获取每个问题的回答
|
||||
questions.forEach(question => {
|
||||
question.answers = db.prepare(`
|
||||
SELECT * FROM answers
|
||||
WHERE question_id = ?
|
||||
ORDER BY created_at ASC
|
||||
`).all(question.id);
|
||||
});
|
||||
|
||||
db.close();
|
||||
return questions;
|
||||
} catch (error) {
|
||||
console.error('获取问题列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新问题
|
||||
function createQuestion(title, content, author) {
|
||||
try {
|
||||
if (!title || !content) {
|
||||
throw new Error('标题和内容不能为空');
|
||||
}
|
||||
|
||||
const xnCorePath = getXNCorePath();
|
||||
if (!xnCorePath) {
|
||||
throw new Error('XNCore环境变量未设置,无法获取数据库路径');
|
||||
}
|
||||
|
||||
const dbPath = xnCorePath + '/database/XNSim.db';
|
||||
if (!dbPath) {
|
||||
throw new Error('无法找到数据库文件');
|
||||
}
|
||||
|
||||
// 打开数据库连接
|
||||
const db = new Database(dbPath);
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO questions (title, content, author)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(title, content, author || '匿名用户');
|
||||
|
||||
db.close();
|
||||
|
||||
if (result.changes > 0) {
|
||||
return {
|
||||
success: true,
|
||||
questionId: result.lastInsertRowid,
|
||||
message: '问题创建成功'
|
||||
};
|
||||
} else {
|
||||
throw new Error('问题创建失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建问题失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加回答
|
||||
function addAnswer(questionId, content, author) {
|
||||
try {
|
||||
if (!content) {
|
||||
throw new Error('回答内容不能为空');
|
||||
}
|
||||
|
||||
const xnCorePath = getXNCorePath();
|
||||
if (!xnCorePath) {
|
||||
throw new Error('XNCore环境变量未设置,无法获取数据库路径');
|
||||
}
|
||||
|
||||
const dbPath = xnCorePath + '/database/XNSim.db';
|
||||
if (!dbPath) {
|
||||
throw new Error('无法找到数据库文件');
|
||||
}
|
||||
|
||||
// 打开数据库连接
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// 检查问题是否存在
|
||||
const question = db.prepare('SELECT id FROM questions WHERE id = ?').get(questionId);
|
||||
if (!question) {
|
||||
db.close();
|
||||
throw new Error('问题不存在');
|
||||
}
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO answers (question_id, content, author)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(questionId, content, author || '匿名用户');
|
||||
|
||||
db.close();
|
||||
|
||||
if (result.changes > 0) {
|
||||
return {
|
||||
success: true,
|
||||
answerId: result.lastInsertRowid,
|
||||
message: '回答添加成功'
|
||||
};
|
||||
} else {
|
||||
throw new Error('回答添加失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加回答失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除问题
|
||||
function deleteQuestion(questionId) {
|
||||
try {
|
||||
const xnCorePath = getXNCorePath();
|
||||
if (!xnCorePath) {
|
||||
throw new Error('XNCore环境变量未设置,无法获取数据库路径');
|
||||
}
|
||||
|
||||
const dbPath = xnCorePath + '/database/XNSim.db';
|
||||
if (!dbPath) {
|
||||
throw new Error('无法找到数据库文件');
|
||||
}
|
||||
|
||||
// 打开数据库连接
|
||||
const db = new Database(dbPath);
|
||||
|
||||
const result = db.prepare('DELETE FROM questions WHERE id = ?').run(questionId);
|
||||
db.close();
|
||||
|
||||
if (result.changes > 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: '问题删除成功'
|
||||
};
|
||||
} else {
|
||||
throw new Error('问题不存在');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除问题失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除回答
|
||||
function deleteAnswer(answerId) {
|
||||
try {
|
||||
const xnCorePath = getXNCorePath();
|
||||
if (!xnCorePath) {
|
||||
throw new Error('XNCore环境变量未设置,无法获取数据库路径');
|
||||
}
|
||||
|
||||
const dbPath = xnCorePath + '/database/XNSim.db';
|
||||
if (!dbPath) {
|
||||
throw new Error('无法找到数据库文件');
|
||||
}
|
||||
|
||||
// 打开数据库连接
|
||||
const db = new Database(dbPath);
|
||||
|
||||
const result = db.prepare('DELETE FROM answers WHERE id = ?').run(answerId);
|
||||
db.close();
|
||||
|
||||
if (result.changes > 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: '回答删除成功'
|
||||
};
|
||||
} else {
|
||||
throw new Error('回答不存在');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除回答失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getATAChapters,
|
||||
getModelsByChapterId,
|
||||
@ -998,5 +1198,10 @@ module.exports = {
|
||||
addDataInterface,
|
||||
updateDataInterface,
|
||||
deleteDataInterface,
|
||||
getDataInterfaceStructs
|
||||
getDataInterfaceStructs,
|
||||
getQuestions,
|
||||
createQuestion,
|
||||
addAnswer,
|
||||
deleteQuestion,
|
||||
deleteAnswer
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user