Files
qihuo/templates/settings.html
T
dekun c5262a0a54 Add responsive mobile layout, records cards, and tablet settings fold fix.
Mobile gets compact trade/records UI with detail modals; static assets are cache-busted and settings cards fold correctly on tablet grid layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 16:42:38 +08:00

617 lines
35 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;align-items:start}
.settings-page .split-grid .card:not(.settings-fold){margin-bottom:0;min-height:100%;height:100%;display:flex;flex-direction:column}
.settings-page .split-grid .settings-fold.card,
.settings-page .split-grid .settings-ctp-fold.card{
margin-bottom:0;min-height:auto !important;height:auto !important;align-self:start;
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-ai-full{margin-bottom:1.25rem}
.settings-ai-full .settings-fold.card{min-height:auto;height:auto}
.settings-ai-usage{margin-bottom:1rem;font-size:.84rem;color:var(--text-muted)}
.settings-ai-usage summary{cursor:pointer;color:var(--accent);font-weight:600;margin-bottom:.4rem}
.settings-ai-usage-body ul{margin:.25rem 0 0 1.1rem;padding:0;line-height:1.55}
.settings-ai-usage-body li{margin:.2rem 0}
.settings-ai-form{max-width:none}
.settings-ai-form input[type="checkbox"]{width:auto;flex-shrink:0;margin:0}
.settings-ai-form label.check-inline{
display:inline-flex;align-items:center;gap:.45rem;width:auto;
cursor:pointer;margin-bottom:0;font-size:.85rem;color:var(--text-muted)
}
.settings-ai-cards-row{
display:grid;grid-template-columns:1fr 1fr;gap:.85rem;margin-bottom:.85rem
}
.settings-ai-card{
border:1px solid var(--border);border-radius:10px;
padding:.85rem 1rem;background:var(--card-inner)
}
.settings-ai-card.is-active{border-color:var(--accent);box-shadow:0 0 0 1px rgba(56,189,248,.25)}
.settings-ai-card-head{
display:flex;align-items:center;justify-content:space-between;gap:.5rem;
margin-bottom:.65rem;font-size:.92rem;font-weight:600;color:var(--text-title)
}
.settings-ai-card .field{margin-bottom:.55rem}
.settings-ai-card .field:last-child{margin-bottom:0}
.settings-ai-daily{
border:1px solid var(--border);border-radius:10px;
padding:.85rem 1rem;background:var(--card-inner);margin-bottom:.85rem
}
.settings-ai-daily-grid{display:grid;grid-template-columns:auto 1fr 1fr;gap:.65rem .75rem;align-items:end}
.settings-ai-daily-grid .check-inline{align-self:center}
@media (max-width:768px){
.settings-ai-cards-row{grid-template-columns:1fr}
.settings-ai-daily-grid{grid-template-columns:1fr}
}
.settings-page .settings-fold.card{padding:0;overflow:hidden}
.settings-page .split-grid .settings-fold.card{min-height:auto;height:auto;align-self:start}
.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;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="roll_max_margin_pct" type="number" step="1" min="1" max="100" value="{{ roll_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>
滚仓保证金上限为滚仓后<strong>总持仓</strong>占用上限(默认 50%,可在下方修改)。
<strong>移动保本</strong>:达 1R 后止损移至开仓价 ± N 跳。
<strong>挂单超时</strong>:限价开仓未成交时,超过设定分钟数自动向柜台撤单(1~60 分钟)。
<span class="text-muted">{{ small_account_margin_rec.label }}。</span>
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.disabled_hint %}
<span class="text-muted" style="display:block;margin-top:.35rem">{{ ctp_status.disabled_hint }}</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" data-simnow-pwd-set="{{ '1' if ctp_cfg.simnow_password_set else '0' }}">
<input type="hidden" name="action" value="ctp">
<div class="settings-ctp-auto card" style="margin-bottom:.85rem;padding:.75rem 1rem">
<label class="settings-ctp-auto-label" style="display:flex;align-items:flex-start;gap:.65rem;cursor:pointer;margin:0">
<input type="checkbox" name="ctp_auto_connect" value="1" {% if ctp_auto_connect %}checked{% endif %}
style="margin-top:.2rem;width:auto">
<span>
<strong>CTP 自动连接</strong>
<span class="hint" style="display:block;margin:.25rem 0 0;font-size:.78rem;line-height:1.55">
开启:盘前自动连接、断线重连、持仓页可连 CTP。关闭:立即断开所有 CTP 连接,不再尝试重连。
SimNow 非交易时段前置常不可用(与快期相同),建议收盘后关闭。
</span>
</span>
</label>
</div>
<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="settings-ai-full">
{% call settings_card('ai', 'AI 分析 · 使用说明') %}
<details class="settings-ai-usage" open>
<summary>使用说明</summary>
<div class="settings-ai-usage-body">
<ul>
<li><strong>触发时机</strong>:开仓成交、平仓入账、日终报告(默认日盘 15:05,可在下方修改)</li>
<li><strong>Ollama</strong>:服务器需能访问填写的地址(如本机 <code>127.0.0.1:11434</code></li>
<li><strong>OpenAI 兼容</strong>:支持 DeepSeek、硅基流动等 OpenAI 格式 API</li>
<li><strong>输出位置</strong>:分析写入导航「AI 消息」;若已配置企业微信,日终报告会同步推送摘要</li>
<li><strong>不替代交易</strong>:AI 仅作复盘与风险提示,下单仍以系统规则与 CTP 为准</li>
</ul>
</div>
</details>
<form action="{{ url_for('settings') }}" method="post" class="settings-ai-form">
<input type="hidden" name="action" value="ai">
<div class="field" style="margin-bottom:.75rem">
<label class="check-inline">
<input type="checkbox" name="ai_enabled" value="1" {% if ai_enabled %}checked{% endif %}>
<span>启用 AI 分析</span>
</label>
</div>
<div class="field" style="margin-bottom:.85rem;max-width:280px">
<label>当前使用的提供商</label>
<select name="ai_provider" id="ai-provider-select">
<option value="ollama" {% if ai_provider == 'ollama' %}selected{% endif %}>本地 Ollama</option>
<option value="openai" {% if ai_provider == 'openai' %}selected{% endif %}>OpenAI 兼容 API</option>
</select>
</div>
<div class="settings-ai-cards-row">
<div class="settings-ai-card{% if ai_provider == 'ollama' %} is-active{% endif %}" data-ai-provider="ollama">
<div class="settings-ai-card-head">
<span>本地 Ollama</span>
{% if ai_provider == 'ollama' %}<span class="badge profit">当前</span>{% endif %}
</div>
<div class="field">
<label>接口地址</label>
<input name="ai_ollama_base_url" type="url" placeholder="http://127.0.0.1:11434" value="{{ ai_ollama_base_url }}">
</div>
<div class="field">
<label>模型名</label>
<input name="ai_ollama_model" type="text" placeholder="qwen2.5:7b" value="{{ ai_ollama_model }}">
</div>
</div>
<div class="settings-ai-card{% if ai_provider == 'openai' %} is-active{% endif %}" data-ai-provider="openai">
<div class="settings-ai-card-head">
<span>OpenAI 兼容</span>
{% if ai_provider == 'openai' %}<span class="badge profit">当前</span>{% endif %}
</div>
<div class="field">
<label>API Base URL</label>
<input name="ai_openai_base_url" type="url" placeholder="https://api.openai.com/v1" value="{{ ai_openai_base_url }}">
</div>
<div class="field">
<label>API Key</label>
<input name="ai_openai_api_key" type="password" placeholder="sk-..." value="{{ ai_openai_api_key }}" autocomplete="off">
</div>
<div class="field">
<label>模型名</label>
<input name="ai_openai_model" type="text" placeholder="gpt-4o-mini" value="{{ ai_openai_model }}">
</div>
</div>
</div>
<div class="settings-ai-daily">
<p class="hint" style="margin:0 0 .65rem">日终报告(国内期货日盘收盘后推送一次)</p>
<div class="settings-ai-daily-grid">
<label class="check-inline">
<input type="checkbox" name="ai_daily_report_enabled" value="1" {% if ai_daily_report_enabled %}checked{% endif %}>
<span>启用</span>
</label>
<div class="field" style="margin:0">
<label>报告时刻(时)</label>
<input name="ai_daily_report_hour" type="number" min="0" max="23" step="1" value="{{ ai_daily_report_hour }}">
</div>
<div class="field" style="margin:0">
<label>报告时刻(分)</label>
<input name="ai_daily_report_minute" type="number" min="0" max="59" step="1" value="{{ ai_daily_report_minute }}">
</div>
</div>
</div>
<button type="submit" class="btn-primary">保存 AI 配置</button>
</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 src="{{ url_for('static', filename='js/settings.js') }}?v={{ asset_v }}"></script>
{% endblock %}