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

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');