v1.2.0: 素材详情文本直接编辑+未保存提示+Markdown切换 + 素材列表显示创建时间/可排序 + API文档(/api-doc + API.md)
This commit is contained in:
+106
-12
@@ -127,6 +127,21 @@ main{flex:1;padding:22px 26px;overflow-y:auto;max-height:calc(100vh - 59px)}
|
||||
.b-extracted{background:rgba(79,140,255,.12);color:#8ab4ff}
|
||||
.b-empty{color:var(--dim);background:var(--panel2)}
|
||||
.mat-name{font-weight:600;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.md-prev{background:#0b0f19;border:1px solid var(--line);border-radius:10px;padding:14px;max-height:380px;overflow-y:auto;font-size:13px;line-height:1.75;word-break:break-word}
|
||||
.md-prev h1,.md-prev h2,.md-prev h3{color:var(--acc);margin:10px 0 6px}
|
||||
.md-prev h1{font-size:20px}.md-prev h2{font-size:17px}.md-prev h3{font-size:15px}
|
||||
.md-prev pre{background:#141a2c;border:1px solid var(--line);border-radius:8px;padding:10px;overflow-x:auto}
|
||||
.md-prev code{background:#1e2740;padding:2px 5px;border-radius:5px;font-size:12px;color:#22d3a5}
|
||||
.md-prev pre code{background:none;padding:0}
|
||||
.md-prev table{border-collapse:collapse;margin:8px 0}
|
||||
.md-prev th,.md-prev td{border:1px solid var(--line);padding:6px 10px}
|
||||
.md-prev ul,.md-prev ol{padding-left:20px}
|
||||
.md-prev li{margin:3px 0}
|
||||
.btn.sm.active{background:rgba(79,140,255,.2);border-color:var(--acc);color:var(--acc)}
|
||||
@keyframes pulse{0%{transform:scale(1)}50%{transform:scale(1.06)}100%{transform:scale(1)}}
|
||||
.btn.pulse{animation:pulse .8s ease infinite}
|
||||
th.sortable{cursor:pointer;user-select:none;white-space:nowrap}
|
||||
th.sortable:hover{color:var(--acc)}
|
||||
.mat-meta{font-size:11px;color:var(--dim)}
|
||||
|
||||
/* 弹窗 */
|
||||
@@ -165,9 +180,11 @@ pre.text-prev{white-space:pre-wrap;word-break:break-word;background:#0b0f19;bord
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo" onclick="renderDash()" title="回到首页" style="cursor:pointer"><div class="ic">🗂️</div>素材库系统</div>
|
||||
<div class="search"><span class="s-ic">🔍</span><input id="searchInput" placeholder="搜索项目、素材内容、关键词… (回车搜索)" value=""></div>
|
||||
<button class="btn ghost" onclick="window.open('/api-doc','_blank')" title="API 文档">📖 API</button>
|
||||
<button class="btn ghost" onclick="openSettings()" title="设置">⚙️ 设置</button>
|
||||
<div class="chips">
|
||||
<div class="chip"><b id="stProj">0</b>项目</div>
|
||||
@@ -225,16 +242,15 @@ pre.text-prev{white-space:pre-wrap;word-break:break-word;background:#0b0f19;bord
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay" id="matModal" hidden>
|
||||
<div class="overlay" id="matModal" hidden onclick="if(event.target===this)closeMatModal()">
|
||||
<div class="modal wide">
|
||||
<div class="m-head"><h3 id="mmTitle">素材详情</h3><button class="x" onclick="closeModal('matModal')">✕</button></div>
|
||||
<div class="m-head"><h3 id="mmTitle">素材详情</h3><button class="x" onclick="closeMatModal()">✕</button></div>
|
||||
<div class="m-body" id="mmBody"></div>
|
||||
<div class="m-foot">
|
||||
<button class="btn ghost" id="mmCopy" onclick="copyCurMat()" style="display:none">📋 复制文本</button>
|
||||
<button class="btn ghost" id="mmEdit" onclick="openTextModal(curMat.id)" style="display:none">✏️ 编辑文本</button>
|
||||
<button class="btn danger" onclick="deleteMaterial()">删除</button>
|
||||
<button class="btn green" id="mmAnalyze" onclick="analyzeOne()">🤖 AI 分析</button>
|
||||
<button class="btn primary" onclick="closeModal('matModal')">关闭</button>
|
||||
<button class="btn primary" onclick="closeMatModal()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -283,11 +299,13 @@ pre.text-prev{white-space:pre-wrap;word-break:break-word;background:#0b0f19;bord
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/lib/marked.min.js"></script>
|
||||
<script>
|
||||
const $=id=>document.getElementById(id);
|
||||
const TYPE_IC={text:'📝',doc:'📄',image:'🖼️',video:'🎬',audio:'🎵',other:'📦'};
|
||||
const TYPE_CLS={text:'t-text',doc:'t-doc',image:'t-image',video:'t-video',audio:'t-audio',other:'t-other'};
|
||||
let state={view:'dash', project:null, cat:'', selectedTags:[], projects:[], meta:null};
|
||||
let sortF=null, sortD=1; // 素材列表排序
|
||||
let polling={};
|
||||
|
||||
function toast(msg,err){const t=$('toast');t.textContent=msg;t.className='toast'+(err?' err':'');t.style.display='block';clearTimeout(t._h);t._h=setTimeout(()=>t.style.display='none',2600);}
|
||||
@@ -385,7 +403,7 @@ async function openProject(id){
|
||||
|
||||
function renderProj(){
|
||||
const p=state.project;
|
||||
const mats=p.materials||[];
|
||||
const mats=sortedMats();
|
||||
const rows=mats.map(m=>{
|
||||
const meta=m.meta||{};
|
||||
const metaStr=[meta.width&&meta.height?meta.width+'×'+meta.height:null, meta.format?meta.format:null, m.ext?m.ext.toUpperCase():null].filter(Boolean).join(' · ');
|
||||
@@ -396,7 +414,9 @@ function renderProj(){
|
||||
return `<tr>
|
||||
<td><span class="type-ic ${TYPE_CLS[m.mtype]||TYPE_CLS.other}">${TYPE_IC[m.mtype]||TYPE_IC.other}</span></td>
|
||||
<td><div class="mat-name" title="${escQ(m.original_name||m.name)}">${esc(m.name)}</div>
|
||||
<div class="mat-meta">${metaStr}${metaStr&&m.file_size?' · ':''}${fmtSize(m.file_size)}</div></td>
|
||||
<div class="mat-meta">${metaStr}</div></td>
|
||||
<td>${fmtSize(m.file_size)}</td>
|
||||
<td>${esc((m.created_at||'').slice(0,16))}</td>
|
||||
<td><span class="badge ${txCls}">${txTxt}</span></td>
|
||||
<td><span class="badge ${stCls}">${stTxt}</span></td>
|
||||
<td style="white-space:nowrap">
|
||||
@@ -451,11 +471,35 @@ function renderProj(){
|
||||
</div>
|
||||
<div class="dash-head" style="margin-bottom:10px"><h2 style="font-size:16px">📄 素材列表 (${mats.length})</h2></div>
|
||||
<div style="overflow-x:auto">
|
||||
<table class="mtable"><thead><tr><th>类型</th><th>名称</th><th>文本抽取</th><th>AI状态</th><th>操作</th></tr></thead>
|
||||
<table class="mtable"><thead><tr>
|
||||
<th class="sortable" onclick="sortBy('mtype')">类型 ${sortIcon('mtype')}</th>
|
||||
<th class="sortable" onclick="sortBy('name')">名称 ${sortIcon('name')}</th>
|
||||
<th class="sortable" onclick="sortBy('file_size')">大小 ${sortIcon('file_size')}</th>
|
||||
<th class="sortable" onclick="sortBy('created_at')">创建时间 ${sortIcon('created_at')}</th>
|
||||
<th>文本抽取</th><th>AI状态</th><th>操作</th>
|
||||
</tr></thead>
|
||||
<tbody>${rows||'<tr><td colspan="5" style="text-align:center;color:var(--dim);padding:30px">暂无素材,快上传吧</td></tr>'}</tbody></table>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ---------- 素材列表排序 ---------- */
|
||||
function sortBy(field){
|
||||
if(sortF===field){sortD=-sortD;}
|
||||
else{sortF=field;sortD=1;}
|
||||
renderProj();
|
||||
}
|
||||
function sortedMats(){
|
||||
const arr=[...(state.project.materials||[])];
|
||||
if(!sortF)return arr;
|
||||
const d=sortD;
|
||||
arr.sort((a,b)=>{
|
||||
if(sortF==='file_size'){return ((a.file_size||0)-(b.file_size||0))*d;}
|
||||
return String(a[sortF]||'').localeCompare(String(b[sortF]||''),'zh')*d;
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
function sortIcon(f){return sortF===f?(sortD===1?'▲':'▼'):'⇅';}
|
||||
|
||||
/* ---------- 项目 CRUD ---------- */
|
||||
let editProjectId=null;
|
||||
function openProjectModal(id){
|
||||
@@ -543,18 +587,25 @@ async function uploadFiles(files){
|
||||
}catch(e){toast(e.message,true);}
|
||||
}
|
||||
|
||||
/* ---------- 素材详情 ---------- */
|
||||
/* ---------- 素材详情(文本可直接编辑 + Markdown 切换 + 未保存提示) ---------- */
|
||||
let curMat=null;
|
||||
let matDirty=false; // 文本是否有未保存修改
|
||||
let mdMode=false; // 是否 Markdown 预览
|
||||
|
||||
async function openMat(id){
|
||||
curMat=await api('/api/materials/'+id);
|
||||
matDirty=false; mdMode=false;
|
||||
renderMatModal();
|
||||
}
|
||||
function renderMatModal(){
|
||||
const m=curMat, meta=m.meta||{}, a=m.analysis;
|
||||
const editable=!!(m.extracted_text!==undefined)&&(m.mtype==='text'||m.extracted_text);
|
||||
const media=m.file_path?(m.mtype==='image'?`<img class="media-prev" src="/files/${m.id}">`
|
||||
:m.mtype==='video'?`<video class="media-prev" controls src="/files/${m.id}"></video>`
|
||||
:m.mtype==='audio'?`<audio controls src="/files/${m.id}" style="width:100%;margin-bottom:10px"></audio>`
|
||||
:''):'';
|
||||
$('mmTitle').textContent=m.name;
|
||||
$('mmCopy').style.display=(m.extracted_text||m.mtype==='text')?'inline-flex':'none';
|
||||
$('mmEdit').style.display=(m.mtype==='text')?'inline-flex':'none';
|
||||
$('mmBody').innerHTML=`
|
||||
${media}
|
||||
<div class="mdl-row">
|
||||
@@ -563,6 +614,18 @@ async function openMat(id){
|
||||
<span>创建 <b>${esc(m.created_at)}</b></span>
|
||||
<span>AI状态 <b>${m.status}</b></span>
|
||||
</div>
|
||||
${editable?`<div class="section">
|
||||
<h5>📄 原始文本 <span style="font-weight:400;font-size:12px;color:var(--dim)">(可直接编辑,点保存后生效)</span>
|
||||
<span style="float:right;display:inline-flex;gap:6px">
|
||||
<button class="btn ghost sm ${!mdMode?'active':''}" onclick="toggleMd(false)">纯文本</button>
|
||||
<button class="btn ghost sm ${mdMode?'active':''}" onclick="toggleMd(true)">Markdown</button>
|
||||
<button class="btn green sm" id="txtSave" onclick="saveMat()">💾 保存</button>
|
||||
</span>
|
||||
</h5>
|
||||
${mdMode
|
||||
? `<div class="md-prev">${marked.parse(esc(m.extracted_text||''))}</div><div style="font-size:11px;color:var(--dim);margin-top:6px">Markdown 预览(只读),编辑请切换回「纯文本」</div>`
|
||||
: `<textarea id="txtEdit" style="min-height:220px;width:100%;line-height:1.7" placeholder="在此直接编辑文本内容…" oninput="onTextEdit()">${esc(m.extracted_text||'')}</textarea>`}
|
||||
</div>`:''}
|
||||
${a?`<div class="section"><h5>🤖 AI 分析结果</h5>
|
||||
${a.detail&&a.detail.vision?`<div class="vision">👁️ 视觉描述:${esc(a.detail.vision)}</div>`:''}
|
||||
<div class="kv"><b>归类</b><span>${esc(a.category||'-')}</span><b>摘要</b><span style="line-height:1.7">${esc(a.summary)}</span></div>
|
||||
@@ -571,13 +634,43 @@ async function openMat(id){
|
||||
<div style="margin-top:10px"><b style="color:var(--dim);font-size:12px">推荐标签</b><div class="kw-tags" style="margin-top:6px">${(a.tags||[]).map(k=>`<span style="color:var(--purple)">${esc(k)}</span>`).join('')}</div></div>
|
||||
</div>`:
|
||||
`<div class="section" style="color:var(--dim);text-align:center">该素材还未进行 AI 分析<br><button class="btn green sm" style="margin-top:10px" onclick="analyzeOne(${m.id})">🤖 立即分析</button></div>`}
|
||||
<div class="section"><h5>📄 文本内容 ${m.text_status==='extracted'?'':'('+m.text_status+')'}</h5>
|
||||
${!editable?`<div class="section"><h5>📄 文本内容 ${m.text_status==='extracted'?'':'('+m.text_status+')'}</h5>
|
||||
${m.extracted_text?`<pre class="text-prev">${esc(m.extracted_text)}</pre>`:`<div style="color:var(--dim);font-size:13px">${m.mtype==='image'?'图片素材无文本内容(已通过视觉模型理解内容)':m.mtype==='video'?'视频素材暂未做语音转写,可结合文件名理解':'该素材无可提取文本'}</div>`}
|
||||
</div>`;
|
||||
</div>`:''}`;
|
||||
openModal('matModal');
|
||||
}
|
||||
function toggleMd(on){mdMode=on;renderMatModal();}
|
||||
function onTextEdit(){
|
||||
matDirty=true;
|
||||
const b=$('txtSave');
|
||||
if(b){b.textContent='💾 保存修改';b.classList.add('pulse');}
|
||||
}
|
||||
async function saveMat(){
|
||||
const ta=$('txtEdit');
|
||||
if(!ta)return;
|
||||
const content=ta.value;
|
||||
try{
|
||||
await api('/api/materials/'+curMat.id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({content})});
|
||||
matDirty=false;
|
||||
toast('已保存(原 AI 分析已作废,可重新分析)');
|
||||
curMat=await api('/api/materials/'+curMat.id);
|
||||
renderMatModal();
|
||||
if(state.view==='proj'&&state.project) await openProject(state.project.id);
|
||||
}catch(e){toast(e.message,true);}
|
||||
}
|
||||
async function closeMatModal(){
|
||||
if(matDirty){
|
||||
const ok=confirm('文本内容有未保存的修改,是否保存?\n确定 = 保存并关闭\n取消 = 放弃修改');
|
||||
if(ok){
|
||||
try{await saveMat();}catch(e){toast(e.message,true);return;}
|
||||
}
|
||||
}
|
||||
matDirty=false;
|
||||
closeModal('matModal');
|
||||
}
|
||||
async function analyzeOne(id){
|
||||
const mid=id||curMat.id;
|
||||
if(matDirty){try{await saveMat();}catch(e){toast(e.message,true);return;}}
|
||||
try{
|
||||
await api('/api/materials/'+mid+'/analyze',{method:'POST'});
|
||||
toast('AI 分析已启动…');
|
||||
@@ -603,9 +696,10 @@ async function genSummary(){
|
||||
function download(id){window.open('/files/'+id,'_blank');}
|
||||
async function delMat(id){
|
||||
if(!confirm('确定删除该素材?'))return;
|
||||
try{await api('/api/materials/'+id,{method:'DELETE'});toast('已删除');if(curMat&&curMat.id===id)closeModal('matModal');await openProject(state.project.id);await loadMeta();}
|
||||
try{await api('/api/materials/'+id,{method:'DELETE'});toast('已删除');if(curMat&&curMat.id===id){matDirty=false;closeModal('matModal');}await openProject(state.project.id);await loadMeta();}
|
||||
catch(e){toast(e.message,true);}
|
||||
}
|
||||
function deleteMaterial(){if(curMat)delMat(curMat.id);}
|
||||
|
||||
/* ---------- 一键复制 ---------- */
|
||||
function copyToClipboard(text){
|
||||
|
||||
Vendored
+6
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user