From 2ef673d0d63f9ead6a09ac622d55435884baf410 Mon Sep 17 00:00:00 2001 From: 24024 <240241002@qq.com> Date: Wed, 21 Jan 2026 20:43:46 +0800 Subject: [PATCH] feat: Implement a new API blueprint for core application functionalities including generation, history, notifications, user stats, and saved prompts, along with a new `SavedPrompt` model and supporting frontend. --- blueprints/api.py | 54 +++++++++++- models.py | 16 ++++ static/js/main.js | 194 +++++++++++++++++++++++++++++++++++++++++-- templates/index.html | 66 ++++++++++++++- 4 files changed, 318 insertions(+), 12 deletions(-) diff --git a/blueprints/api.py b/blueprints/api.py index cf72d0a..1c2e399 100644 --- a/blueprints/api.py +++ b/blueprints/api.py @@ -1,6 +1,6 @@ from flask import Blueprint, request, jsonify, session, current_app from extensions import db, redis_client -from models import User +from models import User, SavedPrompt from middlewares.auth import login_required from services.logger import system_logger import json @@ -223,3 +223,55 @@ def download_proxy(): except Exception as e: system_logger.error(f"代理下载失败: {str(e)}") return jsonify({"error": "下载失败"}), 500 + +@api_bp.route('/api/prompts', methods=['GET']) +@login_required +def list_prompts(): + try: + user_id = session.get('user_id') + prompts = SavedPrompt.query.filter_by(user_id=user_id).order_by(SavedPrompt.created_at.desc()).all() + return jsonify([{ + "id": p.id, + "label": p.title, # Use label/value structure for frontend select + "value": p.prompt + } for p in prompts]) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@api_bp.route('/api/prompts', methods=['POST']) +@login_required +def save_prompt(): + try: + user_id = session.get('user_id') + data = request.json + title = data.get('title') + prompt_text = data.get('prompt') + + if not title or not prompt_text: + return jsonify({"error": "标题和内容不能为空"}), 400 + + # Limit to 50 saved prompts + count = SavedPrompt.query.filter_by(user_id=user_id).count() + if count >= 50: + return jsonify({"error": "收藏数量已达上限 (50)"}), 400 + + p = SavedPrompt(user_id=user_id, title=title, prompt=prompt_text) + db.session.add(p) + db.session.commit() + + return jsonify({"message": "保存成功", "id": p.id}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@api_bp.route('/api/prompts/', methods=['DELETE']) +@login_required +def delete_prompt(id): + try: + user_id = session.get('user_id') + p = SavedPrompt.query.filter_by(id=id, user_id=user_id).first() + if p: + db.session.delete(p) + db.session.commit() + return jsonify({"message": "删除成功"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 diff --git a/models.py b/models.py index b69e1e9..2d76406 100644 --- a/models.py +++ b/models.py @@ -187,3 +187,19 @@ class SystemLog(db.Model): return to_bj_time(self.created_at) user = db.relationship('User', backref=db.backref('logs', lazy='dynamic', order_by='SystemLog.created_at.desc()')) + +class SavedPrompt(db.Model): + """用户收藏的提示词""" + __tablename__ = 'saved_prompts' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + title = db.Column(db.String(100), nullable=False) + prompt = db.Column(db.Text, nullable=False) + created_at = db.Column(db.DateTime, default=get_bj_now) + + @property + def created_at_bj(self): + return to_bj_time(self.created_at) + + user = db.relationship('User', backref=db.backref('saved_prompts', lazy='dynamic', order_by='SavedPrompt.created_at.desc()')) diff --git a/static/js/main.js b/static/js/main.js index fb6b1fb..bf6fb24 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -209,6 +209,8 @@ async function init() { const closeVisualizerBtn = document.getElementById('closeVisualizerBtn'); if (openVisualizerBtn) openVisualizerBtn.onclick = openVisualizerModal; if (closeVisualizerBtn) closeVisualizerBtn.onclick = closeVisualizerModal; + const openSavePromptBtn = document.getElementById('openSavePromptBtn'); + if (openSavePromptBtn) openSavePromptBtn.onclick = openSavePromptModal; if (showHistoryBtn) { showHistoryBtn.onclick = () => { @@ -264,12 +266,7 @@ async function init() { } try { - const r = await fetch('/api/config'); - const d = await r.json(); - fillSelect('modelSelect', d.models); - fillSelect('ratioSelect', d.ratios); - fillSelect('sizeSelect', d.sizes); - fillSelect('promptTpl', [{ label: '✨ 自定义创作', value: 'manual' }, ...d.prompts]); + await refreshPromptsList(); updateCostPreview(); // 初始化时显示默认模型的积分 } catch (e) { console.error(e); } @@ -295,11 +292,15 @@ async function init() { function fillSelect(id, list) { const el = document.getElementById(id); if (!el) return; - // 如果是模型选择,增加积分显示 if (id === 'modelSelect') { el.innerHTML = list.map(i => ``).join(''); } else { - el.innerHTML = list.map(i => ``).join(''); + el.innerHTML = list.map(i => { + // 处理 data-id + const dataIdAttr = i.id ? ` data-id="${i.id}"` : ''; + const disabledAttr = i.disabled ? ' disabled' : ''; + return ``; + }).join(''); } } @@ -441,6 +442,19 @@ document.getElementById('modelSelect').onchange = (e) => { document.getElementById('promptTpl').onchange = (e) => { const area = document.getElementById('manualPrompt'); + const deleteContainer = document.getElementById('deletePromptContainer'); + const selectedOption = e.target.options[e.target.selectedIndex]; + + // 判断是否是用户的收藏项 (通过 data-id 属性) + const promptId = selectedOption.getAttribute('data-id'); + + if (promptId) { + deleteContainer.classList.remove('hidden'); + document.getElementById('deletePromptBtn').onclick = () => openDeleteConfirmModal(promptId); + } else { + deleteContainer.classList.add('hidden'); + } + if (e.target.value !== 'manual') { area.value = e.target.value; area.classList.add('hidden'); @@ -798,6 +812,7 @@ async function loadPointStats() { borderColor: '#6366f1', backgroundColor: 'rgba(99, 102, 241, 0.1)', borderWidth: 3, + fill: true, tension: 0.4, pointRadius: 4, @@ -868,3 +883,166 @@ async function loadPointDetails() { body.innerHTML = '加载失败'; } } + +// --- 自定义提示词逻辑 --- +async function loadUserPrompts() { + try { + const r = await fetch('/api/prompts'); + const d = await r.json(); + return d.error ? [] : d; + } catch (e) { + return []; + } +} + +async function refreshPromptsList() { + try { + const r = await fetch('/api/config'); + const d = await r.json(); + + fillSelect('modelSelect', d.models); + fillSelect('ratioSelect', d.ratios); + fillSelect('sizeSelect', d.sizes); + + const userPrompts = await loadUserPrompts(); + + const mergedPrompts = [ + { label: '✨ 自定义创作', value: 'manual' }, + ...(userPrompts.length > 0 ? [{ label: '--- 我的收藏 ---', value: 'manual', disabled: true }] : []), + ...userPrompts.map(p => ({ id: p.id, label: '⭐ ' + p.label, value: p.value })), + { label: '--- 系统预设 ---', value: 'manual', disabled: true }, + ...d.prompts + ]; + + fillSelect('promptTpl', mergedPrompts); + } catch (e) { console.error(e); } +} + +function openSavePromptModal() { + // Check login + if (document.getElementById('loginEntryBtn').offsetParent !== null) { + showToast('请先登录', 'warning'); + return; + } + + const modal = document.getElementById('savePromptModal'); + const input = document.getElementById('promptTitleInput'); + const prompt = document.getElementById('manualPrompt').value; + + if (!prompt || !prompt.trim()) { + showToast('请先输入一些提示词内容', 'warning'); + return; + } + + if (modal) { + modal.classList.remove('hidden'); + input.value = ''; + setTimeout(() => { + modal.classList.add('opacity-100'); + modal.querySelector('div').classList.remove('scale-95'); + input.focus(); + }, 10); + } +} + +function closeSavePromptModal() { + const modal = document.getElementById('savePromptModal'); + if (modal) { + modal.classList.remove('opacity-100'); + modal.querySelector('div').classList.add('scale-95'); + setTimeout(() => modal.classList.add('hidden'), 300); + } +} + +async function confirmSavePrompt() { + const title = document.getElementById('promptTitleInput').value.trim(); + const prompt = document.getElementById('manualPrompt').value.trim(); + + if (!title) { + showToast('请输入标题', 'warning'); + return; + } + + try { + const r = await fetch('/api/prompts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title, prompt }) + }); + const d = await r.json(); + + if (r.ok) { + showToast('收藏成功', 'success'); + closeSavePromptModal(); + refreshPromptsList(); + } else { + showToast(d.error || '保存失败', 'error'); + } + } catch (e) { + showToast('保存异常', 'error'); + } +} + +async function deleteUserPrompt(id) { + // Legacy function replaced by openDeleteConfirmModal + // Kept for reference or simple fallback if needed, but logic moved to executeDeletePrompt +} + +function openDeleteConfirmModal(id) { + const modal = document.getElementById('deleteConfirmModal'); + const confirmBtn = document.getElementById('confirmDeleteBtn'); + + if (modal && confirmBtn) { + // Store ID on the button + confirmBtn.setAttribute('data-delete-id', id); + confirmBtn.onclick = () => executeDeletePrompt(id); // Bind click + + modal.classList.remove('hidden'); + setTimeout(() => { + modal.classList.add('opacity-100'); + modal.querySelector('div').classList.remove('scale-95'); + }, 10); + } +} + +function closeDeleteConfirmModal() { + const modal = document.getElementById('deleteConfirmModal'); + if (modal) { + modal.classList.remove('opacity-100'); + modal.querySelector('div').classList.add('scale-95'); + setTimeout(() => modal.classList.add('hidden'), 300); + } +} + +async function executeDeletePrompt(id) { + const confirmBtn = document.getElementById('confirmDeleteBtn'); + const originalText = confirmBtn.innerHTML; + confirmBtn.disabled = true; + confirmBtn.innerHTML = ''; + lucide.createIcons(); + + try { + const r = await fetch(`/api/prompts/${id}`, { + method: 'DELETE' + }); + + if (r.ok) { + showToast('已删除', 'success'); + // Reset selection + document.getElementById('promptTpl').value = 'manual'; + document.getElementById('manualPrompt').value = ''; + document.getElementById('manualPrompt').classList.remove('hidden'); + document.getElementById('deletePromptContainer').classList.add('hidden'); + + closeDeleteConfirmModal(); + refreshPromptsList(); + } else { + showToast('删除失败', 'error'); + } + } catch (e) { + showToast('删除异常', 'error'); + } finally { + confirmBtn.disabled = false; + confirmBtn.innerHTML = originalText; + } +} diff --git a/templates/index.html b/templates/index.html index a108116..08a0638 100644 --- a/templates/index.html +++ b/templates/index.html @@ -113,8 +113,23 @@ class="w-full bg-slate-50 border-b border-slate-100 p-3 text-[10px] font-bold text-indigo-600 outline-none cursor-pointer"> - + +
+ + +
@@ -308,7 +323,8 @@

当前可用余额

0 - Pts

+ Pts + 立即充值 @@ -401,6 +417,50 @@
+ + + + + + + {% endblock %} {% block scripts %}