使用cloudflare workers ai搭建的ai绘画站

https://aidraw.foxhank.top
前后端完全基于(白嫖)cloudflare worker&cf worker ai,零服务器成本,但是由于我代码基本靠chatgpt,基本没有前端开发经验,有大佬可以提供一些改进意见嘛,万分感谢
搭建记录:http://www.foxhank.cn/archives/25/

48 个赞

可以很不错(*๓´╰╯`๓):heart:

1 个赞

很赞,学习了

3 个赞

可以生成尺寸大一些的图吗?当做商城封面使用

感谢大佬的分享

生成质量还不错,就像前面佬讲的设置选个尺寸比较多用途,备用以后用得上,谢谢分享

前排围观,认真学习中,收藏了

這個用戶介面不錯

1 个赞

用Claude重新糊了个UI :tieba_001:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>AI绘画喵</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <meta name="description" content="使用先进的AI技术,轻松创作独特的数字艺术作品。探索无限可能,释放你的创意潜能。">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta name="apple-mobile-web-app-title" content="AI绘画">
    
    <link rel="icon" href="https://xx.com" type="image/png">    #修改为你的图床里的照片,作为网站小图标
    <link rel="apple-touch-icon" href="https://xx.com">    #同上一行代码备注,作为苹果设备添加到设备桌面的Web APP的图标
    
    <style>
        :root {
            --primary-color: #4facfe;
            --secondary-color: #00f2fe;
            --background-color: #f5f7fa;
            --text-color: #333;
            --card-background: rgba(255, 255, 255, 0.9);
        }

        body, html {
            margin: 0;
            padding: 0;
            height: 100%;
            width: 100%;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, var(--background-color) 0%, #c3cfe2 100%);
            color: var(--text-color);
        }

        .container {
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            padding: 20px;
            box-sizing: border-box;
        }

        .card {
            background-color: var(--card-background);
            backdrop-filter: blur(10px);
            border-radius: 20px;
            padding: 2rem;
            box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
            width: 90%;
            max-width: 600px;
            display: flex;
            flex-direction: column;
            align-items: center;
            transition: all 0.3s ease;
        }

        .card:hover {
            transform: translateY(-5px);
            box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
        }

        h1 {
            font-size: 2.5rem;
            margin-bottom: 1rem;
            text-align: center;
            background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .image-container {
            width: 100%;
            max-width: 500px;
            height: 350px;
            border-radius: 10px;
            margin-bottom: 1rem;
            background-color: rgba(0, 0, 0, 0.05);
            display: flex;
            justify-content: center;
            align-items: center;
            overflow: hidden;
            position: relative;
        }

        #aiImage {
            max-width: 100%;
            max-height: 100%;
            object-fit: contain;
        }

        .loading-overlay {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(255, 255, 255, 0.8);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 10;
            display: none;
        }

        .loading-spinner {
            border: 5px solid #f3f3f3;
            border-top: 5px solid var(--primary-color);
            border-radius: 50%;
            width: 50px;
            height: 50px;
            animation: spin 1s linear infinite;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        select, input[type="text"] {
            width: 100%;
            padding: 0.75rem;
            margin-bottom: 1rem;
            border: 1px solid #ccc;
            border-radius: 5px;
            font-size: 1rem;
            transition: all 0.3s ease;
        }

        select:focus, input[type="text"]:focus {
            border-color: var(--primary-color);
            box-shadow: 0 0 0 2px rgba(79, 172, 254, 0.2);
        }

        .button-group {
            display: flex;
            justify-content: space-between;
            width: 100%;
        }

        button {
            padding: 0.75rem 1.5rem;
            border-radius: 5px;
            cursor: pointer;
            font-size: 1rem;
            transition: all 0.3s ease;
            border: none;
            outline: none;
        }

        .submit-btn {
            background: #cccccc;
            color: white;
            flex-grow: 1;
            margin-right: 10px;
        }

        .submit-btn.active {
            background: linear-gradient(to right, var(--primary-color) 0%, var(--secondary-color) 100%);
        }

        .submit-btn:hover {
            opacity: 0.9;
            transform: translateY(-2px);
        }

        .download-btn {
            background: #2ecc71;
            color: white;
        }

        .download-btn:hover {
            background: #27ae60;
        }

        .history-btn {
            background: #3498db;
            color: white;
            margin-left: 10px;
        }

        .history-btn:hover {
            background: #2980b9;
        }

        .modal {
            display: none;
            position: fixed;
            z-index: 1;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            overflow: auto;
            background-color: rgba(0,0,0,0.4);
        }

        .modal-content {
            background-color: #fefefe;
            margin: 15% auto;
            padding: 20px;
            border: 1px solid #888;
            width: 80%;
            max-width: 600px;
            border-radius: 10px;
        }

        .close {
            color: #aaa;
            float: right;
            font-size: 28px;
            font-weight: bold;
        }

        .close:hover,
        .close:focus {
            color: black;
            text-decoration: none;
            cursor: pointer;
        }

        .history-item {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
            padding: 10px;
            background-color: #f9f9f9;
            border-radius: 5px;
        }

        .history-item img {
            width: 50px;
            height: 50px;
            object-fit: cover;
            margin-right: 10px;
            border-radius: 5px;
        }

        .history-item-buttons {
            margin-left: auto;
        }

        .history-item-buttons button {
            margin-left: 5px;
            padding: 5px 10px;
            font-size: 0.8rem;
        }

        .redraw-btn {
            background-color: #3498db;
            color: white;
        }

        .delete-btn {
            background-color: #e74c3c;
            color: white;
        }

        .clear-history-btn {
            background-color: #e74c3c;
            color: white;
            margin-top: 10px;
        }

        .theme-toggle {
            position: absolute;
            top: 10px;
            right: 10px;
            background: none;
            border: none;
            font-size: 1.5rem;
            cursor: pointer;
        }

        .tooltip {
            position: relative;
            display: inline-block;
        }

        .tooltip .tooltiptext {
            visibility: hidden;
            width: 120px;
            background-color: #555;
            color: #fff;
            text-align: center;
            border-radius: 6px;
            padding: 5px;
            position: absolute;
            z-index: 1;
            bottom: 125%;
            left: 50%;
            margin-left: -60px;
            opacity: 0;
            transition: opacity 0.3s;
        }

        .tooltip:hover .tooltiptext {
            visibility: visible;
            opacity: 1;
        }

        @media (max-width: 768px) {
            .card {
                width: 95%;
                padding: 1.5rem;
            }

            h1 {
                font-size: 2rem;
            }

            select, input[type="text"], button {
                font-size: 0.9rem;
            }

            .image-container {
                height: 250px;
            }
        }

        @media (max-width: 480px) {
            .card {
                width: 100%;
                border-radius: 0;
            }

            h1 {
                font-size: 1.75rem;
            }

            .button-group {
                flex-direction: column;
            }

            .submit-btn, .download-btn, .history-btn {
                margin: 5px 0;
                width: 100%;
            }
        }

        .dark-theme {
            --background-color: #2c3e50;
            --text-color: #ecf0f1;
            --card-background: rgba(44, 62, 80, 0.9);
        }
    </style>
    <script>
        document.addEventListener('DOMContentLoaded', function () {
            const submitButton = document.getElementById('submitButton');
            const promptInput = document.getElementById('prompt');
            const downloadBtn = document.getElementById('downloadBtn');
            const historyBtn = document.getElementById('historyBtn');
            const modal = document.getElementById('historyModal');
            const closeBtn = document.getElementsByClassName('close')[0];
            const historyList = document.getElementById('historyList');
            const clearHistoryBtn = document.getElementById('clearHistoryBtn');
            const themeToggle = document.getElementById('themeToggle');
            let history = JSON.parse(localStorage.getItem('aiDrawingHistory')) || [];

            function updateHistory(prompt, imageUrl) {
                history.unshift({ prompt, imageUrl, timestamp: new Date().toISOString() });
                if (history.length > 10) history.pop();
                localStorage.setItem('aiDrawingHistory', JSON.stringify(history));
                renderHistory();
            }

            function renderHistory() {
                historyList.innerHTML = '';
                history.forEach((item, index) => {
                    const historyItem = document.createElement('div');
                    historyItem.className = 'history-item';
                    historyItem.innerHTML = `
                        <img src="${item.imageUrl}" alt="${item.prompt}">
                        <div>
                            <span>${item.prompt}</span>
                            <br>
                            <small>${new Date(item.timestamp).toLocaleString()}</small>
                        </div>
                        <div class="history-item-buttons">
                            <button class="redraw-btn tooltip" data-index="${index}">重绘<span class="tooltiptext">使用此提示词重新生成图片</span></button>
                            <button class="delete-btn tooltip" data-index="${index}">删除<span class="tooltiptext">从历史记录中删除此项</span></button>
                        </div>
                    `;
                    historyList.appendChild(historyItem);
                });
            }

            function deleteHistoryItem(index) {
                history.splice(index, 1);
                localStorage.setItem('aiDrawingHistory', JSON.stringify(history));
                renderHistory();
            }

            function clearHistory() {
                if (confirm('确定要清空所有历史记录吗?此操作不可撤销。')) {
                    history = [];
                    localStorage.removeItem('aiDrawingHistory');
                    renderHistory();
                }
            }

            async function generateImage(prompt) {
                submitButton.disabled = true;
                submitButton.textContent = '正在创作...';
                document.querySelector('.loading-overlay').style.display = 'flex';

                const model = document.getElementById('model').value;

                try {
                    const controller = new AbortController();
                    const signal = controller.signal;

                    setTimeout(() => {
                        controller.abort();
                    }, 30000);

                    const response = await fetch(`${window.location.origin}`, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({
                            'model': model,
                            'prompt': prompt
                        }),
                        signal: signal
                    });

                    if (!response.ok) {
                        throw new Error(`请求失败:${response.status} ${response.statusText}`);
                    }

                    const blob = await response.blob();
                    const Image = await blobToBase64(blob);
                    console.log('Base64 Image:', Image);
                    document.getElementById('aiImage').src = `${Image}`;
                    updateHistory(prompt, Image);
                    downloadBtn.style.display = 'block';
                } catch (error) {
                    if (error.name === 'AbortError') {
                        alert('服务器连接超时,请稍后重试。');
                    } else {
                        console.error('Error:', error);
                        alert('生成过程中发生错误,请重试。');
                    }
                } finally {
                    submitButton.textContent = '开始创作';
                    submitButton.disabled = false;
                    document.querySelector('.loading-overlay').style.display = 'none';
                }
            }

            promptInput.addEventListener('input', function() {
                if (this.value.trim() !== '') {
                    submitButton.classList.add('active');
                } else {
                    submitButton.classList.remove('active');
                }
            });

            submitButton.addEventListener('click', function (event) {
                event.preventDefault();
                if (promptInput.value.trim() === '') {
                    alert('请输入描述词');
                    return;
                }
                generateImage(promptInput.value.trim());
            });

            downloadBtn.addEventListener('click', function() {
                const image = document.getElementById('aiImage');
                const link = document.createElement('a');
                link.href = image.src;
                link.download = `ai-artwork-${new Date().toISOString()}.png`;
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
            });
            historyBtn.onclick = function() {
                modal.style.display = 'block';
                renderHistory();
            }

            closeBtn.onclick = function() {
                modal.style.display = 'none';
            }

            window.onclick = function(event) {
                if (event.target == modal) {
                    modal.style.display = 'none';
                }
            }

            historyList.addEventListener('click', function(event) {
                if (event.target.classList.contains('redraw-btn')) {
                    const index = event.target.getAttribute('data-index');
                    const prompt = history[index].prompt;
                    promptInput.value = prompt;
                    modal.style.display = 'none';
                    generateImage(prompt);
                } else if (event.target.classList.contains('delete-btn')) {
                    const index = event.target.getAttribute('data-index');
                    deleteHistoryItem(index);
                }
            });

            clearHistoryBtn.addEventListener('click', clearHistory);

            themeToggle.addEventListener('click', function() {
                document.body.classList.toggle('dark-theme');
                themeToggle.textContent = document.body.classList.contains('dark-theme') ? '🌞' : '🌙';
                localStorage.setItem('theme', document.body.classList.contains('dark-theme') ? 'dark' : 'light');
            });

            // 检查并应用保存的主题
            if (localStorage.getItem('theme') === 'dark') {
                document.body.classList.add('dark-theme');
                themeToggle.textContent = '🌞';
            }

            const blobToBase64 = (blob) => new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onerror = reject;
                reader.onload = () => {
                    resolve(reader.result);
                };
                reader.readAsDataURL(blob);
            });

            // 添加键盘快捷键支持
            document.addEventListener('keydown', function(event) {
                if (event.ctrlKey && event.key === 'Enter') {
                    submitButton.click();
                }
            });

            // 添加拖放支持
            const dropZone = document.querySelector('.image-container');
            dropZone.addEventListener('dragover', (e) => {
                e.preventDefault();
                dropZone.style.border = '2px dashed #4facfe';
            });

            dropZone.addEventListener('dragleave', () => {
                dropZone.style.border = 'none';
            });

            dropZone.addEventListener('drop', (e) => {
                e.preventDefault();
                dropZone.style.border = 'none';
                const file = e.dataTransfer.files[0];
                if (file && file.type.startsWith('image/')) {
                    const reader = new FileReader();
                    reader.onload = (e) => {
                        document.getElementById('aiImage').src = e.target.result;
                    };
                    reader.readAsDataURL(file);
                }
            });
        });
    </script>
</head>
<body>
    <div class="container">
        <div class="card">
            <h1>AI绘画创作平台</h1>
            <div class="image-container">
                <img id="aiImage"
                     src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg=="
                     alt="AI生成的图片">
                <div class="loading-overlay">
                    <div class="loading-spinner"></div>
                </div>
            </div>
            <select id="model">
                <option value="dreamshaper-8-lcm">DreamShaper 8 LCM</option>
                <option value="stable-diffusion-xl-base-1.0" selected>Stable Diffusion XL Base 1.0</option>
                <option value="stable-diffusion-xl-lightning">Stable Diffusion XL Lightning</option>
            </select>
            <input type="text" id="prompt" placeholder="请输入你想要创作的画面描述...">
            <div class="button-group">
                <button type="button" class="submit-btn" id="submitButton">开始创作</button>
                <button type="button" class="download-btn" id="downloadBtn" style="display: none;">下载图片</button>
                <button type="button" class="history-btn" id="historyBtn">历史记录</button>
            </div>
        </div>
    </div>

    <div id="historyModal" class="modal">
        <div class="modal-content">
            <span class="close">&times;</span>
            <h2>历史记录</h2>
            <div id="historyList"></div>
            <button id="clearHistoryBtn" class="clear-history-btn">清空历史记录</button>
        </div>
    </div>

    <button id="themeToggle" class="theme-toggle">🌙</button>
</body>
</html>

可能有点Bug,大佬勿喷 :xhs_008:

6 个赞

感谢佬,部署了!

1 个赞

十分感谢各位大佬的建议 :kissing_heart:L站各个都是人才,说话又好听,我超喜欢这里的
这是我开发的第一个前端项目,一路边学边写,确实收获良多

我靠大佬太厉害了 :scream:这可比我写的美观太多了,我可以把您这个当作前端吗,会在脚注上写上您的名字和网站地址的

1 个赞

:tieba_001:没问题的哈,不过我没有网站 :tieba_027:

1 个赞

那B站主页之类的呢()
必须挂个名啊,要不我超链接到l站主页()

直接定位到L站吧 :kissing_heart:

hhh好的,万分感谢!

感谢佬的分享

厉害呀,最近刚好有文生图的需求,这个的后端用的cd worker ai有限制次数的嘛

每天10万次的限制,昨天发出帖子之后用掉了一千多次,个人使用够够的:joy:
主要还是官方的这个模型效果只能说能用,不是特别理想,没有一些商用模型生成的好

商用模型mj还是好很多,之前体验了google的imagefx,但是限制太多了,感觉这个生成的也还行,提示词这些大佬是怎么解决的呢,我之前的提示词都是按照mj去gpt生成的