```
feat(task-service): 支持异步任务模式进行图片生成 - 实现异步任务提交机制,添加 async=true 参数启用异步模式 - 增加重试机制,最多重试3次任务提交 - 实现轮询机制监控异步任务状态,最大轮询600次(30分钟超时) - 支持多种响应结构的数据解析,增强兼容性 - 优化状态更新逻辑,提供更准确的任务进度反馈 - 调整超时配置,使用不同的超时时间适应不同场景 fix(ui): 修复保存提示词模态框显示问题 - 修改事件处理器阻止默认行为和事件冒泡 - 改进登录状态检查逻辑,通过CSS类判断可见性 - 优化模态框显示/隐藏的过渡动画效果 - 添加内联样式兜底方案确保正确显示 - 修复页面布局相关的位置偏移问题 ```
This commit is contained in:
parent
4431a558f9
commit
85fb484bfa
@ -92,29 +92,135 @@ def sync_images_background(app, record_id, raw_urls):
|
||||
print(f"❌ 更新记录失败: {e}")
|
||||
|
||||
def process_image_generation(app, user_id, task_id, payload, api_key, target_api, cost, use_trial=False):
|
||||
"""异步执行图片生成并存入 Redis"""
|
||||
"""异步执行图片生成并存入 Redis (支持异步任务)"""
|
||||
with app.app_context():
|
||||
# 更新状态为处理中
|
||||
redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "processing", "message": "正如火如荼地绘制中..."}))
|
||||
redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "processing", "message": "任务已提交,正在排队处理..."}))
|
||||
try:
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
# 使用较长的超时时间 (10分钟),确保长耗时任务不被中断
|
||||
resp = requests.post(get_proxied_url(target_api), json=payload, headers=headers, timeout=Config.PROXY_TIMEOUT_GENERATION)
|
||||
|
||||
if resp.status_code != 200:
|
||||
if use_trial:
|
||||
from services.generation_service import refund_points
|
||||
refund_points(user_id, cost)
|
||||
# 1. 提交异步请求 (带重试机制)
|
||||
submit_resp = None
|
||||
last_error = None
|
||||
|
||||
# 记录详细的失败上下文
|
||||
system_logger.error(f"生图任务失败: {resp.text}", user_id=user_id, task_id=task_id, prompt=payload.get('prompt'), model=payload.get('model'))
|
||||
redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "error", "message": resp.text}))
|
||||
return
|
||||
for attempt in range(3):
|
||||
try:
|
||||
# 添加 async=true 参数启用异步模式
|
||||
submit_resp = requests.post(
|
||||
get_proxied_url(target_api),
|
||||
params={"async": "true"},
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=Config.PROXY_TIMEOUT_DEFAULT
|
||||
)
|
||||
|
||||
api_result = resp.json()
|
||||
raw_urls = [item['url'] for item in api_result.get('data', [])]
|
||||
if submit_resp.status_code == 200:
|
||||
break # 成功
|
||||
else:
|
||||
system_logger.warning(f"任务提交失败(第{attempt+1}次): {submit_resp.status_code} - {submit_resp.text[:100]}", user_id=user_id, task_id=task_id)
|
||||
last_error = f"HTTP {submit_resp.status_code}: {submit_resp.text}"
|
||||
except Exception as e:
|
||||
system_logger.warning(f"任务提交异常(第{attempt+1}次): {str(e)}", user_id=user_id, task_id=task_id)
|
||||
last_error = str(e)
|
||||
time.sleep(1) # 短暂等待重试
|
||||
|
||||
# 持久化记录
|
||||
if not submit_resp or submit_resp.status_code != 200:
|
||||
raise Exception(f"任务提交失败(重试3次后): {last_error}")
|
||||
|
||||
submit_result = submit_resp.json()
|
||||
|
||||
# 判断是否返回了 task_id (异步模式)
|
||||
raw_urls = []
|
||||
if 'task_id' in submit_result:
|
||||
remote_task_id = submit_result['task_id']
|
||||
system_logger.info(f"外部异步任务已提交: {remote_task_id}", user_id=user_id, task_id=task_id)
|
||||
|
||||
# 构造查询 URL: .../images/generations -> .../images/tasks/{task_id}
|
||||
poll_url = target_api.replace('/generations', f'/tasks/{remote_task_id}')
|
||||
if poll_url == target_api: # Fallback if replace failed
|
||||
import posixpath
|
||||
base_url = posixpath.dirname(target_api)
|
||||
poll_url = f"{base_url}/tasks/{remote_task_id}"
|
||||
|
||||
system_logger.info(f"开始轮询异步任务: {poll_url}", user_id=user_id, task_id=task_id)
|
||||
|
||||
# 2. 轮询状态
|
||||
max_retries = 600 # 30分钟超时 (平均3s)
|
||||
generation_success = False
|
||||
|
||||
for i in range(max_retries):
|
||||
# 动态调整轮询间隔:前15次(约15秒) 1秒一次,之后 3秒一次
|
||||
sleep_time = 1 if i < 15 else 3
|
||||
if i > 0:
|
||||
time.sleep(sleep_time)
|
||||
|
||||
# 更新本地心跳
|
||||
if i % 5 == 0:
|
||||
elapsed = i if i < 15 else (15 + (i-15)*3)
|
||||
redis_client.setex(f"task:{task_id}", 3600, json.dumps({
|
||||
"status": "processing",
|
||||
"message": f"正在生成中 (已耗时 {elapsed} 秒)..."
|
||||
}))
|
||||
|
||||
try:
|
||||
poll_resp = requests.get(get_proxied_url(poll_url), headers=headers, timeout=Config.PROXY_TIMEOUT_SHORT)
|
||||
|
||||
if poll_resp.status_code != 200:
|
||||
system_logger.warning(f"轮询非 200: {poll_resp.status_code}", user_id=user_id, task_id=task_id)
|
||||
continue
|
||||
|
||||
poll_data = poll_resp.json()
|
||||
remote_status = poll_data.get('status')
|
||||
if not remote_status and 'data' in poll_data and isinstance(poll_data['data'], dict):
|
||||
remote_status = poll_data['data'].get('status')
|
||||
|
||||
if remote_status == 'SUCCESS':
|
||||
|
||||
# 解析结果 (增强鲁棒性)
|
||||
data_node = poll_data.get('data')
|
||||
raw_urls = []
|
||||
|
||||
if isinstance(data_node, dict):
|
||||
# 尝试多层级查找 data.data.data
|
||||
inner_node = data_node.get('data')
|
||||
|
||||
if isinstance(inner_node, dict) and 'data' in inner_node and isinstance(inner_node['data'], list):
|
||||
# data -> data -> data -> [...] (Comfly structure)
|
||||
raw_urls = [item.get('url') for item in inner_node['data'] if isinstance(item, dict) and item.get('url')]
|
||||
elif isinstance(inner_node, list):
|
||||
# data -> data -> [...] (Standard)
|
||||
raw_urls = [item.get('url') for item in inner_node if isinstance(item, dict) and item.get('url')]
|
||||
elif 'url' in data_node:
|
||||
raw_urls = [data_node['url']]
|
||||
elif isinstance(data_node, list):
|
||||
# data -> [...]
|
||||
raw_urls = [item.get('url') for item in data_node if isinstance(item, dict) and item.get('url')]
|
||||
|
||||
# Fallback: check for top-level url
|
||||
if not raw_urls and 'url' in poll_data:
|
||||
raw_urls = [poll_data['url']]
|
||||
|
||||
|
||||
if raw_urls:
|
||||
generation_success = True
|
||||
break
|
||||
elif remote_status == 'FAILURE':
|
||||
raise Exception(f"生成任务失败: {poll_data.get('fail_reason', '未知错误')}")
|
||||
|
||||
except requests.RequestException:
|
||||
continue # 网络波动重试
|
||||
|
||||
if not generation_success:
|
||||
raise Exception("生成任务超时或未获取到结果")
|
||||
|
||||
else:
|
||||
# 兼容旧的同步返回模式
|
||||
raw_urls = [item['url'] for item in submit_result.get('data', [])]
|
||||
|
||||
if not raw_urls:
|
||||
raise Exception("未获取到图片地址")
|
||||
|
||||
# 3. 持久化记录
|
||||
new_record = GenerationRecord(
|
||||
user_id=user_id,
|
||||
prompt=payload.get('prompt'),
|
||||
@ -125,13 +231,13 @@ def process_image_generation(app, user_id, task_id, payload, api_key, target_api
|
||||
db.session.add(new_record)
|
||||
db.session.commit()
|
||||
|
||||
# 后台线程处理:下载 AI 原始图片并同步到私有 MinIO
|
||||
# 4. 后台线程同步 MinIO
|
||||
threading.Thread(
|
||||
target=sync_images_background,
|
||||
args=(app, new_record.id, raw_urls)
|
||||
).start()
|
||||
|
||||
# 存入 Redis 标记完成
|
||||
# 5. 完成
|
||||
system_logger.info(f"生图任务完成", user_id=user_id, task_id=task_id, model=payload.get('model'))
|
||||
redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "complete", "urls": raw_urls}))
|
||||
|
||||
|
||||
@ -210,7 +210,13 @@ async function init() {
|
||||
if (openVisualizerBtn) openVisualizerBtn.onclick = openVisualizerModal;
|
||||
if (closeVisualizerBtn) closeVisualizerBtn.onclick = closeVisualizerModal;
|
||||
const openSavePromptBtn = document.getElementById('openSavePromptBtn');
|
||||
if (openSavePromptBtn) openSavePromptBtn.onclick = openSavePromptModal;
|
||||
if (openSavePromptBtn) {
|
||||
openSavePromptBtn.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openSavePromptModal();
|
||||
};
|
||||
}
|
||||
|
||||
if (showHistoryBtn) {
|
||||
showHistoryBtn.onclick = () => {
|
||||
@ -1045,15 +1051,19 @@ async function refreshPromptsList() {
|
||||
}
|
||||
|
||||
function openSavePromptModal() {
|
||||
// Check login
|
||||
if (document.getElementById('loginEntryBtn').offsetParent !== null) {
|
||||
// Check login by checking visibility of login button
|
||||
const loginBtn = document.getElementById('loginEntryBtn');
|
||||
if (loginBtn && !loginBtn.classList.contains('hidden')) {
|
||||
showToast('请先登录', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = document.getElementById('savePromptModal');
|
||||
const input = document.getElementById('promptTitleInput');
|
||||
const prompt = document.getElementById('manualPrompt').value;
|
||||
const promptArea = document.getElementById('manualPrompt');
|
||||
|
||||
if (!promptArea) return;
|
||||
const prompt = promptArea.value;
|
||||
|
||||
if (!prompt || !prompt.trim()) {
|
||||
showToast('请先输入一些提示词内容', 'warning');
|
||||
@ -1061,22 +1071,57 @@ function openSavePromptModal() {
|
||||
}
|
||||
|
||||
if (modal) {
|
||||
// 使用内联样式强制显示,避免 Tailwind 类可能未编译的问题
|
||||
modal.classList.remove('hidden');
|
||||
input.value = '';
|
||||
modal.style.display = 'flex';
|
||||
modal.style.zIndex = '9999';
|
||||
|
||||
// 重置状态
|
||||
if (input) input.value = ''; // Clear input immediately
|
||||
|
||||
// 强制重绘
|
||||
void modal.offsetWidth;
|
||||
|
||||
// 简单直接的显示
|
||||
modal.style.transition = 'opacity 0.3s ease';
|
||||
modal.style.opacity = '1';
|
||||
|
||||
const innerDiv = modal.querySelector('div');
|
||||
if (innerDiv) {
|
||||
innerDiv.style.transition = 'transform 0.3s ease';
|
||||
innerDiv.classList.remove('scale-95');
|
||||
innerDiv.classList.add('scale-100');
|
||||
// 同样用内联样式兜底
|
||||
innerDiv.style.transform = 'scale(1)';
|
||||
}
|
||||
|
||||
// 延时聚焦
|
||||
setTimeout(() => {
|
||||
modal.classList.add('opacity-100');
|
||||
modal.querySelector('div').classList.remove('scale-95');
|
||||
input.focus();
|
||||
}, 10);
|
||||
if (input) {
|
||||
try { input.focus(); } catch (e) { }
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// 先触发过渡动画
|
||||
modal.style.opacity = '0';
|
||||
|
||||
const innerDiv = modal.querySelector('div');
|
||||
if (innerDiv) {
|
||||
innerDiv.style.transform = 'scale(0.95)';
|
||||
}
|
||||
|
||||
// 等待动画结束后隐藏
|
||||
setTimeout(() => {
|
||||
modal.style.display = 'none';
|
||||
modal.classList.add('hidden');
|
||||
// 清理样式
|
||||
modal.style.zIndex = '';
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -115,7 +115,7 @@
|
||||
</select>
|
||||
<div id="deletePromptContainer" class="hidden relative h-0">
|
||||
<button id="deletePromptBtn"
|
||||
class="absolute -top-10 right-2 w-8 h-8 rounded-lg bg-rose-50 border border-rose-100 text-rose-500 hover:bg-rose-100 transition-colors flex items-center justify-center z-10"
|
||||
class="absolute -top-10 right-12 w-8 h-8 rounded-lg bg-rose-50 border border-rose-100 text-rose-500 hover:bg-rose-100 transition-colors flex items-center justify-center z-10"
|
||||
title="删除这个收藏">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
</button>
|
||||
@ -125,7 +125,7 @@
|
||||
class="w-full p-3 text-xs outline-none resize-none leading-relaxed"
|
||||
placeholder="描述您的需求..."></textarea>
|
||||
<button id="openSavePromptBtn"
|
||||
class="absolute bottom-2 right-2 p-1.5 bg-white/80 backdrop-blur-sm border border-slate-100 text-slate-400 rounded-lg hover:text-indigo-600 hover:border-indigo-200 transition-all opacity-0 group-hover/area:opacity-100"
|
||||
class="absolute bottom-2 right-2 p-1.5 bg-white/80 backdrop-blur-sm border border-slate-100 text-slate-400 rounded-lg hover:text-indigo-600 hover:border-indigo-200 transition-all z-10"
|
||||
title="收藏当前提示词">
|
||||
<i data-lucide="bookmark-plus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
@ -378,106 +378,107 @@
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改密码弹窗 -->
|
||||
<div id="pwdModal"
|
||||
class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-50 flex items-center justify-center hidden opacity-0 transition-opacity duration-300">
|
||||
<div
|
||||
class="bg-white w-full max-w-sm rounded-[2.5rem] shadow-2xl p-10 space-y-8 transform scale-95 transition-transform duration-300">
|
||||
<h2 class="text-2xl font-black text-slate-900">修改登录密码</h2>
|
||||
<form id="pwdForm" class="space-y-5">
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">原密码</label>
|
||||
<input type="password" id="oldPwd" required
|
||||
class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">新密码</label>
|
||||
<input type="password" id="newPwd" required
|
||||
class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||||
</div>
|
||||
<div class="flex gap-4 pt-4">
|
||||
<button type="button" onclick="closePwdModal()"
|
||||
class="flex-1 px-8 py-4 rounded-2xl border border-slate-100 font-bold text-slate-400 hover:bg-slate-50 transition-all">取消</button>
|
||||
<button type="submit"
|
||||
class="flex-1 btn-primary px-8 py-4 rounded-2xl font-bold shadow-lg shadow-indigo-100">确认修改</button>
|
||||
</div>
|
||||
</form>
|
||||
<!-- 修改密码弹窗 -->
|
||||
<div id="pwdModal"
|
||||
class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-50 flex items-center justify-center hidden opacity-0 transition-opacity duration-300">
|
||||
<div
|
||||
class="bg-white w-full max-w-sm rounded-[2.5rem] shadow-2xl p-10 space-y-8 transform scale-95 transition-transform duration-300">
|
||||
<h2 class="text-2xl font-black text-slate-900">修改登录密码</h2>
|
||||
<form id="pwdForm" class="space-y-5">
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">原密码</label>
|
||||
<input type="password" id="oldPwd" required
|
||||
class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">新密码</label>
|
||||
<input type="password" id="newPwd" required
|
||||
class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||||
</div>
|
||||
<div class="flex gap-4 pt-4">
|
||||
<button type="button" onclick="closePwdModal()"
|
||||
class="flex-1 px-8 py-4 rounded-2xl border border-slate-100 font-bold text-slate-400 hover:bg-slate-50 transition-all">取消</button>
|
||||
<button type="submit"
|
||||
class="flex-1 btn-primary px-8 py-4 rounded-2xl font-bold shadow-lg shadow-indigo-100">确认修改</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 拍摄角度设置器弹窗 -->
|
||||
<div id="visualizerModal"
|
||||
class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-[60] flex items-center justify-center hidden opacity-0 transition-opacity duration-300">
|
||||
<div
|
||||
class="bg-white w-full max-w-lg rounded-[2.5rem] shadow-2xl overflow-hidden transform scale-95 transition-transform duration-300 flex flex-col h-[85vh]">
|
||||
<div class="p-6 border-b border-slate-100 flex items-center justify-between flex-shrink-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-indigo-50 text-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<i data-lucide="video" class="w-6 h-6"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-black text-slate-900">拍摄角度设置器</h3>
|
||||
<p class="text-[10px] text-slate-400 font-bold uppercase tracking-widest">相对于参考图设置相机移动路径</p>
|
||||
</div>
|
||||
</div>
|
||||
<button id="closeVisualizerBtn"
|
||||
class="w-10 h-10 rounded-xl hover:bg-slate-100 flex items-center justify-center text-slate-400 transition-colors">
|
||||
<i data-lucide="x" class="w-5 h-5"></i>
|
||||
</button>
|
||||
<!-- 拍摄角度设置器弹窗 -->
|
||||
<div id="visualizerModal"
|
||||
class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-[60] flex items-center justify-center hidden opacity-0 transition-opacity duration-300">
|
||||
<div
|
||||
class="bg-white w-full max-w-lg rounded-[2.5rem] shadow-2xl overflow-hidden transform scale-95 transition-transform duration-300 flex flex-col h-[85vh]">
|
||||
<div class="p-6 border-b border-slate-100 flex items-center justify-between flex-shrink-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-indigo-50 text-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<i data-lucide="video" class="w-6 h-6"></i>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden relative">
|
||||
<iframe id="visualizerFrame" src="/visualizer" class="w-full h-full border-none"></iframe>
|
||||
<div>
|
||||
<h3 class="text-lg font-black text-slate-900">拍摄角度设置器</h3>
|
||||
<p class="text-[10px] text-slate-400 font-bold uppercase tracking-widest">相对于参考图设置相机移动路径</p>
|
||||
</div>
|
||||
</div>
|
||||
<button id="closeVisualizerBtn"
|
||||
class="w-10 h-10 rounded-xl hover:bg-slate-100 flex items-center justify-center text-slate-400 transition-colors">
|
||||
<i data-lucide="x" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden relative">
|
||||
<iframe id="visualizerFrame" src="/visualizer" class="w-full h-full border-none"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 保存提示词弹窗 -->
|
||||
<div id="savePromptModal"
|
||||
class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-50 flex items-center justify-center hidden opacity-0 transition-opacity duration-300">
|
||||
<div
|
||||
class="bg-white w-full max-w-sm rounded-[2rem] shadow-2xl p-8 space-y-6 transform scale-95 transition-transform duration-300">
|
||||
<h2 class="text-xl font-black text-slate-900">收藏提示词</h2>
|
||||
<div class="space-y-2">
|
||||
<label
|
||||
class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">给这个灵感起个名字</label>
|
||||
<input type="text" id="promptTitleInput" placeholder="例如:赛博朋克风格"
|
||||
class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||||
</div>
|
||||
<div class="flex gap-4 pt-2">
|
||||
<button onclick="closeSavePromptModal()"
|
||||
class="flex-1 px-6 py-3 rounded-2xl border border-slate-100 font-bold text-slate-400 hover:bg-slate-50 transition-all">取消</button>
|
||||
<button onclick="confirmSavePrompt()"
|
||||
class="flex-1 btn-primary px-6 py-3 rounded-2xl font-bold shadow-lg shadow-indigo-100">保存</button>
|
||||
</div>
|
||||
<!-- 保存提示词弹窗 -->
|
||||
<div id="savePromptModal"
|
||||
class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-[100] flex items-center justify-center hidden opacity-0 transition-opacity duration-300">
|
||||
<div
|
||||
class="bg-white w-full max-w-sm rounded-[2rem] shadow-2xl p-8 space-y-6 transform scale-95 transition-transform duration-300">
|
||||
<h2 class="text-xl font-black text-slate-900">收藏提示词</h2>
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">给这个灵感起个名字</label>
|
||||
<input type="text" id="promptTitleInput" placeholder="例如:赛博朋克风格"
|
||||
class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||||
</div>
|
||||
<div class="flex gap-4 pt-2">
|
||||
<button onclick="closeSavePromptModal()"
|
||||
class="flex-1 px-6 py-3 rounded-2xl border border-slate-100 font-bold text-slate-400 hover:bg-slate-50 transition-all">取消</button>
|
||||
<button onclick="confirmSavePrompt()"
|
||||
class="flex-1 btn-primary px-6 py-3 rounded-2xl font-bold shadow-lg shadow-indigo-100">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 删除确认弹窗 -->
|
||||
<div id="deleteConfirmModal"
|
||||
class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-50 flex items-center justify-center hidden opacity-0 transition-opacity duration-300">
|
||||
<div
|
||||
class="bg-white w-full max-w-sm rounded-[2rem] shadow-2xl p-8 space-y-6 transform scale-95 transition-transform duration-300">
|
||||
<div class="flex flex-col items-center text-center space-y-4">
|
||||
<div class="w-16 h-16 bg-rose-50 text-rose-500 rounded-2xl flex items-center justify-center mb-2">
|
||||
<i data-lucide="alert-triangle" class="w-8 h-8"></i>
|
||||
</div>
|
||||
<h2 class="text-xl font-black text-slate-900">确认删除?</h2>
|
||||
<p class="text-sm text-slate-500 font-bold leading-relaxed px-4">
|
||||
您确定要删除这个收藏的提示词吗?<br>此操作无法撤销。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 删除确认弹窗 -->
|
||||
<div id="deleteConfirmModal"
|
||||
class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-50 flex items-center justify-center hidden opacity-0 transition-opacity duration-300">
|
||||
<div
|
||||
class="bg-white w-full max-w-sm rounded-[2rem] shadow-2xl p-8 space-y-6 transform scale-95 transition-transform duration-300">
|
||||
<div class="flex flex-col items-center text-center space-y-4">
|
||||
<div class="w-16 h-16 bg-rose-50 text-rose-500 rounded-2xl flex items-center justify-center mb-2">
|
||||
<i data-lucide="alert-triangle" class="w-8 h-8"></i>
|
||||
</div>
|
||||
<h2 class="text-xl font-black text-slate-900">确认删除?</h2>
|
||||
<p class="text-sm text-slate-500 font-bold leading-relaxed px-4">
|
||||
您确定要删除这个收藏的提示词吗?<br>此操作无法撤销。
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-4 pt-2">
|
||||
<button onclick="closeDeleteConfirmModal()"
|
||||
class="flex-1 px-6 py-3 rounded-2xl border border-slate-100 font-bold text-slate-400 hover:bg-slate-50 transition-all">取消</button>
|
||||
<button id="confirmDeleteBtn"
|
||||
class="flex-1 bg-rose-500 text-white px-6 py-3 rounded-2xl font-bold shadow-lg shadow-rose-200 hover:bg-rose-600 transition-all">确认删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4 pt-2">
|
||||
<button onclick="closeDeleteConfirmModal()"
|
||||
class="flex-1 px-6 py-3 rounded-2xl border border-slate-100 font-bold text-slate-400 hover:bg-slate-50 transition-all">取消</button>
|
||||
<button id="confirmDeleteBtn"
|
||||
class="flex-1 bg-rose-500 text-white px-6 py-3 rounded-2xl font-bold shadow-lg shadow-rose-200 hover:bg-rose-600 transition-all">确认删除</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
{% endblock %}
|
||||
Loading…
Reference in New Issue
Block a user