Phase 5: 实体属性扩展功能
- 数据库层:
- 新增 entity_attributes 表存储自定义属性
- 新增 attribute_templates 表管理属性模板
- 新增 attribute_history 表记录属性变更历史
- 后端 API:
- GET/POST /api/v1/projects/{id}/attribute-templates - 属性模板管理
- GET/POST/PUT/DELETE /api/v1/entities/{id}/attributes - 实体属性 CRUD
- GET /api/v1/entities/{id}/attributes/history - 属性变更历史
- GET /api/v1/projects/{id}/entities/search-by-attributes - 属性筛选搜索
- 前端 UI:
- 实体详情面板添加属性展示
- 属性编辑表单(支持文本、数字、日期、单选、多选)
- 属性模板管理界面
- 属性变更历史查看
- 知识库实体卡片显示属性预览
- 属性筛选搜索栏
This commit is contained in:
478
frontend/app.js
478
frontend/app.js
@@ -995,6 +995,22 @@ async function loadKnowledgeBase() {
|
||||
switchView('workbench');
|
||||
setTimeout(() => selectEntity(ent.id), 100);
|
||||
};
|
||||
|
||||
// 渲染属性预览
|
||||
let attrsHtml = '';
|
||||
if (ent.attributes && ent.attributes.length > 0) {
|
||||
attrsHtml = `
|
||||
<div style="margin-top:8px; padding-top:8px; border-top:1px solid #222;">
|
||||
${ent.attributes.slice(0, 3).map(a => `
|
||||
<span style="display:inline-block; background:#1a1a1a; padding:2px 8px; border-radius:4px; font-size:0.75rem; margin-right:6px; margin-bottom:4px;">
|
||||
${a.name}: <span style="color:#00d4ff;">${Array.isArray(a.value) ? a.value.join(', ') : a.value}</span>
|
||||
</span>
|
||||
`).join('')}
|
||||
${ent.attributes.length > 3 ? `<span style="font-size:0.75rem; color:#666;">+${ent.attributes.length - 3}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="kb-entity-name">
|
||||
<span class="entity-type-badge type-${ent.type.toLowerCase()}" style="font-size:0.65rem;margin-right:8px;">${ent.type}</span>
|
||||
@@ -1005,6 +1021,7 @@ async function loadKnowledgeBase() {
|
||||
📍 ${ent.mention_count || 0} 次提及 ·
|
||||
${ent.appears_in?.length || 0} 个文件
|
||||
</div>
|
||||
${attrsHtml}
|
||||
`;
|
||||
entityGrid.appendChild(card);
|
||||
});
|
||||
@@ -1342,36 +1359,453 @@ window.findInferencePath = async function(startEntity, endEntity) {
|
||||
}
|
||||
};
|
||||
|
||||
function renderInferencePaths(data) {
|
||||
const pathsList = document.getElementById('inferencePathsList');
|
||||
|
||||
if (!data.paths || data.paths.length === 0) {
|
||||
pathsList.innerHTML = '<p style="color:#666;">未找到关联路径</p>';
|
||||
// Phase 5: Entity Attributes Management
|
||||
let currentEntityIdForAttributes = null;
|
||||
let currentAttributes = [];
|
||||
let currentTemplates = [];
|
||||
|
||||
// Show entity attributes modal
|
||||
window.showEntityAttributes = async function(entityId) {
|
||||
if (entityId) {
|
||||
currentEntityIdForAttributes = entityId;
|
||||
} else if (selectedEntity) {
|
||||
currentEntityIdForAttributes = selectedEntity;
|
||||
} else {
|
||||
alert('请先选择一个实体');
|
||||
return;
|
||||
}
|
||||
|
||||
pathsList.innerHTML = data.paths.map((path, idx) => {
|
||||
const strengthPercent = Math.round(path.strength * 100);
|
||||
const pathHtml = path.path.map((node, i) => {
|
||||
if (i === 0) {
|
||||
return `<span class="path-node">${node.entity}</span>`;
|
||||
}
|
||||
return `
|
||||
<span class="path-arrow">→ ${node.relation} →</span>
|
||||
<span class="path-node">${node.entity}</span>
|
||||
`;
|
||||
}).join('');
|
||||
const modal = document.getElementById('attributesModal');
|
||||
modal.classList.add('show');
|
||||
|
||||
// Reset form
|
||||
document.getElementById('attributesAddForm').style.display = 'none';
|
||||
document.getElementById('toggleAddAttrBtn').style.display = 'inline-block';
|
||||
document.getElementById('saveAttrBtn').style.display = 'none';
|
||||
|
||||
await loadEntityAttributes();
|
||||
};
|
||||
|
||||
window.hideAttributesModal = function() {
|
||||
document.getElementById('attributesModal').classList.remove('show');
|
||||
currentEntityIdForAttributes = null;
|
||||
};
|
||||
|
||||
async function loadEntityAttributes() {
|
||||
if (!currentEntityIdForAttributes) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/entities/${currentEntityIdForAttributes}/attributes`);
|
||||
if (!res.ok) throw new Error('Failed to load attributes');
|
||||
|
||||
const data = await res.json();
|
||||
currentAttributes = data.attributes || [];
|
||||
|
||||
renderAttributesList();
|
||||
} catch (err) {
|
||||
console.error('Load attributes failed:', err);
|
||||
document.getElementById('attributesList').innerHTML = '<p style="color:#ff6b6b;">加载失败</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderAttributesList() {
|
||||
const container = document.getElementById('attributesList');
|
||||
|
||||
if (currentAttributes.length === 0) {
|
||||
container.innerHTML = '<p style="color:#666; text-align:center; padding:20px;">暂无属性,点击"添加属性"创建</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = currentAttributes.map(attr => {
|
||||
let valueDisplay = attr.value;
|
||||
if (attr.type === 'multiselect' && Array.isArray(attr.value)) {
|
||||
valueDisplay = attr.value.join(', ');
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="inference-path">
|
||||
<div class="inference-path-header">
|
||||
<span style="color:#888;font-size:0.85rem;">路径 ${idx + 1}</span>
|
||||
<span class="path-strength">关联强度: ${strengthPercent}%</span>
|
||||
<div class="attribute-item">
|
||||
<div class="attribute-info">
|
||||
<div class="attribute-name">
|
||||
${attr.name}
|
||||
<span class="attribute-type">${attr.type}</span>
|
||||
</div>
|
||||
<div class="attribute-value">${valueDisplay || '-'}</div>
|
||||
</div>
|
||||
<div class="path-visual">
|
||||
${pathHtml}
|
||||
<div class="attribute-actions">
|
||||
<button class="attribute-btn" onclick="showAttributeHistory('${attr.name}')">历史</button>
|
||||
<button class="attribute-btn delete" onclick="deleteAttribute('${attr.id}')">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.toggleAddAttributeForm = function() {
|
||||
const form = document.getElementById('attributesAddForm');
|
||||
const toggleBtn = document.getElementById('toggleAddAttrBtn');
|
||||
const saveBtn = document.getElementById('saveAttrBtn');
|
||||
|
||||
if (form.style.display === 'none') {
|
||||
form.style.display = 'block';
|
||||
toggleBtn.style.display = 'none';
|
||||
saveBtn.style.display = 'inline-block';
|
||||
} else {
|
||||
form.style.display = 'none';
|
||||
toggleBtn.style.display = 'inline-block';
|
||||
saveBtn.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
window.onAttrTypeChange = function() {
|
||||
const type = document.getElementById('attrType').value;
|
||||
const optionsGroup = document.getElementById('attrOptionsGroup');
|
||||
const valueContainer = document.getElementById('attrValueContainer');
|
||||
|
||||
if (type === 'select' || type === 'multiselect') {
|
||||
optionsGroup.style.display = 'block';
|
||||
} else {
|
||||
optionsGroup.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update value input based on type
|
||||
if (type === 'date') {
|
||||
valueContainer.innerHTML = '<input type="date" id="attrValue">';
|
||||
} else if (type === 'number') {
|
||||
valueContainer.innerHTML = '<input type="number" id="attrValue" placeholder="输入数字">';
|
||||
} else {
|
||||
valueContainer.innerHTML = '<input type="text" id="attrValue" placeholder="输入属性值">';
|
||||
}
|
||||
};
|
||||
|
||||
window.saveAttribute = async function() {
|
||||
if (!currentEntityIdForAttributes) return;
|
||||
|
||||
const name = document.getElementById('attrName').value.trim();
|
||||
const type = document.getElementById('attrType').value;
|
||||
let value = document.getElementById('attrValue').value;
|
||||
const changeReason = document.getElementById('attrChangeReason').value.trim();
|
||||
|
||||
if (!name) {
|
||||
alert('请输入属性名称');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle options for select/multiselect
|
||||
let options = null;
|
||||
if (type === 'select' || type === 'multiselect') {
|
||||
const optionsStr = document.getElementById('attrOptions').value.trim();
|
||||
if (optionsStr) {
|
||||
options = optionsStr.split(',').map(o => o.trim()).filter(o => o);
|
||||
}
|
||||
|
||||
// Handle multiselect value
|
||||
if (type === 'multiselect' && value) {
|
||||
value = value.split(',').map(v => v.trim()).filter(v => v);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle number type
|
||||
if (type === 'number' && value) {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/entities/${currentEntityIdForAttributes}/attributes`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
type,
|
||||
value,
|
||||
options,
|
||||
change_reason: changeReason
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed to save attribute');
|
||||
|
||||
// Reset form
|
||||
document.getElementById('attrName').value = '';
|
||||
document.getElementById('attrValue').value = '';
|
||||
document.getElementById('attrOptions').value = '';
|
||||
document.getElementById('attrChangeReason').value = '';
|
||||
|
||||
// Reload attributes
|
||||
await loadEntityAttributes();
|
||||
|
||||
// Hide form
|
||||
toggleAddAttributeForm();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Save attribute failed:', err);
|
||||
alert('保存失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
window.deleteAttribute = async function(attributeId) {
|
||||
if (!confirm('确定要删除这个属性吗?')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/entities/${currentEntityIdForAttributes}/attributes/${attributeId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed to delete attribute');
|
||||
|
||||
await loadEntityAttributes();
|
||||
} catch (err) {
|
||||
console.error('Delete attribute failed:', err);
|
||||
alert('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// Attribute History
|
||||
window.showAttributeHistory = async function(attributeName) {
|
||||
if (!currentEntityIdForAttributes) return;
|
||||
|
||||
const modal = document.getElementById('attrHistoryModal');
|
||||
modal.classList.add('show');
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/entities/${currentEntityIdForAttributes}/attributes/history?attribute_name=${encodeURIComponent(attributeName)}`);
|
||||
if (!res.ok) throw new Error('Failed to load history');
|
||||
|
||||
const data = await res.json();
|
||||
renderAttributeHistory(data.history, attributeName);
|
||||
} catch (err) {
|
||||
console.error('Load history failed:', err);
|
||||
document.getElementById('attrHistoryContent').innerHTML = '<p style="color:#ff6b6b;">加载失败</p>';
|
||||
}
|
||||
};
|
||||
|
||||
window.hideAttrHistoryModal = function() {
|
||||
document.getElementById('attrHistoryModal').classList.remove('show');
|
||||
};
|
||||
|
||||
function renderAttributeHistory(history, attributeName) {
|
||||
const container = document.getElementById('attrHistoryContent');
|
||||
|
||||
if (history.length === 0) {
|
||||
container.innerHTML = `<p style="color:#666; text-align:center;">属性 "${attributeName}" 暂无变更历史</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = history.map(h => {
|
||||
const date = new Date(h.changed_at).toLocaleString();
|
||||
return `
|
||||
<div class="history-item">
|
||||
<div class="history-header">
|
||||
<span>${h.changed_by || '系统'}</span>
|
||||
<span>${date}</span>
|
||||
</div>
|
||||
<div class="history-change">
|
||||
<span class="history-old">${h.old_value || '(无)'}</span>
|
||||
<span class="history-arrow">→</span>
|
||||
<span class="history-new">${h.new_value || '(无)'}</span>
|
||||
</div>
|
||||
${h.change_reason ? `<div style="color:#666; margin-top:4px; font-size:0.8rem;">原因: ${h.change_reason}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Attribute Templates Management
|
||||
window.showAttributeTemplates = async function() {
|
||||
const modal = document.getElementById('attrTemplatesModal');
|
||||
modal.classList.add('show');
|
||||
|
||||
document.getElementById('templateForm').style.display = 'none';
|
||||
document.getElementById('toggleTemplateBtn').style.display = 'inline-block';
|
||||
document.getElementById('saveTemplateBtn').style.display = 'none';
|
||||
|
||||
await loadAttributeTemplates();
|
||||
};
|
||||
|
||||
window.hideAttrTemplatesModal = function() {
|
||||
document.getElementById('attrTemplatesModal').classList.remove('show');
|
||||
};
|
||||
|
||||
async function loadAttributeTemplates() {
|
||||
if (!currentProject) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/projects/${currentProject.id}/attribute-templates`);
|
||||
if (!res.ok) throw new Error('Failed to load templates');
|
||||
|
||||
currentTemplates = await res.json();
|
||||
renderTemplatesList();
|
||||
} catch (err) {
|
||||
console.error('Load templates failed:', err);
|
||||
document.getElementById('templatesList').innerHTML = '<p style="color:#ff6b6b;">加载失败</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderTemplatesList() {
|
||||
const container = document.getElementById('templatesList');
|
||||
|
||||
if (currentTemplates.length === 0) {
|
||||
container.innerHTML = '<p style="color:#666; text-align:center; padding:20px;">暂无模板,点击"新建模板"创建</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = currentTemplates.map(t => {
|
||||
const optionsStr = t.options ? `选项: ${t.options.join(', ')}` : '';
|
||||
return `
|
||||
<div class="template-item">
|
||||
<div class="template-info">
|
||||
<div class="template-name">
|
||||
${t.name}
|
||||
<span class="attribute-type">${t.type}</span>
|
||||
${t.is_required ? '<span style="color:#ff6b6b;">*</span>' : ''}
|
||||
</div>
|
||||
<div class="template-desc">${t.description || ''} ${optionsStr}</div>
|
||||
</div>
|
||||
<div class="attribute-actions">
|
||||
<button class="attribute-btn delete" onclick="deleteTemplate('${t.id}')">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.toggleTemplateForm = function() {
|
||||
const form = document.getElementById('templateForm');
|
||||
const toggleBtn = document.getElementById('toggleTemplateBtn');
|
||||
const saveBtn = document.getElementById('saveTemplateBtn');
|
||||
|
||||
if (form.style.display === 'none') {
|
||||
form.style.display = 'block';
|
||||
toggleBtn.style.display = 'none';
|
||||
saveBtn.style.display = 'inline-block';
|
||||
} else {
|
||||
form.style.display = 'none';
|
||||
toggleBtn.style.display = 'inline-block';
|
||||
saveBtn.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
window.onTemplateTypeChange = function() {
|
||||
const type = document.getElementById('templateType').value;
|
||||
const optionsGroup = document.getElementById('templateOptionsGroup');
|
||||
|
||||
if (type === 'select' || type === 'multiselect') {
|
||||
optionsGroup.style.display = 'block';
|
||||
} else {
|
||||
optionsGroup.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
window.saveTemplate = async function() {
|
||||
if (!currentProject) return;
|
||||
|
||||
const name = document.getElementById('templateName').value.trim();
|
||||
const type = document.getElementById('templateType').value;
|
||||
const description = document.getElementById('templateDesc').value.trim();
|
||||
const isRequired = document.getElementById('templateRequired').checked;
|
||||
const defaultValue = document.getElementById('templateDefault').value.trim();
|
||||
|
||||
if (!name) {
|
||||
alert('请输入模板名称');
|
||||
return;
|
||||
}
|
||||
|
||||
let options = null;
|
||||
if (type === 'select' || type === 'multiselect') {
|
||||
const optionsStr = document.getElementById('templateOptions').value.trim();
|
||||
if (optionsStr) {
|
||||
options = optionsStr.split(',').map(o => o.trim()).filter(o => o);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/projects/${currentProject.id}/attribute-templates`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
type,
|
||||
description,
|
||||
options,
|
||||
is_required: isRequired,
|
||||
default_value: defaultValue || null
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed to save template');
|
||||
|
||||
// Reset form
|
||||
document.getElementById('templateName').value = '';
|
||||
document.getElementById('templateDesc').value = '';
|
||||
document.getElementById('templateOptions').value = '';
|
||||
document.getElementById('templateDefault').value = '';
|
||||
document.getElementById('templateRequired').checked = false;
|
||||
|
||||
await loadAttributeTemplates();
|
||||
toggleTemplateForm();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Save template failed:', err);
|
||||
alert('保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
window.deleteTemplate = async function(templateId) {
|
||||
if (!confirm('确定要删除这个模板吗?')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/projects/${currentProject.id}/attribute-templates/${templateId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed to delete template');
|
||||
|
||||
await loadAttributeTemplates();
|
||||
} catch (err) {
|
||||
console.error('Delete template failed:', err);
|
||||
alert('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// Search entities by attributes
|
||||
window.searchByAttributes = async function() {
|
||||
if (!currentProject) return;
|
||||
|
||||
const filterName = document.getElementById('attrFilterName').value;
|
||||
const filterValue = document.getElementById('attrFilterValue').value;
|
||||
const filterOp = document.getElementById('attrFilterOp').value;
|
||||
|
||||
if (!filterName || !filterValue) {
|
||||
alert('请输入筛选条件');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const filters = JSON.stringify([{ name: filterName, value: filterValue, operator: filterOp }]);
|
||||
const res = await fetch(`${API_BASE}/projects/${currentProject.id}/entities/search-by-attributes?filters=${encodeURIComponent(filters)}`);
|
||||
|
||||
if (!res.ok) throw new Error('Search failed');
|
||||
|
||||
const entities = await res.json();
|
||||
|
||||
// Update entity grid
|
||||
const grid = document.getElementById('kbEntityGrid');
|
||||
if (entities.length === 0) {
|
||||
grid.innerHTML = '<p style="color:#666; grid-column:1/-1; text-align:center;">未找到匹配的实体</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = entities.map(ent => `
|
||||
<div class="kb-entity-card" onclick="switchView('workbench'); setTimeout(() => selectEntity('${ent.id}'), 100);">
|
||||
<div class="kb-entity-name">
|
||||
<span class="entity-type-badge type-${ent.type.toLowerCase()}" style="font-size:0.65rem;margin-right:8px;">${ent.type}</span>
|
||||
${ent.name}
|
||||
</div>
|
||||
<div class="kb-entity-def">${ent.definition || '暂无定义'}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
} catch (err) {
|
||||
console.error('Search by attributes failed:', err);
|
||||
alert('搜索失败');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1270,6 +1270,142 @@
|
||||
0%, 60%, 100% { transform: translateY(0); }
|
||||
30% { transform: translateY(-10px); }
|
||||
}
|
||||
/* Phase 5: Entity Attributes */
|
||||
.attributes-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.attribute-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: #0a0a0a;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.attribute-info {
|
||||
flex: 1;
|
||||
}
|
||||
.attribute-name {
|
||||
font-weight: 500;
|
||||
color: #e0e0e0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.attribute-value {
|
||||
color: #00d4ff;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.attribute-type {
|
||||
font-size: 0.7rem;
|
||||
color: #666;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.attribute-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.attribute-btn {
|
||||
background: transparent;
|
||||
border: 1px solid #333;
|
||||
color: #888;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.attribute-btn:hover {
|
||||
border-color: #00d4ff;
|
||||
color: #00d4ff;
|
||||
}
|
||||
.attribute-btn.delete:hover {
|
||||
border-color: #ff6b6b;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
.attributes-add-form {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
.templates-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.template-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: #0a0a0a;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.template-info {
|
||||
flex: 1;
|
||||
}
|
||||
.template-name {
|
||||
font-weight: 500;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.template-desc {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.history-item {
|
||||
padding: 12px;
|
||||
background: #0a0a0a;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
color: #888;
|
||||
}
|
||||
.history-change {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.history-old {
|
||||
color: #ff6b6b;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.history-new {
|
||||
color: #4ecdc4;
|
||||
}
|
||||
.history-arrow {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Phase 5: Attribute filter in KB */
|
||||
.attribute-filter-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #141414;
|
||||
border-radius: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.attribute-filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.attribute-filter-item select,
|
||||
.attribute-filter-item input {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
color: #e0e0e0;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1441,7 +1577,32 @@
|
||||
<div class="kb-main">
|
||||
<!-- Entities Section -->
|
||||
<div class="kb-section active" id="kbEntitiesSection">
|
||||
<h3 style="margin-bottom:16px;">所有实体</h3>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
|
||||
<h3>所有实体</h3>
|
||||
<button class="btn btn-small" onclick="showAttributeTemplates()">🏷️ 管理属性模板</button>
|
||||
</div>
|
||||
|
||||
<!-- Attribute Filter Bar -->
|
||||
<div class="attribute-filter-bar">
|
||||
<div class="attribute-filter-item">
|
||||
<label>属性筛选:</label>
|
||||
<input type="text" id="attrFilterName" placeholder="属性名 (如: 职位)" style="width:120px;">
|
||||
</div>
|
||||
<div class="attribute-filter-item">
|
||||
<select id="attrFilterOp">
|
||||
<option value="eq">等于</option>
|
||||
<option value="contains">包含</option>
|
||||
<option value="gt">大于</option>
|
||||
<option value="lt">小于</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="attribute-filter-item">
|
||||
<input type="text" id="attrFilterValue" placeholder="属性值">
|
||||
</div>
|
||||
<button class="btn-icon" onclick="searchByAttributes()">🔍 搜索</button>
|
||||
<button class="btn-icon" onclick="loadKnowledgeBase()">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="kb-entity-grid" id="kbEntityGrid"></div>
|
||||
</div>
|
||||
<!-- Relations Section -->
|
||||
@@ -1581,6 +1742,114 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Entity Attributes Modal -->
|
||||
<div class="modal-overlay" id="attributesModal">
|
||||
<div class="modal" style="max-width: 600px;">
|
||||
<h3 class="modal-header">实体属性</h3>
|
||||
<div id="attributesContent">
|
||||
<div class="attributes-list" id="attributesList"></div>
|
||||
<div class="attributes-add-form" id="attributesAddForm" style="display:none;">
|
||||
<h4 style="color:#888;font-size:0.9rem;margin:16px 0 12px;">添加新属性</h4>
|
||||
<div class="form-group">
|
||||
<label>属性名称</label>
|
||||
<input type="text" id="attrName" placeholder="如: 职位、部门、年龄">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>属性类型</label>
|
||||
<select id="attrType" onchange="onAttrTypeChange()">
|
||||
<option value="text">文本</option>
|
||||
<option value="number">数字</option>
|
||||
<option value="date">日期</option>
|
||||
<option value="select">单选</option>
|
||||
<option value="multiselect">多选</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="attrOptionsGroup" style="display:none;">
|
||||
<label>选项 (用逗号分隔)</label>
|
||||
<input type="text" id="attrOptions" placeholder="选项1, 选项2, 选项3">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>属性值</label>
|
||||
<div id="attrValueContainer">
|
||||
<input type="text" id="attrValue" placeholder="输入属性值">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>变更原因 (可选)</label>
|
||||
<input type="text" id="attrChangeReason" placeholder="为什么添加/修改这个属性">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" onclick="hideAttributesModal()">关闭</button>
|
||||
<button class="btn" onclick="toggleAddAttributeForm()" id="toggleAddAttrBtn">添加属性</button>
|
||||
<button class="btn" onclick="saveAttribute()" id="saveAttrBtn" style="display:none;">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attribute History Modal -->
|
||||
<div class="modal-overlay" id="attrHistoryModal">
|
||||
<div class="modal" style="max-width: 700px;">
|
||||
<h3 class="modal-header">属性变更历史</h3>
|
||||
<div id="attrHistoryContent" style="max-height: 400px; overflow-y: auto;">
|
||||
<p style="color:#666;">加载中...</p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" onclick="hideAttrHistoryModal()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attribute Templates Modal -->
|
||||
<div class="modal-overlay" id="attrTemplatesModal">
|
||||
<div class="modal" style="max-width: 700px;">
|
||||
<h3 class="modal-header">属性模板管理</h3>
|
||||
<div id="attrTemplatesContent">
|
||||
<div class="templates-list" id="templatesList"></div>
|
||||
<div class="template-form" id="templateForm" style="display:none; margin-top: 20px; padding-top: 20px; border-top: 1px solid #333;">
|
||||
<h4 style="color:#888;font-size:0.9rem;margin-bottom:12px;">新建模板</h4>
|
||||
<div class="form-group">
|
||||
<label>模板名称</label>
|
||||
<input type="text" id="templateName" placeholder="如: 职位">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>类型</label>
|
||||
<select id="templateType" onchange="onTemplateTypeChange()">
|
||||
<option value="text">文本</option>
|
||||
<option value="number">数字</option>
|
||||
<option value="date">日期</option>
|
||||
<option value="select">单选</option>
|
||||
<option value="multiselect">多选</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>描述 (可选)</label>
|
||||
<input type="text" id="templateDesc" placeholder="属性的用途说明">
|
||||
</div>
|
||||
<div class="form-group" id="templateOptionsGroup" style="display:none;">
|
||||
<label>选项 (用逗号分隔)</label>
|
||||
<input type="text" id="templateOptions" placeholder="选项1, 选项2, 选项3">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="templateRequired"> 必填属性
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>默认值 (可选)</label>
|
||||
<input type="text" id="templateDefault" placeholder="默认值">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" onclick="hideAttrTemplatesModal()">关闭</button>
|
||||
<button class="btn" onclick="toggleTemplateForm()" id="toggleTemplateBtn">新建模板</button>
|
||||
<button class="btn" onclick="saveTemplate()" id="saveTemplateBtn" style="display:none;">保存模板</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Entity Editor Modal -->
|
||||
<div class="modal-overlay" id="entityModal">
|
||||
<div class="modal">
|
||||
@@ -1691,6 +1960,7 @@
|
||||
<!-- Context Menu -->
|
||||
<div class="context-menu" id="contextMenu">
|
||||
<div class="context-menu-item" onclick="editEntity()">✏️ 编辑实体</div>
|
||||
<div class="context-menu-item" onclick="showEntityAttributes()">🏷️ 管理属性</div>
|
||||
<div class="context-menu-item" onclick="showMergeModal()">🔄 合并实体</div>
|
||||
<div class="context-menu-divider"></div>
|
||||
<div class="context-menu-item" onclick="createEntityFromSelection()">➕ 标记为实体</div>
|
||||
|
||||
Reference in New Issue
Block a user