'commit'
This commit is contained in:
@@ -13,6 +13,9 @@ from Tools.T6_Export import export_excel, DorisExcelExporter, extract_hourly_pri
|
|||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
import zipfile
|
import zipfile
|
||||||
|
import subprocess
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from starlette.background import BackgroundTask
|
||||||
from Model.YltAnalyticsModel import (
|
from Model.YltAnalyticsModel import (
|
||||||
StationBase,
|
StationBase,
|
||||||
CompetitorStation,
|
CompetitorStation,
|
||||||
@@ -95,6 +98,55 @@ async def export_prices_zip():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AiReportRequest(BaseModel):
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/export/ai-report-docx")
|
||||||
|
async def export_ai_report_docx(req: AiReportRequest):
|
||||||
|
content = req.content
|
||||||
|
if not content:
|
||||||
|
raise HTTPException(status_code=400, detail="Content is empty")
|
||||||
|
|
||||||
|
# Create temp markdown file
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False, encoding="utf-8") as tmp_md:
|
||||||
|
tmp_md.write(content)
|
||||||
|
tmp_md_path = tmp_md.name
|
||||||
|
|
||||||
|
output_docx_path = tmp_md_path.replace(".md", ".docx")
|
||||||
|
|
||||||
|
# Check template
|
||||||
|
template_path = "static/template/templates.docx"
|
||||||
|
cmd = ['pandoc', '-s', tmp_md_path, '-o', output_docx_path, '--resource-path=static']
|
||||||
|
|
||||||
|
# Only add reference doc if it exists, but the user requested it specifically.
|
||||||
|
# We'll check if it exists, if not, we might fail or warn, but let's try to include it if possible.
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
cmd.extend(['--reference-doc', template_path])
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
# Clean up
|
||||||
|
if os.path.exists(tmp_md_path):
|
||||||
|
os.remove(tmp_md_path)
|
||||||
|
raise HTTPException(status_code=500, detail=f"Pandoc conversion failed: {str(e)}")
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
if os.path.exists(tmp_md_path):
|
||||||
|
os.remove(tmp_md_path)
|
||||||
|
if os.path.exists(output_docx_path):
|
||||||
|
os.remove(output_docx_path)
|
||||||
|
|
||||||
|
return FileResponse(
|
||||||
|
output_docx_path,
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
filename="AI分析报告.docx",
|
||||||
|
background=BackgroundTask(cleanup)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/ai/pricing/strategy-summary")
|
@router.get("/api/ai/pricing/strategy-summary")
|
||||||
async def ai_pricing_strategy_summary():
|
async def ai_pricing_strategy_summary():
|
||||||
async def generate_stream():
|
async def generate_stream():
|
||||||
|
|||||||
Binary file not shown.
@@ -278,6 +278,42 @@ body {
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Markdown Table Styling in AI Result */
|
||||||
|
.ai-result table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 16px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
background-color: var(--card-bg); /* Use card bg to distinguish from ai-box bg */
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden; /* For rounded corners on table */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-result th,
|
||||||
|
.ai-result td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--card-border); /* Explicit border color */
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-result th {
|
||||||
|
background-color: rgba(15, 23, 42, 0.8);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-result tr:nth-child(even) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.02); /* Slight zebra striping */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-result tr:hover td {
|
||||||
|
background-color: var(--table-row-hover);
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom Scrollbar Webkit */
|
/* Custom Scrollbar Webkit */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
|||||||
@@ -15,9 +15,13 @@
|
|||||||
<div class="dashboard-title">⚡ 充电站电价分析驾驶舱</div>
|
<div class="dashboard-title">⚡ 充电站电价分析驾驶舱</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button class="btn-primary" @click="exportAllPrices" :disabled="exporting">
|
<button class="btn-primary" @click="exportAllPrices" :disabled="exporting">
|
||||||
<span v-if="!exporting">📊 一键导出分析报告</span>
|
<span v-if="!exporting">📊 导出分时段电价表</span>
|
||||||
<span v-else>⏳ 导出中...</span>
|
<span v-else>⏳ 导出中...</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn-primary" @click="exportAiReport" :disabled="exportingReport || !aiText" :title="!aiText ? '请先生成AI分析报告' : ''">
|
||||||
|
<span v-if="!exportingReport">📑 导出分析报告</span>
|
||||||
|
<span v-else>⏳ 生成中...</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const aiText = ref("")
|
|||||||
const placeholder = ref("点击左侧按钮查询四家供应商最新24小时分时电价,再使用右侧AI综合分析功能对各司定价策略进行对比。")
|
const placeholder = ref("点击左侧按钮查询四家供应商最新24小时分时电价,再使用右侧AI综合分析功能对各司定价策略进行对比。")
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const exporting = ref(false)
|
const exporting = ref(false)
|
||||||
|
const exportingReport = ref(false)
|
||||||
const aiLoading = ref(false)
|
const aiLoading = ref(false)
|
||||||
const aiBoxRef = ref(null)
|
const aiBoxRef = ref(null)
|
||||||
const priceTableRows = ref([])
|
const priceTableRows = ref([])
|
||||||
@@ -122,6 +123,35 @@ 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 () => {
|
const startAiAnalysis = async () => {
|
||||||
if (aiLoading.value) return
|
if (aiLoading.value) return
|
||||||
aiText.value = ""
|
aiText.value = ""
|
||||||
@@ -210,6 +240,6 @@ const getPriceColor = (price) => {
|
|||||||
return '#f1f5f9' // Slate-100
|
return '#f1f5f9' // Slate-100
|
||||||
}
|
}
|
||||||
|
|
||||||
return {apiBase,operators,aiText,placeholder,loading,exporting,aiLoading,priceTableRows,loadAllOperatorsPrices,exportAllPrices,startAiAnalysis,formatCell,getPriceColor,renderedAiText,aiBoxRef}
|
return {apiBase,operators,aiText,placeholder,loading,exporting,exportingReport,aiLoading,priceTableRows,loadAllOperatorsPrices,exportAllPrices,exportAiReport,startAiAnalysis,formatCell,getPriceColor,renderedAiText,aiBoxRef}
|
||||||
}
|
}
|
||||||
}).mount("#app")
|
}).mount("#app")
|
||||||
|
|||||||
BIN
static/templates/templates.docx
Normal file
BIN
static/templates/templates.docx
Normal file
Binary file not shown.
Reference in New Issue
Block a user