This commit is contained in:
HuangHai
2026-01-20 09:23:15 +08:00
parent 65ebbf1673
commit 5e21423101
9 changed files with 875 additions and 0 deletions

View File

@@ -0,0 +1,226 @@
import logging
import uuid
import asyncio
from datetime import datetime
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from sqlalchemy.sql import text
from Util.BananaClient import BananaClient
from Util.LlmUtil import get_llm_response
from DbKit.Db import Db
router = APIRouter(prefix="/haibao")
logger = logging.getLogger("HaiBaoController")
class GenerateRequest(BaseModel):
prompt: str
width: int = 1024
height: int = 1024
@router.on_event("startup")
async def startup_event():
"""初始化时检查并创建表"""
db = Db()
await db.init_db()
# Doris 建表语句
create_table_sql = """
CREATE TABLE IF NOT EXISTS haibao_history (
id VARCHAR(50) COMMENT "ID",
prompt TEXT COMMENT "提示词",
image_url VARCHAR(500) COMMENT "图片URL",
scheme_content TEXT COMMENT "文案方案",
created_at DATETIME COMMENT "创建时间"
)
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
);
"""
try:
# 使用 engine 直接执行 DDL
async with db.engine.begin() as conn:
await conn.execute(text(create_table_sql))
# 尝试添加列(如果表已存在但列不存在)
# Doris 不支持 IF NOT EXISTS for ADD COLUMN directly nicely in all versions without error if exists
# 所以这里简单捕获异常,如果列已存在则忽略
try:
alter_sql = "ALTER TABLE haibao_history ADD COLUMN scheme_content TEXT COMMENT '文案方案'"
await conn.execute(text(alter_sql))
except Exception as e:
# 忽略列已存在的错误
pass
logger.info("海报历史表检查/更新成功")
except Exception as e:
logger.error(f"海报历史表创建/更新失败: {e}")
class RefineRequest(BaseModel):
prompt: str
@router.post("/refine")
async def refine_prompt(req: RefineRequest):
"""润色提示词"""
try:
refine_system_prompt = "你是一个资深的AI绘画提示词专家。你的任务是将用户简短的描述扩充为一段详细、高质量的画面描述提示词用于生成宣传海报。"
refine_user_prompt = f"""
请根据以下主题,为充电企业“驿来特”设计一张宣传海报的画面描述。
主题:{req.prompt}
要求:
1. 描述画面主体、背景、光影、色彩、构图。
2. 风格要求现代感、科技感、精美、3D渲染风格或高品质插画风格。
3. 融入新能源、绿色环保、充电桩等元素。
4. 直接输出提示词内容,不要包含“好的”、“以下是”等无关废话。
5. 字数在100-300字之间。
"""
refined_prompt = ""
try:
async for chunk in get_llm_response(query_text=refine_user_prompt, system_prompt=refine_system_prompt, stream=False):
refined_prompt += chunk
except Exception as e:
logger.error(f"提示词润色失败: {e}")
raise Exception("润色服务暂时不可用")
return {"refined_prompt": refined_prompt}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/generate")
async def generate_poster(req: GenerateRequest):
"""生成海报及文案"""
try:
# 并行执行生图和生文
client = BananaClient()
# 1. 构造生图任务 (包含智能润色判断)
async def generate_image_task():
final_prompt = req.prompt
# 智能判断如果提示词太短少于50字则认为用户未进行润色自动执行润色
# 如果用户使用了"一键扩写"功能,提示词通常会很长,这里就会跳过自动润色,尊重用户的修改
if len(final_prompt) < 50:
logger.info(f"提示词较短({len(final_prompt)}字),执行自动润色...")
refine_system_prompt = "你是一个资深的AI绘画提示词专家。你的任务是将用户简短的描述扩充为一段详细、高质量的画面描述提示词用于生成宣传海报。"
refine_user_prompt = f"""
请根据以下主题,为充电企业“驿来特”设计一张宣传海报的画面描述。
主题:{req.prompt}
要求:
1. 描述画面主体、背景、光影、色彩、构图。
2. 风格要求现代感、科技感、精美、3D渲染风格或高品质插画风格。
3. 融入新能源、绿色环保、充电桩等元素。
4. 直接输出提示词内容,不要包含“好的”、“以下是”等无关废话。
5. 字数在100-300字之间。
"""
refined_prompt = ""
try:
async for chunk in get_llm_response(query_text=refine_user_prompt, system_prompt=refine_system_prompt, stream=False):
refined_prompt += chunk
if refined_prompt and refined_prompt.strip():
final_prompt = refined_prompt
except Exception as e:
logger.error(f"自动润色失败,使用原始提示词: {e}")
logger.info(f"Final generation prompt: {final_prompt}")
# 1.2 调用生图
resp = await client.generate_image(prompt=final_prompt, size=f"{req.width}x{req.height}")
obs_urls = await client.download_and_upload_to_obs(resp)
if not obs_urls:
raise Exception("未获取到有效的图片URL")
return obs_urls[0]
# 2. 构造生文任务
async def generate_text_task():
scheme_prompt = f"""
你是一个专业的社群运营专家。请为充电企业“驿来特”撰写一段发在微信群里的宣传文案。
主题:{req.prompt}
要求:
1. 语气亲切、有吸引力,适合微信社群传播。
2. 突出“驿来特”品牌,强调新能源、优惠、便利等特点(根据主题自由发挥)。
3. 包含适当的emoji表情增加趣味性。
4. 字数控制在150字以内。
5. 格式清晰,分段合理。
"""
# get_llm_response 是一个异步生成器 (stream=True by default) 或者直接返回 (stream=False)
# 这里我们强制 stream=False 获取完整文本
text_response = ""
# LlmUtil.get_llm_response 默认为 stream=True我们需要修改调用方式或适配
# 查看 LlmUtil 源码,如果 stream=False它 yield 内容。
# 所以我们需要迭代它
async for chunk in get_llm_response(query_text=scheme_prompt, stream=False):
text_response += chunk
return text_response
# 3. 并行执行
image_url, scheme_content = await asyncio.gather(generate_image_task(), generate_text_task())
# 4. 保存到数据库
db = Db()
record_id = str(uuid.uuid4())
created_at = datetime.now()
insert_sql = """
INSERT INTO haibao_history (id, prompt, image_url, scheme_content, created_at)
VALUES (:id, :prompt, :image_url, :scheme_content, :created_at)
"""
params = {
"id": record_id,
"prompt": req.prompt,
"image_url": image_url,
"scheme_content": scheme_content,
"created_at": created_at
}
async with db.get_session() as session:
async with session.begin():
await session.execute(text(insert_sql), params)
return {
"id": record_id,
"image_url": image_url,
"prompt": req.prompt,
"scheme_content": scheme_content,
"created_at": created_at.strftime("%Y-%m-%d %H:%M:%S")
}
except Exception as e:
logger.error(f"生成海报/文案失败: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/history")
async def get_history():
"""获取海报生成历史"""
db = Db()
sql = "SELECT * FROM haibao_history ORDER BY created_at DESC LIMIT 50"
try:
result = await db.find(sql)
formatted_result = []
for item in result:
item_dict = dict(item) if not isinstance(item, dict) else item
if isinstance(item_dict.get('created_at'), datetime):
item_dict['created_at'] = item_dict['created_at'].strftime("%Y-%m-%d %H:%M:%S")
# 确保 scheme_content 存在
if 'scheme_content' not in item_dict:
item_dict['scheme_content'] = ""
formatted_result.append(item_dict)
return formatted_result
except Exception as e:
logger.error(f"获取历史失败: {e}")
return []

View File

@@ -37,6 +37,7 @@ logger.info("驿来特AI智能分析系统模块导入完成")
from Controller.YltAnalyticsController import router as ylt_router, init_db, close_db
from Controller.DegreeController import router as degree_router
from Controller.HaiBaoController import router as haibao_router
from Util.Win32Patch import patch
from Util.RedisKit import RedisKit
@@ -63,6 +64,7 @@ app.mount("/static", StaticFiles(directory=static_dir), name="static")
app.include_router(ylt_router)
app.include_router(degree_router)
app.include_router(haibao_router)
if __name__ == "__main__":

Binary file not shown.

299
static/HaiBao/css/app.css Normal file
View File

@@ -0,0 +1,299 @@
:root {
--bg-color: #0f172a; /* Slate 900 */
--card-bg: #1e293b; /* Slate 800 */
--card-border: #334155; /* Slate 700 */
--text-primary: #f1f5f9; /* Slate 100 */
--text-secondary: #94a3b8; /* Slate 400 */
--accent-color: #3b82f6; /* Blue 500 */
--accent-hover: #2563eb; /* Blue 600 */
--primary-color: #3b82f6; /* Match accent color */
--border-radius: 12px;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
body {
margin: 0;
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
background-image: radial-gradient(circle at 50% 0%, #1e293b 0%, #0f172a 100%);
min-height: 100vh;
}
.app-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* Header */
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30px;
padding: 20px 30px;
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
}
.header h1 {
margin: 0;
font-size: 24px;
display: flex;
align-items: center;
gap: 12px;
background: linear-gradient(to right, #60a5fa, #a78bfa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header-subtitle {
color: var(--text-secondary);
font-size: 14px;
margin-top: 4px;
}
/* Main Layout */
.main-layout {
display: grid;
grid-template-columns: 400px 1fr;
gap: 24px;
margin-bottom: 30px;
}
/* Input Section */
.input-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
padding: 24px;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
gap: 20px;
height: fit-content;
}
.preset-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.preset-tag {
cursor: pointer;
transition: all 0.2s;
background-color: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
color: #93c5fd;
}
.preset-tag:hover {
transform: translateY(-2px);
background-color: rgba(59, 130, 246, 0.2);
color: white;
}
/* Overriding Element Plus Input styles for dark mode */
.el-textarea__inner {
background-color: #0f172a !important;
border-color: var(--card-border) !important;
color: var(--text-primary) !important;
box-shadow: none !important;
}
.el-textarea__inner:focus {
border-color: var(--accent-color) !important;
}
.generate-btn {
width: 100%;
height: 48px;
font-size: 16px;
margin-top: 10px;
background: linear-gradient(135deg, var(--accent-color), var(--accent-hover));
border: none;
font-weight: 600;
}
.generate-btn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
/* Preview Section */
.preview-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
padding: 24px;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 600px;
position: relative;
}
.preview-image {
max-width: 100%;
max-height: 600px;
border-radius: 8px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
transition: transform 0.3s;
}
.empty-state {
text-align: center;
color: var(--text-secondary);
}
.empty-icon {
font-size: 64px;
margin-bottom: 20px;
display: block;
opacity: 0.3;
}
/* History Section */
.history-section {
background: var(--card-bg);
border: 1px solid var(--card-border);
padding: 24px;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
}
.section-title {
margin-top: 0;
margin-bottom: 24px;
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
border-left: 4px solid var(--accent-color);
padding-left: 12px;
}
.history-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 20px;
}
.history-item {
position: relative;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
aspect-ratio: 1;
border: 1px solid var(--card-border);
transition: transform 0.2s, box-shadow 0.2s;
}
.history-img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s;
}
.history-item:hover {
transform: translateY(-4px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
border-color: var(--accent-color);
}
.history-item:hover .history-img {
transform: scale(1.1);
}
.history-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(to top, rgba(15, 23, 42, 0.95), rgba(15, 23, 42, 0.7));
color: var(--text-primary);
padding: 12px;
transform: translateY(100%);
transition: transform 0.3s;
font-size: 12px;
backdrop-filter: blur(4px);
}
.history-item:hover .history-overlay {
transform: translateY(0);
}
.history-prompt {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 6px;
color: #e2e8f0;
}
.history-time {
color: #94a3b8;
font-size: 10px;
}
/* Loading Animation */
.generating-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(4px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
border-radius: var(--border-radius);
color: var(--text-primary);
}
.loader {
width: 48px;
height: 48px;
border: 4px solid rgba(59, 130, 246, 0.3);
border-bottom-color: var(--accent-color);
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
margin-bottom: 20px;
}
@keyframes rotation {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Overriding Element Plus generic styles */
.el-button--plain {
background-color: transparent !important;
border-color: var(--card-border) !important;
color: var(--text-secondary) !important;
}
.el-button--plain:hover {
border-color: var(--accent-color) !important;
color: var(--accent-color) !important;
background-color: rgba(59, 130, 246, 0.1) !important;
}
.el-tag--plain {
background-color: rgba(30, 41, 59, 0.5) !important;
border-color: var(--card-border) !important;
color: var(--text-secondary) !important;
}

View File

@@ -0,0 +1,55 @@
/* Scheme Box Styles */
.scheme-box {
flex: 1;
background: #0f172a; /* Slate 900 */
border: 1px solid var(--card-border);
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
height: 100%;
min-height: 300px;
position: relative;
}
.scheme-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--card-border);
font-weight: 600;
color: var(--text-primary);
font-size: 16px;
}
.scheme-content {
flex: 1;
white-space: pre-wrap;
line-height: 1.8;
color: var(--text-secondary);
font-size: 15px;
overflow-y: auto;
max-height: 500px;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
/* Scrollbar Styling for Scheme Content */
.scheme-content::-webkit-scrollbar {
width: 6px;
}
.scheme-content::-webkit-scrollbar-track {
background: transparent;
}
.scheme-content::-webkit-scrollbar-thumb {
background-color: var(--card-border);
border-radius: 3px;
}
.scheme-content::-webkit-scrollbar-thumb:hover {
background-color: var(--text-secondary);
}

160
static/HaiBao/index.html Normal file
View File

@@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>驿来特 - 智能海报生成工作台</title>
<link rel="stylesheet" href="/static/css/element-plus.index.css">
<link rel="stylesheet" href="css/app.css">
<link rel="stylesheet" href="css/scheme.css">
</head>
<body>
<div id="app" class="app-container">
<!-- Header -->
<header class="header">
<div>
<h1>🎨 驿来特智能海报生成工作台</h1>
<div class="header-subtitle">基于 AI 大模型,快速生成高质量企业宣传海报</div>
</div>
<div>
<a href="/static/index.html">
<el-button plain>返回首页</el-button>
</a>
</div>
</header>
<!-- Main Content -->
<div class="main-layout">
<!-- Left: Input & Controls -->
<div class="input-card">
<div class="section-title">创意工坊</div>
<div>
<div style="margin-bottom: 10px; font-weight: bold;">预设主题</div>
<div class="preset-tags">
<el-tag
v-for="tag in presets"
:key="tag"
class="preset-tag"
effect="plain"
@click="usePreset(tag)"
>
{{ tag }}
</el-tag>
</div>
</div>
<div v-loading="refining" element-loading-text="AI 正在为您扩写创意..." element-loading-background="rgba(15, 23, 42, 0.8)">
<div style="margin-bottom: 10px; font-weight: bold;">提示词 (Prompt)</div>
<el-input
v-model="prompt"
:rows="6"
type="textarea"
placeholder="请输入关键词或简短描述AI 将自动为您扩充细节。例如:清明节、未来科技感充电站..."
resize="none"
:disabled="refining"
></el-input>
</div>
<div style="display: flex; gap: 10px; margin-top: 10px;">
<el-button
type="success"
class="generate-btn"
plain
@click="handleRefine"
:loading="refining"
style="flex: 1;"
>
{{ refining ? 'AI 思考中...' : '✨ AI 创意扩写' }}
</el-button>
<el-button
type="primary"
class="generate-btn"
@click="handleGenerate"
:loading="generating"
style="flex: 1;"
>
{{ generating ? '绘制中...' : '立即生成海报' }}
</el-button>
</div>
<div style="margin-top: 10px; color: #909399; font-size: 12px;">
* 提示点击“AI 创意扩写”可自动丰富细节,点击“立即生成海报”直接出图(如内容较短系统会自动优化)。
</div>
</div>
<!-- Right: Preview -->
<div class="preview-card">
<div v-if="generating" class="generating-overlay">
<span class="loader"></span>
<p>AI 正在构思画面并绘制,请稍候...</p>
</div>
<div v-if="currentImage" style="width: 100%; display: flex; flex-direction: column; align-items: center;">
<div style="display: flex; gap: 20px; width: 100%; align-items: flex-start;">
<!-- Image Preview -->
<div style="flex: 1; display: flex; flex-direction: column; align-items: center;">
<img :src="currentImage.image_url" class="preview-image" alt="Generated Poster">
<div style="margin-top: 15px;">
<a :href="currentImage.image_url" target="_blank">
<el-button type="success" plain size="small">查看原图</el-button>
</a>
</div>
</div>
<!-- Scheme Text Preview -->
<div class="scheme-box" v-if="currentImage.scheme_content">
<div class="scheme-header">
<span>📋 朋友圈文案</span>
<el-button type="primary" link size="small" @click="copyScheme">复制文案</el-button>
</div>
<div class="scheme-content">
{{ currentImage.scheme_content }}
</div>
</div>
</div>
<div style="margin-top: 15px; text-align: center; width: 100%;">
<p style="font-weight: bold; margin-bottom: 5px;">{{ currentImage.prompt }}</p>
<p style="color: #909399; font-size: 12px;">{{ currentImage.created_at }}</p>
</div>
</div>
<div v-else-if="!generating" class="empty-state">
<span class="empty-icon">🖼️</span>
<p>在左侧输入描述,开始创作第一张海报吧</p>
</div>
</div>
</div>
<!-- History Gallery -->
<div class="history-section">
<h3 class="section-title">📜 创作历史</h3>
<div class="history-grid" v-if="historyList.length > 0">
<div
class="history-item"
v-for="item in historyList"
:key="item.id"
@click="viewHistory(item)"
>
<img :src="item.image_url" loading="lazy" class="history-img">
<div class="history-overlay">
<div class="history-prompt">{{ item.prompt }}</div>
<div class="history-time">{{ item.created_at }}</div>
</div>
</div>
</div>
<div v-else style="text-align: center; color: #909399; padding: 20px;">
暂无历史记录
</div>
</div>
</div>
<!-- Scripts -->
<script src="/static/js/vue.global.js"></script>
<script src="/static/js/element-plus.index.full.js"></script>
<script src="/static/js/element-plus.zh-cn.min.js"></script>
<script src="/static/js/axios.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>

120
static/HaiBao/js/app.js Normal file
View File

@@ -0,0 +1,120 @@
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;
try {
const res = await axios.post('/haibao/refine', {
prompt: prompt.value
});
if (res.data.refined_prompt) {
prompt.value = res.data.refined_prompt;
ElementPlus.ElMessage.success('创意扩写完成');
}
} catch (e) {
console.error('Refine failed', e);
ElementPlus.ElMessage.error('扩写失败: ' + (e.response?.data?.detail || 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

@@ -40,12 +40,24 @@
</div>
<p>对我司的各场站营业情况进行 <strong>分析,查询</strong></p>
</div>
<div class="ad-item">
<div class="ad-icon-wrapper">
<span class="ad-icon">🎨</span>
</div>
<p>新增 <strong>智能海报生成</strong> 功能,未来将结合业务数据,一键生成精美的数据战报与营销海报</p>
</div>
<div class="ad-item">
<div class="ad-icon-wrapper">
<span class="ad-icon">🎯</span>
</div>
<p>未来:可以根据用户充电信息,形成用户画像,结合企业微信,实现 <strong>用户广告的精准推送</strong></p>
</div>
<div class="ad-item">
<div class="ad-icon-wrapper">
<span class="ad-icon">🧭</span>
</div>
<p>未来:基于 <strong>LBS位置服务</strong>,智能对比周边竞对场站的价格与配套(快充、休息室等),精准引导用户选择我司优势站点</p>
</div>
</div>
<div class="ad-footer">
<p class="auto-close-text">{{ adCountdown }} 秒后自动关闭</p>
@@ -60,6 +72,7 @@
<div class="nav-tabs">
<button class="nav-tab" :class="{active: activeTab==='dashboard'}" @click="activeTab='dashboard'">分时电价分析</button>
<button class="nav-tab" :class="{active: activeTab==='degree'}" @click="activeTab='degree'">智能数据查询</button>
<a href="HaiBao/index.html" class="nav-tab" style="text-decoration: none; display: inline-block;">智能海报生成</a>
</div>
<div class="controls" v-if="activeTab==='dashboard'">