Improve auth and download UI states
This commit is contained in:
@@ -2,6 +2,9 @@
|
|||||||
# Generate SECRET_KEY with: python -c "import secrets; print(secrets.token_hex(32))"
|
# Generate SECRET_KEY with: python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
SECRET_KEY=change-me-to-a-long-random-secret
|
SECRET_KEY=change-me-to-a-long-random-secret
|
||||||
|
|
||||||
|
# Runtime mode is not set by docker compose. Local python runs default to development;
|
||||||
|
# the Docker image sets APP_ENV=production and DEBUG=false in the image.
|
||||||
|
|
||||||
# Required for first deployment. Used to create or rotate the admin account on startup.
|
# Required for first deployment. Used to create or rotate the admin account on startup.
|
||||||
ADMIN_USERNAME=admin
|
ADMIN_USERNAME=admin
|
||||||
ADMIN_PASSWORD=change-me
|
ADMIN_PASSWORD=change-me
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ WORKDIR /app
|
|||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
ENV MPLBACKEND=Agg
|
ENV MPLBACKEND=Agg
|
||||||
|
ENV APP_ENV=production
|
||||||
|
ENV DEBUG=false
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends fontconfig fonts-noto-cjk \
|
&& apt-get install -y --no-install-recommends fontconfig fonts-noto-cjk \
|
||||||
|
|||||||
+6
-1
@@ -25,8 +25,13 @@ def env_bool(name: str, default: bool = False) -> bool:
|
|||||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def app_env() -> str:
|
||||||
|
return os.environ.get("APP_ENV", "development").strip().lower() or "development"
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
DEBUG = env_bool("DEBUG", False)
|
APP_ENV = app_env()
|
||||||
|
DEBUG = env_bool("DEBUG", APP_ENV in {"dev", "development", "local"})
|
||||||
SECRET_KEY = os.environ.get("SECRET_KEY") or secrets.token_hex(32)
|
SECRET_KEY = os.environ.get("SECRET_KEY") or secrets.token_hex(32)
|
||||||
SECRET_KEY_GENERATED = not bool(os.environ.get("SECRET_KEY"))
|
SECRET_KEY_GENERATED = not bool(os.environ.get("SECRET_KEY"))
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ services:
|
|||||||
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
|
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
|
||||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?Set ADMIN_PASSWORD in .env}
|
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?Set ADMIN_PASSWORD in .env}
|
||||||
DATABASE_URL: ${DATABASE_URL:-sqlite:////app/data/pipe_survival_0331.db}
|
DATABASE_URL: ${DATABASE_URL:-sqlite:////app/data/pipe_survival_0331.db}
|
||||||
DEBUG: ${DEBUG:-false}
|
|
||||||
APP_TIMEZONE: ${APP_TIMEZONE:-Asia/Shanghai}
|
APP_TIMEZONE: ${APP_TIMEZONE:-Asia/Shanghai}
|
||||||
MAX_UPLOAD_BYTES: ${MAX_UPLOAD_BYTES:-16777216}
|
MAX_UPLOAD_BYTES: ${MAX_UPLOAD_BYTES:-16777216}
|
||||||
FUSION_MODEL_CORE_DIR: ${FUSION_MODEL_CORE_DIR:-/app/model_core}
|
FUSION_MODEL_CORE_DIR: ${FUSION_MODEL_CORE_DIR:-/app/model_core}
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ app = create_app()
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=5005, debug=Config.DEBUG, use_reloader=False)
|
app.run(host="0.0.0.0", port=5005, debug=Config.DEBUG, use_reloader=Config.DEBUG)
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -119,6 +119,10 @@
|
|||||||
opacity: .7;
|
opacity: .7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-btn[aria-busy="true"] {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.ui-btn-primary:disabled {
|
.ui-btn-primary:disabled {
|
||||||
background: #cbd5e1;
|
background: #cbd5e1;
|
||||||
}
|
}
|
||||||
@@ -175,6 +179,11 @@
|
|||||||
border-top-color: #fff;
|
border-top-color: #fff;
|
||||||
animation: spin .75s linear infinite;
|
animation: spin .75s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-btn-secondary .spinner {
|
||||||
|
border-color: rgba(0, 94, 184, .22);
|
||||||
|
border-top-color: #005EB8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
|
|||||||
@@ -142,6 +142,26 @@
|
|||||||
showAppNotification(message, category === 'error' ? 'error' : 'info');
|
showAppNotification(message, category === 'error' ? 'error' : 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-download-action]').forEach((button) => {
|
||||||
|
const restoreDelay = 3000;
|
||||||
|
const originalHtml = button.innerHTML;
|
||||||
|
const pendingLabel = button.dataset.downloadPending || '下载中...';
|
||||||
|
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
if (button.getAttribute('aria-disabled') === 'true') return;
|
||||||
|
|
||||||
|
button.setAttribute('aria-disabled', 'true');
|
||||||
|
button.setAttribute('aria-busy', 'true');
|
||||||
|
button.innerHTML = `<span class="spinner" aria-hidden="true"></span><span>${pendingLabel}</span>`;
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
button.innerHTML = originalHtml;
|
||||||
|
button.removeAttribute('aria-disabled');
|
||||||
|
button.removeAttribute('aria-busy');
|
||||||
|
}, restoreDelay);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
|||||||
@@ -37,11 +37,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid min-w-0 grid-cols-1 gap-2 sm:grid-cols-2 md:flex md:w-auto md:flex-nowrap">
|
<div class="grid min-w-0 grid-cols-1 gap-2 sm:grid-cols-2 md:flex md:w-auto md:flex-nowrap">
|
||||||
<a class="ui-btn ui-btn-compact ui-btn-secondary w-full px-3 md:w-auto" href="{{ url_for('main.download_file', record_id=record.id, file_type='original') }}">
|
<a class="ui-btn ui-btn-compact ui-btn-secondary w-full px-3 md:w-auto" href="{{ url_for('main.download_file', record_id=record.id, file_type='original') }}" data-download-action data-download-pending="下载中...">
|
||||||
<span class="material-symbols-outlined text-lg">description</span>
|
<span class="material-symbols-outlined text-lg">description</span>
|
||||||
原始文件
|
原始文件
|
||||||
</a>
|
</a>
|
||||||
<a class="ui-btn ui-btn-compact ui-btn-primary w-full px-3 md:w-auto" href="{{ url_for('main.download_file', record_id=record.id, file_type='prediction') }}">
|
<a class="ui-btn ui-btn-compact ui-btn-primary w-full px-3 md:w-auto" href="{{ url_for('main.download_file', record_id=record.id, file_type='prediction') }}" data-download-action data-download-pending="下载中...">
|
||||||
<span class="material-symbols-outlined text-lg">download</span>
|
<span class="material-symbols-outlined text-lg">download</span>
|
||||||
预测结果
|
预测结果
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@
|
|||||||
<h1 class="text-3xl font-extrabold tracking-tight sm:text-4xl">管道健康状态与剩余寿命评估</h1>
|
<h1 class="text-3xl font-extrabold tracking-tight sm:text-4xl">管道健康状态与剩余寿命评估</h1>
|
||||||
<p class="mt-2 max-w-3xl text-sm leading-6 text-textSub">上传标准数据文件,系统将生成管道的剩余寿命分析图、健康等级摘要、剩余寿命判断和电子表格预测报告。</p>
|
<p class="mt-2 max-w-3xl text-sm leading-6 text-textSub">上传标准数据文件,系统将生成管道的剩余寿命分析图、健康等级摘要、剩余寿命判断和电子表格预测报告。</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ url_for('main.download_template') }}" class="ui-btn ui-btn-secondary text-primary">
|
<a href="{{ url_for('main.download_template') }}" class="ui-btn ui-btn-secondary text-primary" data-download-action data-download-pending="下载中...">
|
||||||
<span class="material-symbols-outlined text-lg">download</span>
|
<span class="material-symbols-outlined text-lg">download</span>
|
||||||
下载数据模板
|
下载数据模板
|
||||||
</a>
|
</a>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
<span class="material-symbols-outlined text-lg">monitoring</span>
|
<span class="material-symbols-outlined text-lg">monitoring</span>
|
||||||
查看结果页
|
查看结果页
|
||||||
</a>
|
</a>
|
||||||
<a id="excelBtn" href="#" class="ui-btn ui-btn-sm ui-btn-primary">
|
<a id="excelBtn" href="#" class="ui-btn ui-btn-sm ui-btn-primary" data-download-action data-download-pending="下载中...">
|
||||||
<span class="material-symbols-outlined text-lg">download</span>
|
<span class="material-symbols-outlined text-lg">download</span>
|
||||||
下载结果表
|
下载结果表
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
+24
-2
@@ -44,6 +44,10 @@
|
|||||||
background: #fff7f7 !important;
|
background: #fff7f7 !important;
|
||||||
box-shadow: 0 0 0 3px rgba(220, 38, 38, .12) !important;
|
box-shadow: 0 0 0 3px rgba(220, 38, 38, .12) !important;
|
||||||
}
|
}
|
||||||
|
.captcha-token {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
.dot-grid {
|
.dot-grid {
|
||||||
background-image: radial-gradient(circle at 1px 1px, rgba(148,163,184,.30) 1.2px, transparent 0);
|
background-image: radial-gradient(circle at 1px 1px, rgba(148,163,184,.30) 1.2px, transparent 0);
|
||||||
background-size: 42px 42px;
|
background-size: 42px 42px;
|
||||||
@@ -188,7 +192,7 @@
|
|||||||
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 text-lg">verified_user</span>
|
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 text-lg">verified_user</span>
|
||||||
<input name="captcha" required data-field-label="验证码" class="w-full pl-11 pr-4 py-3.5 rounded-xl bg-[#eceff3] border border-transparent focus:border-primary focus:ring-0" placeholder="请输入验证码" />
|
<input name="captcha" required data-field-label="验证码" class="w-full pl-11 pr-4 py-3.5 rounded-xl bg-[#eceff3] border border-transparent focus:border-primary focus:ring-0" placeholder="请输入验证码" />
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl bg-blueSoft text-textMain border border-blue-100 h-[50px] flex items-center justify-center font-black tracking-[0.18em] italic">{{ captcha }}</div>
|
<div class="captcha-token rounded-xl bg-blueSoft text-textMain border border-blue-100 h-[50px] flex items-center justify-center font-black tracking-[0.18em] italic" aria-label="验证码" data-captcha-token>{{ captcha }}</div>
|
||||||
<a href="{{ url_for('main.login') }}" class="ui-btn ui-btn-field ui-btn-secondary px-0" aria-label="刷新验证码">
|
<a href="{{ url_for('main.login') }}" class="ui-btn ui-btn-field ui-btn-secondary px-0" aria-label="刷新验证码">
|
||||||
<span class="material-symbols-outlined">refresh</span>
|
<span class="material-symbols-outlined">refresh</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -232,7 +236,7 @@
|
|||||||
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 text-lg">verified_user</span>
|
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 text-lg">verified_user</span>
|
||||||
<input name="captcha" required data-field-label="验证码" {{ 'disabled' if not allow_registration }} class="w-full pl-11 pr-4 py-3.5 rounded-xl bg-[#eceff3] border border-transparent focus:border-primary focus:ring-0 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-400" placeholder="请输入验证码" />
|
<input name="captcha" required data-field-label="验证码" {{ 'disabled' if not allow_registration }} class="w-full pl-11 pr-4 py-3.5 rounded-xl bg-[#eceff3] border border-transparent focus:border-primary focus:ring-0 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-400" placeholder="请输入验证码" />
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl bg-blueSoft text-textMain border border-blue-100 h-[50px] flex items-center justify-center font-black tracking-[0.18em] italic">{{ captcha }}</div>
|
<div class="captcha-token rounded-xl bg-blueSoft text-textMain border border-blue-100 h-[50px] flex items-center justify-center font-black tracking-[0.18em] italic" aria-label="验证码" data-captcha-token>{{ captcha }}</div>
|
||||||
<a href="{{ url_for('main.register') }}" class="ui-btn ui-btn-field ui-btn-secondary px-0 {{ 'pointer-events-none opacity-50' if not allow_registration }}" aria-label="刷新验证码" aria-disabled="{{ 'true' if not allow_registration else 'false' }}">
|
<a href="{{ url_for('main.register') }}" class="ui-btn ui-btn-field ui-btn-secondary px-0 {{ 'pointer-events-none opacity-50' if not allow_registration }}" aria-label="刷新验证码" aria-disabled="{{ 'true' if not allow_registration else 'false' }}">
|
||||||
<span class="material-symbols-outlined">refresh</span>
|
<span class="material-symbols-outlined">refresh</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -423,6 +427,24 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-captcha-token]').forEach((token) => {
|
||||||
|
token.addEventListener('contextmenu', (event) => event.preventDefault());
|
||||||
|
token.addEventListener('selectstart', (event) => event.preventDefault());
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('copy', (event) => {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection || selection.rangeCount === 0) return;
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
const includesCaptcha = Array.from(document.querySelectorAll('[data-captcha-token]')).some((token) => {
|
||||||
|
return range.intersectsNode(token);
|
||||||
|
});
|
||||||
|
if (includesCaptcha) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('forgotPasswordBtn')?.addEventListener('click', () => {
|
document.getElementById('forgotPasswordBtn')?.addEventListener('click', () => {
|
||||||
showAppNotification('请联系管理员获取一次性重置链接后设置新密码。', 'info', '密码重置');
|
showAppNotification('请联系管理员获取一次性重置链接后设置新密码。', 'info', '密码重置');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<span class="material-symbols-outlined text-lg">refresh</span>
|
<span class="material-symbols-outlined text-lg">refresh</span>
|
||||||
重新分析
|
重新分析
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ result.excel_url }}" class="ui-btn ui-btn-primary">
|
<a href="{{ result.excel_url }}" class="ui-btn ui-btn-primary" data-download-action data-download-pending="导出中...">
|
||||||
<span class="material-symbols-outlined text-lg">download</span>
|
<span class="material-symbols-outlined text-lg">download</span>
|
||||||
导出结果表
|
导出结果表
|
||||||
</a>
|
</a>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<div class="flex justify-between gap-4"><dt class="text-textSub">样本数量</dt><dd class="font-bold">{{ result.sample_count }}</dd></div>
|
<div class="flex justify-between gap-4"><dt class="text-textSub">样本数量</dt><dd class="font-bold">{{ result.sample_count }}</dd></div>
|
||||||
<div class="flex justify-between gap-4"><dt class="text-textSub">输出文件</dt><dd class="font-bold">电子表格</dd></div>
|
<div class="flex justify-between gap-4"><dt class="text-textSub">输出文件</dt><dd class="font-bold">电子表格</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
<a href="{{ result.excel_url }}" class="ui-btn ui-btn-secondary mt-5 w-full text-primary">
|
<a href="{{ result.excel_url }}" class="ui-btn ui-btn-secondary mt-5 w-full text-primary" data-download-action data-download-pending="下载中...">
|
||||||
<span class="material-symbols-outlined text-lg">table_view</span>
|
<span class="material-symbols-outlined text-lg">table_view</span>
|
||||||
查看完整样本列表
|
查看完整样本列表
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user