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.
This commit is contained in:
parent
455a63f20f
commit
2ef673d0d6
@ -1,6 +1,6 @@
|
|||||||
from flask import Blueprint, request, jsonify, session, current_app
|
from flask import Blueprint, request, jsonify, session, current_app
|
||||||
from extensions import db, redis_client
|
from extensions import db, redis_client
|
||||||
from models import User
|
from models import User, SavedPrompt
|
||||||
from middlewares.auth import login_required
|
from middlewares.auth import login_required
|
||||||
from services.logger import system_logger
|
from services.logger import system_logger
|
||||||
import json
|
import json
|
||||||
@ -223,3 +223,55 @@ def download_proxy():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
system_logger.error(f"代理下载失败: {str(e)}")
|
system_logger.error(f"代理下载失败: {str(e)}")
|
||||||
return jsonify({"error": "下载失败"}), 500
|
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/<int:id>', 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
|
||||||
|
|||||||
16
models.py
16
models.py
@ -187,3 +187,19 @@ class SystemLog(db.Model):
|
|||||||
return to_bj_time(self.created_at)
|
return to_bj_time(self.created_at)
|
||||||
|
|
||||||
user = db.relationship('User', backref=db.backref('logs', lazy='dynamic', order_by='SystemLog.created_at.desc()'))
|
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()'))
|
||||||
|
|||||||
@ -209,6 +209,8 @@ async function init() {
|
|||||||
const closeVisualizerBtn = document.getElementById('closeVisualizerBtn');
|
const closeVisualizerBtn = document.getElementById('closeVisualizerBtn');
|
||||||
if (openVisualizerBtn) openVisualizerBtn.onclick = openVisualizerModal;
|
if (openVisualizerBtn) openVisualizerBtn.onclick = openVisualizerModal;
|
||||||
if (closeVisualizerBtn) closeVisualizerBtn.onclick = closeVisualizerModal;
|
if (closeVisualizerBtn) closeVisualizerBtn.onclick = closeVisualizerModal;
|
||||||
|
const openSavePromptBtn = document.getElementById('openSavePromptBtn');
|
||||||
|
if (openSavePromptBtn) openSavePromptBtn.onclick = openSavePromptModal;
|
||||||
|
|
||||||
if (showHistoryBtn) {
|
if (showHistoryBtn) {
|
||||||
showHistoryBtn.onclick = () => {
|
showHistoryBtn.onclick = () => {
|
||||||
@ -264,12 +266,7 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/config');
|
await refreshPromptsList();
|
||||||
const d = await r.json();
|
|
||||||
fillSelect('modelSelect', d.models);
|
|
||||||
fillSelect('ratioSelect', d.ratios);
|
|
||||||
fillSelect('sizeSelect', d.sizes);
|
|
||||||
fillSelect('promptTpl', [{ label: '✨ 自定义创作', value: 'manual' }, ...d.prompts]);
|
|
||||||
updateCostPreview(); // 初始化时显示默认模型的积分
|
updateCostPreview(); // 初始化时显示默认模型的积分
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) { console.error(e); }
|
||||||
|
|
||||||
@ -295,11 +292,15 @@ async function init() {
|
|||||||
function fillSelect(id, list) {
|
function fillSelect(id, list) {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
// 如果是模型选择,增加积分显示
|
|
||||||
if (id === 'modelSelect') {
|
if (id === 'modelSelect') {
|
||||||
el.innerHTML = list.map(i => `<option value="${i.value}" data-cost="${i.cost || 0}">${i.label} (${i.cost || 0}积分)</option>`).join('');
|
el.innerHTML = list.map(i => `<option value="${i.value}" data-cost="${i.cost || 0}">${i.label} (${i.cost || 0}积分)</option>`).join('');
|
||||||
} else {
|
} else {
|
||||||
el.innerHTML = list.map(i => `<option value="${i.value}">${i.label}</option>`).join('');
|
el.innerHTML = list.map(i => {
|
||||||
|
// 处理 data-id
|
||||||
|
const dataIdAttr = i.id ? ` data-id="${i.id}"` : '';
|
||||||
|
const disabledAttr = i.disabled ? ' disabled' : '';
|
||||||
|
return `<option value="${i.value}"${dataIdAttr}${disabledAttr}>${i.label}</option>`;
|
||||||
|
}).join('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,6 +442,19 @@ document.getElementById('modelSelect').onchange = (e) => {
|
|||||||
|
|
||||||
document.getElementById('promptTpl').onchange = (e) => {
|
document.getElementById('promptTpl').onchange = (e) => {
|
||||||
const area = document.getElementById('manualPrompt');
|
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') {
|
if (e.target.value !== 'manual') {
|
||||||
area.value = e.target.value;
|
area.value = e.target.value;
|
||||||
area.classList.add('hidden');
|
area.classList.add('hidden');
|
||||||
@ -798,6 +812,7 @@ async function loadPointStats() {
|
|||||||
borderColor: '#6366f1',
|
borderColor: '#6366f1',
|
||||||
backgroundColor: 'rgba(99, 102, 241, 0.1)',
|
backgroundColor: 'rgba(99, 102, 241, 0.1)',
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
|
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
pointRadius: 4,
|
pointRadius: 4,
|
||||||
@ -868,3 +883,166 @@ async function loadPointDetails() {
|
|||||||
body.innerHTML = '<tr><td colspan="4" class="px-8 py-10 text-center text-rose-500">加载失败</td></tr>';
|
body.innerHTML = '<tr><td colspan="4" class="px-8 py-10 text-center text-rose-500">加载失败</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 自定义提示词逻辑 ---
|
||||||
|
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 = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>';
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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">
|
class="w-full bg-slate-50 border-b border-slate-100 p-3 text-[10px] font-bold text-indigo-600 outline-none cursor-pointer">
|
||||||
<option value="manual">✨ 自定义创作</option>
|
<option value="manual">✨ 自定义创作</option>
|
||||||
</select>
|
</select>
|
||||||
<textarea id="manualPrompt" rows="4" class="w-full p-3 text-xs outline-none resize-none leading-relaxed"
|
<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"
|
||||||
|
title="删除这个收藏">
|
||||||
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="relative group/area">
|
||||||
|
<textarea id="manualPrompt" rows="4"
|
||||||
|
class="w-full p-3 text-xs outline-none resize-none leading-relaxed"
|
||||||
placeholder="描述您的需求..."></textarea>
|
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"
|
||||||
|
title="收藏当前提示词">
|
||||||
|
<i data-lucide="bookmark-plus" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -308,7 +323,8 @@
|
|||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
<p class="text-xs font-bold opacity-80 mb-2">当前可用余额</p>
|
<p class="text-xs font-bold opacity-80 mb-2">当前可用余额</p>
|
||||||
<h4 class="text-5xl font-black tracking-tighter mb-4"><span id="modalPointsDisplay">0</span>
|
<h4 class="text-5xl font-black tracking-tighter mb-4"><span id="modalPointsDisplay">0</span>
|
||||||
<span class="text-lg opacity-60">Pts</span></h4>
|
<span class="text-lg opacity-60">Pts</span>
|
||||||
|
</h4>
|
||||||
<a href="/buy"
|
<a href="/buy"
|
||||||
class="inline-flex items-center gap-2 bg-white/20 hover:bg-white/30 px-5 py-2.5 rounded-xl text-xs font-bold backdrop-blur-md transition-all">
|
class="inline-flex items-center gap-2 bg-white/20 hover:bg-white/30 px-5 py-2.5 rounded-xl text-xs font-bold backdrop-blur-md transition-all">
|
||||||
<i data-lucide="plus-circle" class="w-4 h-4"></i> 立即充值
|
<i data-lucide="plus-circle" class="w-4 h-4"></i> 立即充值
|
||||||
@ -401,6 +417,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
</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>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user