Files
qihuo/templates/settings.html
T
dekun 382a9a0e14 Use Sina-only market K-lines and editable admin login synced to .env.
Market page uses Sina for quotes and bars with an auto-follow toggle and incremental chart updates while panning. Settings lets users change username and password, persisting to the database and .env.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 13:53:12 +08:00

575 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{# Copyright (c) 2025-2026 马建军. All rights reserved. 专有软件,详见 LICENSE.zh-CN.txt #}
{% extends "base.html" %}
{% block title %}系统设置 - 国内期货 · 交易复盘系统{% endblock %}
{% block extra_css %}
<style>
.settings-page{display:flex;flex-direction:column;gap:1.25rem}
.settings-page .split-grid{margin-bottom:0}
.settings-page .split-grid .card{margin-bottom:0;min-height:100%;height:100%;display:flex;flex-direction:column}
.settings-page .split-grid .card > form,
.settings-page .split-grid .card > .card-inner,
.settings-page .split-grid .settings-fold-body > form,
.settings-page .split-grid .settings-fold-body > .card-inner{flex:1;display:flex;flex-direction:column}
.settings-password-form{display:grid;grid-template-columns:1fr 1fr;gap:.65rem .75rem}
.settings-password-form .field-full{grid-column:1/-1}
.settings-password-form .field label{font-size:.78rem}
.settings-password-form input{padding:.55rem .7rem;font-size:.85rem}
.settings-tips{flex:1;display:flex;flex-direction:column;justify-content:center;gap:.5rem;margin:0;padding:0;list-style:none;font-size:.85rem;color:var(--text-muted);line-height:1.55}
.settings-tips li{padding-left:1rem;position:relative}
.settings-tips li::before{content:"";position:absolute;left:0;top:.55em;width:5px;height:5px;border-radius:50%;background:var(--accent)}
.settings-ctp-grid{display:grid;grid-template-columns:1fr 1fr;gap:.65rem .75rem}
.settings-ctp-grid .field-full{grid-column:1/-1}
.settings-ctp-wrap .card-body{padding-top:0}
.settings-ctp-cards-row{
display:grid;grid-template-columns:1fr 1fr;gap:.75rem;
align-items:start;margin-bottom:.75rem;
}
.settings-ctp-cards-row .settings-ctp-fold.card{margin-bottom:0;height:100%}
.settings-ctp-cards-row .settings-ctp-grid{
grid-template-columns:repeat(6,minmax(0,1fr));
gap:.5rem .6rem;
}
.settings-ctp-cards-row .settings-ctp-grid .field{grid-column:span 2}
.settings-ctp-cards-row .settings-ctp-grid .field-ctp-front-span{grid-column:span 3}
.settings-ctp-cards-row .settings-ctp-grid .field label{font-size:.75rem}
.settings-ctp-cards-row .settings-ctp-grid input,
.settings-ctp-cards-row .settings-ctp-grid select{
padding:.45rem .55rem;font-size:.8rem;
}
.settings-ctp-fold.card{
margin-bottom:.75rem;padding:0;overflow:hidden;
border:1px solid var(--border);border-radius:8px;background:var(--card-inner);
}
.settings-ctp-fold.card:last-of-type{margin-bottom:0}
.settings-ctp-fold-head{
width:100%;display:flex;align-items:center;justify-content:space-between;gap:.75rem;
padding:.7rem 1rem;margin:0;border:none;background:transparent;cursor:pointer;
font-size:.92rem;font-weight:600;color:var(--text-title);text-align:left;
}
.settings-ctp-fold-head:hover{color:var(--accent)}
.settings-ctp-fold-title{display:flex;align-items:center;gap:.5rem}
.settings-ctp-fold-chevron{
flex-shrink:0;font-size:.72rem;color:var(--text-muted);
transition:transform .2s ease;
}
.settings-ctp-fold.is-collapsed .settings-ctp-fold-chevron{transform:rotate(-90deg)}
.settings-ctp-fold-body{padding:0 1rem .85rem}
.settings-ctp-fold.is-collapsed .settings-ctp-fold-body{display:none}
.settings-ctp-status{font-size:.82rem;color:var(--text-muted);margin-top:.75rem;line-height:1.5}
.settings-backup-table{width:100%;border-collapse:collapse;font-size:.82rem;margin-top:.65rem}
.settings-backup-table th,.settings-backup-table td{padding:.45rem .5rem;border-bottom:1px solid var(--border);text-align:left}
.settings-backup-table th{color:var(--text-muted);font-weight:600}
.settings-backup-restore{
margin-top:.85rem;padding:.75rem .85rem;border-radius:8px;
border:1px solid var(--border);background:var(--card-inner);
font-size:.82rem;color:var(--text-muted);line-height:1.6;
}
.settings-backup-restore summary{cursor:pointer;color:var(--text-title);font-weight:600}
.settings-backup-meta{font-size:.82rem;color:var(--text-muted);line-height:1.55;margin:.35rem 0 .65rem}
.settings-backup-actions{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem .65rem}
.settings-backup-download{color:var(--accent);text-decoration:none;font-weight:600}
.settings-backup-download:hover{text-decoration:underline}
.settings-admin-row .settings-compact-card{font-size:.78rem}
.settings-admin-row .settings-compact-card .hint,
.settings-admin-row .settings-backup-meta,
.settings-admin-row .settings-backup-restore{font-size:.72rem;line-height:1.5}
.settings-admin-row .settings-backup-table{font-size:.7rem}
.settings-admin-row .settings-backup-table th,
.settings-admin-row .settings-backup-table td{padding:.35rem .4rem}
.settings-admin-row .settings-backup-table td:first-child code{word-break:break-all;font-size:.68rem}
.settings-admin-row .field label{font-size:.72rem}
.settings-admin-row .field input{padding:.4rem .55rem;font-size:.78rem}
.settings-admin-row .settings-backup-config{display:grid;grid-template-columns:1fr;gap:.45rem;margin-bottom:.55rem}
.settings-admin-row .settings-backup-actions{margin-top:.35rem}
.settings-admin-row .settings-backup-actions .btn-primary,
.settings-admin-row .settings-compact-card > form .btn-primary{padding:.42rem .7rem;font-size:.78rem}
.settings-admin-row .settings-password-form{grid-template-columns:1fr;gap:.45rem .55rem}
.settings-admin-row .settings-password-form input{padding:.4rem .55rem;font-size:.78rem}
.settings-page .settings-fold.card{padding:0;overflow:hidden}
.settings-page .split-grid .settings-fold.card{min-height:auto;height:auto}
.settings-fold-head{
width:100%;display:flex;align-items:center;justify-content:space-between;gap:.75rem;
padding:1rem 1rem .85rem;margin:0;border:none;background:transparent;cursor:pointer;
text-align:left;
}
.settings-fold-head:hover .settings-fold-title{color:var(--accent)}
.settings-fold-title{
display:flex;align-items:center;gap:.5rem;font-size:1.15rem;font-weight:600;
color:var(--text-label);letter-spacing:.03em;
}
.settings-fold-title:before{
content:"";width:4px;height:16px;flex-shrink:0;
background:linear-gradient(180deg,var(--accent),var(--accent-2));
border-radius:2px;box-shadow:0 0 8px var(--card-glow);
}
.settings-fold-chevron{flex-shrink:0;font-size:.72rem;color:var(--text-muted);transition:transform .2s ease}
.settings-fold.is-collapsed .settings-fold-chevron{transform:rotate(-90deg)}
.settings-fold-body{padding:0 1rem 1rem;flex:1;display:flex;flex-direction:column}
.settings-fold.is-collapsed .settings-fold-body{display:none}
.settings-admin-row .settings-fold-head{padding:.75rem .85rem .6rem}
.settings-admin-row .settings-fold-title{font-size:.95rem}
.settings-admin-row .settings-fold-body{padding:0 .85rem .85rem}
@media(max-width:900px){
.settings-admin-row{grid-template-columns:1fr}
.settings-password-form{grid-template-columns:1fr}
.settings-ctp-cards-row{grid-template-columns:1fr}
.settings-ctp-grid{grid-template-columns:1fr}
.settings-ctp-cards-row .settings-ctp-grid .field,
.settings-ctp-cards-row .settings-ctp-grid .field-ctp-front-span{grid-column:span 1}
}
</style>
{% endblock %}
{% block content %}
{% macro settings_card(key, title, extra_class='') %}
<div class="card settings-fold is-collapsed {{ extra_class }}" data-settings-fold="{{ key }}">
<button type="button" class="settings-fold-head" aria-expanded="false">
<span class="settings-fold-title">{{ title }}</span>
<span class="settings-fold-chevron" aria-hidden="true"></span>
</button>
<div class="settings-fold-body">
{{ caller() }}
</div>
</div>
{% endmacro %}
<div class="settings-page">
<div class="split-grid">
{% call settings_card('nav', '导航显示') %}
<form action="{{ url_for('settings') }}" method="post">
<input type="hidden" name="action" value="nav">
<p class="hint" style="margin-bottom:.75rem">关闭后顶栏隐藏对应入口,直接访问 URL 也会跳转回下单监控。</p>
<div class="check-row">
{% for key, label in nav_toggles.items() %}
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;white-space:nowrap">
<input type="checkbox" name="nav_{{ key }}" {% if nav_items[key] %}checked{% endif %}>
<span>{{ label }}</span>
</label>
{% endfor %}
</div>
<button type="submit" class="btn-primary" style="margin-top:.75rem">保存导航</button>
</form>
{% endcall %}
{% call settings_card('trading', '交易模式') %}
<form action="{{ url_for('settings') }}" method="post">
<input type="hidden" name="action" value="trading">
<div class="form-grid">
<div class="field">
<label>交易通道</label>
<select name="trading_mode">
<option value="simulation" {% if trading_mode == 'simulation' %}selected{% endif %}>SimNowvnpy CTP</option>
<option value="live" {% if trading_mode == 'live' %}selected{% endif %}>期货公司 CTP(后期接入)</option>
</select>
</div>
<div class="field">
<label>计仓模式</label>
<select name="position_sizing_mode" id="position-sizing-mode">
<option value="fixed" {% if position_sizing_mode == 'fixed' %}selected{% endif %}>固定手数</option>
<option value="amount" {% if position_sizing_mode in ('amount', 'risk') %}selected{% endif %}>固定金额</option>
</select>
</div>
<div class="field" id="field-fixed-lots" {% if position_sizing_mode in ('amount', 'risk') %}hidden{% endif %}>
<label>固定手数(手)</label>
<input name="fixed_lots" type="number" step="1" min="1" value="{{ fixed_lots }}">
</div>
<div class="field" id="field-fixed-amount" {% if position_sizing_mode not in ('amount', 'risk') %}hidden{% endif %}>
<label>固定金额(元)</label>
<input name="fixed_amount" type="number" step="1" min="1" value="{{ fixed_amount }}">
</div>
<div class="field">
<label>保证金占用上限(%</label>
<input name="max_margin_pct" type="number" step="1" min="1" max="100" value="{{ max_margin_pct }}">
</div>
<div class="field">
<label>移动保本缓冲(最小变动价位倍数)</label>
<input name="trailing_be_tick_buffer" type="number" step="1" min="1" max="20" value="{{ trailing_be_tick_buffer }}">
</div>
<div class="field">
<label>开仓挂单超时(分钟)</label>
<input name="pending_order_timeout_min" type="number" step="1" min="1" max="60" value="{{ pending_order_timeout_min }}">
</div>
</div>
<button type="submit" class="btn-primary" style="margin-top:.75rem">保存交易设置</button>
<p class="hint" style="margin-top:.75rem;margin-bottom:0">
保证金上限用于开仓校验与品种最大手数估算(默认 30%)。<strong>移动保本</strong>:达 1R 后止损移至开仓价 ± N 跳。
<strong>挂单超时</strong>:限价开仓未成交时,超过设定分钟数自动向柜台撤单(1~60 分钟)。CTP 账号与前置在下方「CTP 连接」中配置。
</p>
</form>
{% endcall %}
</div>
{% call settings_card('ctp', 'CTP 连接', 'settings-ctp-wrap') %}
<p class="hint" style="margin-bottom:.85rem">
投资者代码、密码、前置地址在此维护(优先于 <code>.env</code>)。保存后将自动断开并用新地址重连 CTP。
{% if ctp_status.connected %}
<span class="badge profit" style="margin-left:.35rem">已连接</span>
{% elif ctp_status.connecting %}
<span class="badge planned" style="margin-left:.35rem">连接中</span>
{% elif ctp_status.last_error %}
<span class="text-loss" style="display:block;margin-top:.35rem">{{ ctp_status.last_error }}</span>
{% endif %}
</p>
<form action="{{ url_for('settings') }}" method="post" id="ctp-settings-form">
<input type="hidden" name="action" value="ctp">
<div class="settings-ctp-cards-row">
<div class="settings-ctp-fold card is-collapsed" data-ctp-fold="simnow">
<button type="button" class="settings-ctp-fold-head" aria-expanded="false">
<span class="settings-ctp-fold-title">
SimNow 模拟盘
{% if trading_mode == 'simulation' %}<span class="badge planned" style="font-size:.7rem">当前通道</span>{% endif %}
</span>
<span class="settings-ctp-fold-chevron" aria-hidden="true"></span>
</button>
<div class="settings-ctp-fold-body">
<div class="settings-ctp-grid">
<div class="field">
<label>投资者代码</label>
<input name="simnow_user" value="{{ ctp_cfg.simnow_user }}" placeholder="非手机号">
</div>
<div class="field">
<label>交易密码</label>
<input id="simnow_password" name="simnow_password" type="password"
autocomplete="off" spellcheck="false"
placeholder="{% if ctp_cfg.simnow_password_set %}已设置:须重新输入才会更新{% else %}SimNow 交易密码(必填){% endif %}">
<p class="hint" style="margin:.25rem 0 0;font-size:.75rem">
与快期相同密码,保存前须在此<strong>手打</strong>;留空则不改。下方「修改密码」是网页登录密码,不是 SimNow。
</p>
</div>
<div class="field">
<label>经纪商代码</label>
<input name="simnow_broker_id" value="{{ ctp_cfg.simnow_broker_id }}">
</div>
<div class="field">
<label>柜台环境</label>
<select name="simnow_env">
<option value="实盘" {% if ctp_cfg.simnow_env == '实盘' %}selected{% endif %}>实盘(看穿式,推荐)</option>
<option value="测试" {% if ctp_cfg.simnow_env == '测试' %}selected{% endif %}>测试</option>
</select>
</div>
<div class="field">
<label>AppID</label>
<input name="simnow_app_id" value="{{ ctp_cfg.simnow_app_id }}">
</div>
<div class="field">
<label>授权编码</label>
<input name="simnow_auth_code" value="{{ ctp_cfg.simnow_auth_code }}">
</div>
<div class="field field-ctp-front-span">
<label>行情前置</label>
<input name="simnow_md_address" value="{{ ctp_cfg.simnow_md_address }}" placeholder="tcp://180.168.146.187:10211">
</div>
<div class="field field-ctp-front-span">
<label>交易前置</label>
<input name="simnow_td_address" value="{{ ctp_cfg.simnow_td_address }}" placeholder="tcp://180.168.146.187:10201">
</div>
</div>
</div>
</div>
<div class="settings-ctp-fold card is-collapsed" data-ctp-fold="live">
<button type="button" class="settings-ctp-fold-head" aria-expanded="false">
<span class="settings-ctp-fold-title">
期货公司实盘
{% if trading_mode == 'live' %}<span class="badge planned" style="font-size:.7rem">当前通道</span>{% endif %}
</span>
<span class="settings-ctp-fold-chevron" aria-hidden="true"></span>
</button>
<div class="settings-ctp-fold-body">
<div class="settings-ctp-grid">
<div class="field">
<label>投资者代码</label>
<input name="ctp_live_user" value="{{ ctp_cfg.ctp_live_user }}">
</div>
<div class="field">
<label>交易密码</label>
<input name="ctp_live_password" type="password" autocomplete="new-password"
placeholder="{% if ctp_cfg.ctp_live_password_set %}已设置,留空不修改{% else %}实盘密码{% endif %}">
</div>
<div class="field">
<label>经纪商代码</label>
<input name="ctp_live_broker_id" value="{{ ctp_cfg.ctp_live_broker_id }}">
</div>
<div class="field">
<label>柜台环境</label>
<select name="ctp_live_env">
<option value="实盘" {% if ctp_cfg.ctp_live_env == '实盘' %}selected{% endif %}>实盘</option>
<option value="测试" {% if ctp_cfg.ctp_live_env == '测试' %}selected{% endif %}>测试</option>
</select>
</div>
<div class="field">
<label>AppID</label>
<input name="ctp_live_app_id" value="{{ ctp_cfg.ctp_live_app_id }}">
</div>
<div class="field">
<label>授权编码</label>
<input name="ctp_live_auth_code" value="{{ ctp_cfg.ctp_live_auth_code }}">
</div>
<div class="field field-ctp-front-span">
<label>行情前置</label>
<input name="ctp_live_md_address" value="{{ ctp_cfg.ctp_live_md_address }}" placeholder="tcp://...">
</div>
<div class="field field-ctp-front-span">
<label>交易前置</label>
<input name="ctp_live_td_address" value="{{ ctp_cfg.ctp_live_td_address }}" placeholder="tcp://...">
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="btn-primary">保存 CTP 配置</button>
<p class="settings-ctp-status">
官方第一套:<code>180.168.146.187:10201/10211</code>
第二套(云服务器常用):<code>182.254.243.31:30001/30011</code>
7×24<code>182.254.243.31:40001/40011</code>(部分账号在 40001 会报「不合法登录」,与快期前置保持一致)。
详见 <code>docs/SIMNOW.md</code>
</p>
</form>
{% endcall %}
<div class="split-grid">
{% call settings_card('quote', '行情说明') %}
<div class="card-inner">
<p class="hint" style="font-size:.88rem;line-height:1.6;margin:0">
当前行情源:<strong class="text-accent">{{ quote_label }}</strong><br>
CTP 已连接时使用<strong>柜台行情</strong>;未连接时回退新浪接口。<br>
合约代码按同花顺格式(如 ag2608、IF2606)。
</p>
</div>
{% endcall %}
{% call settings_card('wechat', '企业微信推送') %}
<form action="{{ url_for('settings') }}" method="post">
<input type="hidden" name="action" value="wechat">
<div class="field" style="margin-bottom:.75rem">
<label>Webhook 地址</label>
<input name="wechat_webhook" type="url" placeholder="https://qyapi.weixin.qq.com/..." value="{{ webhook }}">
</div>
<button type="submit" class="btn-primary">保存</button>
<p class="hint" style="margin-top:.75rem;margin-bottom:0">在企业微信群添加机器人后,粘贴 Webhook 地址保存。</p>
</form>
{% endcall %}
</div>
<div class="split-grid settings-admin-row">
{% call settings_card('backup', '数据备份与恢复', 'settings-compact-card') %}
<p class="settings-backup-meta">
自动备份目录:<code>{{ backup_dir }}</code>
{% if backup_last_at %} · 上次备份 {{ backup_last_at.replace('T', ' ') }}{% else %} · 尚未备份{% endif %}
{% if backup_running %} · <span style="color:var(--accent)">备份进行中…</span>{% endif %}
</p>
<form action="{{ url_for('settings') }}" method="post" style="margin-bottom:.55rem">
<input type="hidden" name="action" value="backup_config">
<div class="settings-backup-config">
<div class="field">
<label style="display:flex;align-items:center;gap:.45rem;cursor:pointer">
<input type="checkbox" name="backup_auto_enabled" value="1" {% if backup_auto_enabled %}checked{% endif %}>
<span>启用每日自动备份</span>
</label>
</div>
<div class="field">
<label>自动备份时刻(023 点)</label>
<input name="backup_auto_hour" type="number" min="0" max="23" step="1" value="{{ backup_auto_hour }}">
</div>
<div class="field">
<label>保留最近份数</label>
<input name="backup_keep_count" type="number" min="5" max="200" step="1" value="{{ backup_keep_count }}">
</div>
</div>
<button type="submit" class="btn-primary">保存备份策略</button>
</form>
<div class="settings-backup-actions">
<form action="{{ url_for('settings') }}" method="post">
<input type="hidden" name="action" value="backup_now">
<button type="submit" class="btn-primary" {% if backup_running %}disabled{% endif %}>立即备份</button>
</form>
</div>
<p class="hint" style="margin:.5rem 0 0">备份含 <code>futures.db</code><code>uploads/</code>,默认恢复至 <code>{{ backup_restore_dir }}</code></p>
{% if backup_items %}
<table class="settings-backup-table">
<thead>
<tr><th>文件名</th><th>大小</th><th>时间</th><th></th></tr>
</thead>
<tbody>
{% for item in backup_items %}
<tr>
<td><code>{{ item.name }}</code></td>
<td>{{ item.size_mb }} MB</td>
<td>{{ item.mtime.replace('T', ' ')[:16] }}</td>
<td><a href="{{ url_for('api_backup_download', filename=item.name) }}" class="settings-backup-download">下载</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="hint" style="margin-top:.5rem;margin-bottom:0">暂无备份,可点「立即备份」。</p>
{% endif %}
<details class="settings-backup-restore">
<summary>备份恢复说明</summary>
<ol style="margin:.5rem 0 0 1rem;padding:0">
<li>下载 <code>.tar.gz</code> 到目标服务器(如 <code>/root/</code>)。</li>
<li>解压:<code>tar -xzf qihuo_backup_*.tar.gz</code></li>
<li>执行:<code>chmod +x restore.sh &amp;&amp; ./restore.sh</code></li>
<li>指定目录:<code>RESTORE_DIR=/opt/qihuo ./restore.sh</code></li>
<li>部署代码、配置 <code>.env</code> 后重启服务。</li>
</ol>
</details>
{% endcall %}
{% call settings_card('password', '登录账号', 'settings-compact-card') %}
<form action="{{ url_for('settings') }}" method="post" class="settings-password-form">
<input type="hidden" name="action" value="password">
<div class="field field-full">
<label>用户名</label>
<input name="admin_username" type="text" value="{{ username }}" required maxlength="64"
pattern="[A-Za-z0-9_.@-]+" autocomplete="username">
</div>
<div class="field field-full">
<label>原密码</label>
<input name="old_password" type="password" required autocomplete="current-password">
</div>
<div class="field">
<label>新密码</label>
<input name="new_password" type="password" minlength="6" placeholder="留空则不修改" autocomplete="new-password">
</div>
<div class="field">
<label>确认新密码</label>
<input name="new_password2" type="password" minlength="6" placeholder="修改密码时填写" autocomplete="new-password">
</div>
<div class="field-full">
<button type="submit" class="btn-primary">保存账号</button>
</div>
<p class="hint" style="margin:.45rem 0 0;font-size:.72rem">保存后写入数据库,并同步至 <code>.env</code><code>ADMIN_USERNAME</code> / <code>ADMIN_PASSWORD</code></p>
</form>
{% endcall %}
</div>
<div class="split-grid">
{% call settings_card('tips', '使用提示') %}
<ul class="settings-tips">
<li>下单监控:连接 CTP 后下单、看持仓与可开仓品种</li>
<li>策略交易:趋势回调自动补仓;顺势加仓需先开仓</li>
<li>手续费:默认 CTP 柜台费率,连接后点同步</li>
<li>手机端:浏览器菜单可「添加到主屏幕」安装 App</li>
</ul>
{% endcall %}
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
(function () {
var sel = document.getElementById('position-sizing-mode');
var lotsField = document.getElementById('field-fixed-lots');
var amountField = document.getElementById('field-fixed-amount');
function syncSizingFields() {
if (!sel) return;
var isAmount = sel.value === 'amount';
if (lotsField) lotsField.hidden = isAmount;
if (amountField) amountField.hidden = !isAmount;
}
if (sel) sel.addEventListener('change', syncSizingFields);
syncSizingFields();
var SETTINGS_FOLD_KEY = 'qihuo_settings_fold';
function setSettingsFold(el, collapsed) {
if (!el) return;
el.classList.toggle('is-collapsed', collapsed);
var head = el.querySelector('.settings-fold-head');
if (head) head.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
}
function saveSettingsFoldState() {
var state = {};
document.querySelectorAll('[data-settings-fold]').forEach(function (el) {
state[el.getAttribute('data-settings-fold')] = el.classList.contains('is-collapsed');
});
try { localStorage.setItem(SETTINGS_FOLD_KEY, JSON.stringify(state)); } catch (e) { /* ignore */ }
}
function loadSettingsFoldState() {
try {
var raw = localStorage.getItem(SETTINGS_FOLD_KEY);
if (!raw) return;
var state = JSON.parse(raw);
document.querySelectorAll('[data-settings-fold]').forEach(function (el) {
var key = el.getAttribute('data-settings-fold');
if (Object.prototype.hasOwnProperty.call(state, key)) {
setSettingsFold(el, !!state[key]);
}
});
} catch (e) { /* ignore */ }
}
document.querySelectorAll('.settings-fold-head').forEach(function (btn) {
btn.addEventListener('click', function () {
var panel = btn.closest('[data-settings-fold]');
if (!panel) return;
setSettingsFold(panel, !panel.classList.contains('is-collapsed'));
saveSettingsFoldState();
});
});
loadSettingsFoldState();
var CTP_FOLD_KEY = 'qihuo_ctp_fold';
function setCtpFold(el, collapsed) {
if (!el) return;
el.classList.toggle('is-collapsed', collapsed);
var head = el.querySelector('.settings-ctp-fold-head');
if (head) head.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
}
function saveCtpFoldState() {
var state = {};
document.querySelectorAll('[data-ctp-fold]').forEach(function (el) {
state[el.getAttribute('data-ctp-fold')] = el.classList.contains('is-collapsed');
});
try { localStorage.setItem(CTP_FOLD_KEY, JSON.stringify(state)); } catch (e) { /* ignore */ }
}
function loadCtpFoldState() {
try {
var raw = localStorage.getItem(CTP_FOLD_KEY);
if (!raw) return;
var state = JSON.parse(raw);
document.querySelectorAll('[data-ctp-fold]').forEach(function (el) {
var key = el.getAttribute('data-ctp-fold');
if (Object.prototype.hasOwnProperty.call(state, key)) {
setCtpFold(el, !!state[key]);
}
});
} catch (e) { /* ignore */ }
}
document.querySelectorAll('.settings-ctp-fold-head').forEach(function (btn) {
btn.addEventListener('click', function () {
var panel = btn.closest('[data-ctp-fold]');
if (!panel) return;
setCtpFold(panel, !panel.classList.contains('is-collapsed'));
saveCtpFoldState();
});
});
loadCtpFoldState();
var ctpForm = document.getElementById('ctp-settings-form');
if (ctpForm) {
ctpForm.addEventListener('submit', function (ev) {
var ctpCard = document.querySelector('[data-settings-fold="ctp"]');
if (ctpCard) setSettingsFold(ctpCard, false);
var simnowFold = document.querySelector('[data-ctp-fold="simnow"]');
if (simnowFold) setCtpFold(simnowFold, false);
var pwd = document.getElementById('simnow_password');
var pwdVal = pwd && pwd.value ? pwd.value.trim() : '';
var pwdWasSet = {{ 'true' if ctp_cfg.simnow_password_set else 'false' }};
if (pwdWasSet && !pwdVal) {
var ok = window.confirm(
'SimNow 交易密码为空,保存后不会更新密码(仍用旧密码)。\n\n'
+ '若快期已改密,请取消后在「交易密码」框手打新密码再保存。\n\n仍要保存其他项?'
);
if (!ok) ev.preventDefault();
}
});
}
})();
</script>
{% endblock %}