```
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 config import Config
|
||||||
from extensions import db, redis_client, migrate, s3_client
|
from extensions import db, redis_client, migrate, s3_client
|
||||||
from blueprints.auth import auth_bp
|
from blueprints.auth import auth_bp
|
||||||
@ -142,6 +142,11 @@ def create_app():
|
|||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
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')
|
return render_template('index.html')
|
||||||
|
|
||||||
@app.route('/ocr')
|
@app.route('/ocr')
|
||||||
|
|||||||
@ -189,7 +189,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 比例和尺寸 -->
|
<!-- 比例和数量 -->
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-xs font-bold text-slate-500 uppercase tracking-wider flex items-center gap-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 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>
|
</select>
|
||||||
</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_0.5rem_center] bg-[length:1rem]">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-xs font-bold text-slate-500 uppercase tracking-wider flex items-center gap-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>生成数量
|
<i data-lucide="copy" class="w-4 h-4"></i>生成数量
|
||||||
@ -218,6 +211,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="space-y-2">
|
||||||
<label class="text-xs font-bold text-slate-500 uppercase tracking-wider flex items-center gap-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);
|
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() {
|
async function checkAuth() {
|
||||||
const r = await fetch('/api/auth/me');
|
const r = await fetch('/api/auth/me');
|
||||||
@ -384,40 +413,49 @@
|
|||||||
|
|
||||||
// 加载配置
|
// 加载配置
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
const r = await fetch('/api/config');
|
try {
|
||||||
config = await r.json();
|
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');
|
const modelSelect = document.getElementById('modelSelect');
|
||||||
modelSelect.innerHTML = '';
|
const sizeGroup = document.getElementById('sizeGroup');
|
||||||
for (const [name, info] of Object.entries(config.models || {})) {
|
if (modelSelect && sizeGroup) {
|
||||||
const opt = document.createElement('option');
|
sizeGroup.classList.toggle('hidden', !SIZE_MODELS.includes(modelSelect.value));
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -566,7 +604,12 @@
|
|||||||
|
|
||||||
// 提示词模板选择
|
// 提示词模板选择
|
||||||
document.getElementById('promptTpl').onchange = function() {
|
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() {
|
async function loadHistory() {
|
||||||
const r = await fetch('/api/history?page=1&per_page=20');
|
try {
|
||||||
const d = await r.json();
|
const r = await fetch('/api/history?page=1&per_page=20');
|
||||||
const list = document.getElementById('historyList');
|
const d = await r.json();
|
||||||
|
const list = document.getElementById('historyList');
|
||||||
|
|
||||||
if (!d.records || d.records.length === 0) {
|
if (!d.history || d.history.length === 0) {
|
||||||
list.innerHTML = `
|
list.innerHTML = `
|
||||||
<div class="text-center py-10 text-slate-400">
|
<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>
|
<i data-lucide="inbox" class="w-12 h-12 mx-auto mb-2 opacity-50"></i>
|
||||||
<p class="text-sm font-bold">暂无历史记录</p>
|
<p class="text-sm font-bold">暂无历史记录</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.innerHTML = '';
|
list.innerHTML = '';
|
||||||
for (const record of d.records) {
|
for (const record of d.history) {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'bg-white rounded-2xl border border-slate-100 overflow-hidden';
|
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">
|
const firstImg = record.urls?.[0];
|
||||||
${img ? `<img src="${img}" class="w-16 h-16 rounded-xl object-cover cursor-pointer" onclick="openImagePreview('${img}')">` : ''}
|
const img = firstImg?.thumb || firstImg?.url || '';
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<p class="text-xs font-bold text-slate-700 truncate">${record.prompt || '无提示词'}</p>
|
item.innerHTML = `
|
||||||
<p class="text-[10px] text-slate-400 mt-1">${record.created_at || ''}</p>
|
<div class="flex gap-3 p-3">
|
||||||
<div class="flex gap-1 mt-2">
|
${img ? `<img src="${img}" class="w-16 h-16 rounded-xl object-cover cursor-pointer" onclick="openImagePreview('${img}')">` : ''}
|
||||||
<span class="text-[9px] bg-indigo-50 text-indigo-600 px-2 py-0.5 rounded-full font-bold">${record.model || ''}</span>
|
<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>
|
</div>
|
||||||
</div>
|
`;
|
||||||
`;
|
list.appendChild(item);
|
||||||
list.appendChild(item);
|
}
|
||||||
|
lucide.createIcons();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载历史记录失败', e);
|
||||||
}
|
}
|
||||||
lucide.createIcons();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新积分消耗预览
|
// 更新积分消耗预览
|
||||||
function updateCostPreview() {
|
function updateCostPreview() {
|
||||||
const model = document.getElementById('modelSelect').value;
|
const modelSelect = document.getElementById('modelSelect');
|
||||||
const num = parseInt(document.getElementById('numSelect').value);
|
const numSelect = document.getElementById('numSelect');
|
||||||
const isPremium = document.getElementById('isPremium').checked;
|
const isPremium = document.getElementById('isPremium');
|
||||||
|
const costPreview = document.getElementById('costPreview');
|
||||||
|
|
||||||
let cost = 1;
|
if (!modelSelect || !numSelect || !isPremium || !costPreview) return;
|
||||||
const modelInfo = config.models?.[model];
|
|
||||||
if (modelInfo?.cost) cost = modelInfo.cost;
|
|
||||||
|
|
||||||
if (isPremium) cost *= 2;
|
const selectedOption = modelSelect.options[modelSelect.selectedIndex];
|
||||||
cost *= num;
|
let baseCost = parseInt(selectedOption?.getAttribute('data-cost') || 0);
|
||||||
|
let num = parseInt(numSelect.value || 1);
|
||||||
|
let totalCost = baseCost * num;
|
||||||
|
|
||||||
const preview = document.getElementById('costPreview');
|
if (isPremium.checked) totalCost *= 2;
|
||||||
preview.innerText = `预计消耗: ${cost} 积分`;
|
|
||||||
preview.classList.remove('hidden');
|
costPreview.innerText = `本次生成将消耗 ${totalCost} 积分`;
|
||||||
|
costPreview.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件绑定
|
// 事件绑定
|
||||||
document.getElementById('generateBtn').onclick = generateImage;
|
document.getElementById('generateBtn').onclick = generateImage;
|
||||||
document.getElementById('modelSelect').onchange = updateCostPreview;
|
document.getElementById('modelSelect').onchange = () => {
|
||||||
|
updateSizeVisibility();
|
||||||
|
updateCostPreview();
|
||||||
|
};
|
||||||
document.getElementById('numSelect').onchange = updateCostPreview;
|
document.getElementById('numSelect').onchange = updateCostPreview;
|
||||||
document.getElementById('isPremium').onchange = updateCostPreview;
|
document.getElementById('isPremium').onchange = updateCostPreview;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user