From 9d1ce0c3c54ad27c0e636f08d8989488c19fe747 Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Wed, 21 Jan 2026 07:51:52 +0800 Subject: [PATCH] 'commit' --- Config/Config.py | 20 +- Config/__pycache__/Config.cpython-310.pyc | Bin 1626 -> 1623 bytes static/css/dashboard.css | 255 +++++++++++++++ static/css/douyin.css | 273 ++++++++++++++++ static/css/index.css | 152 +++++++++ static/css/query.css | 287 ++++++++++++++++ static/dashboard.html | 90 +++++ static/douyin.html | 168 ++++++++++ static/index.html | 381 +++------------------- static/index_old.html | 352 ++++++++++++++++++++ static/js/dashboard.js | 313 ++++++++++++++++++ static/js/douyin.js | 202 ++++++++++++ static/js/query.js | 216 ++++++++++++ static/query.html | 88 +++++ 14 files changed, 2447 insertions(+), 350 deletions(-) create mode 100644 static/css/dashboard.css create mode 100644 static/css/douyin.css create mode 100644 static/css/index.css create mode 100644 static/css/query.css create mode 100644 static/dashboard.html create mode 100644 static/douyin.html create mode 100644 static/index_old.html create mode 100644 static/js/dashboard.js create mode 100644 static/js/douyin.js create mode 100644 static/js/query.js create mode 100644 static/query.html diff --git a/Config/Config.py b/Config/Config.py index a60510b..de13f46 100644 --- a/Config/Config.py +++ b/Config/Config.py @@ -1,16 +1,16 @@ # 黄海在公司内网开发时的配置信息 -# DORIS_HOST = "10.10.14.204" -# DORIS_PORT = 9030 -# DORIS_FENODES = "10.10.14.204:8030" -# REDIS_HOST = '10.10.14.14' -# REDIS_PASSWORD = None # 如果没有密码则设为 None +DORIS_HOST = "10.10.14.204" +DORIS_PORT = 9030 +DORIS_FENODES = "10.10.14.204:8030" +REDIS_HOST = '10.10.14.14' +REDIS_PASSWORD = None # 如果没有密码则设为 None # 黄海在家开发时的配置信息 -DORIS_HOST = "www.hzkjai.com" -DORIS_PORT = 27025 -DORIS_FENODES = "www.hzkjai.com:27024" -REDIS_HOST = '127.0.0.1' -REDIS_PASSWORD = "DsideaL147258369" +# DORIS_HOST = "www.hzkjai.com" +# DORIS_PORT = 27025 +# DORIS_FENODES = "www.hzkjai.com:27024" +# REDIS_HOST = '127.0.0.1' +# REDIS_PASSWORD = "DsideaL147258369" # 视觉模型配置 VL_MODEL_NAME = "qwen3-vl-flash" diff --git a/Config/__pycache__/Config.cpython-310.pyc b/Config/__pycache__/Config.cpython-310.pyc index 696a5c72020aac30c25bdaf642b80cc22bc051ea..beab9d58f21da558def0c3f304d32b065ca1cd67 100644 GIT binary patch delta 351 zcmcb`bDf7fpO=@50SK%`3MO*r)C)xkr3gm}r--Bq&SppvO%(!?VyO&ZHY1P}PZb7| z5_7quL{cO%#HHr4rwXPDr81=or%2CXjS@|fi4seZjS^3hi;_r@kCIGLh>`;GrBf86 zWKxu(WK)!*0$jZXN*r19V zE@Wupx7mSFjZsj*r8qMsHPOe=#N5c#!r07maw^k$##@s`nJ-R$&aBCMiz~?0#WOhG z!#_CW)?`_hHpZyQOITu!Zn3#I#fJv@M6ssi7iAXT;(#b~ak?c2Ww|>0yST;&xdsRL m`vtoO-9(R$TeAt%>e-0(^)D2 delta 355 zcmcc4bBl*NpO=@50SMkE=TGF$sb`52N)d_@P7zKOoXwCTk}3owMN=8TY(^j{mMRP+ z#piNIiKIwih)d38PZdlRN@YqFPLZ0!8YP+{9VM0`6D6J^8zqq<7bTe@A0-9kOQ$GA z$)qSo$)+ep$)zYq$)~79DWs?dGk9sLRq>UVm+NIzWoIR3>LusrW=_mxV5ky76|^!k zH!w1(;xsfe*E0Y@!zcll;>?uPL?1&Fb0bp=V>8RmI*e+JlT(=1F-A=mX1>ViH~9&( zCT|p1kgJPlaJ+|qa7ff-DV8?ITay>E#2Q7hxj4m#2Khv>rsNl87DsVFl({%Xi9uPe x&i*d0@j + + + + + 分时电价分析 - 驿来特AI智能大脑 + + + +
+
+ ⚡ 驿来特AI智能大脑 + ↩ 返回首页 + +
+ + +
+
+ +
+ +
+
+
+ 🕒 分时电价明细 +
+
+ + + + + + + + + + + + + +
时间{{ op.label }}
{{ row.hour }} + {{ formatCell(val.price) }} +
+
+
+
+ + +
+
+
📈 价格趋势对比
+
+ + +
+
+ +
+ +
+ +
+ 🤖 AI 调价策略建议 + +
+
+
+ 点击“开始分析”获取AI智能定价建议 +
+
+
+
+
+
+ + + + + + + diff --git a/static/douyin.html b/static/douyin.html new file mode 100644 index 0000000..bbfcd0e --- /dev/null +++ b/static/douyin.html @@ -0,0 +1,168 @@ + + + + + + 抖音知识库 - 驿来特AI智能大脑 + + + + + + +
+ + ← 返回首页 + + + + +
+
+

⚡ AI正在阅读您的知识库并提炼精华,请稍候...

+
+
+ | +
+ +
+ +
+
+

+ 抖音知识库 + + +

+

自动解析视频、提取文案,构建企业充电知识图谱

+
+ + + + + +
+ + {{ douyinLoading ? '解析处理中...' : '开始解析' }} + +
+
+ + +
+ +
+
+
+
+
+ + {{ record.status }} + + + {{ formatDate(record.create_time) }} + +
+

+ {{ record.video_name || '处理中...' }} +

+ + 📺 点击观看视频 + +
+ + Del + +
+ + +
+ Error: {{ record.error_msg }} +
+ + +
+

视频文案

+

+ {{ record.transcript }} +

+ + {{ record.expanded ? '收起' : '展开全文' }} + +
+ + +
+
+ {{ record.showOriginal ? '收起原始链接' : '查看原始链接信息' }} +
+
+ {{ record.original_text }} +
+
+
+
+
+ +
+

暂无记录,请粘贴链接开始解析

+
+
+
+
+ + + + + + + + diff --git a/static/index.html b/static/index.html index 9a6f451..c2d9334 100644 --- a/static/index.html +++ b/static/index.html @@ -1,352 +1,53 @@ - + - - -驿来特AI智能数据分析平台 - - - - - + + + 驿来特AI智能大脑系统 + + -
- - -
-
-
-

⚡ 系统特性介绍

-
-
-
-
- 📱 -
-

本系统采用 手机爬虫 获取4家充电供应商准实时各时段电价

-
-
-
- 🧠 -
-

结合 数据仓库与AI技术,对我司电价进行智能分析,给出定价建议

-
-
-
- 📊 -
-

对我司的各场站营业情况进行 分析,查询

-
-
-
- 🎨 -
-

新增 智能海报生成 功能,未来将结合业务数据,一键生成精美的数据战报与营销海报

-
-
-
- 🎥 -
-

新增 抖音知识库:支持视频解析、知识获取与总结、博主专栏订阅,自动生成 充电企业知识日报,助力企业构建专属知识库

-
-
-
- 🎯 -
-

未来:可以根据用户充电信息,形成用户画像,结合企业微信,实现 用户广告的精准推送

-
-
-
- 🧭 -
-

未来:基于 LBS位置服务,智能对比周边竞对场站的价格与配套(快充、休息室等),精准引导用户选择我司优势站点

-
-
- -
-
-
- -
-
⚡ 驿来特AI智能数据分析平台
- - - -
- - -
-
- -
-
- - -
-
-
-
全网供应商24小时电价监控
-
- - - - - - - - - - - - - - - - -
时段{{ op.label }}
{{ row.hour }} - {{ formatCell(cell.price) }} -
- 数据加载中... -
-
-
-
-
- -
-
- 智能决策分析助手 - -
-
-
-
当前分析任务
-
- 请根据爬取的各供应商分时电价等信息,对各司的定价策略, - 与我司(驿来特)的定价策略进行综合对比,分析我司可能存在的潜在问题。 -
-
+
+
+

驿来特AI智能大脑系统

+

集成多源数据采集、智能分析决策、知识图谱构建与自动化营销的一站式AI赋能平台

+
-
-
-
-
+
+ + +
📊
+

分时电价分析

+

实时监控4家主流充电供应商电价,结合AI智能算法分析竞对策略,为您提供精准的定价调整建议。

+
进入系统 →
+
- -
- - -
-
-

⚡ AI正在阅读您的知识库并提炼精华,请稍候...

-
-
- | -
- -
+ + +
🧠
+

智能数据查询

+

基于企业私有数据的AI问答助手。通过自然语言交互,快速查询场站运营数据、生成图表与分析报告。

+
开始查询 →
+
-
-

- 抖音知识库 - - -

-

自动解析视频、提取文案,构建企业充电知识图谱

-
+ + +
🎥
+

抖音知识库

+

全自动解析抖音视频,提取核心文案与知识点。支持博主订阅、每日知识简报生成,构建企业专属知识库。

+
管理知识库 →
+
- - - - -
- - {{ douyinLoading ? '解析处理中...' : '开始解析' }} - -
-
- - -
- -
-
-
-
-
- - {{ record.status }} - - - {{ formatDate(record.create_time) }} - -
-

- {{ record.video_name || '处理中...' }} -

- - 📺 点击观看视频 - -
- - Del - -
- - -
- Error: {{ record.error_msg }} -
- - -
-

视频文案

-

- {{ record.transcript }} -

- - {{ record.expanded ? '收起' : '展开全文' }} - -
- - -
-
- {{ record.showOriginal ? '收起原始链接' : '查看原始链接信息' }} -
-
- {{ record.original_text }} -
-
-
-
-
- -
-
- - -
- -
-
-

手机扫码访问

-
- -
-

驿来特AI智能数据查询

-

基于大语言模型,为您提供实时、精准的业务数据分析

- -
- - - -
- -
- - {{ text }} - -
-
- -
-
-
- 分析结果 - 完成 - 生成中 -
- - 停止生成 - -
- -
-
-

正在分析数据,请稍候...

-
- | -
-
- -
- - - - - - - diff --git a/static/index_old.html b/static/index_old.html new file mode 100644 index 0000000..9a6f451 --- /dev/null +++ b/static/index_old.html @@ -0,0 +1,352 @@ + + + + + +驿来特AI智能数据分析平台 + + + + + + + +
+ + +
+
+
+

⚡ 系统特性介绍

+
+
+
+
+ 📱 +
+

本系统采用 手机爬虫 获取4家充电供应商准实时各时段电价

+
+
+
+ 🧠 +
+

结合 数据仓库与AI技术,对我司电价进行智能分析,给出定价建议

+
+
+
+ 📊 +
+

对我司的各场站营业情况进行 分析,查询

+
+
+
+ 🎨 +
+

新增 智能海报生成 功能,未来将结合业务数据,一键生成精美的数据战报与营销海报

+
+
+
+ 🎥 +
+

新增 抖音知识库:支持视频解析、知识获取与总结、博主专栏订阅,自动生成 充电企业知识日报,助力企业构建专属知识库

+
+
+
+ 🎯 +
+

未来:可以根据用户充电信息,形成用户画像,结合企业微信,实现 用户广告的精准推送

+
+
+
+ 🧭 +
+

未来:基于 LBS位置服务,智能对比周边竞对场站的价格与配套(快充、休息室等),精准引导用户选择我司优势站点

+
+
+ +
+
+
+ +
+
⚡ 驿来特AI智能数据分析平台
+ + + +
+ + +
+
+ +
+
+ + +
+
+
+
全网供应商24小时电价监控
+
+ + + + + + + + + + + + + + + + +
时段{{ op.label }}
{{ row.hour }} + {{ formatCell(cell.price) }} +
+ 数据加载中... +
+
+
+
+
+ +
+
+ 智能决策分析助手 + +
+
+
+
当前分析任务
+
+ 请根据爬取的各供应商分时电价等信息,对各司的定价策略, + 与我司(驿来特)的定价策略进行综合对比,分析我司可能存在的潜在问题。 +
+
+ +
+
+
+
+ + +
+ + +
+
+

⚡ AI正在阅读您的知识库并提炼精华,请稍候...

+
+
+ | +
+ +
+ +
+

+ 抖音知识库 + + +

+

自动解析视频、提取文案,构建企业充电知识图谱

+
+ + + + + +
+ + {{ douyinLoading ? '解析处理中...' : '开始解析' }} + +
+
+ + +
+ +
+
+
+
+
+ + {{ record.status }} + + + {{ formatDate(record.create_time) }} + +
+

+ {{ record.video_name || '处理中...' }} +

+ + 📺 点击观看视频 + +
+ + Del + +
+ + +
+ Error: {{ record.error_msg }} +
+ + +
+

视频文案

+

+ {{ record.transcript }} +

+ + {{ record.expanded ? '收起' : '展开全文' }} + +
+ + +
+
+ {{ record.showOriginal ? '收起原始链接' : '查看原始链接信息' }} +
+
+ {{ record.original_text }} +
+
+
+
+
+ +
+

暂无记录,请粘贴链接开始解析

+
+
+
+ + +
+ +
+
+

手机扫码访问

+
+ +
+

驿来特AI智能数据查询

+

基于大语言模型,为您提供实时、精准的业务数据分析

+ +
+ + + +
+ +
+ + {{ text }} + +
+
+ +
+
+
+ 分析结果 + 完成 + 生成中 +
+ + 停止生成 + +
+ +
+
+

正在分析数据,请稍候...

+
+ | +
+
+ +
+ + + + + + + + + diff --git a/static/js/dashboard.js b/static/js/dashboard.js new file mode 100644 index 0000000..1a99de4 --- /dev/null +++ b/static/js/dashboard.js @@ -0,0 +1,313 @@ +const { createApp, ref, computed, watch, nextTick, onMounted } = Vue; + +createApp({ + setup() { + // ========================================== + // Common State + // ========================================== + const apiBase = ref(window.location.origin || "http://localhost:8000"); + const isMobile = ref(window.innerWidth <= 768); + + // Handle window resize + window.addEventListener('resize', () => { + isMobile.value = window.innerWidth <= 768; + if (chartInstance) chartInstance.resize(); + }); + + // ========================================== + // Dashboard State & Logic + // ========================================== + const operators = ref([ + {label:"新电途",value:"新电途"}, + {label:"特来电",value:"特来电"}, + {label:"驿来特",value:"驿来特"}, + {label:"艾特吉易充",value:"艾特吉易充"} + ]); + // Default select all or specific one? App.js had "驿来特" + const selectedOperator = ref("驿来特"); + + const aiText = ref(""); + const loading = ref(false); + const exporting = ref(false); + const exportingReport = ref(false); + const aiLoading = ref(false); + const aiBoxRef = ref(null); + const priceTableRows = ref([]); + const hourlyPricesByOperator = ref({}); + let chartInstance = null; + const chartType = ref('line'); + + // ECharts Initialization + const initChart = () => { + const dom = document.getElementById("chart"); + if (dom && !chartInstance) { + if (typeof echarts === 'undefined') { + console.error("ECharts not loaded"); + return; + } + chartInstance = echarts.init(dom); + } + }; + + const renderChart = () => { + if (!chartInstance) return; + + const series = operators.value.map(op => { + const seriesData = []; + for (let h = 0; h < 24; h++) { + const list = hourlyPricesByOperator.value[op.value] || []; + seriesData.push(list[h] !== undefined ? list[h] : null); + } + + if (chartType.value === 'bar') { + return { + name: op.label, + type: "bar", + barGap: 0, + emphasis: { focus: 'series' }, + data: seriesData + }; + } else { + return { + name: op.label, + type: "line", + smooth: true, + emphasis: { focus: 'series' }, + data: seriesData + }; + } + }); + + const hours = []; + for (let h = 0; h < 24; h++) { + hours.push(h.toString().padStart(2,"0") + ":00"); + } + + const option = { + backgroundColor: 'transparent', + tooltip: { + trigger: "axis", + backgroundColor: 'rgba(30, 41, 59, 0.9)', + borderColor: '#334155', + textStyle: { color: '#f1f5f9' }, + axisPointer: { type: chartType.value === 'bar' ? 'shadow' : 'line' } + }, + legend: { + data: operators.value.map(o => o.label), + textStyle: { color: "#94a3b8" }, + bottom: 0 + }, + xAxis: { + type: "category", + data: hours, + 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' } } + }, + grid: { left: 50, right: 30, top: 40, bottom: 40 }, + series: series + }; + + chartInstance.setOption(option, true); + }; + + const buildPriceTable = () => { + const rows = []; + for (let h = 0; h < 24; h++) { + const row = {hour: (h.toString().padStart(2,"0") + ":00"), values: []}; + operators.value.forEach(op => { + const series = hourlyPricesByOperator.value[op.value] || []; + const price = series[h] !== undefined ? series[h] : null; + row.values.push({operator: op.value, price}); + }); + rows.push(row); + } + priceTableRows.value = rows; + renderChart(); + }; + + const loadAllOperatorsPrices = async () => { + loading.value = true; + try{ + const res = await axios.get(apiBase.value + "/api/operators/hourly-prices"); + if (res && res.data && Array.isArray(res.data.operators)) { + const dict = {}; + res.data.operators.forEach(item => { + dict[item.operator] = item.series || []; + }); + hourlyPricesByOperator.value = dict; + buildPriceTable(); + } + }catch(e){ + console.error(e); + }finally{ + loading.value = false; + } + }; + + const exportAllPrices = async () => { + try{ + exporting.value = true; + const res = await axios.get(apiBase.value + "/api/export/prices-zip",{responseType:"blob"}); + const blob = new Blob([res.data],{type:"application/zip"}); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "多供应商电价导出.zip"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }catch(e){ + console.error(e); + }finally{ + exporting.value = false; + } + }; + + const exportAiReport = async () => { + if (!aiText.value) { + alert("请先生成AI分析报告"); + return; + } + try { + exportingReport.value = true; + const res = await axios.post(apiBase.value + "/api/export/ai-report-docx", { + content: aiText.value + }, {responseType: "blob"}); + + const blob = new Blob([res.data], {type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "AI分析报告.docx"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + } catch (e) { + console.error(e); + alert("导出失败,请检查是否安装Pandoc"); + } finally { + exportingReport.value = false; + } + }; + + const startAiAnalysis = async () => { + if (aiLoading.value) return; + aiText.value = ""; + aiLoading.value = true; + try{ + const response = await fetch(apiBase.value + "/api/ai/pricing/strategy-summary"); + 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}); + aiText.value += chunk; + await Vue.nextTick(); + if (aiBoxRef.value) { + aiBoxRef.value.scrollTop = aiBoxRef.value.scrollHeight; + } + } + }catch(e){ + console.error(e); + aiText.value += "\n(分析过程出错: " + e.message + ")"; + }finally{ + aiLoading.value = false; + } + }; + + const formatCell = v => { + if (v === null || v === undefined || v === "") return "-"; + if (typeof v === "number") { + if (Number.isNaN(v)) return "-"; + return v.toFixed(2); + } + return v; + }; + + const getPriceColor = (price) => { + if (price === null || price === undefined) return 'inherit'; + if (price > 1.2) return '#ef4444'; // Red-500 + if (price < 0.8) return '#10b981'; // Emerald-500 + return '#f1f5f9'; // Slate-100 + }; + + // 内置简易 Markdown 解析器 + const simpleMarkdown = (text) => { + if (!text) return ''; + let lines = text.split('\n'); + let html = ''; + let inList = false; + + const parseInline = (str) => { + return str + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/`(.*?)`/g, '$1'); + }; + + for (let line of lines) { + let trimmed = line.trim(); + if (!trimmed) continue; + + if (trimmed.startsWith('### ')) { + if (inList) { html += ''; inList = false; } + html += `

${parseInline(trimmed.substring(4))}

`; + } + else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) { + if (!inList) { html += '
    '; inList = true; } + let content = trimmed.replace(/^(- |\d+\. )/, ''); + html += `
  • ${parseInline(content)}
  • `; + } + else { + if (inList) { html += '
'; inList = false; } + html += `

${parseInline(trimmed)}

`; + } + } + if (inList) html += ''; + return html; + }; + + const renderedAiText = computed(() => { + return simpleMarkdown(aiText.value); + }); + + onMounted(() => { + initChart(); + loadAllOperatorsPrices(); + }); + + return { + operators, + loading, + exporting, + exportingReport, + aiLoading, + aiText, + aiBoxRef, + priceTableRows, + hourlyPricesByOperator, + chartType, + + // Actions + loadAllOperatorsPrices, + exportAllPrices, + exportAiReport, + startAiAnalysis, + formatCell, + getPriceColor, + renderedAiText + }; + } +}).mount('#app'); diff --git a/static/js/douyin.js b/static/js/douyin.js new file mode 100644 index 0000000..a031d8d --- /dev/null +++ b/static/js/douyin.js @@ -0,0 +1,202 @@ +const { createApp, ref, computed, onMounted } = Vue; + +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(''); + + // Simple Markdown Parser (Zero Dependency) + 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, '$1') // Bold + .replace(/`(.*?)`/g, '$1'); // Code + }; + + for (let line of lines) { + let trimmed = line.trim(); + if (!trimmed) continue; + + // Headers + if (trimmed.startsWith('### ')) { + if (inList) { html += ''; inList = false; } + html += `

${parseInline(trimmed.substring(4))}

`; + } + // Lists + else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) { + if (!inList) { html += '
    '; inList = true; } + let content = trimmed.replace(/^(- |\d+\. )/, ''); + html += `
  • ${parseInline(content)}
  • `; + } + // Paragraphs + else { + if (inList) { html += '
'; inList = false; } + html += `

${parseInline(trimmed)}

`; + } + } + if (inList) html += ''; + return html; + }; + + const renderedSummary = computed(() => { + if (!summaryText.value) return ''; + try { + return simpleMarkdown(summaryText.value); + } catch (e) { + console.error("Simple markdown error:", e); + return summaryText.value; + } + }); + + // 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"); diff --git a/static/js/query.js b/static/js/query.js new file mode 100644 index 0000000..7e67c9a --- /dev/null +++ b/static/js/query.js @@ -0,0 +1,216 @@ +const { createApp, ref, computed, watch, nextTick, onMounted } = Vue; + +createApp({ + setup() { + const apiBase = ref(window.location.origin || "http://localhost:8000"); + + // Query State + const userQuery = ref(''); + const queryLoading = ref(false); + const queryResult = ref(''); + const eventSource = ref(null); + const rowsAcc = ref([]); + const bufferRow = ref(null); + const examples = [ + "查询12月份充电量TOP 10场站的充电情况", + "查询净月商贸城站12月的充电量", + "查询12月企业充电量排名的TOP 10", + "查询所有场站的近3个月的充电情况,找出变化最大的前10名" + ]; + + // Helpers + const isPlainObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]'; + const colSynonyms = { + name: ['name', 'station', 'station_name', 'site', '场站', '场站名称', '站点', '站名'], + total: ['total', 'total_kwh', 'sum_kwh', 'energy_total', '总电量', '总充电量', '总度', '总度数'], + peak: ['peak', 'peak_kwh', 'energy_peak', '峰', '峰时电量'], + flat: ['flat', 'flat_kwh', 'energy_flat', '平', '平时电量'], + valley: ['valley', 'valley_kwh', 'energy_valley', '谷', '谷时电量'], + }; + const pickKey = (obj, keys) => keys.find(k => Object.prototype.hasOwnProperty.call(obj, k)); + const pickVal = (obj, keys) => { + const k = pickKey(obj, keys); + return k ? obj[k] : undefined; + }; + + const formatNumber = (n) => { + if (n === null || n === undefined || n === '') return ''; + const v = parseFloat(String(n).replace(/,/g, '')); + if (isNaN(v)) return String(n); + return v.toLocaleString('zh-CN', { maximumFractionDigits: 2 }); + }; + + const pushRow = (row) => { + if (!row || !row.name) return; + rowsAcc.value.push(row); + const r = row; + const line = `| ${r.name ?? ''} | ${formatNumber(r.total)} | ${formatNumber(r.peak)} | ${formatNumber(r.flat)} | ${formatNumber(r.valley)} |`; + + if (rowsAcc.value.length === 1) { + const header = `| 场站名称 | 总电量 | 峰 | 平 | 谷 |\n| --- | ---: | ---: | ---: | ---: |`; + if (queryResult.value && !queryResult.value.endsWith('\n')) { + queryResult.value += '\n\n'; + } else if (queryResult.value) { + queryResult.value += '\n'; + } + queryResult.value += header + '\n'; + } + queryResult.value += line + '\n'; + }; + + const handleDegreeSearch = () => { + if (!userQuery.value.trim()) { + if (typeof ElementPlus !== 'undefined') ElementPlus.ElMessage.warning('请输入您的问题'); + else alert('请输入您的问题'); + return; + } + + if (queryLoading.value) { + if (eventSource.value) { + eventSource.value.close(); + } + } + + queryLoading.value = true; + queryResult.value = ''; + rowsAcc.value = []; + bufferRow.value = null; + + const url = `${apiBase.value}/degree/chat?q=${encodeURIComponent(userQuery.value)}`; + + try { + const es = new EventSource(url); + eventSource.value = es; + + es.onmessage = (event) => { + const eventData = event.data.trim(); + if (eventData === '[DONE]') { + queryLoading.value = false; + es.close(); + renderCharts(); + return; + } + + try { + const data = JSON.parse(event.data); + + let chunk = ''; + if (data.content) chunk = data.content; + else if (data.markdown) chunk = data.markdown; + else if (data.chunk) chunk = data.chunk; + else if (typeof data === 'string') chunk = data; + + if (chunk) { + queryResult.value += chunk; + } + } catch (e) { + if (eventData && eventData !== '[DONE]') { + queryResult.value += event.data; + } + } + }; + + es.onerror = (err) => { + console.error('SSE Error:', err); + queryLoading.value = false; + es.close(); + if (!queryResult.value) { + queryResult.value = '查询过程出现异常,未能获取到数据'; + } + }; + } catch (err) { + console.error('Failed to create EventSource:', err); + queryLoading.value = false; + if (typeof ElementPlus !== 'undefined') ElementPlus.ElMessage.error('无法建立连接'); + else alert('无法建立连接'); + } + }; + + const stopDegreeGeneration = () => { + if (eventSource.value) { + eventSource.value.close(); + eventSource.value = null; + } + queryLoading.value = false; + }; + + const setExample = (text) => { + userQuery.value = text; + handleDegreeSearch(); + }; + + const renderCharts = () => { + if (typeof echarts === 'undefined') return; + nextTick(() => { + const containers = document.querySelectorAll('.echarts-container'); + containers.forEach(container => { + if (container.getAttribute('data-rendered') === 'true') return; + try { + const configStr = decodeURIComponent(container.getAttribute('data-config')).trim(); + if (!configStr.startsWith('{') || !configStr.endsWith('}')) return; + const config = JSON.parse(configStr); + let chart = echarts.getInstanceByDom(container); + if (!chart) { + chart = echarts.init(container, 'dark'); + window.addEventListener('resize', () => chart.resize()); + } + chart.setOption(config); + container.setAttribute('data-rendered', 'true'); + container.style.opacity = '1'; + } catch (e) { + if (queryLoading.value) return; + console.error('ECharts rendering error:', e); + container.innerHTML = `

图表渲染失败: ${e.message}

`; + } + }); + }); + }; + + // 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 `
`; + } + return false; + } + } + }); + } + + 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); + } + return cleanText; + } catch (e) { + console.error('Markdown parsing error:', e); + return queryResult.value; + } + }); + + watch(queryResult, () => { + renderCharts(); + }); + + return { + userQuery, + queryLoading, + queryResult, + examples, + handleDegreeSearch, + stopDegreeGeneration, + setExample, + renderedResult + }; + } +}).use(ElementPlus).mount('#app'); diff --git a/static/query.html b/static/query.html new file mode 100644 index 0000000..dc1ade9 --- /dev/null +++ b/static/query.html @@ -0,0 +1,88 @@ + + + + + + 智能数据查询 - 驿来特AI智能大脑 + + + + +
+
+ ⚡ 驿来特AI智能大脑 + ↩ 返回首页 +
+ +
+
+

驿来特AI智能数据查询

+

基于大语言模型,为您提供实时、精准的业务数据分析

+ +
+ + + +
+ +
+ + {{ text }} + +
+
+ +
+
+
+ 分析结果 + 完成 + 生成中 +
+ + 停止生成 + +
+ +
+
+

正在分析数据,请稍候...

+
+ | +
+
+
+ + + + + + + + + +