diff --git a/app.py b/app.py index 4f240d2..ed55e6f 100644 --- a/app.py +++ b/app.py @@ -551,6 +551,28 @@ def login_required(f): return wrap +@app.route("/") +def index(): + if session.get("logged_in"): + return redirect(url_for("plans")) + return redirect(url_for("login")) + + +@app.route("/manifest.webmanifest") +def web_manifest(): + response = app.send_static_file("manifest.json") + response.mimetype = "application/manifest+json" + return response + + +@app.route("/sw.js") +def service_worker(): + response = app.send_static_file("sw.js") + response.headers["Cache-Control"] = "no-cache" + response.headers["Service-Worker-Allowed"] = "/" + return response + + @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": diff --git a/static/css/responsive.css b/static/css/responsive.css new file mode 100644 index 0000000..12c9970 --- /dev/null +++ b/static/css/responsive.css @@ -0,0 +1,489 @@ +/* 响应式布局 — 电脑 / 平板 / 手机 + PWA 独立窗口 */ + +:root { + --safe-top: env(safe-area-inset-top, 0px); + --safe-right: env(safe-area-inset-right, 0px); + --safe-bottom: env(safe-area-inset-bottom, 0px); + --safe-left: env(safe-area-inset-left, 0px); + --touch-min: 44px; +} + +html { + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; +} + +body { + padding-left: var(--safe-left); + padding-right: var(--safe-right); + padding-bottom: var(--safe-bottom); +} + +.page-wrap { + padding-top: var(--safe-top); +} + +.header-bar { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: .5rem .75rem; + margin-bottom: .85rem; + min-height: var(--touch-min); +} + +.nav-toggle { + display: none; + width: var(--touch-min); + height: var(--touch-min); + border: 1px solid var(--toggle-border); + border-radius: 10px; + background: var(--toggle-bg); + cursor: pointer; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; + padding: 0; + flex-shrink: 0; +} + +.nav-toggle span { + display: block; + width: 18px; + height: 2px; + background: var(--text-primary); + border-radius: 2px; + transition: transform .2s, opacity .2s; +} + +.nav-toggle[aria-expanded="true"] span:nth-child(1) { + transform: translateY(7px) rotate(45deg); +} + +.nav-toggle[aria-expanded="true"] span:nth-child(2) { + opacity: 0; +} + +.nav-toggle[aria-expanded="true"] span:nth-child(3) { + transform: translateY(-7px) rotate(-45deg); +} + +.header-tools { + position: static; + justify-content: flex-start; + flex-wrap: wrap; +} + +.user-bar { + position: static; + text-align: right; + justify-self: end; +} + +.pwa-install-btn { + padding: .38rem .7rem; + border-radius: 999px; + border: 1px solid var(--accent); + background: transparent; + color: var(--accent); + font-size: .72rem; + cursor: pointer; + white-space: nowrap; + width: auto; + flex-shrink: 0; + min-height: 32px; +} + +.pwa-install-btn:hover { + background: var(--dir-bg); +} + +.pwa-ios-hint { + display: none; + font-size: .72rem; + color: var(--text-muted); + padding: .5rem .75rem; + margin: 0 0 .75rem; + border-radius: 10px; + border: 1px dashed var(--card-border); + background: var(--card-inner); + line-height: 1.5; +} + +.pwa-ios-hint.show { + display: block; +} + +.nav-backdrop { + display: none; + position: fixed; + inset: 0; + background: var(--modal-mask); + z-index: 90; + border: none; + padding: 0; + cursor: pointer; +} + +.nav-backdrop.show { + display: block; +} + +@media (min-width: 1025px) { + .site-header { + padding: 1.5rem 1.5rem 1.25rem; + } + + .site-nav { + justify-content: center; + } + + .main { + padding: 1.5rem 1.75rem; + } +} + +@media (min-width: 768px) and (max-width: 1024px) { + .site-header { + padding: 1.25rem 1rem 1rem; + } + + .site-title { + font-size: 1.5rem; + margin-bottom: 1rem; + } + + .site-nav { + gap: .4rem; + justify-content: center; + } + + .site-nav a { + padding: .5rem .85rem; + font-size: .82rem; + } + + .main { + padding: 1.25rem 1rem; + } + + .card { + padding: 1.25rem; + } + + .split-grid { + grid-template-columns: 1fr; + } + + .form-compact .line-4, + .form-compact .line-5 { + grid-template-columns: repeat(2, 1fr); + } + + .form-compact .line-plan-2 { + grid-template-columns: repeat(2, 1fr); + } + + .pos-metrics { + grid-template-columns: repeat(2, 1fr); + } + + .review-detail-grid { + grid-template-columns: repeat(2, 1fr); + } + + .stat-grid-summary { + grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); + } +} + +@media (max-width: 767px) { + .nav-toggle { + display: inline-flex; + } + + .header-bar { + grid-template-columns: auto 1fr; + grid-template-areas: + "toggle tools" + "user user"; + } + + .nav-toggle { grid-area: toggle; } + .header-tools { grid-area: tools; justify-content: flex-end; } + .user-bar { + grid-area: user; + text-align: center; + width: 100%; + } + + .site-header { + padding: .85rem .75rem .75rem; + text-align: left; + } + + .site-title { + font-size: 1.15rem; + margin-bottom: .65rem; + text-align: center; + } + + .site-title-sub { + font-size: .58rem; + letter-spacing: .14em; + } + + .site-nav { + position: fixed; + top: 0; + left: 0; + width: min(86vw, 320px); + height: 100dvh; + flex-direction: column; + align-items: stretch; + justify-content: flex-start; + gap: .35rem; + padding: calc(var(--safe-top) + 3.5rem) 1rem 1.5rem; + background: var(--card-bg); + border-right: 1px solid var(--card-border); + box-shadow: var(--shadow-card-hover); + z-index: 100; + transform: translateX(-105%); + transition: transform .28s ease; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + + .site-nav.open { + transform: translateX(0); + } + + .site-nav a { + width: 100%; + text-align: left; + padding: .75rem 1rem; + font-size: .9rem; + min-height: var(--touch-min); + display: flex; + align-items: center; + border-radius: 10px; + } + + .main { + padding: .85rem .75rem 1.25rem; + } + + .card { + padding: 1rem; + border-radius: 12px; + margin-bottom: 1rem; + } + + .card h2 { + font-size: 1rem; + } + + .form-compact .line-2, + .form-compact .line-3, + .form-compact .line-4, + .form-compact .line-5, + .form-compact .line-plan-1, + .form-compact .line-plan-2 { + grid-template-columns: 1fr; + } + + .form-compact-review .tag-grid { + grid-template-columns: repeat(2, 1fr); + } + + .form-compact-review .kline-row { + grid-template-columns: 1fr 1fr; + } + + .form-grid { + grid-template-columns: 1fr; + } + + .split-grid { + grid-template-columns: 1fr; + gap: 1rem; + } + + .split-grid .card { + min-height: auto; + } + + .pos-metrics { + grid-template-columns: repeat(2, 1fr); + } + + .review-detail-grid { + grid-template-columns: 1fr; + } + + .review-detail-item.wide { + grid-column: span 1; + } + + .stat-grid, + .stat-grid-summary { + grid-template-columns: repeat(2, 1fr); + gap: .65rem; + } + + .stat-item { + padding: .75rem .5rem; + } + + .stat-item .value { + font-size: 1.1rem; + } + + .filter-row .field { + width: 100%; + min-width: 0; + } + + .trade-toolbar { + flex-direction: column; + align-items: stretch; + } + + .profile-row { + grid-template-columns: 1fr; + gap: .25rem; + } + + .modal-box { + padding: 1rem; + border-radius: 12px; + max-height: calc(100dvh - 1rem); + } + + .modal-box.review-modal-fullscreen { + width: 100%; + height: 100dvh; + border-radius: 0; + } + + th, td { + padding: .55rem .45rem; + font-size: .8rem; + } + + .card-scroll { + max-height: none; + } + + .stats-card-head { + flex-direction: column; + align-items: stretch; + } + + .stats-view-field { + width: 100%; + } + + input, select, textarea, button { + font-size: 16px; + } + + .form-compact input, + .form-compact select { + font-size: 16px; + } +} + +@media (max-width: 479px) { + .stat-grid, + .stat-grid-summary { + grid-template-columns: 1fr 1fr; + } + + .theme-switch-btn { + padding: .35rem .55rem; + font-size: .7rem; + } + + .pos-metrics { + grid-template-columns: 1fr; + } +} + +@media (display-mode: standalone) { + .site-header { + padding-top: max(.75rem, var(--safe-top)); + } + + .pwa-install-btn, + .pwa-ios-hint { + display: none !important; + } +} + +@media (max-width: 767px) and (orientation: landscape) { + .site-nav { + width: min(50vw, 280px); + padding-top: calc(var(--safe-top) + 2.5rem); + } +} + +@media (hover: none) and (pointer: coarse) { + .site-nav a, + .btn-del, + .trade-actions a, + .trade-actions button, + .preset-tabs a { + min-height: var(--touch-min); + } + + .card:hover { + transform: none; + } + + .list-item:hover { + box-shadow: none; + } + + .stat-item:hover { + transform: none; + } +} + +.table-responsive { + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.table-responsive table { + min-width: 560px; +} + +body.login-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + min-height: 100dvh; + padding: 1rem; + padding-top: max(1rem, var(--safe-top)); + padding-bottom: max(1rem, var(--safe-bottom)); +} + +@media (max-width: 767px) { + body.login-page { + align-items: flex-start; + padding: .75rem; + padding-top: max(.75rem, var(--safe-top)); + } + + body.login-page .login-wrap { + max-width: 100%; + } + + body.login-page .login-box { + padding: 1.75rem 1.25rem 1.5rem; + } +} diff --git a/static/icons/icon-192.png b/static/icons/icon-192.png new file mode 100644 index 0000000..d12d4ad Binary files /dev/null and b/static/icons/icon-192.png differ diff --git a/static/icons/icon-512.png b/static/icons/icon-512.png new file mode 100644 index 0000000..473ab5a Binary files /dev/null and b/static/icons/icon-512.png differ diff --git a/static/icons/icon.svg b/static/icons/icon.svg new file mode 100644 index 0000000..22959c3 --- /dev/null +++ b/static/icons/icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/static/js/nav.js b/static/js/nav.js new file mode 100644 index 0000000..b1f7c1d --- /dev/null +++ b/static/js/nav.js @@ -0,0 +1,53 @@ +(function () { + var toggle = document.getElementById('nav-toggle'); + var nav = document.getElementById('site-nav'); + var backdrop = document.getElementById('nav-backdrop'); + if (!toggle || !nav) return; + + function openNav() { + nav.classList.add('open'); + if (backdrop) { + backdrop.hidden = false; + backdrop.classList.add('show'); + } + toggle.setAttribute('aria-expanded', 'true'); + document.body.style.overflow = 'hidden'; + } + + function closeNav() { + nav.classList.remove('open'); + if (backdrop) { + backdrop.classList.remove('show'); + backdrop.hidden = true; + } + toggle.setAttribute('aria-expanded', 'false'); + document.body.style.overflow = ''; + } + + function isMobileNav() { + return window.matchMedia('(max-width: 767px)').matches; + } + + toggle.addEventListener('click', function () { + if (nav.classList.contains('open')) closeNav(); + else openNav(); + }); + + if (backdrop) { + backdrop.addEventListener('click', closeNav); + } + + nav.querySelectorAll('a').forEach(function (link) { + link.addEventListener('click', function () { + if (isMobileNav()) closeNav(); + }); + }); + + window.addEventListener('resize', function () { + if (!isMobileNav()) closeNav(); + }); + + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') closeNav(); + }); +})(); diff --git a/static/js/pwa.js b/static/js/pwa.js new file mode 100644 index 0000000..8f8f5bc --- /dev/null +++ b/static/js/pwa.js @@ -0,0 +1,76 @@ +(function () { + var deferredPrompt = null; + var installBtn = document.getElementById('pwa-install-btn'); + var iosHint = document.getElementById('pwa-ios-hint'); + + function isStandalone() { + return window.matchMedia('(display-mode: standalone)').matches + || window.navigator.standalone === true; + } + + function isIOS() { + return /iPad|iPhone|iPod/.test(navigator.userAgent) + && !window.MSStream; + } + + function updateThemeColor() { + var meta = document.getElementById('meta-theme-color'); + if (!meta) return; + var theme = document.documentElement.getAttribute('data-theme'); + meta.setAttribute('content', theme === 'light' ? '#e8eef8' : '#050508'); + } + + function showInstallBtn() { + if (installBtn && !isStandalone()) { + installBtn.hidden = false; + } + } + + function showIosHint() { + if (iosHint && isIOS() && !isStandalone()) { + iosHint.classList.add('show'); + } + } + + if ('serviceWorker' in navigator) { + window.addEventListener('load', function () { + navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch(function () { /* ignore */ }); + }); + } + + window.addEventListener('beforeinstallprompt', function (e) { + e.preventDefault(); + deferredPrompt = e; + showInstallBtn(); + }); + + if (installBtn) { + installBtn.addEventListener('click', function () { + if (!deferredPrompt) return; + deferredPrompt.prompt(); + deferredPrompt.userChoice.then(function () { + deferredPrompt = null; + installBtn.hidden = true; + }); + }); + } + + window.addEventListener('appinstalled', function () { + deferredPrompt = null; + if (installBtn) installBtn.hidden = true; + if (iosHint) iosHint.classList.remove('show'); + }); + + document.addEventListener('DOMContentLoaded', function () { + updateThemeColor(); + showIosHint(); + if (!isStandalone() && !deferredPrompt && installBtn) { + installBtn.hidden = true; + } + }); + + document.addEventListener('click', function (e) { + var pick = e.target.closest('[data-theme-pick]'); + if (pick) setTimeout(updateThemeColor, 80); + }); +})(); diff --git a/static/manifest.json b/static/manifest.json new file mode 100644 index 0000000..a2d5c1b --- /dev/null +++ b/static/manifest.json @@ -0,0 +1,33 @@ +{ + "name": "国内期货交易监控复盘系统", + "short_name": "期货监控", + "description": "期货交易监控、持仓管理、复盘与统计分析", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "any", + "background_color": "#050508", + "theme_color": "#050508", + "lang": "zh-CN", + "categories": ["finance", "productivity"], + "icons": [ + { + "src": "/static/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/static/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/static/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/static/sw.js b/static/sw.js new file mode 100644 index 0000000..7fb3f4b --- /dev/null +++ b/static/sw.js @@ -0,0 +1,61 @@ +var CACHE_VERSION = 'qihuo-v2'; +var STATIC_CACHE = CACHE_VERSION + '-static'; +var STATIC_ASSETS = [ + '/static/css/tech.css', + '/static/css/responsive.css', + '/static/js/theme.js', + '/static/js/nav.js', + '/static/js/pwa.js', + '/static/js/symbol.js', + '/static/icons/icon-192.png', + '/static/icons/icon-512.png', + '/static/manifest.json', + '/login' +]; + +self.addEventListener('install', function (event) { + event.waitUntil( + caches.open(STATIC_CACHE).then(function (cache) { + return cache.addAll(STATIC_ASSETS).catch(function () { /* ignore partial */ }); + }).then(function () { return self.skipWaiting(); }) + ); +}); + +self.addEventListener('activate', function (event) { + event.waitUntil( + caches.keys().then(function (keys) { + return Promise.all(keys.filter(function (k) { + return k.startsWith('qihuo-') && k !== STATIC_CACHE; + }).map(function (k) { return caches.delete(k); })); + }).then(function () { return self.clients.claim(); }) + ); +}); + +self.addEventListener('fetch', function (event) { + var req = event.request; + if (req.method !== 'GET') return; + + var url = new URL(req.url); + if (url.origin !== self.location.origin) return; + + if (url.pathname.indexOf('/static/') === 0) { + event.respondWith( + caches.match(req).then(function (cached) { + return cached || fetch(req).then(function (res) { + var copy = res.clone(); + caches.open(STATIC_CACHE).then(function (cache) { cache.put(req, copy); }); + return res; + }); + }) + ); + return; + } + + if (req.mode === 'navigate' || (req.headers.get('accept') || '').indexOf('text/html') !== -1) { + event.respondWith( + fetch(req).catch(function () { + return caches.match('/login'); + }) + ); + } +}); diff --git a/templates/base.html b/templates/base.html index 5e53117..0c6da80 100644 --- a/templates/base.html +++ b/templates/base.html @@ -2,7 +2,16 @@ - + + + + + + + + + + {% block title %}国内期货监控系统{% endblock %} + + {% block extra_js %}{% endblock %} diff --git a/templates/login.html b/templates/login.html index 0d367ac..aeb265f 100644 --- a/templates/login.html +++ b/templates/login.html @@ -2,7 +2,15 @@ - + + + + + + + + + 系统登录