目录
[4.1 后端开发:简洁可靠的 API 服务与 Coze 对接](#4.1 后端开发:简洁可靠的 API 服务与 Coze 对接)
[4.1.1 环境初始化与配置管理](#4.1.1 环境初始化与配置管理)
[4.1.2 前端页面托管](#4.1.2 前端页面托管)
[4.1.3 核心 API 接口:/generate_images](#4.1.3 核心 API 接口:/generate_images)
[4.1.4 服务启动](#4.1.4 服务启动)
[4.2 前端开发:全流程交互与体验优化](#4.2 前端开发:全流程交互与体验优化)
[4.2.1 界面设计:美观与实用兼顾](#4.2.1 界面设计:美观与实用兼顾)
[4.2.2 核心交互逻辑:全流程状态管理](#4.2.2 核心交互逻辑:全流程状态管理)
[4.2.3 细节体验优化:让应用更 "贴心"](#4.2.3 细节体验优化:让应用更 “贴心”)
[4.3 配置与部署:简单可移植](#4.3 配置与部署:简单可移植)
[步骤 1:创建.env 文件,配置敏感信息](#步骤 1:创建.env 文件,配置敏感信息)
[步骤 2:安装依赖包](#步骤 2:安装依赖包)
[步骤 3:启动服务](#步骤 3:启动服务)
[1. 轻量化技术栈的价值](#1. 轻量化技术栈的价值)
[2. 前后端协同的核心原则](#2. 前后端协同的核心原则)
[3. 细节体验决定应用的质感](#3. 细节体验决定应用的质感)
[4. 配置与代码解耦的最佳实践](#4. 配置与代码解耦的最佳实践)
[5. AI 应用开发的新趋势](#5. AI 应用开发的新趋势)
在 AI 应用开发的轻量化时代,借助低代码平台与 Python Web 框架的组合,我们能快速将「自然语言生成专属图片」的需求落地为可直接使用的 Web 应用。本文将以历史主题图片生成器为例,从技术选型、核心逻辑开发、前端交互设计到完整部署,拆解如何基于 Coze(扣子)工作流、Flask 框架快速搭建一款功能完整、体验流畅的 AI 图片生成 Web 应用,最终实现「输入历史问题,一键生成相关可视化图片」的核心需求。
一、项目背景与核心需求
作为历史学习的辅助工具,我们希望打造一款轻量化 Web 应用,让用户通过自然语言输入历史相关问题(如 "古罗马的建筑特色""唐代服饰特点"),即可快速获取对应的主题图片,同时支持历史查询记录、图片大图预览、响应式适配等实用功能,兼顾功能完整性 与用户体验。
基于需求,我们确定了核心技术目标:
- 后端实现与 Coze 工作流的对接,完成图片生成接口开发,支持跨域、异常处理;
- 前端实现简洁美观的交互界面,包含输入、加载、结果展示、错误提示等全流程状态;
- 实现本地历史记录缓存、图片懒加载、大图预览等细节优化;
- 保证应用的可移植性,通过环境变量管理敏感配置(如 API 令牌)。
二、技术栈选型:轻量化组合,高效落地
本次项目采用前后端一体化开发模式(Flask 托管前端页面),核心技术栈围绕「轻量化、易上手、生态完善」原则选型,无需复杂的分布式架构,单人即可快速开发部署,适合 AI 小应用的快速落地:
后端技术
- Flask:轻量级 Python Web 框架,核心代码简洁,扩展灵活,适合快速搭建 API 服务,同时支持直接托管静态 HTML 页面,实现前后端一体化部署;
- Coze Python SDK:官方提供的 Python 开发工具包,快速对接 Coze 工作流,无需手动处理 HTTP 请求,简化 AI 能力调用;
- python-dotenv:管理环境变量,将 API 令牌、工作流 ID 等敏感配置从代码中解耦,避免硬编码泄露;
- flask-cors:解决前端跨域请求问题,适配本地开发与线上部署的跨域场景;
- jsonify/send_file:Flask 内置工具,分别实现 JSON 格式响应、静态 HTML 页面托管。
前端技术
- 原生 HTML/CSS/JavaScript:无前端框架依赖,降低开发与部署成本,适合轻量应用;
- CSS3:实现渐变背景、卡片阴影、响应式布局、动画效果(加载动画、hover 交互),提升界面美观度;
- 原生 DOM 操作:实现输入、提交、结果渲染、模态框等交互逻辑,无需引入额外框架;
- localStorage:本地缓存查询历史记录,无需后端数据库,简化数据存储;
- 图片懒加载(loading="lazy"):优化页面加载性能,避免多图片同时加载导致的卡顿。
运行环境
- Python 3.8+
- 主流浏览器(Chrome/Firefox/Edge/ Safari)
- Coze 平台账号(获取 API 令牌与工作流 ID)
三、核心架构设计:前后端协同逻辑
本项目采用极简的前后端架构,无中间件、无数据库,核心分为「前端交互层」与「后端服务层」,两层通过 HTTP 接口实现数据交互,整体流程清晰易懂,以下是核心架构与执行流程:
整体架构
历史主题图片生成器
├─ 后端服务层(Flask):托管前端页面 + 提供图片生成API + 对接Coze工作流
│ ├─ 静态资源托管:通过send_file返回index.html前端页面
│ ├─ API接口:/generate_images(POST),处理前端请求,返回图片URL
│ ├─ Coze对接:通过SDK初始化客户端,执行工作流,获取图片结果
│ ├─ 配置管理:通过dotenv从.env文件读取环境变量
│ └─ 异常处理:捕获接口请求、Coze调用、数据解析中的异常,返回友好错误信息
└─ 前端交互层(原生HTML/CSS/JS):用户交互 + 结果展示 + 本地缓存
├─ 输入层:文本输入框 + 提交按钮,支持回车提交
├─ 状态层:加载中、错误提示、无内容提示等全流程状态展示
├─ 结果层:图片卡片布局,支持懒加载、加载失败兜底
├─ 交互层:历史记录点击复用、图片大图预览、模态框关闭
└─ 缓存层:通过localStorage实现历史记录的增删、缓存与渲染
核心执行流程
- 用户在前端输入历史问题,点击提交 / 按回车,前端做非空校验后,显示加载状态,禁用提交按钮;
- 前端通过 fetch 发送 POST 请求到后端
/generate_images接口,携带用户输入的文本参数; - 后端接口接收请求,先校验输入与环境配置(API 令牌、工作流 ID),初始化 Coze 客户端;
- 后端调用 Coze 工作流,传入用户输入参数,执行图片生成逻辑,获取返回结果;
- 后端解析返回结果,提取图片 URL,取前 2 张返回给前端,同时捕获所有异常并返回错误信息;
- 前端接收后端响应,关闭加载状态,启用提交按钮:
- 若返回错误,显示错误提示(5 秒后自动隐藏);
- 若返回图片 URL,渲染图片卡片,实现懒加载与加载失败兜底;
- 前端将用户输入添加到本地历史记录(去重、保留最近 10 条),并渲染到页面;
- 用户可点击图片查看大图,或点击历史记录快速复用查询条件。
四、核心功能开发:关键代码解析与设计思路
接下来我们拆解项目的核心开发环节,针对后端 Coze 对接 、前端交互优化 、细节体验打磨三个核心部分,分析关键代码的设计思路与实现细节,同时解读其中的技术要点与最佳实践。
4.1 后端开发:简洁可靠的 API 服务与 Coze 对接
后端核心代码仅百行左右,却实现了配置管理、跨域支持、接口开发、AI 对接、异常处理全功能,遵循「极简设计」原则,同时保证代码的健壮性与可维护性。
4.1.1 环境初始化与配置管理
python
import os
from flask import Flask, request, jsonify, send_file
from flask_cors import CORS
from dotenv import load_dotenv
from cozepy import Coze, TokenAuth, COZE_CN_BASE_URL
import json
# 加载.env文件中的环境变量
load_dotenv()
# 初始化Flask应用
app = Flask(__name__)
# 允许所有跨域请求,简化开发
CORS(app)
设计思路:
- 使用
python-dotenv的load_dotenv()加载.env文件,将COZE_API_TOKEN(Coze API 令牌) 、WORKFLOW_ID(图片生成工作流 ID) 配置在环境变量中,代码与配置解耦,便于不同环境(开发 / 线上)切换,同时避免敏感信息硬编码; - 初始化 Flask 应用后,直接调用
CORS(app)开启全局跨域支持,无需单独为接口配置,适合轻量应用的快速开发。
4.1.2 前端页面托管
python
@app.route('/')
def index():
return send_file('index.html')
设计思路:
- 将前端核心页面
index.html与后端代码放在同一目录,通过 Flask 的send_file直接托管,实现前后端一体化部署,部署时仅需启动 Flask 服务,无需单独配置 Nginx 等静态资源服务器,简化部署流程。
4.1.3 核心 API 接口:/generate_images
这是后端的核心代码,实现了请求处理、配置校验、Coze 对接、结果解析、异常捕获全逻辑,是前后端与 Coze 平台的桥梁:
python
@app.route('/generate_images', methods=['POST'])
def generate_images():
try:
# 1. 获取并校验前端传入的参数
user_input = request.json.get('input', '')
if not user_input:
return jsonify({'error': '输入不能为空'}), 400
# 2. 从环境变量获取配置并校验
api_token = os.environ.get("COZE_API_TOKEN")
workflow_id = os.environ.get("WORKFLOW_ID")
if not api_token or not workflow_id:
return jsonify({'error': 'API令牌或工作流ID未配置'}), 500
# 3. 初始化Coze客户端(国内节点)
coze = Coze(
auth=TokenAuth(token=api_token),
base_url=COZE_CN_BASE_URL
)
# 4. 调用Coze工作流,执行图片生成
workflow = coze.workflows.runs.create(
workflow_id=workflow_id,
parameters={"input": user_input}
)
# 5. 解析返回结果,提取图片URL
if hasattr(workflow, 'data') and workflow.data:
data = json.loads(workflow.data)
data_content = data['output']
# 取前2张图片返回,控制结果数量
return jsonify({'images': data_content[:2]})
# 无结果时的兜底
return jsonify({'error': '未生成图片,请重试'}), 500
# 全局异常捕获,避免服务崩溃,返回友好错误信息
except Exception as e:
return jsonify({'error': str(e)}), 500
关键设计要点:
- 参数校验:先校验前端输入是否为空,再校验环境配置是否完整,提前拦截无效请求,减少后续资源消耗;
- Coze 客户端初始化 :指定
COZE_CN_BASE_URL国内节点,保证接口调用的稳定性与速度;使用TokenAuth令牌认证,符合 Coze 平台的安全规范; - 工作流调用 :通过 Coze SDK 的
workflows.runs.create方法快速执行工作流,无需手动构造请求体与处理响应,简化开发; - 结果解析 :针对 Coze 工作流的返回结构,解析
workflow.data获取图片 URL 列表,取前 2 张返回,避免过多图片导致前端加载卡顿; - 异常处理 :使用
try-except捕获所有可能的异常(如网络错误、Coze 接口报错、数据解析错误等),将异常信息转为字符串返回给前端,同时返回 500 状态码,便于前端排查问题; - 状态码规范:400(客户端参数错误)、500(服务端错误),遵循 HTTP 状态码的设计规范,让前端可根据状态码做不同的处理。
4.1.4 服务启动
python
if __name__ == '__main__':
app.run(debug=True, port=5000)
设计思路:
- 开启
debug=True,开发阶段代码修改后自动重启服务,提升开发效率(线上部署时需改为False); - 指定端口
5000,避免与其他服务端口冲突,便于本地调试。
4.2 前端开发:全流程交互与体验优化
前端采用原生技术开发,核心围绕「用户体验 」与「功能完整性」展开,实现了从输入到结果展示的全流程状态管理,同时加入了诸多细节优化,让轻量应用也有出色的交互体验。
4.2.1 界面设计:美观与实用兼顾
前端界面采用分层设计,分为头部标题区、输入区、状态提示区、结果展示区、历史记录区、页脚区,结构清晰,用户可快速找到核心操作区域:
- 视觉设计:使用渐变色背景、卡片阴影、圆角、边框底纹等 CSS3 特性,打造现代简约的视觉风格,避免单调;
- 响应式布局 :通过
flex+grid布局结合媒体查询,适配电脑、平板、手机等不同设备,手机端自动将输入框与按钮改为垂直布局,图片卡片改为单列展示; - 状态可视化:为加载、错误、无内容、图片加载中等状态设计专属的视觉展示,让用户清晰知道当前操作的结果;
- 交互反馈 :按钮、图片卡片添加
hover动画(上浮、阴影加深),点击按钮时有按压效果,图片加载时有占位符,提升交互的趣味性与反馈感。
4.2.2 核心交互逻辑:全流程状态管理
前端的核心 JavaScript 代码实现了生成图片 、展示图片 、错误提示 、历史记录四大核心功能,同时做了完善的状态管理,避免用户重复提交、操作混乱:
- 生成图片函数(generateImages):做输入非空校验→添加历史记录→显示加载状态→禁用提交按钮→发送 fetch 请求→处理后端响应→关闭加载状态→启用提交按钮,形成完整的操作闭环;
- 图片展示(displayImages):实现图片懒加载→加载中占位符→加载完成替换→加载失败兜底,同时为每张图片绑定大图预览事件,提升用户体验;
- 错误提示(showError):显示错误信息并在 5 秒后自动隐藏,避免错误信息长期占用页面空间,同时使用红色主题突出错误提示,便于用户注意;
- 历史记录(renderHistory) :通过
localStorage本地缓存,实现记录的增删(去重、保留最近 10 条)、渲染、点击复用,无需后端数据库,简化开发。
4.2.3 细节体验优化:让应用更 "贴心"
优秀的应用往往体现在细节,本次前端开发加入了多个细节优化点,兼顾实用性 与流畅性:
- 回车提交:输入框支持按 Enter 键提交,提升操作效率;
- 页面聚焦:页面加载完成后,输入框自动获得焦点,用户可直接输入,无需手动点击;
- 图片懒加载 :使用
loading="lazy"实现图片懒加载,只有当图片进入视口时才会加载,优化页面初始加载速度; - 大图预览:通过模态框实现图片大图预览,点击图片即可查看高清图,点击模态框外部或关闭按钮可关闭,操作符合用户习惯;
- 历史记录优化:历史记录超过 30 字自动省略并显示完整标题,只保留最近 10 条记录,避免记录过多导致页面杂乱;
- 按钮状态管理:加载过程中禁用提交按钮,避免用户重复点击导致多次请求;
- 无内容兜底:未生成图片时,显示 "暂无图片" 提示,避免页面空白。
4.3 配置与部署:简单可移植
本项目的配置与部署流程极简,仅需三步即可完成本地运行,稍作修改即可部署到线上服务器:
步骤 1:创建.env 文件,配置敏感信息
在项目根目录创建.env文件,添加以下内容,替换为自己的 Coze API 令牌与工作流 ID:
COZE_API_TOKEN=你的Coze API令牌
WORKFLOW_ID=你的图片生成工作流ID
注意 :.env文件不要提交到代码仓库(可添加到.gitignore),避免敏感信息泄露。
步骤 2:安装依赖包
在项目根目录执行以下命令,安装所有后端依赖:
bash
pip install flask flask-cors python-dotenv cozepy
步骤 3:启动服务
直接运行 Python 后端代码,即可启动服务:
bash
python app.py
启动后,在浏览器访问http://127.0.0.1:5000,即可使用历史主题图片生成器。
线上部署建议
若需部署到线上服务器,仅需做以下简单修改:
-
将 Flask 的
debug=True改为debug=False,避免调试信息泄露与安全风险; -
使用 Gunicorn 作为 WSGI 服务器,替代 Flask 内置的开发服务器,提升服务的稳定性与性能:
bash# 安装Gunicorn pip install gunicorn # 启动服务 gunicorn -w 4 -b 0.0.0.0:5000 app:app -
配置 Nginx 反向代理(可选),实现域名访问、静态资源缓存、HTTPS 配置等;
-
确保服务器已安装 Python 3.8+,并配置好环境变量。
五、项目扩展方向:基于基础版本的功能升级
本项目实现了核心的图片生成功能,基于现有代码架构,可轻松进行功能扩展,适配更多使用场景,以下是几个推荐的扩展方向,供大家参考:
- 图片数量可配置:前端添加图片数量选择框(如 1/2/3 张),后端接收数量参数,动态返回对应数量的图片;
- 图片下载功能:为每张图片添加下载按钮,通过 JavaScript 实现图片本地下载;
- 历史记录删除:支持单条 / 全部历史记录删除,提升历史记录管理的灵活性;
- 图片分类展示:根据历史查询的主题,对图片进行分类,实现主题化的图片管理;
- 多语言支持:添加中英文切换功能,适配海外用户;
- 后端数据库支持:引入 SQLite/MySQL 数据库,替代 localStorage,实现历史记录的跨设备同步;
- 图片缓存:对生成的图片 URL 进行本地缓存,避免相同查询重复调用 Coze 接口,减少 API 消耗;
- 输入提示 / 联想:添加历史高频查询提示、输入联想功能,提升用户输入效率;
- 深色模式:添加浅色 / 深色模式切换,适配不同的使用场景(如夜间学习);
- 接口限流:添加接口限流功能,避免恶意请求导致的服务压力与 API 消耗。
六、项目总结与技术思考
本次历史主题图片生成器的开发,是轻量化 AI 应用落地的典型实践,通过 Flask+Coze 工作流的组合,仅用数百行代码就实现了一款功能完整、体验流畅的 Web 应用,核心收获与技术思考如下:
1. 轻量化技术栈的价值
在 AI 小应用、原型验证、个人项目的开发中,无需追求复杂的技术架构,轻量化的技术栈组合(如 Flask + 原生前端)能大幅提升开发效率,降低部署与维护成本,同时满足核心的功能与体验需求。Coze 平台的工作流则进一步简化了 AI 能力的调用,让开发者无需关注 AI 模型的训练与部署,只需专注于业务逻辑与用户体验。
2. 前后端协同的核心原则
前后端协同的核心是清晰的接口规范 与完善的状态管理:后端需提供规范的 API 接口,明确请求参数、响应格式、状态码,同时做好异常处理,返回友好的错误信息;前端需实现全流程的状态管理,覆盖加载、成功、失败、无内容等所有场景,让用户清晰知道当前操作的结果,提升交互体验。
3. 细节体验决定应用的质感
一款优秀的应用,不仅需要实现核心功能,更需要在细节上打磨:如回车提交、自动聚焦、图片懒加载、错误提示自动隐藏、历史记录优化等,这些细节看似微小,却能大幅提升用户的使用体验,让轻量应用也有出色的质感。
4. 配置与代码解耦的最佳实践
将敏感配置(如 API 令牌、工作流 ID)从代码中解耦,通过环境变量管理,是开发的最佳实践:既避免了敏感信息的硬编码泄露,又便于不同环境(开发 / 线上)的配置切换,提升了代码的可移植性与安全性。
5. AI 应用开发的新趋势
随着低代码平台与 AI SDK 的不断完善,AI 应用的开发门槛大幅降低,开发者的核心工作从「AI 模型开发」转向「业务逻辑整合」与「用户体验设计」。未来,更多的 AI 应用将以轻量化、场景化的形式落地,围绕具体的业务需求,将 AI 能力与实际场景结合,为用户提供更实用、更便捷的服务。
七、项目完整代码获取
本文的完整项目代码(包含后端 Python 代码、前端 HTML/CSS/JavaScript 代码、.env 配置示例)可直接复用,仅需替换 Coze 的 API 令牌与工作流 ID,即可快速启动服务。你可以根据自己的需求,对代码进行修改与扩展,打造属于自己的 AI 图片生成应用。
写在最后:本次项目是 AI 轻量化应用落地的一次简单尝试,希望通过本文的拆解,能为各位开发者提供一些思路与参考,让更多的人能快速将自己的 AI 想法落地为可直接使用的应用。在 AI 时代,技术的价值在于落地与应用,愿我们都能在轻量化开发中,享受技术带来的乐趣与价值。
完整代码:
后端:
python
import os
from flask import Flask, request, jsonify, send_file
from flask_cors import CORS
from dotenv import load_dotenv
from cozepy import Coze, TokenAuth, COZE_CN_BASE_URL
import json
load_dotenv()
app = Flask(__name__)
CORS(app) # 允许跨域请求
@app.route('/')
def index():
return send_file('index.html')
@app.route('/generate_images', methods=['POST'])
def generate_images():
try:
user_input = request.json.get('input', '')
if not user_input:
return jsonify({'error': '输入不能为空'}), 400
api_token = os.environ.get("COZE_API_TOKEN")
workflow_id = os.environ.get("WORKFLOW_ID")
if not api_token:
return jsonify({'error': 'API令牌未配置'}), 500
# 初始化Coze客户端
coze = Coze(
auth=TokenAuth(token=api_token),
base_url=COZE_CN_BASE_URL
)
# 执行工作流
workflow = coze.workflows.runs.create(
workflow_id=workflow_id,
parameters={
"input": user_input
}
)
print(workflow.data)
# 提取前两个图片URL
if hasattr(workflow, 'data') and workflow.data:
# 假设返回的数据结构中有图片URL
# 这里需要根据实际API返回结构调整
# 以下为示例代码,可能需要调整
data = json.loads(workflow.data)
data_content = data['output']
# 取前两个元素
return jsonify({'images': data_content[:2]})
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(debug=True, port=5000)
前端:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>历史老师</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: rgba(255, 255, 255, 0.95);
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
padding: 30px;
position: relative;
overflow: hidden;
}
.container::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 5px;
background: linear-gradient(90deg, #3498db, #2c3e50);
}
header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
h1 {
color: #2c3e50;
margin-bottom: 10px;
font-size: 2.5rem;
}
.description {
color: #7f8c8d;
font-size: 1.1rem;
max-width: 600px;
margin: 0 auto;
}
.input-section {
margin-bottom: 30px;
background-color: #f8f9fa;
padding: 20px;
border-radius: 10px;
}
.input-container {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
#user-input {
flex: 1;
min-width: 250px;
padding: 15px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
#user-input:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}
#submit-btn {
padding: 15px 30px;
background: linear-gradient(to right, #3498db, #2980b9);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.3s;
box-shadow: 0 4px 6px rgba(52, 152, 219, 0.3);
}
#submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(52, 152, 219, 0.4);
}
#submit-btn:active {
transform: translateY(0);
}
#submit-btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.results-section h2 {
margin-bottom: 20px;
color: #2c3e50;
padding-bottom: 10px;
border-bottom: 2px solid #eee;
}
.images-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 25px;
margin-top: 20px;
}
.image-card {
background-color: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
height: 100%;
}
.image-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
}
.image-wrapper {
position: relative;
width: 100%;
height: 250px;
overflow: hidden;
background-color: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
}
.image-wrapper img {
width: 100%;
height: 100%;
object-fit: contain;
transition: transform 0.5s ease;
}
.image-card:hover .image-wrapper img {
transform: scale(1.03);
}
.image-info {
padding: 15px;
text-align: center;
background: linear-gradient(to bottom, #f8f9fa, #e9ecef);
border-top: 1px solid #eee;
}
.loading {
text-align: center;
padding: 40px 20px;
display: none;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
color: #e74c3c;
padding: 15px;
background-color: #fadbd8;
border-radius: 8px;
margin-bottom: 20px;
display: none;
border-left: 4px solid #e74c3c;
}
.history-section {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.history-section h2 {
margin-bottom: 15px;
color: #2c3e50;
}
#history-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.history-item {
padding: 8px 15px;
background-color: #eaf2f8;
border-radius: 20px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.9rem;
}
.history-item:hover {
background-color: #d6eaf8;
transform: translateY(-2px);
}
footer {
text-align: center;
margin-top: 40px;
color: #7f8c8d;
font-size: 0.9rem;
padding-top: 20px;
border-top: 1px solid #eee;
}
.no-content {
text-align: center;
padding: 40px 20px;
color: #7f8c8d;
font-style: italic;
}
.image-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #7f8c8d;
background-color: #f8f9fa;
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 20px;
}
h1 {
font-size: 2rem;
}
.input-container {
flex-direction: column;
}
#submit-btn {
width: 100%;
}
.images-container {
grid-template-columns: 1fr;
}
}
/* 图片模态框 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
overflow: auto;
}
.modal-content {
display: block;
margin: 5% auto;
max-width: 90%;
max-height: 80%;
width: auto;
height: auto;
}
.close {
position: absolute;
top: 20px;
right: 30px;
color: #f1f1f1;
font-size: 40px;
font-weight: bold;
cursor: pointer;
transition: 0.3s;
}
.close:hover {
color: #bbb;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>历史老师</h1>
<p class="description">输入历史相关问题,获取相关图片展示。探索历史的视觉之旅!</p>
</header>
<div class="input-section">
<div class="input-container">
<input type="text" id="user-input" placeholder="请输入历史相关问题,例如:'展示古罗马的建筑'或'唐代服饰特点'">
<button id="submit-btn">生成图片</button>
</div>
</div>
<div id="error-message" class="error"></div>
<div id="loading" class="loading">
<div class="spinner"></div>
<p>正在生成图片,请稍候...</p>
</div>
<div class="results-section">
<h2>生成的图片</h2>
<div id="images-container" class="images-container">
<div class="no-content" id="no-images">
<p>暂无图片,请输入问题并点击生成按钮</p>
</div>
</div>
</div>
<div class="history-section">
<h2>历史记录</h2>
<div id="history-list"></div>
</div>
<footer>
<p>历史老师图片生成器 © 2023 | 探索历史,视觉呈现</p>
</footer>
</div>
<!-- 图片模态框 -->
<div id="imageModal" class="modal">
<span class="close">×</span>
<img class="modal-content" id="modalImage">
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const userInput = document.getElementById('user-input');
const submitBtn = document.getElementById('submit-btn');
const imagesContainer = document.getElementById('images-container');
const noImages = document.getElementById('no-images');
const loading = document.getElementById('loading');
const errorMessage = document.getElementById('error-message');
const historyList = document.getElementById('history-list');
const modal = document.getElementById('imageModal');
const modalImg = document.getElementById('modalImage');
const closeModal = document.querySelector('.close');
let history = JSON.parse(localStorage.getItem('history')) || [];
// 显示历史记录
function renderHistory() {
historyList.innerHTML = '';
if (history.length === 0) {
historyList.innerHTML = '<p class="no-content">暂无历史记录</p>';
return;
}
history.forEach((item, index) => {
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
historyItem.textContent = item.length > 30 ? item.substring(0, 30) + '...' : item;
historyItem.title = item;
historyItem.addEventListener('click', () => {
userInput.value = item;
generateImages();
});
historyList.appendChild(historyItem);
});
}
renderHistory();
// 生成图片函数
function generateImages() {
const inputText = userInput.value.trim();
if (!inputText) {
showError('请输入问题');
return;
}
// 添加到历史记录
if (!history.includes(inputText)) {
history.unshift(inputText);
// 只保留最近10条记录
if (history.length > 10) {
history.pop();
}
localStorage.setItem('history', JSON.stringify(history));
renderHistory();
}
// 显示加载状态
loading.style.display = 'block';
errorMessage.style.display = 'none';
submitBtn.disabled = true;
// 发送请求到后端
fetch('/generate_images', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ input: inputText })
})
.then(response => response.json())
.then(data => {
loading.style.display = 'none';
submitBtn.disabled = false;
if (data.error) {
showError(data.error);
return;
}
if (data.images && data.images.length > 0) {
displayImages(data.images, inputText);
} else {
showError('未获取到图片,请重试');
}
})
.catch(error => {
loading.style.display = 'none';
submitBtn.disabled = false;
showError('网络错误: ' + error.message);
});
}
// 显示图片
function displayImages(images, query) {
noImages.style.display = 'none';
imagesContainer.innerHTML = '';
images.forEach((imageUrl, index) => {
const imageCard = document.createElement('div');
imageCard.className = 'image-card';
const imageWrapper = document.createElement('div');
imageWrapper.className = 'image-wrapper';
const img = document.createElement('img');
img.src = imageUrl;
img.alt = `关于"${query}"的图片 ${index + 1}`;
img.loading = 'lazy'; // 懒加载
// 图片加载中
const placeholder = document.createElement('div');
placeholder.className = 'image-placeholder';
placeholder.innerHTML = '<div>加载中...</div>';
imageWrapper.appendChild(placeholder);
// 图片加载完成后直接显示
img.onload = function() {
imageWrapper.removeChild(placeholder);
imageWrapper.appendChild(img);
};
img.onerror = function() {
placeholder.innerHTML = '<div>图片加载失败</div>';
};
// 点击图片查看大图
imageWrapper.addEventListener('click', () => {
modal.style.display = 'block';
modalImg.src = imageUrl;
modalImg.alt = img.alt;
});
const imageInfo = document.createElement('div');
imageInfo.className = 'image-info';
imageInfo.textContent = `图片 ${index + 1}`;
imageCard.appendChild(imageWrapper);
imageCard.appendChild(imageInfo);
imagesContainer.appendChild(imageCard);
// 预加载图片
const preloadImg = new Image();
preloadImg.src = imageUrl;
});
}
// 显示错误信息
function showError(message) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
// 5秒后自动隐藏错误信息
setTimeout(() => {
errorMessage.style.display = 'none';
}, 5000);
}
// 关闭模态框
closeModal.addEventListener('click', () => {
modal.style.display = 'none';
});
// 点击模态框外部也可关闭
window.addEventListener('click', (event) => {
if (event.target === modal) {
modal.style.display = 'none';
}
});
// 绑定事件
submitBtn.addEventListener('click', generateImages);
userInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
generateImages();
}
});
// 页面加载后自动聚焦到输入框
userInput.focus();
});
</script>
</body>
</html>