基于 Coze 工作流搭建历史主题图片生成器

目录

一、项目背景与核心需求

二、技术栈选型:轻量化组合,高效落地

后端技术

前端技术

运行环境

三、核心架构设计:前后端协同逻辑

整体架构

核心执行流程

四、核心功能开发:关键代码解析与设计思路

[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 应用,让用户通过自然语言输入历史相关问题(如 "古罗马的建筑特色""唐代服饰特点"),即可快速获取对应的主题图片,同时支持历史查询记录、图片大图预览、响应式适配等实用功能,兼顾功能完整性用户体验

基于需求,我们确定了核心技术目标:

  1. 后端实现与 Coze 工作流的对接,完成图片生成接口开发,支持跨域、异常处理;
  2. 前端实现简洁美观的交互界面,包含输入、加载、结果展示、错误提示等全流程状态;
  3. 实现本地历史记录缓存、图片懒加载、大图预览等细节优化;
  4. 保证应用的可移植性,通过环境变量管理敏感配置(如 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实现历史记录的增删、缓存与渲染

核心执行流程

  1. 用户在前端输入历史问题,点击提交 / 按回车,前端做非空校验后,显示加载状态,禁用提交按钮;
  2. 前端通过 fetch 发送 POST 请求到后端/generate_images接口,携带用户输入的文本参数;
  3. 后端接口接收请求,先校验输入与环境配置(API 令牌、工作流 ID),初始化 Coze 客户端;
  4. 后端调用 Coze 工作流,传入用户输入参数,执行图片生成逻辑,获取返回结果;
  5. 后端解析返回结果,提取图片 URL,取前 2 张返回给前端,同时捕获所有异常并返回错误信息;
  6. 前端接收后端响应,关闭加载状态,启用提交按钮:
    • 若返回错误,显示错误提示(5 秒后自动隐藏);
    • 若返回图片 URL,渲染图片卡片,实现懒加载与加载失败兜底;
  7. 前端将用户输入添加到本地历史记录(去重、保留最近 10 条),并渲染到页面;
  8. 用户可点击图片查看大图,或点击历史记录快速复用查询条件。

四、核心功能开发:关键代码解析与设计思路

接下来我们拆解项目的核心开发环节,针对后端 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-dotenvload_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

关键设计要点

  1. 参数校验:先校验前端输入是否为空,再校验环境配置是否完整,提前拦截无效请求,减少后续资源消耗;
  2. Coze 客户端初始化 :指定COZE_CN_BASE_URL国内节点,保证接口调用的稳定性与速度;使用TokenAuth令牌认证,符合 Coze 平台的安全规范;
  3. 工作流调用 :通过 Coze SDK 的workflows.runs.create方法快速执行工作流,无需手动构造请求体与处理响应,简化开发;
  4. 结果解析 :针对 Coze 工作流的返回结构,解析workflow.data获取图片 URL 列表,取前 2 张返回,避免过多图片导致前端加载卡顿;
  5. 异常处理 :使用try-except捕获所有可能的异常(如网络错误、Coze 接口报错、数据解析错误等),将异常信息转为字符串返回给前端,同时返回 500 状态码,便于前端排查问题;
  6. 状态码规范: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 界面设计:美观与实用兼顾

前端界面采用分层设计,分为头部标题区、输入区、状态提示区、结果展示区、历史记录区、页脚区,结构清晰,用户可快速找到核心操作区域:

  1. 视觉设计:使用渐变色背景、卡片阴影、圆角、边框底纹等 CSS3 特性,打造现代简约的视觉风格,避免单调;
  2. 响应式布局 :通过flex+grid布局结合媒体查询,适配电脑、平板、手机等不同设备,手机端自动将输入框与按钮改为垂直布局,图片卡片改为单列展示;
  3. 状态可视化:为加载、错误、无内容、图片加载中等状态设计专属的视觉展示,让用户清晰知道当前操作的结果;
  4. 交互反馈 :按钮、图片卡片添加hover动画(上浮、阴影加深),点击按钮时有按压效果,图片加载时有占位符,提升交互的趣味性与反馈感。
4.2.2 核心交互逻辑:全流程状态管理

前端的核心 JavaScript 代码实现了生成图片展示图片错误提示历史记录四大核心功能,同时做了完善的状态管理,避免用户重复提交、操作混乱:

  1. 生成图片函数(generateImages):做输入非空校验→添加历史记录→显示加载状态→禁用提交按钮→发送 fetch 请求→处理后端响应→关闭加载状态→启用提交按钮,形成完整的操作闭环;
  2. 图片展示(displayImages):实现图片懒加载→加载中占位符→加载完成替换→加载失败兜底,同时为每张图片绑定大图预览事件,提升用户体验;
  3. 错误提示(showError):显示错误信息并在 5 秒后自动隐藏,避免错误信息长期占用页面空间,同时使用红色主题突出错误提示,便于用户注意;
  4. 历史记录(renderHistory) :通过localStorage本地缓存,实现记录的增删(去重、保留最近 10 条)、渲染、点击复用,无需后端数据库,简化开发。
4.2.3 细节体验优化:让应用更 "贴心"

优秀的应用往往体现在细节,本次前端开发加入了多个细节优化点,兼顾实用性流畅性

  1. 回车提交:输入框支持按 Enter 键提交,提升操作效率;
  2. 页面聚焦:页面加载完成后,输入框自动获得焦点,用户可直接输入,无需手动点击;
  3. 图片懒加载 :使用loading="lazy"实现图片懒加载,只有当图片进入视口时才会加载,优化页面初始加载速度;
  4. 大图预览:通过模态框实现图片大图预览,点击图片即可查看高清图,点击模态框外部或关闭按钮可关闭,操作符合用户习惯;
  5. 历史记录优化:历史记录超过 30 字自动省略并显示完整标题,只保留最近 10 条记录,避免记录过多导致页面杂乱;
  6. 按钮状态管理:加载过程中禁用提交按钮,避免用户重复点击导致多次请求;
  7. 无内容兜底:未生成图片时,显示 "暂无图片" 提示,避免页面空白。

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,即可使用历史主题图片生成器。

线上部署建议

若需部署到线上服务器,仅需做以下简单修改:

  1. 将 Flask 的debug=True改为debug=False,避免调试信息泄露与安全风险;

  2. 使用 Gunicorn 作为 WSGI 服务器,替代 Flask 内置的开发服务器,提升服务的稳定性与性能:

    bash 复制代码
    # 安装Gunicorn
    pip install gunicorn
    # 启动服务
    gunicorn -w 4 -b 0.0.0.0:5000 app:app
  3. 配置 Nginx 反向代理(可选),实现域名访问、静态资源缓存、HTTPS 配置等;

  4. 确保服务器已安装 Python 3.8+,并配置好环境变量。

五、项目扩展方向:基于基础版本的功能升级

本项目实现了核心的图片生成功能,基于现有代码架构,可轻松进行功能扩展,适配更多使用场景,以下是几个推荐的扩展方向,供大家参考:

  1. 图片数量可配置:前端添加图片数量选择框(如 1/2/3 张),后端接收数量参数,动态返回对应数量的图片;
  2. 图片下载功能:为每张图片添加下载按钮,通过 JavaScript 实现图片本地下载;
  3. 历史记录删除:支持单条 / 全部历史记录删除,提升历史记录管理的灵活性;
  4. 图片分类展示:根据历史查询的主题,对图片进行分类,实现主题化的图片管理;
  5. 多语言支持:添加中英文切换功能,适配海外用户;
  6. 后端数据库支持:引入 SQLite/MySQL 数据库,替代 localStorage,实现历史记录的跨设备同步;
  7. 图片缓存:对生成的图片 URL 进行本地缓存,避免相同查询重复调用 Coze 接口,减少 API 消耗;
  8. 输入提示 / 联想:添加历史高频查询提示、输入联想功能,提升用户输入效率;
  9. 深色模式:添加浅色 / 深色模式切换,适配不同的使用场景(如夜间学习);
  10. 接口限流:添加接口限流功能,避免恶意请求导致的服务压力与 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>历史老师图片生成器 &copy; 2023 | 探索历史,视觉呈现</p>
        </footer>
    </div>

    <!-- 图片模态框 -->
    <div id="imageModal" class="modal">
        <span class="close">&times;</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>
相关推荐
zhaoyin19942 小时前
fiddler抓包工具使用
前端·测试工具·fiddler
IT研究所2 小时前
信创浪潮下 ITSM 的价值重构与实践赋能
大数据·运维·人工智能·安全·低代码·重构·自动化
AI职业加油站2 小时前
Python技术应用工程师:互联网行业技能赋能者
大数据·开发语言·人工智能·python·数据分析
I'mChloe2 小时前
机器学习核心分支:深入解析非监督学习
人工智能·学习·机器学习
J_Xiong01172 小时前
【Agents篇】06:Agent 的感知模块——多模态输入处理
人工智能·ai agent·视觉感知
深蓝海域知识库2 小时前
深蓝海域中标大型机电企业大模型知识工程平台项目
大数据·人工智能
爱吃泡芙的小白白2 小时前
机器学习中的“隐形之手”:偏置项深入探讨与资源全导航
人工智能·机器学习
爱打代码的小林2 小时前
用 PyTorch 实现 CBOW 模型
人工智能·pytorch·python
Doris8932 小时前
【 Vue】 Vue3全面讲解文档
前端·javascript·vue.js