This commit is contained in:
HuangHai
2026-01-21 08:41:47 +08:00
parent 306a3a4127
commit e51dc18d06
40 changed files with 1309 additions and 965 deletions

View File

@@ -12,6 +12,7 @@ createApp({
window.addEventListener('resize', () => {
isMobile.value = window.innerWidth <= 768;
if (chartInstance) chartInstance.resize();
if (trendChartInstance) trendChartInstance.resize();
});
// ==========================================
@@ -35,7 +36,9 @@ createApp({
const priceTableRows = ref([]);
const hourlyPricesByOperator = ref({});
let chartInstance = null;
let trendChartInstance = null;
const chartType = ref('line');
const trendDays = ref(7);
// ECharts Initialization
const initChart = () => {
@@ -49,6 +52,17 @@ createApp({
}
};
const initTrendChart = () => {
const dom = document.getElementById("trendChart");
if (dom && !trendChartInstance) {
if (typeof echarts === 'undefined') {
console.error("ECharts not loaded");
return;
}
trendChartInstance = echarts.init(dom);
}
};
const renderChart = () => {
if (!chartInstance) return;
@@ -152,6 +166,65 @@ createApp({
}
};
const loadTrendData = async () => {
try {
const res = await axios.get(apiBase.value + "/api/operators/price-trends?days=" + trendDays.value);
if (res && res.data) {
renderTrendChart(res.data);
}
} catch (e) {
console.error("Failed to load trend data:", e);
}
};
const renderTrendChart = (data) => {
if (!trendChartInstance) initTrendChart();
if (!trendChartInstance) return;
const option = {
backgroundColor: 'transparent',
tooltip: {
trigger: "axis",
backgroundColor: 'rgba(30, 41, 59, 0.9)',
borderColor: '#334155',
textStyle: { color: '#f1f5f9' }
},
legend: {
data: data.series.map(s => s.name),
textStyle: { color: "#94a3b8" },
top: 10
},
grid: { left: 50, right: 30, top: 60, bottom: 40 },
xAxis: {
type: "category",
data: data.dates.map(d => d.split('-').slice(1).join('/')), // Show MM/DD
axisLine: { lineStyle: { color: "#475569" } },
axisLabel: { color: "#94a3b8" }
},
yAxis: {
type: "value",
name: "元/度",
nameTextStyle: { color: "#94a3b8" },
axisLine: { lineStyle: { color: "#475569" } },
axisLabel: { color: "#94a3b8" },
splitLine: { lineStyle: { color: "#334155", type: 'dashed' } },
min: (value) => (value.min * 0.95).toFixed(2),
max: (value) => (value.max * 1.05).toFixed(2)
},
series: data.series.map(s => ({
name: s.name,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
data: s.data,
emphasis: { focus: 'series' }
}))
};
trendChartInstance.setOption(option, true);
};
const exportAllPrices = async () => {
try{
exporting.value = true;
@@ -244,48 +317,102 @@ createApp({
return '#f1f5f9'; // Slate-100
};
// 内置简易 Markdown 解析器
// Configure Marked
if (typeof marked !== 'undefined') {
marked.use({
gfm: true,
breaks: true
});
}
// 降级用的简易 Markdown 解析器
const simpleMarkdown = (text) => {
if (!text) return '';
let lines = text.split('\n');
let html = '';
let inList = false;
const parseInline = (str) => {
return str
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/`(.*?)`/g, '<code style="background:#f1f5f9; padding:2px 4px; border-radius:4px; color:#334155;">$1</code>');
.replace(/`(.*?)`/g, '<code style="background:rgba(148, 163, 184, 0.1); padding:2px 4px; border-radius:4px;">$1</code>');
};
for (let line of lines) {
let trimmed = line.trim();
if (!trimmed) continue;
if (!trimmed) {
if (inList) { html += '</ul>'; inList = false; }
html += '<br>';
continue;
}
if (trimmed.startsWith('### ')) {
if (inList) { html += '</ul>'; inList = false; }
html += `<h3>${parseInline(trimmed.substring(4))}</h3>`;
}
else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) {
} else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) {
if (!inList) { html += '<ul>'; inList = true; }
let content = trimmed.replace(/^(- |\d+\. )/, '');
html += `<li>${parseInline(content)}</li>`;
}
else {
} else {
if (inList) { html += '</ul>'; inList = false; }
html += `<p style="margin-bottom:8px;">${parseInline(trimmed)}</p>`;
html += `<p>${parseInline(trimmed)}</p>`;
}
}
if (inList) html += '</ul>';
return html;
};
// 增强的 Markdown & LaTeX 解析器
const renderMarkdownAndLatex = (text) => {
if (!text) return '';
try {
// 1. 处理 LaTeX (简单替换,先处理 $$ 再处理 $)
let processedText = text;
// 处理块级 LaTeX: $$ ... $$
processedText = processedText.replace(/\$\$\s*([\s\S]*?)\s*\$\$/g, (match, formula) => {
try {
if (typeof katex !== 'undefined') {
return '<div class="katex-block">' + katex.renderToString(formula, { displayMode: true, throwOnError: false }) + '</div>';
}
return match;
} catch (e) {
return match;
}
});
// 处理行内 LaTeX: $ ... $
processedText = processedText.replace(/\$([^\$\n]+?)\$/g, (match, formula) => {
try {
if (typeof katex !== 'undefined') {
return katex.renderToString(formula, { displayMode: false, throwOnError: false });
}
return match;
} catch (e) {
return match;
}
});
// 2. 使用 marked 解析 Markdown
if (typeof marked !== 'undefined') {
return marked.parse(processedText);
} else {
// 降级使用之前的 simpleMarkdown
return simpleMarkdown(processedText);
}
} catch (e) {
console.error('Markdown/LaTeX rendering error:', e);
return text;
}
};
const renderedAiText = computed(() => {
return simpleMarkdown(aiText.value);
return renderMarkdownAndLatex(aiText.value);
});
onMounted(() => {
initChart();
initTrendChart();
loadAllOperatorsPrices();
loadTrendData();
});
return {
@@ -299,9 +426,11 @@ createApp({
priceTableRows,
hourlyPricesByOperator,
chartType,
trendDays,
// Actions
loadAllOperatorsPrices,
loadTrendData,
exportAllPrices,
exportAiReport,
startAiAnalysis,

View File

@@ -15,53 +15,96 @@ createApp({
const summaryLoading = ref(false);
const summaryText = ref('');
// Simple Markdown Parser (Zero Dependency)
// 降级用的简易 Markdown 解析器
const simpleMarkdown = (text) => {
if (!text) return '';
let lines = text.split('\n');
let html = '';
let inList = false;
// Helper: Parse inline styles
const parseInline = (str) => {
return str
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // Bold
.replace(/`(.*?)`/g, '<code style="background:#f1f5f9; padding:2px 4px; border-radius:4px;">$1</code>'); // Code
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/`(.*?)`/g, '<code style="background:rgba(148, 163, 184, 0.1); padding:2px 4px; border-radius:4px;">$1</code>');
};
for (let line of lines) {
let trimmed = line.trim();
if (!trimmed) continue;
// Headers
if (!trimmed) {
if (inList) { html += '</ul>'; inList = false; }
html += '<br>';
continue;
}
if (trimmed.startsWith('### ')) {
if (inList) { html += '</ul>'; inList = false; }
html += `<h3 style="margin-top:16px; margin-bottom:8px; font-weight:bold; font-size:1.1em;">${parseInline(trimmed.substring(4))}</h3>`;
}
// Lists
else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) {
if (!inList) { html += '<ul style="padding-left:20px; margin:0;">'; inList = true; }
html += `<h3>${parseInline(trimmed.substring(4))}</h3>`;
} else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) {
if (!inList) { html += '<ul>'; inList = true; }
let content = trimmed.replace(/^(- |\d+\. )/, '');
html += `<li style="margin-bottom:4px;">${parseInline(content)}</li>`;
}
// Paragraphs
else {
html += `<li>${parseInline(content)}</li>`;
} else {
if (inList) { html += '</ul>'; inList = false; }
html += `<p style="margin-bottom:8px;">${parseInline(trimmed)}</p>`;
html += `<p>${parseInline(trimmed)}</p>`;
}
}
if (inList) html += '</ul>';
return html;
};
// Configure Marked
if (typeof marked !== 'undefined') {
marked.use({
gfm: true,
breaks: true
});
}
// 增强的 Markdown & LaTeX 解析器
const renderMarkdownAndLatex = (text) => {
if (!text) return '';
try {
// 1. 处理 LaTeX (简单替换,先处理 $$ 再处理 $)
let processedText = text;
// 处理块级 LaTeX: $$ ... $$
processedText = processedText.replace(/\$\$\s*([\s\S]*?)\s*\$\$/g, (match, formula) => {
try {
if (typeof katex !== 'undefined') {
return '<div class="katex-block">' + katex.renderToString(formula, { displayMode: true, throwOnError: false }) + '</div>';
}
return match;
} catch (e) {
return match;
}
});
// 处理行内 LaTeX: $ ... $
processedText = processedText.replace(/\$([^\$\n]+?)\$/g, (match, formula) => {
try {
if (typeof katex !== 'undefined') {
return katex.renderToString(formula, { displayMode: false, throwOnError: false });
}
return match;
} catch (e) {
return match;
}
});
// 2. 使用 marked 解析 Markdown
if (typeof marked !== 'undefined') {
return marked.parse(processedText);
} else {
// 降级使用之前的 simpleMarkdown
return simpleMarkdown(processedText);
}
} catch (e) {
console.error('Markdown/LaTeX rendering error:', e);
return text;
}
};
const renderedSummary = computed(() => {
if (!summaryText.value) return '';
try {
return simpleMarkdown(summaryText.value);
} catch (e) {
console.error("Simple markdown error:", e);
return summaryText.value;
}
return renderMarkdownAndLatex(summaryText.value);
});
// Methods

144
static/js/haibao.js Normal file
View File

@@ -0,0 +1,144 @@
const { createApp, ref, onMounted } = Vue;
const app = createApp({
setup() {
const prompt = ref('');
const generating = ref(false);
const refining = ref(false);
const currentImage = ref(null);
const historyList = ref([]);
const presets = ['清明节', '春节', '国庆十一', '端午节', '中秋节', '绿色出行', '低碳生活', '未来科技充电站'];
// Load history
const loadHistory = async () => {
try {
const res = await axios.get('/haibao/history');
historyList.value = res.data;
// If history exists, show the latest one as current
if (historyList.value.length > 0 && !currentImage.value) {
currentImage.value = historyList.value[0];
}
} catch (e) {
console.error('Failed to load history', e);
ElementPlus.ElMessage.error('加载历史记录失败');
}
};
// Refine Prompt
const handleRefine = async () => {
if (!prompt.value.trim()) {
ElementPlus.ElMessage.warning('请先输入一些关键词');
return;
}
refining.value = true;
const originalPrompt = prompt.value;
let isFirstChunk = true;
try {
const response = await fetch('/haibao/refine', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ prompt: originalPrompt })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
if (isFirstChunk) {
prompt.value = '';
isFirstChunk = false;
}
prompt.value += text;
}
ElementPlus.ElMessage.success('创意扩写完成');
} catch (e) {
console.error('Refine failed', e);
ElementPlus.ElMessage.error('扩写失败: ' + e.message);
} finally {
refining.value = false;
}
};
// Generate
const handleGenerate = async () => {
if (!prompt.value.trim()) {
ElementPlus.ElMessage.warning('请输入提示词');
return;
}
generating.value = true;
try {
const res = await axios.post('/haibao/generate', {
prompt: prompt.value
});
currentImage.value = res.data;
ElementPlus.ElMessage.success('生成成功');
// Insert new item to history locally to avoid delay
historyList.value.unshift(res.data);
} catch (e) {
console.error('Generation failed', e);
ElementPlus.ElMessage.error('生成失败: ' + (e.response?.data?.detail || e.message));
} finally {
generating.value = false;
}
};
// Use preset
const usePreset = (text) => {
prompt.value = text;
};
// View history item
const viewHistory = (item) => {
currentImage.value = item;
window.scrollTo({ top: 0, behavior: 'smooth' });
};
// Copy Scheme
const copyScheme = async () => {
if (!currentImage.value || !currentImage.value.scheme_content) return;
try {
await navigator.clipboard.writeText(currentImage.value.scheme_content);
ElementPlus.ElMessage.success('文案已复制到剪贴板');
} catch (err) {
console.error('Failed to copy text: ', err);
ElementPlus.ElMessage.error('复制失败,请手动复制');
}
};
onMounted(() => {
loadHistory();
});
return {
prompt,
generating,
currentImage,
historyList,
presets,
handleGenerate,
handleRefine,
usePreset,
viewHistory,
copyScheme
};
}
});
app.use(ElementPlus);
app.mount('#app');

View File

@@ -168,34 +168,104 @@ createApp({
// Configure Marked
if (typeof marked !== 'undefined') {
marked.use({
gfm: true,
breaks: true,
renderer: {
code(code, language) {
if (language === 'echarts') {
const id = 'chart-' + Math.random().toString(36).substr(2, 9);
return `<div class="echarts-container" id="${id}" style="width: 100%; height: 400px; margin: 20px 0; background: rgba(30, 41, 59, 0.5); border: 1px solid rgba(148, 163, 184, 0.1); border-radius: 12px; padding: 20px;" data-config="${encodeURIComponent(code)}"></div>`;
}
return false;
}
const renderer = new marked.Renderer();
const oldCode = renderer.code.bind(renderer);
renderer.code = function(code, language) {
if (language === 'echarts') {
const id = 'chart-' + Math.random().toString(36).substr(2, 9);
return `<div class="echarts-container" id="${id}" style="width: 100%; height: 400px; margin: 20px 0; background: rgba(30, 41, 59, 0.5); border: 1px solid rgba(148, 163, 184, 0.1); border-radius: 12px; padding: 20px;" data-config="${encodeURIComponent(code)}"></div>`;
}
return oldCode(code, language);
};
marked.setOptions({
renderer: renderer,
gfm: true,
breaks: true
});
}
const renderedResult = computed(() => {
if (!queryResult.value) return '';
try {
// Remove escapes that might break markdown
const cleanText = queryResult.value.replace(/\\([\*_`#\[\]\(\)!>-])/g, '$1');
if (typeof marked !== 'undefined') {
return marked.parse(cleanText);
// 降级用的简易 Markdown 解析器
const simpleMarkdown = (text) => {
if (!text) return '';
let lines = text.split('\n');
let html = '';
let inList = false;
const parseInline = (str) => {
return str
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/`(.*?)`/g, '<code style="background:rgba(148, 163, 184, 0.1); padding:2px 4px; border-radius:4px;">$1</code>');
};
for (let line of lines) {
let trimmed = line.trim();
if (!trimmed) {
if (inList) { html += '</ul>'; inList = false; }
html += '<br>';
continue;
}
if (trimmed.startsWith('### ')) {
if (inList) { html += '</ul>'; inList = false; }
html += `<h3>${parseInline(trimmed.substring(4))}</h3>`;
} else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) {
if (!inList) { html += '<ul>'; inList = true; }
let content = trimmed.replace(/^(- |\d+\. )/, '');
html += `<li>${parseInline(content)}</li>`;
} else {
if (inList) { html += '</ul>'; inList = false; }
html += `<p>${parseInline(trimmed)}</p>`;
}
return cleanText;
} catch (e) {
console.error('Markdown parsing error:', e);
return queryResult.value;
}
if (inList) html += '</ul>';
return html;
};
// 增强的 Markdown & LaTeX 解析器
const renderMarkdownAndLatex = (text) => {
if (!text) return '';
try {
// 1. 处理 LaTeX (简单替换,先处理 $$ 再处理 $)
let processedText = text;
// 处理块级 LaTeX: $$ ... $$
processedText = processedText.replace(/\$\$\s*([\s\S]*?)\s*\$\$/g, (match, formula) => {
try {
if (typeof katex !== 'undefined') {
return '<div class="katex-block">' + katex.renderToString(formula, { displayMode: true, throwOnError: false }) + '</div>';
}
return match;
} catch (e) {
return match;
}
});
// 处理行内 LaTeX: $ ... $
processedText = processedText.replace(/\$([^\$\n]+?)\$/g, (match, formula) => {
try {
if (typeof katex !== 'undefined') {
return katex.renderToString(formula, { displayMode: false, throwOnError: false });
}
return match;
} catch (e) {
return match;
}
});
// 2. 使用 marked 解析 Markdown
if (typeof marked !== 'undefined') {
// 直接解析处理后的文本,marked 会处理 Markdown 转义
return marked.parse(processedText);
} else {
// 降级使用之前的 simpleMarkdown
return simpleMarkdown(processedText);
}
} catch (e) {
console.error('Markdown/LaTeX rendering error:', e);
return text;
}
};
const renderedResult = computed(() => {
return renderMarkdownAndLatex(queryResult.value);
});
watch(queryResult, () => {