feat(advisor): 补全投顾流程入口(确认目标 → 查看方案书),并修复页面缺缓存版本

## 1. 投顾流程此前是断的

投顾工作台只有 4 个「生成草案」操作 + 1 个只读列表,而**确认目标**与**查看方案书**
这两个端点虽然存在却**没有入口** —— 于是目标永远停在 `pending_confirmation`、
方案书永远停在 `pending`。

实测客户 9001 正是如此:`confirmed_at: null`、方案书 `review_status=pending`。
而「录入客户目标」成功后的提示是"已提交,等待确认与审核",**没有下一步可点**。

改动:

- 新增操作卡「目标确认与方案书」:输入客户 ID → 查询目标 → 显示目标状态、
  方案书状态、收益区间 / 回撤 / 期限 / 基准 / 确认时间 → **确认目标** / **查看方案书**。
- **录入目标后自动跳到目标状态页**,把确认按钮摆在眼前,而不是停在"已提交"。
- `ADVISOR_CONFIRM_GOAL` / `ADVISOR_GOAL_BOOK` 补登记进 `api-client.js`
  (`ADVISOR_CUSTOMER_GOAL` 早已注册却从未被使用)。
- 404「当前投资目标不存在」按**正常情况**处理(给下一步提示),不当故障报警。

⚠️ **审核与发布不在此范围**:`review_book` / `publish_book` 都要求 `admin=True`
(见 `investment_goal_service`),那是管理员的动作,投顾侧到"确认 + 看方案书"为止。
方案书仍会停在 `pending`,除非管理员侧也补上入口。

## 2. 9 个页面的 script 标签缺缓存版本参数

投顾工作台的 script 是 `dashboard.js`(**没有 `?v=`**),另有 8 个页面同样如此
(客户的持仓/下单/流水/盈亏/测评/成交明细、管理员工作台、运营工作台)。
浏览器会一直用缓存的旧 JS —— **改了代码也看不到效果**,这是"功能很奇怪"的一部分。

已统一补上 `?v=20260913`,投顾页的 js/css 提到 `-2`。

## 3. 过程中我损坏过 8 个文件,已恢复

第一次批量补版本参数时我用了 PowerShell `Get-Content -Raw` + `Set-Content`:
**PS 5.1 默认按 ANSI/GBK 读**,把 UTF-8 中文读成乱码再写回
(`角色与权限` → `瑙掕壊鏉冮檺`)并写入 BOM。`tests/unit/api/test_portal_frontend.py`
的断言当场抓到了它(找不到「角色与权限」)。

已 `git checkout` 恢复全部 8 个文件,改用 Python 重做(显式 UTF-8、不写 BOM、
保持原换行),现在每个文件的改动都是 **1 增 1 删**。

验证:`tests/unit/api/test_portal_frontend.py` 35 passed;unit+contract **1395 passed**;
ruff 通过;mypy 251 文件 0 错;确认目标实测 200
(`pending_confirmation` → `confirmed`,写入 `confirmed_at`),
重复确认被状态机以 409 拒绝。
This commit is contained in:
2026-09-13 23:56:27 +08:00
parent 3ada1f6c87
commit 421fed8569
11 changed files with 135 additions and 11 deletions
+2
View File
@@ -60,6 +60,8 @@ const ENDPOINTS = Object.freeze({
ADVISOR_RECOMMEND: { method: 'POST', path: '/api/v1/advisor/recommendations', idempotent: true },
ADVISOR_CREATE_GOAL: { method: 'POST', path: '/api/v1/advisor/investment-goals', idempotent: true },
ADVISOR_CUSTOMER_GOAL: { method: 'GET', path: '/api/v1/advisor/customers/{customerId}/investment-goals/current' },
ADVISOR_CONFIRM_GOAL: { method: 'POST', path: '/api/v1/advisor/investment-goals/{goalNo}/confirmations', idempotent: true },
ADVISOR_GOAL_BOOK: { method: 'GET', path: '/api/v1/advisor/investment-goals/{goalNo}/goal-book' },
OFFSITE_MAILS: { method: 'GET', path: '/api/v1/offsite-fund/mails' },
OFFSITE_MAILBOX: { method: 'GET', path: '/api/v1/offsite-fund/mailbox-status' },
});
@@ -1 +1 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>资金流水 · 南方财富</title><link rel="stylesheet" href="/static/portal/common/base.css"><link rel="stylesheet" href="/static/portal/common/customer-list.css"><link rel="stylesheet" href="/static/portal/customer/cash-ledger/cash-ledger.css"></head><body><main id="main-content" class="page-shell"><div class="page-heading"><div><h1 class="page-heading__title">资金流水</h1><p class="page-heading__description">记录模拟交易引起的扣款、回款与交易后余额变化。</p></div><div class="page-heading__actions"><button class="button" type="button" data-reload>刷新</button></div></div><section class="panel panel--flush"><div class="panel__header"><h2 class="panel__title">账户资金明细</h2></div><div class="panel__body" data-list-content></div><div class="customer-list__footer"><span>每页最多 20 条</span><button class="button" type="button" data-next-page>下一页</button></div></section></main><script type="module" src="/static/portal/customer/cash-ledger/cash-ledger.js"></script></body></html>
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>资金流水 · 南方财富</title><link rel="stylesheet" href="/static/portal/common/base.css"><link rel="stylesheet" href="/static/portal/common/customer-list.css"><link rel="stylesheet" href="/static/portal/customer/cash-ledger/cash-ledger.css"></head><body><main id="main-content" class="page-shell"><div class="page-heading"><div><h1 class="page-heading__title">资金流水</h1><p class="page-heading__description">记录模拟交易引起的扣款、回款与交易后余额变化。</p></div><div class="page-heading__actions"><button class="button" type="button" data-reload>刷新</button></div></div><section class="panel panel--flush"><div class="panel__header"><h2 class="panel__title">账户资金明细</h2></div><div class="panel__body" data-list-content></div><div class="customer-list__footer"><span>每页最多 20 条</span><button class="button" type="button" data-next-page>下一页</button></div></section></main><script type="module" src="/static/portal/customer/cash-ledger/cash-ledger.js?v=20260913"></script></body></html>
@@ -1 +1 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>我的持仓 · 南方财富</title><link rel="stylesheet" href="/static/portal/common/base.css"><link rel="stylesheet" href="/static/portal/common/customer-list.css"><link rel="stylesheet" href="/static/portal/customer/holdings/holdings.css"></head><body><main id="main-content" class="page-shell"><div class="page-heading"><div><h1 class="page-heading__title">我的持仓</h1><p class="page-heading__description">持仓份额、市值和盈亏均由 T006 后端接口返回。</p></div><div class="page-heading__actions"><button class="button" type="button" data-reload>刷新</button></div></div><section class="panel panel--flush"><div class="panel__header"><h2 class="panel__title">持仓明细</h2></div><div class="panel__body" data-list-content></div></section></main><script type="module" src="/static/portal/customer/holdings/holdings.js"></script></body></html>
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>我的持仓 · 南方财富</title><link rel="stylesheet" href="/static/portal/common/base.css"><link rel="stylesheet" href="/static/portal/common/customer-list.css"><link rel="stylesheet" href="/static/portal/customer/holdings/holdings.css"></head><body><main id="main-content" class="page-shell"><div class="page-heading"><div><h1 class="page-heading__title">我的持仓</h1><p class="page-heading__description">持仓份额、市值和盈亏均由 T006 后端接口返回。</p></div><div class="page-heading__actions"><button class="button" type="button" data-reload>刷新</button></div></div><section class="panel panel--flush"><div class="panel__header"><h2 class="panel__title">持仓明细</h2></div><div class="panel__body" data-list-content></div></section></main><script type="module" src="/static/portal/customer/holdings/holdings.js?v=20260913"></script></body></html>
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>交易记录 · 南方财富</title><link rel="stylesheet" href="/static/portal/common/base.css"><link rel="stylesheet" href="/static/portal/common/customer-list.css"><link rel="stylesheet" href="/static/portal/customer/orders/orders.css"></head><body><main id="main-content" class="page-shell"><div class="page-heading"><div><h1 class="page-heading__title">交易记录</h1><p class="page-heading__description">按提交时间倒序展示模拟委托,首版市价委托会立即全额成交。</p></div><div class="page-heading__actions"><a class="button" href="/portal/customer/transactions/">成交明细</a><button class="button" type="button" data-reload>刷新</button></div></div><section class="panel panel--flush"><div class="panel__header"><h2 class="panel__title">委托记录</h2></div><div class="panel__body" data-list-content></div><div class="customer-list__footer"><span>每页最多 20 条</span><button class="button" type="button" data-next-page>下一页</button></div></section></main><script type="module" src="/static/portal/customer/orders/orders.js"></script></body></html>
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>交易记录 · 南方财富</title><link rel="stylesheet" href="/static/portal/common/base.css"><link rel="stylesheet" href="/static/portal/common/customer-list.css"><link rel="stylesheet" href="/static/portal/customer/orders/orders.css"></head><body><main id="main-content" class="page-shell"><div class="page-heading"><div><h1 class="page-heading__title">交易记录</h1><p class="page-heading__description">按提交时间倒序展示模拟委托,首版市价委托会立即全额成交。</p></div><div class="page-heading__actions"><a class="button" href="/portal/customer/transactions/">成交明细</a><button class="button" type="button" data-reload>刷新</button></div></div><section class="panel panel--flush"><div class="panel__header"><h2 class="panel__title">委托记录</h2></div><div class="panel__body" data-list-content></div><div class="customer-list__footer"><span>每页最多 20 条</span><button class="button" type="button" data-next-page>下一页</button></div></section></main><script type="module" src="/static/portal/customer/orders/orders.js?v=20260913"></script></body></html>
@@ -1 +1 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>收益明细 · 南方财富</title><link rel="stylesheet" href="/static/portal/common/base.css"><link rel="stylesheet" href="/static/portal/common/customer-list.css"><link rel="stylesheet" href="/static/portal/customer/profit-loss/profit-loss.css"></head><body><main id="main-content" class="page-shell"><div class="page-heading"><div><h1 class="page-heading__title">收益明细</h1><p class="page-heading__description">仅展示后端已计算的累计盈亏、今日盈亏和持仓盈亏,不推导缺失的已实现收益。</p></div><div class="page-heading__actions"><a class="button" href="/portal/customer/cash-ledger/">资金流水</a><button class="button" type="button" data-reload>刷新</button></div></div><section data-list-content></section></main><script type="module" src="/static/portal/customer/profit-loss/profit-loss.js"></script></body></html>
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>收益明细 · 南方财富</title><link rel="stylesheet" href="/static/portal/common/base.css"><link rel="stylesheet" href="/static/portal/common/customer-list.css"><link rel="stylesheet" href="/static/portal/customer/profit-loss/profit-loss.css"></head><body><main id="main-content" class="page-shell"><div class="page-heading"><div><h1 class="page-heading__title">收益明细</h1><p class="page-heading__description">仅展示后端已计算的累计盈亏、今日盈亏和持仓盈亏,不推导缺失的已实现收益。</p></div><div class="page-heading__actions"><a class="button" href="/portal/customer/cash-ledger/">资金流水</a><button class="button" type="button" data-reload>刷新</button></div></div><section data-list-content></section></main><script type="module" src="/static/portal/customer/profit-loss/profit-loss.js?v=20260913"></script></body></html>
@@ -20,6 +20,6 @@
<div class="questionnaire-progress" aria-hidden="true"><span data-progress-bar></span></div>
<section class="questionnaire-panel panel fade-in" data-questionnaire aria-live="polite"></section>
</main>
<script type="module" src="/static/portal/customer/risk-questionnaire/risk-questionnaire.js"></script>
<script type="module" src="/static/portal/customer/risk-questionnaire/risk-questionnaire.js?v=20260913"></script>
</body>
</html>
@@ -1 +1 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>成交明细 · 南方财富</title><link rel="stylesheet" href="/static/portal/common/base.css"><link rel="stylesheet" href="/static/portal/common/customer-list.css"><link rel="stylesheet" href="/static/portal/customer/transactions/transactions.css"></head><body><main id="main-content" class="page-shell"><div class="page-heading"><div><h1 class="page-heading__title">成交明细</h1><p class="page-heading__description">成交价格、份额、费用与净额均来自 T007 后端记录。</p></div><div class="page-heading__actions"><button class="button" type="button" data-reload>刷新</button></div></div><section class="panel panel--flush"><div class="panel__header"><h2 class="panel__title">成交记录</h2></div><div class="panel__body" data-list-content></div><div class="customer-list__footer"><span>每页最多 20 条</span><button class="button" type="button" data-next-page>下一页</button></div></section></main><script type="module" src="/static/portal/customer/transactions/transactions.js"></script></body></html>
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>成交明细 · 南方财富</title><link rel="stylesheet" href="/static/portal/common/base.css"><link rel="stylesheet" href="/static/portal/common/customer-list.css"><link rel="stylesheet" href="/static/portal/customer/transactions/transactions.css"></head><body><main id="main-content" class="page-shell"><div class="page-heading"><div><h1 class="page-heading__title">成交明细</h1><p class="page-heading__description">成交价格、份额、费用与净额均来自 T007 后端记录。</p></div><div class="page-heading__actions"><button class="button" type="button" data-reload>刷新</button></div></div><section class="panel panel--flush"><div class="panel__header"><h2 class="panel__title">成交记录</h2></div><div class="panel__body" data-list-content></div><div class="customer-list__footer"><span>每页最多 20 条</span><button class="button" type="button" data-next-page>下一页</button></div></section></main><script type="module" src="/static/portal/customer/transactions/transactions.js?v=20260913"></script></body></html>
@@ -11,6 +11,22 @@ const CONTENT_TYPE_LABELS = {
advisor_recommendation_plan: '产品推荐方案',
};
//: 目标与方案书的状态口径(`investment_goal_service` 的状态机):
//: 目标 `pending_confirmation` --确认--> `confirmed`;
//: 方案书 `pending` --管理员审核--> `approved` --发布--> `published`。
//: ⚠️ 审核与发布都要求 `admin=True`(见 `review_book` / `publish_book`),
//: 所以投顾这边到"确认目标 + 查看方案书"为止,剩下两步归管理员。
const GOAL_STATUS_LABELS = {
pending_confirmation: '待确认',
confirmed: '已确认',
};
const BOOK_STATUS_LABELS = {
pending: '待审核',
approved: '审核通过',
published: '已发布',
rejected: '已退回',
};
if (requireAdvisor()) {
mountShell({ active: 'advisor-dashboard', mode: 'advisor' });
const context = getAuthContext();
@@ -52,6 +68,94 @@ if (requireAdvisor()) {
output.innerHTML = `<form class="advisor-form" data-goal-form><div class="advisor-form__grid"><label class="form-field"><span class="form-field__label">客户 ID(可选)</span><input class="form-field__input" name="customer_id" type="number" min="1" placeholder="留空表示当前账号"></label><label class="form-field"><span class="form-field__label">基准名称</span><input class="form-field__input" name="benchmark_name" required maxlength="128" value="中证全债指数"></label><label class="form-field"><span class="form-field__label">年化收益下限(%)</span><input class="form-field__input" name="annualized_return_lower_pct" type="number" min="0" max="100" step="0.01" required value="3"></label><label class="form-field"><span class="form-field__label">年化收益上限(%)</span><input class="form-field__input" name="annualized_return_upper_pct" type="number" min="0" max="100" step="0.01" required value="6"></label><label class="form-field"><span class="form-field__label">最大回撤(%)</span><input class="form-field__input" name="max_drawdown_pct" type="number" min="0" max="100" step="0.01" required value="10"></label><label class="form-field"><span class="form-field__label">投资期限(月)</span><input class="form-field__input" name="investment_horizon_months" type="number" min="1" max="600" required value="36"></label><label class="form-field"><span class="form-field__label">流动性要求</span><select class="form-field__input" name="liquidity_requirement"><option value="daily">可随时使用</option><option value="within_7_days">7 日内可使用</option><option value="within_30_days">30 日内可使用</option><option value="over_30_days">30 日后可使用</option></select></label><label class="form-field advisor-form__wide"><span class="form-field__label">备注</span><textarea class="form-field__input advisor-form__textarea" name="notes" maxlength="1000" rows="3" placeholder="记录客户目标背景(不得填写收益承诺)"></textarea></label></div><button class="button button--primary" type="submit">保存客户目标</button></form>`;
output.querySelector('[data-goal-form]').addEventListener('submit', submitGoal);
}
// ---- 目标确认与方案书:把"录入目标"之后断掉的流程接上 ----
//
// 此前工作台只有 4 个"生成草案"的操作 + 1 个只读列表,而**确认目标**与
// **查看方案书**这两个端点虽然存在却没有入口,于是目标永远停在
// `pending_confirmation`、方案书永远停在 `pending`(实测客户 9001 正是如此)。
function renderGoalStatusForm() {
output.innerHTML = `<div class="advisor-inline-form"><label class="form-field"><span class="form-field__label">客户 ID</span><input class="form-field__input" data-goal-customer type="number" min="1" value="9001"></label><button class="button button--primary" type="button" data-load-goal>查询目标</button></div><p class="advisor-output__message">查到目标后可在此确认目标、查看方案书。</p>`;
output.querySelector('[data-load-goal]').addEventListener('click', () => {
const value = Number(output.querySelector('[data-goal-customer]').value);
if (!value) { showAlert('请填写客户 ID。'); return; }
loadGoalStatus(value);
});
}
async function loadGoalStatus(customerId) {
clearAlert();
output.innerHTML = '<div class="advisor-output__loading">正在查询客户目标…</div>';
try {
const response = await apiClient.get('ADVISOR_CUSTOMER_GOAL', { pathParams: { customerId } });
renderGoalStatus(response.data ?? response);
} catch (error) {
apiClient.reportError(error);
// 404「当前投资目标不存在」是**正常情况**(客户还没录入目标),
// 要给出下一步提示,而不是当成故障报警。
if (error.status === 404) {
showAlert(`客户 ${customerId} 还没有投资目标,请先用「录入客户目标」创建。`, 'info');
output.innerHTML = '<div class="advisor-output__placeholder">该客户暂无投资目标。</div>';
return;
}
showAlert(error.message || '查询未完成,请稍后重试。');
output.innerHTML = '<div class="advisor-output__placeholder">查询失败,请检查权限或稍后重试。</div>';
}
}
function renderGoalStatus(goal) {
const goalNo = goal.goal_no;
const status = goal.status;
const book = goal.goal_book || {};
const canConfirm = status === 'pending_confirmation';
const rows = [
['目标编号', goalNo],
['客户', goal.customer_id],
['年化收益区间', `${goal.annualized_return_lower_pct}% ~ ${goal.annualized_return_upper_pct}%`],
['最大回撤', `${goal.max_drawdown_pct}%`],
['投资期限', `${goal.investment_horizon_months} 个月`],
['业绩基准', goal.benchmark_name],
['确认时间', goal.confirmed_at ? formatDateTime(goal.confirmed_at) : '尚未确认'],
];
output.innerHTML = `<div class="advisor-output__header"><strong>客户目标</strong><span class="status-tag status-tag--active">${escapeHtml(GOAL_STATUS_LABELS[status] || status || '--')}</span></div><div class="detail-facts">${rows.map(([label, value]) => `<div class="detail-facts__row"><span>${escapeHtml(label)}</span><strong>${escapeHtml(String(value ?? '--'))}</strong></div>`).join('')}<div class="detail-facts__row"><span>方案书</span><strong>${escapeHtml(BOOK_STATUS_LABELS[book.review_status] || book.review_status || '--')}</strong></div></div><div class="advisor-inline-form">${canConfirm ? '<button class="button button--primary" type="button" data-confirm-goal>确认目标</button>' : ''}<button class="button" type="button" data-view-book>查看方案书</button></div><p class="advisor-output__message">${canConfirm ? '确认后目标才可用于资产配置与推荐;方案书需由管理员审核发布。' : '方案书的审核与发布由管理员完成,投顾侧到此为止。'}</p>`;
output.querySelector('[data-confirm-goal]')?.addEventListener('click', () => confirmGoal(goalNo));
output.querySelector('[data-view-book]')?.addEventListener('click', () => viewGoalBook(goalNo));
}
async function confirmGoal(goalNo) {
clearAlert();
try {
await apiClient.post('ADVISOR_CONFIRM_GOAL', { confirmed: true }, { pathParams: { goalNo } });
showAlert('目标已确认。', 'info');
const customerId = Number(output.querySelector('[data-goal-customer]')?.value || 9001);
await loadGoalStatus(customerId);
} catch (error) {
apiClient.reportError(error);
showAlert(error.message || '确认未完成,请稍后重试。');
}
}
async function viewGoalBook(goalNo) {
clearAlert();
try {
const response = await apiClient.get('ADVISOR_GOAL_BOOK', { pathParams: { goalNo } });
const data = response.data ?? response;
const sections = data.content?.sections || {};
const objective = sections.investment_objective || {};
const expectation = objective.annualized_return_expectation_pct || {};
const rows = [
['业绩基准', objective.benchmark_name],
['年化收益期望', `${expectation.lower ?? '--'}% ~ ${expectation.upper ?? '--'}%`],
['最大回撤', sections.risk_boundary?.maximum_drawdown_pct ? `${sections.risk_boundary.maximum_drawdown_pct}%` : null],
['投资期限', sections.investment_horizon?.months ? `${sections.investment_horizon.months} 个月` : null],
['流动性', sections.liquidity?.description],
];
output.innerHTML = `<div class="advisor-output__header"><strong>投资目标方案书</strong><span class="status-tag status-tag--active">${escapeHtml(BOOK_STATUS_LABELS[data.review_status] || data.review_status || '--')}</span></div><div class="detail-facts">${rows.map(([label, value]) => `<div class="detail-facts__row"><span>${escapeHtml(label)}</span><strong>${escapeHtml(String(value ?? '--'))}</strong></div>`).join('')}</div>${(data.content?.disclosures || []).map((item) => `<p class="advisor-output__message">${escapeHtml(item)}</p>`).join('')}`;
} catch (error) {
apiClient.reportError(error);
showAlert(error.message || '方案书读取失败。');
}
}
async function submitGoal(event) {
event.preventDefault(); clearAlert();
const form = event.currentTarget; const value = (name) => form.elements[name].value.trim();
@@ -59,7 +163,24 @@ if (requireAdvisor()) {
if (lower > upper) { showAlert('年化收益下限不能高于上限。'); return; }
const body = { annualized_return_lower_pct: value('annualized_return_lower_pct'), annualized_return_upper_pct: value('annualized_return_upper_pct'), max_drawdown_pct: value('max_drawdown_pct'), liquidity_requirement: value('liquidity_requirement'), investment_horizon_months: Number(value('investment_horizon_months')), benchmark_name: value('benchmark_name'), notes: value('notes') || null };
if (value('customer_id')) body.customer_id = Number(value('customer_id'));
await runAction('ADVISOR_CREATE_GOAL', body, '客户目标已提交,等待确认与审核');
clearAlert();
output.innerHTML = '<div class="advisor-output__loading">正在保存客户目标…</div>';
try {
const response = await apiClient.post('ADVISOR_CREATE_GOAL', body);
const data = response.data ?? response;
// 创建出的目标处于 `pending_confirmation`,**必须确认才能用于后续分析**。
// 所以这里直接跳到目标状态页,把确认按钮摆在眼前,而不是停在"已提交"。
if (body.customer_id) {
showAlert('客户目标已保存,请确认目标。', 'info');
await loadGoalStatus(body.customer_id);
return;
}
renderResult('客户目标已保存,等待确认与审核', data);
} catch (error) {
apiClient.reportError(error);
showAlert(error.message || '保存未完成,请稍后重试。');
output.innerHTML = '<div class="advisor-output__placeholder">请求失败,请检查权限或稍后重试。</div>';
}
}
async function runAction(endpoint, body, title) {
clearAlert(); output.innerHTML = '<div class="advisor-output__loading">正在请求服务端分析…</div>';
@@ -69,6 +190,7 @@ if (requireAdvisor()) {
function openAction(action) {
clearAlert();
if (action === 'goal') { renderGoalForm(); return; }
if (action === 'goal-status') { renderGoalStatusForm(); return; }
if (action === 'recommend') {
output.innerHTML = '<div class="advisor-inline-form"><label class="form-field"><span class="form-field__label">推荐数量</span><select class="form-field__input" data-recommend-limit><option value="1">1 个</option><option value="2">2 个</option><option value="3" selected>3 个</option></select></label><button class="button button--primary" type="button" data-submit-recommend>生成草案</button></div>';
output.querySelector('[data-submit-recommend]').addEventListener('click', () => runAction('ADVISOR_RECOMMEND', { limit: Number(output.querySelector('[data-recommend-limit]').value) }, '推荐方案草案'));
@@ -1,3 +1,3 @@
<!doctype html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>投顾工作台 · 南方财富</title><link rel="stylesheet" href="/static/portal/common/base.css"><link rel="stylesheet" href="/static/portal/common/operations.css"><link rel="stylesheet" href="/static/portal/employee-advisor/dashboard/dashboard.css?v=20260913"></head>
<body><main id="main-content" class="page-shell operations-shell advisor-shell"><section class="operations-hero advisor-hero fade-in"><div class="operations-hero__content"><p class="operations-kicker">客户洞察与方案协作</p><h1 class="operations-hero__title">投顾工作台</h1><p class="operations-hero__copy">围绕客户目标、组合分析与已发布方案开展合规的投顾协作,所有建议均需经过审核流程。</p><div class="operations-hero__signals"><span>客户目标</span><span>组合分析</span><span>方案发布</span></div></div><div class="operations-hero__meta"><strong data-advisor-name>投顾人员</strong><span data-advisor-scope>权限加载中</span></div></section><section class="metric-grid" data-advisor-metrics aria-label="投顾概览"></section><section class="advisor-workspace"><article class="panel panel--flush advisor-panel advisor-actions"><div class="panel__header"><div><h2 class="panel__title">工作操作</h2><p class="section-heading__meta">调用已授权的投顾分析能力,结果仅作为方案草案</p></div></div><div class="panel__body"><div class="action-grid"><button class="action-card" type="button" data-action="portfolio"><strong>组合分析</strong><span>查看持仓集中度、行业暴露与风险提示</span></button><button class="action-card" type="button" data-action="allocation"><strong>资产配置</strong><span>按已确认目标生成约束下的配置建议</span></button><button class="action-card" type="button" data-action="recommend"><strong>生成推荐草案</strong><span>基于适当性与流动性筛选场内基金</span></button><button class="action-card" type="button" data-action="goal"><strong>录入客户目标</strong><span>记录收益区间、回撤边界与投资期限</span></button></div><div class="form-alert" data-action-alert role="status"></div><div class="advisor-output" data-action-output aria-live="polite"><div class="advisor-output__placeholder">选择一项操作,结果将在这里展示。</div></div></div></article><article class="panel panel--flush advisor-panel"><div class="panel__header"><div><h2 class="panel__title">已发布方案</h2><p class="section-heading__meta">服务端已审核并发布的客户方案</p></div><button class="button table-action" type="button" data-refresh>刷新</button></div><div class="panel__body" data-recommendations></div></article></section><section class="advisor-grid"><article class="panel panel--flush advisor-panel advisor-panel--accent"><div class="panel__header"><div><h2 class="panel__title">工作边界</h2><p class="section-heading__meta">建议生成与审核留痕</p></div></div><div class="panel__body"><ul class="operations-list"><li class="operations-list__item"><strong>先读画像与目标</strong><span>仅访问分配范围内客户,不展示未授权信息。</span></li><li class="operations-list__item"><strong>再做组合分析</strong><span>分析结果是工作草案,不代替人工判断。</span></li><li class="operations-list__item"><strong>最后提交审核</strong><span>发布前保留审核、版本与审计记录。</span></li></ul></div></article></section></main><script type="module" src="/static/portal/employee-advisor/dashboard/dashboard.js"></script></body></html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>投顾工作台 · 南方财富</title><link rel="stylesheet" href="/static/portal/common/base.css"><link rel="stylesheet" href="/static/portal/common/operations.css"><link rel="stylesheet" href="/static/portal/employee-advisor/dashboard/dashboard.css?v=20260913-2"></head>
<body><main id="main-content" class="page-shell operations-shell advisor-shell"><section class="operations-hero advisor-hero fade-in"><div class="operations-hero__content"><p class="operations-kicker">客户洞察与方案协作</p><h1 class="operations-hero__title">投顾工作台</h1><p class="operations-hero__copy">围绕客户目标、组合分析与已发布方案开展合规的投顾协作,所有建议均需经过审核流程。</p><div class="operations-hero__signals"><span>客户目标</span><span>组合分析</span><span>方案发布</span></div></div><div class="operations-hero__meta"><strong data-advisor-name>投顾人员</strong><span data-advisor-scope>权限加载中</span></div></section><section class="metric-grid" data-advisor-metrics aria-label="投顾概览"></section><section class="advisor-workspace"><article class="panel panel--flush advisor-panel advisor-actions"><div class="panel__header"><div><h2 class="panel__title">工作操作</h2><p class="section-heading__meta">调用已授权的投顾分析能力,结果仅作为方案草案</p></div></div><div class="panel__body"><div class="action-grid"><button class="action-card" type="button" data-action="portfolio"><strong>组合分析</strong><span>查看持仓集中度、行业暴露与风险提示</span></button><button class="action-card" type="button" data-action="allocation"><strong>资产配置</strong><span>按已确认目标生成约束下的配置建议</span></button><button class="action-card" type="button" data-action="recommend"><strong>生成推荐草案</strong><span>基于适当性与流动性筛选场内基金</span></button><button class="action-card" type="button" data-action="goal"><strong>录入客户目标</strong><span>记录收益区间、回撤边界与投资期限</span></button><button class="action-card" type="button" data-action="goal-status"><strong>目标确认与方案书</strong><span>查看客户目标状态、确认目标、查看方案书</span></button></div><div class="form-alert" data-action-alert role="status"></div><div class="advisor-output" data-action-output aria-live="polite"><div class="advisor-output__placeholder">选择一项操作,结果将在这里展示。</div></div></div></article><article class="panel panel--flush advisor-panel"><div class="panel__header"><div><h2 class="panel__title">已发布方案</h2><p class="section-heading__meta">服务端已审核并发布的客户方案</p></div><button class="button table-action" type="button" data-refresh>刷新</button></div><div class="panel__body" data-recommendations></div></article></section><section class="advisor-grid"><article class="panel panel--flush advisor-panel advisor-panel--accent"><div class="panel__header"><div><h2 class="panel__title">工作边界</h2><p class="section-heading__meta">建议生成与审核留痕</p></div></div><div class="panel__body"><ul class="operations-list"><li class="operations-list__item"><strong>先读画像与目标</strong><span>仅访问分配范围内客户,不展示未授权信息。</span></li><li class="operations-list__item"><strong>再做组合分析</strong><span>分析结果是工作草案,不代替人工判断。</span></li><li class="operations-list__item"><strong>最后提交审核</strong><span>发布前保留审核、版本与审计记录。</span></li></ul></div></article></section></main><script type="module" src="/static/portal/employee-advisor/dashboard/dashboard.js?v=20260913-2"></script></body></html>
@@ -39,6 +39,6 @@
</main>
<dialog class="operations-dialog" data-admin-detail><div class="operations-dialog__header"><h2 data-detail-title>详情</h2><button class="button icon-button" type="button" data-close-detail aria-label="关闭">×</button></div><div class="operations-dialog__body" data-detail-body></div><div class="operations-dialog__footer"><button class="button" type="button" data-close-detail>关闭</button></div></dialog>
<dialog class="operations-dialog" data-admin-action><form data-admin-action-form><div class="operations-dialog__header"><h2 data-admin-action-title>确认操作</h2><button class="button icon-button" type="button" data-close-admin-action aria-label="关闭">×</button></div><div class="operations-dialog__body"><p class="admin-action-copy" data-admin-action-copy></p><label class="form-field" data-admin-comment-field hidden><span class="form-field__label">审核意见</span><textarea class="form-field__input admin-comment" name="comment" maxlength="1000"></textarea></label><div class="form-alert" data-admin-action-alert></div></div><div class="operations-dialog__footer"><button class="button" type="button" data-close-admin-action>取消</button><button class="button button--primary" type="submit">确认提交</button></div></form></dialog>
<script type="module" src="/static/portal/employee-console/workspace/workspace.js"></script>
<script type="module" src="/static/portal/employee-console/workspace/workspace.js?v=20260913"></script>
</body>
</html>
@@ -1,3 +1,3 @@
<!doctype html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>运营工作台 · 南方财富</title><link rel="stylesheet" href="/static/portal/common/base.css"><link rel="stylesheet" href="/static/portal/common/operations.css"><link rel="stylesheet" href="/static/portal/employee-operations/dashboard/dashboard.css?v=20260913"></head>
<body><main id="main-content" class="page-shell operations-shell operator-shell"><section class="operations-hero operator-hero fade-in"><div class="operations-hero__content"><p class="operations-kicker">场外运营与资料处理</p><h1 class="operations-hero__title">运营工作台</h1><p class="operations-hero__copy">集中查看收件箱、识别任务与规则状态,所有场外运营流程与场内模拟交易数据严格隔离。</p><div class="operations-hero__signals"><span>邮件收件</span><span>识别队列</span><span>规则核对</span></div></div><div class="operations-hero__meta"><strong data-operator-name>运营人员</strong><span data-operator-scope>权限加载中</span></div></section><section class="metric-grid" data-operator-metrics aria-label="运营概览"></section><section class="operator-grid"><article class="panel panel--flush"><div class="panel__header"><div><h2 class="panel__title">场外基金收件箱</h2><p class="section-heading__meta">仅展示未删除的运营邮件</p></div><button class="button table-action" type="button" data-refresh>刷新</button></div><div class="panel__body" data-mails></div></article><article class="panel panel--flush operator-status"><div class="panel__header"><div><h2 class="panel__title">运行状态</h2><p class="section-heading__meta">收件游标与识别监控</p></div></div><div class="panel__body" data-mailbox></div></article></section></main><script type="module" src="/static/portal/employee-operations/dashboard/dashboard.js"></script></body></html>
<body><main id="main-content" class="page-shell operations-shell operator-shell"><section class="operations-hero operator-hero fade-in"><div class="operations-hero__content"><p class="operations-kicker">场外运营与资料处理</p><h1 class="operations-hero__title">运营工作台</h1><p class="operations-hero__copy">集中查看收件箱、识别任务与规则状态,所有场外运营流程与场内模拟交易数据严格隔离。</p><div class="operations-hero__signals"><span>邮件收件</span><span>识别队列</span><span>规则核对</span></div></div><div class="operations-hero__meta"><strong data-operator-name>运营人员</strong><span data-operator-scope>权限加载中</span></div></section><section class="metric-grid" data-operator-metrics aria-label="运营概览"></section><section class="operator-grid"><article class="panel panel--flush"><div class="panel__header"><div><h2 class="panel__title">场外基金收件箱</h2><p class="section-heading__meta">仅展示未删除的运营邮件</p></div><button class="button table-action" type="button" data-refresh>刷新</button></div><div class="panel__body" data-mails></div></article><article class="panel panel--flush operator-status"><div class="panel__header"><div><h2 class="panel__title">运行状态</h2><p class="section-heading__meta">收件游标与识别监控</p></div></div><div class="panel__body" data-mailbox></div></article></section></main><script type="module" src="/static/portal/employee-operations/dashboard/dashboard.js?v=20260913"></script></body></html>