```
feat(app): 添加移动端检测和配置优化 - 添加request和redirect导入以支持移动端检测 - 实现用户代理检测逻辑,自动重定向移动设备到/mobile页面 - 优化mobile.html模板中的UI布局和标签文本 - 移动端特定功能:调整尺寸选择器显示逻辑,仅在特定模型时显示 - 添加fillSelect工具函数统一处理下拉选项填充 - 集成用户收藏提示词功能,合并系统预设和用户自定义模板 - 改进错误处理机制,在配置加载和历史记录加载中添加try-catch - 优化历史记录数据显示,适配新的数据结构字段 - 增强成本预览功能,实时计算积分消耗并在UI上展示 ```
This commit is contained in:
parent
b515bdaed1
commit
fbdb232502
7
app.py
7
app.py
@ -1,4 +1,4 @@
|
||||
from flask import Flask, render_template, jsonify, Response, stream_with_context
|
||||
from flask import Flask, render_template, jsonify, Response, stream_with_context, request, redirect
|
||||
from config import Config
|
||||
from extensions import db, redis_client, migrate, s3_client
|
||||
from blueprints.auth import auth_bp
|
||||
@ -142,6 +142,11 @@ def create_app():
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
# 检测是否为移动端访问
|
||||
user_agent = request.headers.get('User-Agent', '').lower()
|
||||
mobile_keywords = ['mobile', 'android', 'iphone', 'ipad', 'ipod', 'windows phone', 'blackberry']
|
||||
if any(keyword in user_agent for keyword in mobile_keywords):
|
||||
return redirect('/mobile')
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/ocr')
|
||||
|
||||
@ -189,7 +189,7 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 比例和尺寸 -->
|
||||
<!-- 比例和数量 -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-bold text-slate-500 uppercase tracking-wider flex items-center gap-2">
|
||||
@ -198,13 +198,6 @@
|
||||
<select id="ratioSelect" class="w-full bg-white border border-slate-200 rounded-xl p-3.5 text-sm font-bold outline-none focus:border-indigo-500 transition-all appearance-none bg-[url('data:image/svg+xml;charset=US-ASCII,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"%2394a3b8\" stroke-width=\"2\"><polyline points=\"6 9 12 15 18 9\"></polyline></svg>')] bg-no-repeat bg-[right_0.5rem_center] bg-[length:1rem]">
|
||||
</select>
|
||||
</div>
|
||||
<div id="sizeGroup" class="hidden space-y-2">
|
||||
<label class="text-xs font-bold text-slate-500 uppercase tracking-wider flex items-center gap-2">
|
||||
<i data-lucide="maximize" class="w-4 h-4"></i>输出尺寸
|
||||
</label>
|
||||
<select id="sizeSelect" class="w-full bg-white border border-slate-200 rounded-xl p-3.5 text-sm font-bold outline-none focus:border-indigo-500 transition-all appearance-none bg-[url('data:image/svg+xml;charset=US-ASCII,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"%2394a3b8\" stroke-width=\"2\"><polyline points=\"6 9 12 15 18 9\"></polyline></svg>')] bg-no-repeat bg-[right_0.5rem_center] bg-[length:1rem]">
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-bold text-slate-500 uppercase tracking-wider flex items-center gap-2">
|
||||
<i data-lucide="copy" class="w-4 h-4"></i>生成数量
|
||||
@ -218,6 +211,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出尺寸 (仅特定模型显示) -->
|
||||
<div id="sizeGroup" class="hidden space-y-2">
|
||||
<label class="text-xs font-bold text-slate-500 uppercase tracking-wider flex items-center gap-2">
|
||||
<i data-lucide="maximize" class="w-4 h-4"></i>输出尺寸
|
||||
</label>
|
||||
<select id="sizeSelect" class="w-full bg-white border border-slate-200 rounded-xl p-3.5 text-sm font-bold outline-none focus:border-indigo-500 transition-all appearance-none bg-[url('data:image/svg+xml;charset=US-ASCII,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"%2394a3b8\" stroke-width=\"2\"><polyline points=\"6 9 12 15 18 9\"></polyline></svg>')] bg-no-repeat bg-[right_1rem_center] bg-[length:1rem]">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 提示词 -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-bold text-slate-500 uppercase tracking-wider flex items-center gap-2">
|
||||
@ -355,6 +357,33 @@
|
||||
setTimeout(() => { toast.style.animation = 'slideOutRight 0.3s ease-in'; setTimeout(() => toast.remove(), 300); }, 3000);
|
||||
}
|
||||
|
||||
// 填充下拉选择框
|
||||
function fillSelect(id, list) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el || !list) return;
|
||||
|
||||
if (id === 'modelSelect') {
|
||||
el.innerHTML = list.map(i => `<option value="${i.value}" data-cost="${i.cost || 0}">${i.label} (${i.cost || 0}积分)</option>`).join('');
|
||||
} else {
|
||||
el.innerHTML = list.map(i => {
|
||||
const dataIdAttr = i.id ? ` data-id="${i.id}"` : '';
|
||||
const disabledAttr = i.disabled ? ' disabled' : '';
|
||||
return `<option value="${i.value}"${dataIdAttr}${disabledAttr}>${i.label}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载用户收藏的提示词
|
||||
async function loadUserPrompts() {
|
||||
try {
|
||||
const r = await fetch('/api/prompts');
|
||||
const d = await r.json();
|
||||
return d.error ? [] : d;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
async function checkAuth() {
|
||||
const r = await fetch('/api/auth/me');
|
||||
@ -384,40 +413,49 @@
|
||||
|
||||
// 加载配置
|
||||
async function loadConfig() {
|
||||
const r = await fetch('/api/config');
|
||||
config = await r.json();
|
||||
try {
|
||||
const r = await fetch('/api/config');
|
||||
config = await r.json();
|
||||
|
||||
// 填充模型选择
|
||||
// 填充模型选择
|
||||
fillSelect('modelSelect', config.models);
|
||||
|
||||
// 填充比例选择
|
||||
fillSelect('ratioSelect', config.ratios);
|
||||
|
||||
// 填充尺寸选择
|
||||
fillSelect('sizeSelect', config.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 },
|
||||
...config.prompts
|
||||
];
|
||||
|
||||
fillSelect('promptTpl', mergedPrompts);
|
||||
|
||||
// 初始化尺寸选择器显示状态
|
||||
updateSizeVisibility();
|
||||
} catch (e) {
|
||||
console.error('加载配置失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 尺寸模型常量 (与PC端一致)
|
||||
const SIZE_MODELS = ['nano-banana-2', 'gemini-3.1-flash-image-preview'];
|
||||
|
||||
// 更新尺寸选择器显示状态
|
||||
function updateSizeVisibility() {
|
||||
const modelSelect = document.getElementById('modelSelect');
|
||||
modelSelect.innerHTML = '';
|
||||
for (const [name, info] of Object.entries(config.models || {})) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = name;
|
||||
opt.innerText = info.display_name || name;
|
||||
if (info.default) opt.selected = true;
|
||||
modelSelect.appendChild(opt);
|
||||
}
|
||||
|
||||
// 填充比例选择
|
||||
const ratioSelect = document.getElementById('ratioSelect');
|
||||
ratioSelect.innerHTML = '';
|
||||
for (const ratio of (config.ratios || ['1:1', '16:9', '9:16', '4:3', '3:4'])) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ratio;
|
||||
opt.innerText = ratio;
|
||||
if (ratio === '1:1') opt.selected = true;
|
||||
ratioSelect.appendChild(opt);
|
||||
}
|
||||
|
||||
// 填充提示词模板
|
||||
const promptTpl = document.getElementById('promptTpl');
|
||||
if (config.prompt_templates) {
|
||||
for (const tpl of config.prompt_templates) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = tpl.content;
|
||||
opt.innerText = tpl.name;
|
||||
promptTpl.appendChild(opt);
|
||||
}
|
||||
const sizeGroup = document.getElementById('sizeGroup');
|
||||
if (modelSelect && sizeGroup) {
|
||||
sizeGroup.classList.toggle('hidden', !SIZE_MODELS.includes(modelSelect.value));
|
||||
}
|
||||
}
|
||||
|
||||
@ -566,7 +604,12 @@
|
||||
|
||||
// 提示词模板选择
|
||||
document.getElementById('promptTpl').onchange = function() {
|
||||
document.getElementById('manualPrompt').value = this.value === 'manual' ? '' : this.value;
|
||||
const manualPrompt = document.getElementById('manualPrompt');
|
||||
if (this.value === 'manual') {
|
||||
manualPrompt.value = '';
|
||||
} else {
|
||||
manualPrompt.value = this.value;
|
||||
}
|
||||
};
|
||||
|
||||
// 图片生成
|
||||
@ -734,64 +777,77 @@
|
||||
|
||||
// 加载历史记录
|
||||
async function loadHistory() {
|
||||
const r = await fetch('/api/history?page=1&per_page=20');
|
||||
const d = await r.json();
|
||||
const list = document.getElementById('historyList');
|
||||
try {
|
||||
const r = await fetch('/api/history?page=1&per_page=20');
|
||||
const d = await r.json();
|
||||
const list = document.getElementById('historyList');
|
||||
|
||||
if (!d.records || d.records.length === 0) {
|
||||
list.innerHTML = `
|
||||
<div class="text-center py-10 text-slate-400">
|
||||
<i data-lucide="inbox" class="w-12 h-12 mx-auto mb-2 opacity-50"></i>
|
||||
<p class="text-sm font-bold">暂无历史记录</p>
|
||||
</div>
|
||||
`;
|
||||
lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
if (!d.history || d.history.length === 0) {
|
||||
list.innerHTML = `
|
||||
<div class="text-center py-10 text-slate-400">
|
||||
<i data-lucide="inbox" class="w-12 h-12 mx-auto mb-2 opacity-50"></i>
|
||||
<p class="text-sm font-bold">暂无历史记录</p>
|
||||
</div>
|
||||
`;
|
||||
lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = '';
|
||||
for (const record of d.records) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'bg-white rounded-2xl border border-slate-100 overflow-hidden';
|
||||
const img = record.thumbnail_url || (record.image_urls?.[0]) || '';
|
||||
item.innerHTML = `
|
||||
<div class="flex gap-3 p-3">
|
||||
${img ? `<img src="${img}" class="w-16 h-16 rounded-xl object-cover cursor-pointer" onclick="openImagePreview('${img}')">` : ''}
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs font-bold text-slate-700 truncate">${record.prompt || '无提示词'}</p>
|
||||
<p class="text-[10px] text-slate-400 mt-1">${record.created_at || ''}</p>
|
||||
<div class="flex gap-1 mt-2">
|
||||
<span class="text-[9px] bg-indigo-50 text-indigo-600 px-2 py-0.5 rounded-full font-bold">${record.model || ''}</span>
|
||||
list.innerHTML = '';
|
||||
for (const record of d.history) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'bg-white rounded-2xl border border-slate-100 overflow-hidden';
|
||||
|
||||
// 获取第一张图片的缩略图
|
||||
const firstImg = record.urls?.[0];
|
||||
const img = firstImg?.thumb || firstImg?.url || '';
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="flex gap-3 p-3">
|
||||
${img ? `<img src="${img}" class="w-16 h-16 rounded-xl object-cover cursor-pointer" onclick="openImagePreview('${img}')">` : ''}
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs font-bold text-slate-700 truncate">${record.prompt || '无提示词'}</p>
|
||||
<p class="text-[10px] text-slate-400 mt-1">${record.created_at || ''}</p>
|
||||
<div class="flex gap-1 mt-2">
|
||||
<span class="text-[9px] bg-indigo-50 text-indigo-600 px-2 py-0.5 rounded-full font-bold">${record.model || ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
list.appendChild(item);
|
||||
`;
|
||||
list.appendChild(item);
|
||||
}
|
||||
lucide.createIcons();
|
||||
} catch (e) {
|
||||
console.error('加载历史记录失败', e);
|
||||
}
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// 更新积分消耗预览
|
||||
function updateCostPreview() {
|
||||
const model = document.getElementById('modelSelect').value;
|
||||
const num = parseInt(document.getElementById('numSelect').value);
|
||||
const isPremium = document.getElementById('isPremium').checked;
|
||||
const modelSelect = document.getElementById('modelSelect');
|
||||
const numSelect = document.getElementById('numSelect');
|
||||
const isPremium = document.getElementById('isPremium');
|
||||
const costPreview = document.getElementById('costPreview');
|
||||
|
||||
let cost = 1;
|
||||
const modelInfo = config.models?.[model];
|
||||
if (modelInfo?.cost) cost = modelInfo.cost;
|
||||
if (!modelSelect || !numSelect || !isPremium || !costPreview) return;
|
||||
|
||||
if (isPremium) cost *= 2;
|
||||
cost *= num;
|
||||
const selectedOption = modelSelect.options[modelSelect.selectedIndex];
|
||||
let baseCost = parseInt(selectedOption?.getAttribute('data-cost') || 0);
|
||||
let num = parseInt(numSelect.value || 1);
|
||||
let totalCost = baseCost * num;
|
||||
|
||||
const preview = document.getElementById('costPreview');
|
||||
preview.innerText = `预计消耗: ${cost} 积分`;
|
||||
preview.classList.remove('hidden');
|
||||
if (isPremium.checked) totalCost *= 2;
|
||||
|
||||
costPreview.innerText = `本次生成将消耗 ${totalCost} 积分`;
|
||||
costPreview.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 事件绑定
|
||||
document.getElementById('generateBtn').onclick = generateImage;
|
||||
document.getElementById('modelSelect').onchange = updateCostPreview;
|
||||
document.getElementById('modelSelect').onchange = () => {
|
||||
updateSizeVisibility();
|
||||
updateCostPreview();
|
||||
};
|
||||
document.getElementById('numSelect').onchange = updateCostPreview;
|
||||
document.getElementById('isPremium').onchange = updateCostPreview;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user