2026-01-21 07:51:52 +08:00
|
|
|
|
const { createApp, ref, computed, onMounted } = Vue;
|
|
|
|
|
|
|
2026-01-21 10:10:30 +08:00
|
|
|
|
// 1. 全局渲染器 - 纯手写的高鲁棒性解析器,不再依赖 marked
|
|
|
|
|
|
const globalRenderMarkdown = (text) => {
|
|
|
|
|
|
if (!text) return '';
|
|
|
|
|
|
|
|
|
|
|
|
// 0. 预处理:去除 AI 可能返回的代码块标记
|
|
|
|
|
|
let cleanText = text.trim();
|
|
|
|
|
|
// 去除开头的 ```markdown 或 ```
|
|
|
|
|
|
cleanText = cleanText.replace(/^```(markdown)?\s*/i, '');
|
|
|
|
|
|
// 去除结尾的 ```
|
|
|
|
|
|
cleanText = cleanText.replace(/```\s*$/, '');
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 预处理:按行分割
|
|
|
|
|
|
const lines = cleanText.split('\n');
|
|
|
|
|
|
let html = '';
|
|
|
|
|
|
let inList = false;
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
|
|
|
|
let line = lines[i];
|
|
|
|
|
|
// 关键:去除行首尾空白,解决缩进导致的解析失败
|
|
|
|
|
|
let trimmed = line.trim();
|
|
|
|
|
|
|
|
|
|
|
|
// 空行处理
|
|
|
|
|
|
if (!trimmed) {
|
|
|
|
|
|
if (inList) { html += '</ul>\n'; inList = false; }
|
|
|
|
|
|
continue; // 忽略空行,或者可以加 <br>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 内联格式处理(加粗、代码、链接)
|
|
|
|
|
|
// 加粗 **text** -> <strong>text</strong>
|
|
|
|
|
|
trimmed = trimmed.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
|
|
|
|
// 代码 `text` -> <code>text</code>
|
|
|
|
|
|
trimmed = trimmed.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 块级元素处理
|
|
|
|
|
|
// 标题 ### Title
|
|
|
|
|
|
if (trimmed.startsWith('###')) {
|
|
|
|
|
|
if (inList) { html += '</ul>\n'; inList = false; }
|
|
|
|
|
|
html += `<h3>${trimmed.replace(/^###\s*/, '')}</h3>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (trimmed.startsWith('##')) {
|
|
|
|
|
|
if (inList) { html += '</ul>\n'; inList = false; }
|
|
|
|
|
|
html += `<h2>${trimmed.replace(/^##\s*/, '')}</h2>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (trimmed.startsWith('#')) {
|
|
|
|
|
|
if (inList) { html += '</ul>\n'; inList = false; }
|
|
|
|
|
|
html += `<h1>${trimmed.replace(/^#\s*/, '')}</h1>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 列表 - Item 或 * Item
|
|
|
|
|
|
else if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
|
|
|
|
|
if (!inList) { html += '<ul>\n'; inList = true; }
|
|
|
|
|
|
html += `<li>${trimmed.substring(2)}</li>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 数字列表 1. Item
|
|
|
|
|
|
else if (/^\d+\.\s/.test(trimmed)) {
|
|
|
|
|
|
// 简单起见,数字列表也用 ul,或者你可以维护一个 ordered list 状态
|
|
|
|
|
|
if (!inList) { html += '<ul>\n'; inList = true; }
|
|
|
|
|
|
html += `<li>${trimmed.replace(/^\d+\.\s/, '')}</li>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 普通段落
|
|
|
|
|
|
else {
|
|
|
|
|
|
if (inList) { html += '</ul>\n'; inList = false; }
|
|
|
|
|
|
html += `<p>${trimmed}</p>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (inList) { html += '</ul>\n'; }
|
|
|
|
|
|
|
|
|
|
|
|
return html;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-21 07:51:52 +08:00
|
|
|
|
createApp({
|
|
|
|
|
|
setup() {
|
|
|
|
|
|
const apiBase = ref(window.location.origin || "http://localhost:8000");
|
|
|
|
|
|
|
|
|
|
|
|
// Douyin State
|
|
|
|
|
|
const shareText = ref('');
|
|
|
|
|
|
const douyinLoading = ref(false);
|
|
|
|
|
|
const douyinRecords = ref([]);
|
|
|
|
|
|
let douyinTimer = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Douyin Summary State
|
|
|
|
|
|
const showSummaryDialog = ref(false);
|
|
|
|
|
|
const summaryLoading = ref(false);
|
|
|
|
|
|
const summaryText = ref('');
|
|
|
|
|
|
|
2026-01-21 10:10:30 +08:00
|
|
|
|
// 使用全局渲染器
|
|
|
|
|
|
const renderedSummary = computed(() => {
|
|
|
|
|
|
if (!summaryText.value) return '';
|
|
|
|
|
|
const html = globalRenderMarkdown(summaryText.value);
|
2026-01-21 07:51:52 +08:00
|
|
|
|
return html;
|
2026-01-21 10:10:30 +08:00
|
|
|
|
});
|
2026-01-21 07:51:52 +08:00
|
|
|
|
|
2026-01-21 10:10:30 +08:00
|
|
|
|
// 这里的配置仍然保留,作为双重保险
|
2026-01-21 08:41:47 +08:00
|
|
|
|
if (typeof marked !== 'undefined') {
|
2026-01-21 10:10:30 +08:00
|
|
|
|
const m = (typeof marked.marked === 'function') ? marked.marked : marked;
|
|
|
|
|
|
if (m.setOptions) {
|
|
|
|
|
|
m.setOptions({
|
|
|
|
|
|
gfm: true,
|
|
|
|
|
|
breaks: true,
|
|
|
|
|
|
mangle: false,
|
|
|
|
|
|
headerIds: false
|
|
|
|
|
|
});
|
2026-01-21 07:51:52 +08:00
|
|
|
|
}
|
2026-01-21 10:10:30 +08:00
|
|
|
|
}
|
2026-01-21 07:51:52 +08:00
|
|
|
|
|
|
|
|
|
|
// Methods
|
|
|
|
|
|
const startParsing = async () => {
|
|
|
|
|
|
if (!shareText.value.trim()) return;
|
|
|
|
|
|
douyinLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await axios.post(apiBase.value + '/api/parse', { text: shareText.value });
|
|
|
|
|
|
if (response.data.id || (response.data.ids && response.data.ids.length > 0)) {
|
|
|
|
|
|
shareText.value = '';
|
|
|
|
|
|
fetchDouyinRecords();
|
|
|
|
|
|
if (typeof ElementPlus !== 'undefined') {
|
|
|
|
|
|
const count = response.data.ids ? response.data.ids.length : 1;
|
|
|
|
|
|
ElementPlus.ElMessage.success(`成功提交 ${count} 个解析任务`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error:', error);
|
|
|
|
|
|
if (typeof ElementPlus !== 'undefined') ElementPlus.ElMessage.error('解析请求失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
douyinLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const fetchDouyinRecords = async (isManual) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await axios.get(apiBase.value + '/api/records');
|
|
|
|
|
|
const newRecords = response.data;
|
|
|
|
|
|
douyinRecords.value = newRecords.map(newRec => {
|
|
|
|
|
|
const oldRec = douyinRecords.value.find(r => r.id === newRec.id);
|
|
|
|
|
|
return {
|
|
|
|
|
|
...newRec,
|
|
|
|
|
|
expanded: oldRec ? oldRec.expanded : false,
|
|
|
|
|
|
showOriginal: oldRec ? oldRec.showOriginal : false
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
if (isManual === true || (isManual && isManual.type === 'click')) {
|
|
|
|
|
|
if (typeof ElementPlus !== 'undefined') ElementPlus.ElMessage.success('列表已刷新');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error fetching records:', error);
|
|
|
|
|
|
if (isManual === true || (isManual && isManual.type === 'click')) {
|
|
|
|
|
|
if (typeof ElementPlus !== 'undefined') ElementPlus.ElMessage.error('刷新失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const deleteRecord = async (id) => {
|
|
|
|
|
|
if (!confirm('确定要删除这条记录吗?')) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await axios.delete(apiBase.value + `/api/records/${id}`);
|
|
|
|
|
|
fetchDouyinRecords();
|
|
|
|
|
|
if (typeof ElementPlus !== 'undefined') ElementPlus.ElMessage.success('删除成功');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error deleting:', error);
|
|
|
|
|
|
if (typeof ElementPlus !== 'undefined') ElementPlus.ElMessage.error('删除失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const openSummaryDialog = () => {
|
|
|
|
|
|
showSummaryDialog.value = true;
|
|
|
|
|
|
if (!summaryText.value) {
|
|
|
|
|
|
fetchDouyinSummary();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSummaryClose = () => {
|
|
|
|
|
|
showSummaryDialog.value = false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const fetchDouyinSummary = async () => {
|
|
|
|
|
|
if (summaryLoading.value) return;
|
|
|
|
|
|
summaryText.value = "";
|
|
|
|
|
|
summaryLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(apiBase.value + "/api/douyin/summary", {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
|
|
body: JSON.stringify({ids: []})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.body) return;
|
|
|
|
|
|
const reader = response.body.getReader();
|
|
|
|
|
|
const decoder = new TextDecoder();
|
|
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
|
const {done, value} = await reader.read();
|
|
|
|
|
|
if (done) break;
|
|
|
|
|
|
const chunk = decoder.decode(value, {stream: true});
|
|
|
|
|
|
summaryText.value += chunk;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error(e);
|
|
|
|
|
|
summaryText.value += "\n(总结过程出错: " + e.message + ")";
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
summaryLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const statusColor = (status) => {
|
|
|
|
|
|
switch(status) {
|
|
|
|
|
|
case 'COMPLETED': return '#10b981';
|
|
|
|
|
|
case 'FAILED': return '#ef4444';
|
|
|
|
|
|
case 'PROCESSING': return '#3b82f6';
|
|
|
|
|
|
default: return '#9ca3af';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const statusType = (status) => {
|
|
|
|
|
|
switch(status) {
|
|
|
|
|
|
case 'COMPLETED': return 'success';
|
|
|
|
|
|
case 'FAILED': return 'danger';
|
|
|
|
|
|
case 'PROCESSING': return 'primary';
|
|
|
|
|
|
default: return 'info';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const formatDate = (dateStr) => {
|
|
|
|
|
|
if (!dateStr) return '';
|
|
|
|
|
|
return new Date(dateStr).toLocaleString();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
fetchDouyinRecords();
|
|
|
|
|
|
if (douyinTimer) clearInterval(douyinTimer);
|
|
|
|
|
|
douyinTimer = setInterval(() => {
|
|
|
|
|
|
fetchDouyinRecords();
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
shareText, douyinLoading, douyinRecords,
|
|
|
|
|
|
startParsing, fetchDouyinRecords, deleteRecord, statusColor, statusType, formatDate,
|
|
|
|
|
|
showSummaryDialog, summaryLoading, summaryText, renderedSummary,
|
|
|
|
|
|
openSummaryDialog, handleSummaryClose, fetchDouyinSummary
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}).use(ElementPlus).mount("#app");
|