'commit'
This commit is contained in:
226
Controller/HaiBaoController.py
Normal file
226
Controller/HaiBaoController.py
Normal 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 []
|
||||
BIN
Controller/__pycache__/HaiBaoController.cpython-310.pyc
Normal file
BIN
Controller/__pycache__/HaiBaoController.cpython-310.pyc
Normal file
Binary file not shown.
2
Start.py
2
Start.py
@@ -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__":
|
||||
|
||||
BIN
Util/__pycache__/BananaClient.cpython-310.pyc
Normal file
BIN
Util/__pycache__/BananaClient.cpython-310.pyc
Normal file
Binary file not shown.
299
static/HaiBao/css/app.css
Normal file
299
static/HaiBao/css/app.css
Normal 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;
|
||||
}
|
||||
55
static/HaiBao/css/scheme.css
Normal file
55
static/HaiBao/css/scheme.css
Normal 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
160
static/HaiBao/index.html
Normal 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
120
static/HaiBao/js/app.js
Normal 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');
|
||||
@@ -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'">
|
||||
|
||||
Reference in New Issue
Block a user