修改前端页面

This commit is contained in:
zlt 2026-08-11 09:42:40 +08:00
parent 314250eee1
commit 46f1a09a72
1 changed files with 137 additions and 33 deletions

View File

@ -42,6 +42,9 @@
.op-btn{flex:none;width:150px;}
.op-btn .el-button{width:100%;}
.op-row .d{color:#999;font-size:12px;line-height:1.5;padding-top:3px;}
.set-card{border:1px solid #333;border-radius:6px;padding:10px 12px;margin-bottom:10px;}
.set-sum{color:#ddd;font-size:13px;margin-bottom:8px;line-height:1.6;}
.set-fields{display:flex;flex-wrap:wrap;gap:10px 16px;align-items:center;color:#999;font-size:12px;}
</style>
</head>
<body>
@ -228,52 +231,77 @@
<!-- 5) 组合操作 + 设置 -->
<div class="panel tblk">
<h3>组合操作</h3>
<div class="muted" style="margin-bottom:8px">对整个组合的手动动作。带「…」的会先弹窗让你填数值;危险动作要二次确认。</div>
<div class="muted" style="margin-bottom:8px">对整个组合的手动动作。数值都基于当前实际持仓算出、并预览影响金额,不用盲填;危险动作要二次确认。</div>
<div class="op-row">
<span class="op-btn"><el-button size="small" :type="ov.buy_halt?'success':'warning'" @click="ov.buy_halt?pResumeBuy():pHaltBuy()">{{ ov.buy_halt?'恢复买入':'暂停买入' }}</el-button></span>
<span class="d">停止一切新增买入并撤销在途买入指令;卖出与止损不受影响。当前:{{ ov.buy_halt?'已暂停买入':'买入正常' }}。</span>
<span class="d">停止一切新增买入并撤销在途买入指令;卖出与止损不受影响。当前:{{ ov.buy_halt?'已暂停买入':'买入正常' }}。</span>
</div>
<div class="op-row">
<span class="op-btn"><el-button size="small" :type="ov.exec_halt?'success':''" @click="ov.exec_halt?pResumeAll():pHaltAll()">{{ ov.exec_halt?'结束休假':'休假模式' }}</el-button></span>
<span class="d">所有自动动作暂停,只保留对账与日报(长假或手动接管时用)。当前:{{ ov.exec_halt?'休假中':'正常运行' }}。</span>
<span class="d">所有自动动作暂停,只保留对账与日报。当前:{{ ov.exec_halt?'休假中':'正常运行' }}。</span>
</div>
<div class="op-row">
<span class="op-btn"><el-button size="small" @click="pReduce()">降仓…</el-button></span>
<span class="d">按占总规模的百分比整体减少敞口。优先级:停新买 → 清弱票 → 收利润 → 等比微减。</span>
<div class="op-row" style="align-items:center">
<span class="op-btn"><el-button size="small" type="primary" :disabled="!posMoveValid" @click="doPosMove()">调整仓位</el-button></span>
<span class="d">
当前总仓位 <b>{{ pct(ov.portfolio_pct) }}</b>(市值 {{ money(ov.portfolio_mv) }} / 规模 {{ money(ov.scale) }}),可用资金 {{ money(ov.cash_source==='ws'?ov.cash_avail:ov.cash_est) }}。
目标 <el-input v-model="tgtPos" size="small" style="width:96px"><template #append>%</template></el-input>
<span v-if="posMovePreview" style="margin-left:8px;color:#bbb">{{ posMovePreview }}</span>
</span>
</div>
<div class="op-row">
<span class="op-btn"><el-button size="small" @click="pIncrease()">升仓…</el-button></span>
<span class="d">按占总规模的百分比整体提高敞口。来源:持仓补到目标 + 候选池新票建仓。</span>
</div>
<div class="op-row">
<span class="op-btn"><el-button size="small" :disabled="!ov.sector_ready" @click="pSectorExit()">清仓某行业…</el-button></span>
<span class="d">卖光指定行业的全部持仓。{{ ov.sector_ready?'':'(行业数据未就绪,暂不可用)' }}</span>
<div class="op-row" style="align-items:center">
<span class="op-btn"><el-button size="small" :disabled="!secSel" @click="doSectorExit()">清仓行业</el-button></span>
<span class="d">
卖光某个行业的全部持仓,从下面选(不用手打):
<el-select v-model="secSel" size="small" style="width:240px" placeholder="选持仓行业" :disabled="!heldSectors.length" clearable>
<el-option v-for="s in heldSectors" :key="s.sector" :label="s.sector+''+s.count+'只 '+money(s.mv)+''" :value="s.sector"></el-option>
</el-select>
<span v-if="!heldSectors.length" class="muted"> —— {{ ov.sector_ready===false?'行业数据未就绪':'当前无带行业的持仓' }}</span>
</span>
</div>
<div class="op-row">
<span class="op-btn"><el-button size="small" type="danger" @click="pLiquidate()">一键清仓(紧急)</el-button></span>
<span class="d">全部持仓按最快节奏卖出、不做择时优化。用于紧急离场,需二次确认。</span>
<span class="d">全部持仓{{ heldPositions.length }} 只,市值 {{ money(ov.portfolio_mv) }}按最快节奏卖出、不做择时优化。紧急离场,需二次确认。</span>
</div>
</div>
<div class="panel tblk">
<h3>设置</h3>
<el-table :data="traderParams" size="small" border>
<el-table-column label="项" min-width="240"><template #default="s">{{ s.row.desc || s.row.key }}</template></el-table-column>
<el-table-column label="当前值" width="240">
<template #default="s">
<el-select v-if="s.row.key==='PMS_AUTONOMY'" v-model="s.row.value" size="small">
<div class="muted" style="margin-bottom:8px">每组先给一句「现在是什么状态」的合成说明,再让你改其中的项。改完点保存即时生效。</div>
<div class="set-card">
<div class="set-sum">资金与仓位纪律:{{ sumPosition }}</div>
<div class="set-fields" v-if="pmap['PMS_TOTAL_SCALE']">
<span>总操作规模 <el-input v-model="pmap['PMS_TOTAL_SCALE'].value" size="small" style="width:130px"><template #append></template></el-input></span>
<span>总仓上限 <el-input v-model="pmap['PMS_PORTFOLIO_CAP'].value" size="small" style="width:88px" placeholder="0~1"></el-input></span>
<span>单股上限 <el-input v-model="pmap['PMS_STOCK_CAP'].value" size="small" style="width:88px" placeholder="0~1"></el-input></span>
<span>最多持仓 <el-input v-model="pmap['PMS_MAX_NAMES'].value" size="small" style="width:82px"><template #append></template></el-input></span>
<span>预留现金 <el-input v-model="pmap['PMS_CASH_RESERVE'].value" size="small" style="width:88px" placeholder="0~1"></el-input></span>
</div>
</div>
<div class="set-card">
<div class="set-sum">补仓策略:{{ sumDca }}</div>
<div class="set-fields" v-if="pmap['PMS_DCA_MAX_RATIO']">
<span>是否补仓 <el-switch v-model="dcaOn"></el-switch></span>
<template v-if="dcaOn">
<span>上限(占底仓) <el-input v-model="pmap['PMS_DCA_MAX_RATIO'].value" size="small" style="width:88px" placeholder="0~1"></el-input></span>
<span>评估档(浮亏) <el-input v-model="pmap['PMS_DCA_TRIGGERS'].value" size="small" style="width:130px" placeholder="-0.08,-0.15"></el-input></span>
<span>深档确认 <el-input v-model="pmap['PMS_DCA_DEEP_CONFIRM'].value" size="small" style="width:88px"></el-input></span>
<span>回踩止损 <el-input v-model="pmap['PMS_FILL_MAX_LOSS'].value" size="small" style="width:88px"></el-input></span>
</template>
</div>
</div>
<div class="set-card">
<div class="set-sum">自主档位:{{ sumAutonomy }}</div>
<div class="set-fields" v-if="pmap['PMS_AUTONOMY']">
<el-select v-model="pmap['PMS_AUTONOMY'].value" size="small" style="width:200px">
<el-option label="全自动" value="full"></el-option>
<el-option label="只提议不下手" value="propose_only"></el-option>
<el-option label="暂停" value="off"></el-option>
</el-select>
<el-input v-else v-model="s.row.value" size="small"></el-input>
</template>
</el-table-column>
</el-table>
<div class="row" style="margin-top:8px">
</div>
</div>
<div class="row" style="margin-top:10px">
<el-button size="small" type="primary" :disabled="!dirtyCount" @click="saveParams">保存修改 ({{ dirtyCount }})</el-button>
<el-button size="small" @click="loadParams">放弃</el-button>
<span class="muted">改「自主档位」或「总仓上限」会即时生效;调低总仓上限可能触发降仓提议。</span>
<span class="muted">补仓(浮亏时逢跌补仓):「补仓上限」设为 0 即关闭补仓;「补仓评估档」是触发补仓的浮亏档位(如 -0.08,-0.15);到「深档确认」及更深的补仓永远要你点头。</span>
<span class="muted">关闭补仓=把「补仓上限」设为 0调低总仓上限可能触发降仓提议。</span>
</div>
</div>
@ -1194,15 +1222,19 @@ createApp({
// 个股
function actExitStock(row) { quickCmd('EXIT_STOCK', { ts_code: row.ts_code }, '清仓 ' + nm(row.ts_code) + '?系统会在窗口内择机把它卖光。'); }
async function actReduceStock(row) {
const v = await promptNum('减到目标仓位(占总规模,%', '减仓 ' + nm(row.ts_code));
const curPct = row.pct_of_scale != null ? (row.pct_of_scale * 100).toFixed(1) + '%' : '—';
const v = await promptNum('当前 ' + row.total_qty + ' 股,市值 ' + money(row.market_value)
+ '(占规模 ' + curPct + ')。减到目标仓位(占总规模,%', '减仓 ' + nm(row.ts_code));
if (v != null) quickCmd('REDUCE_STOCK', { ts_code: row.ts_code, target_pct: v + '%' });
}
async function actStop(row) {
const v = await promptNum('止损价(元)', '设止损 ' + nm(row.ts_code));
const v = await promptNum('现价 ' + row.price + ',成本 ' + (row.avg_cost || '—')
+ '。止损价(元,跌破即挂卖)', '设止损 ' + nm(row.ts_code));
if (v != null) quickCmd('SET_STOP_PRICE', { ts_code: row.ts_code, price: v });
}
async function actTargetPrice(row) {
const v = await promptNum('目标价(元)', '设目标价 ' + nm(row.ts_code));
const v = await promptNum('现价 ' + row.price + ',成本 ' + (row.avg_cost || '—')
+ '。目标价(元,到价生成止盈提议)', '设目标价 ' + nm(row.ts_code));
if (v != null) quickCmd('SET_TARGET_PRICE', { ts_code: row.ts_code, price: v });
}
function actFreeze(row) {
@ -1228,6 +1260,76 @@ createApp({
let name; try { name = (await ElementPlus.ElMessageBox.prompt('要清仓的行业名称', '清仓某行业', {})).value; } catch (e) { return; }
if (name) quickCmd('SECTOR_EXIT', { sector: name }, '清仓行业「' + name + '」的全部持仓?');
}
// ---- 组合仓位调整: 显示当前 + 算出金额, 不盲填 ----
const pctOf = v => (v == null || v === '') ? '—' : (Number(v) * 100).toFixed(0) + '%';
const tgtPos = ref(null);
const posMoveValid = computed(() => {
const t = Number(tgtPos.value);
return tgtPos.value !== null && tgtPos.value !== '' && !isNaN(t) && t >= 0 && Number(ov.value.scale || 0) > 0;
});
const posMovePreview = computed(() => {
if (!posMoveValid.value) return '';
const cur = Number(ov.value.portfolio_pct || 0), scale = Number(ov.value.scale || 0);
const delta = Number(tgtPos.value) / 100 - cur, amt = Math.abs(delta) * scale;
if (Math.abs(delta) < 0.001) return '与当前仓位一致无需调整';
return delta < 0
? '→ 降仓:从 ' + pctOf(cur) + ' 降到 ' + tgtPos.value + '%,释放约 ' + money(amt) + ' 元。'
: '→ 升仓:从 ' + pctOf(cur) + ' 升到 ' + tgtPos.value + '%,动用约 ' + money(amt) + ' 元。';
});
function doPosMove() {
if (!posMoveValid.value) { ElementPlus.ElMessage.warning('请输入目标仓位'); return; }
const cur = Number(ov.value.portfolio_pct || 0), scale = Number(ov.value.scale || 0);
const delta = Number(tgtPos.value) / 100 - cur, amt = Math.abs(delta) * scale;
if (Math.abs(delta) < 0.001) { ElementPlus.ElMessage.info('与当前仓位一致'); return; }
if (delta < 0) quickCmd('REDUCE_EXPOSURE', { pct: (Math.abs(delta) * 100).toFixed(1) + '%' },
'降仓到 ' + tgtPos.value + '%,释放约 ' + money(amt) + ' 元?');
else quickCmd('INCREASE_EXPOSURE', { pct: (delta * 100).toFixed(1) + '%' },
'升仓到 ' + tgtPos.value + '%,动用约 ' + money(amt)
+ ' 元(可用资金 ' + money(ov.value.cash_source === 'ws' ? ov.value.cash_avail : ov.value.cash_est) + '');
}
// ---- 清仓行业: 下拉选持仓行业, 不手输 ----
const heldSectors = computed(() => {
const m = {};
(positions.value || []).filter(p => (p.total_qty || 0) > 0 && p.sector).forEach(p => {
if (!m[p.sector]) m[p.sector] = { sector: p.sector, count: 0, mv: 0 };
m[p.sector].count++; m[p.sector].mv += (p.market_value || 0);
});
return Object.values(m).sort((a, b) => b.mv - a.mv);
});
const secSel = ref('');
function doSectorExit() {
if (!secSel.value) { ElementPlus.ElMessage.warning('请选择行业'); return; }
const x = heldSectors.value.find(v => v.sector === secSel.value);
quickCmd('SECTOR_EXIT', { sector: secSel.value },
'清仓行业「' + secSel.value + '」的全部持仓(' + (x ? x.count : '?') + ' 只,约 '
+ (x ? money(x.mv) : '?') + ' 元)?');
}
// ---- 设置: 参数映射 + 组合摘要 (给用户看合成结果, 不是孤立参数) ----
const pmap = computed(() => { const m = {}; (params.value || []).forEach(p => m[p.key] = p); return m; });
const pval = k => { const p = pmap.value[k]; return p ? p.value : null; };
const dcaOn = computed({
get: () => Number(pval('PMS_DCA_MAX_RATIO') || 0) > 0,
set: v => { const p = pmap.value['PMS_DCA_MAX_RATIO']; if (!p) return; p.value = v ? (Number(p.value) > 0 ? p.value : 0.5) : 0; }
});
const sumPosition = computed(() =>
'规模 ' + money(pval('PMS_TOTAL_SCALE')) + ' 元;总仓 ≤ ' + pctOf(pval('PMS_PORTFOLIO_CAP'))
+ ',单股 ≤ ' + pctOf(pval('PMS_STOCK_CAP')) + ',最多 ' + (pval('PMS_MAX_NAMES') || '—')
+ ' 只,预留现金 ' + pctOf(pval('PMS_CASH_RESERVE')) + '。');
const sumDca = computed(() => {
if (!dcaOn.value) return '已关闭 —— 浮亏时不补仓。';
const trg = String(pval('PMS_DCA_TRIGGERS') || '').split(',').map(x => x.trim()).filter(Boolean)
.map(x => Math.abs(Number(x) * 100).toFixed(0) + '%').join('、');
const dc = Math.abs(Number(pval('PMS_DCA_DEEP_CONFIRM') || 0) * 100).toFixed(0) + '%';
const fl = Math.abs(Number(pval('PMS_FILL_MAX_LOSS') || 0) * 100).toFixed(0) + '%';
return '开启,上限为底仓的 ' + pctOf(pval('PMS_DCA_MAX_RATIO')) + '。浮亏 ' + (trg || '—')
+ ' 各评估一次补仓;浮亏 ' + dc + ' 及更深的补仓永远需你确认;浮亏超过 ' + fl
+ ' 就不再回踩补足、转入补仓评估。';
});
const sumAutonomy = computed(() => ({
full: '全自动 —— 系统自主提议并直接执行。',
propose_only: '只提议不下手 —— 系统提议,等你在「等我拍板」里确认。',
off: '暂停 —— 系统不自主提议。'
}[pval('PMS_AUTONOMY')] || (pval('PMS_AUTONOMY') || '—')));
// ===================== /交易员视图 =====================
@ -1444,7 +1546,9 @@ createApp({
loadChanges,
mode, tx, nm, fullName, nameMap, attention, heldPositions, traderParams,
propWhy, nextStep, scrollTo, actExitStock, actReduceStock, rowMore,
pHaltBuy, pResumeBuy, pHaltAll, pResumeAll, pReduce, pIncrease, pLiquidate, pSectorExit };
pHaltBuy, pResumeBuy, pHaltAll, pResumeAll, pReduce, pIncrease, pLiquidate, pSectorExit,
pctOf, tgtPos, posMoveValid, posMovePreview, doPosMove, heldSectors, secSel, doSectorExit,
pmap, pval, dcaOn, sumPosition, sumDca, sumAutonomy };
}
}).use(ElementPlus).mount('#app');
</script>