rebuild(audit): verify page state matrix

This commit is contained in:
leefer
2026-07-30 11:12:37 +08:00
parent b79b4ba280
commit 4eafa1cc3f
13 changed files with 386 additions and 37 deletions
@@ -15,6 +15,7 @@ type AccountMembership = { user_id: number; username: string; is_admin: boolean;
const ui = useUiStore();
const accounts = ref<AccountMembership[]>([]);
const loading = ref(true);
const selectedId = ref<number | null>(null);
const duration = ref("1_month");
const dailyLimit = ref(50);
@@ -30,11 +31,15 @@ const sortedAccounts = computed(() =>
);
async function load(): Promise<void> {
loading.value = true;
errorMessage.value = "";
try {
accounts.value = await api.get<AccountMembership[]>("/admin/memberships");
if (selectedId.value === null && accounts.value[0]) select(accounts.value[0]);
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : "会员列表读取失败。";
} finally {
loading.value = false;
}
}
@@ -76,7 +81,9 @@ onMounted(load);
<div class="system-grid member-grid">
<section class="card">
<header class="card-header"><h2>账号会员状态</h2><span class="tag">{{ accounts.length }} 个账号</span></header>
<div class="table-wrap">
<p v-if="loading" class="panel-loading muted">正在读取会员列表</p>
<p v-else-if="errorMessage" class="panel-loading field-error" role="alert">{{ errorMessage }}</p>
<div v-else class="table-wrap">
<table class="data-table">
<thead><tr><th>账号</th><th>身份</th><th>会员状态</th><th>到期时间</th><th class="num" :aria-sort="limitDescending ? 'descending' : 'ascending'"><button class="sort-button" type="button" @click="limitDescending = !limitDescending">每日上限 <span aria-hidden="true">{{ limitDescending ? "↓" : "↑" }}</span></button></th></tr></thead>
<tbody>
@@ -17,6 +17,7 @@ type ModelItem = {
const ui = useUiStore();
const models = ref<ModelItem[]>([]);
const loading = ref(true);
const editingId = ref<number | null>(null);
const primaryId = ref<number | null>(null);
const fallbackId = ref<number | null>(null);
@@ -45,12 +46,16 @@ function edit(model: ModelItem): void {
}
async function load(): Promise<void> {
loading.value = true;
errorMessage.value = "";
try {
models.value = await api.get<ModelItem[]>("/admin/models");
primaryId.value = models.value.find((item) => item.is_primary)?.id ?? null;
fallbackId.value = models.value.find((item) => item.is_fallback)?.id ?? null;
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : "模型池读取失败。";
} finally {
loading.value = false;
}
}
@@ -139,11 +144,15 @@ onMounted(load);
<section class="card">
<header class="card-header"><h2>模型池</h2><span class="tag">{{ models.length }} / 20</span></header>
<div class="model-list">
<button v-for="model in models" :key="model.id" class="model-row" :class="{ active: editingId === model.id }" type="button" @click="edit(model)">
<span><strong>{{ model.display_name }}</strong><small>{{ model.model_identifier }}</small></span>
<span class="model-tags"><span v-if="model.is_primary" class="tag">主模型</span><span v-if="model.is_fallback" class="tag">辅助</span></span>
</button>
<p v-if="!models.length" class="panel-loading muted">尚未添加模型</p>
<p v-if="loading" class="panel-loading muted">正在读取模型池</p>
<p v-else-if="errorMessage" class="panel-loading field-error" role="alert">{{ errorMessage }}</p>
<template v-else>
<button v-for="model in models" :key="model.id" class="model-row" :class="{ active: editingId === model.id }" type="button" @click="edit(model)">
<span><strong>{{ model.display_name }}</strong><small>{{ model.model_identifier }}</small></span>
<span class="model-tags"><span v-if="model.is_primary" class="tag">主模型</span><span v-if="model.is_fallback" class="tag">辅助</span></span>
</button>
<p v-if="!models.length" class="panel-loading muted">尚未添加模型</p>
</template>
</div>
</section>
<div class="system-stack">
@@ -10,14 +10,16 @@ const ui = useUiStore();
const items = ref<AlertItem[]>([]);
const unreadOnly = ref(false);
const loading = ref(true);
const error = ref("");
const form = reactive({ title: "", remind_date: new Date().toISOString().slice(0, 10), code: "", content: "" });
async function load(): Promise<void> {
loading.value = true;
error.value = "";
try {
const result = await reviewApi.alerts(unreadOnly.value);
items.value = result.items;
ui.alertUnread = result.unread_count;
} catch (reason) { ui.showToast(reason instanceof Error ? reason.message : "提醒读取失败"); }
} catch (reason) { error.value = reason instanceof Error ? reason.message : "提醒读取失败"; }
finally { loading.value = false; }
}
async function create(): Promise<void> {
@@ -39,6 +41,7 @@ onMounted(load);
<div class="alerts-toolbar"><div class="segmented"><button type="button" :class="{ active: !unreadOnly }" @click="select(false)">全部</button><button type="button" :class="{ active: unreadOnly }" @click="select(true)">未读</button></div><button class="btn btn-small" type="button" @click="markAll">全部已读</button></div>
<form class="alert-create" @submit.prevent="create"><div class="alert-create-grid"><label class="field"><span class="field-label">标题</span><input v-model="form.title" class="input" maxlength="80" required /></label><label class="field"><span class="field-label">提醒日期</span><input v-model="form.remind_date" class="input" type="date" required /></label><label class="field"><span class="field-label">股票代码(可选)</span><input v-model="form.code" class="input" maxlength="12" /></label></div><label class="field"><span class="field-label">提醒内容(可选)</span><textarea v-model="form.content" class="textarea" maxlength="500"></textarea></label><div class="form-actions"><button class="btn btn-primary" type="submit">保存提醒</button></div></form>
<div v-if="loading" class="workspace-state">正在读取提醒</div>
<EmptyState v-else-if="error" title="提醒暂不可用" :description="error" />
<div v-else-if="items.length" class="alert-list"><article v-for="item in items" :key="item.id" :class="{ unread: !item.is_read && item.due }"><div><strong>{{ item.title }}</strong><span class="tag">{{ item.due ? (item.is_read ? "已读" : "未读") : "未到期" }}</span></div><p>{{ item.content }}</p><small>{{ item.available_date }}<template v-if="item.code"> · <MarketEntityLink :entity="{ entity_type: 'stock', identifier: item.code, code: item.code, name: item.code, sector: null }" /></template></small><div class="table-actions"><button v-if="item.due && !item.is_read" class="btn btn-small" type="button" @click="mark(item)">标为已读</button><button class="btn btn-small" type="button" @click="remove(item)">删除</button></div></article></div>
<EmptyState v-else title="暂无提醒" description="可以创建站内提醒,策略跟踪反馈也会自动汇总到这里。" />
</div>
@@ -12,6 +12,8 @@ const ui = useUiStore();
const messages = ref<AssistantMessage[]>([]);
const question = ref("");
const loading = ref(false);
const historyLoading = ref(false);
const historyError = ref("");
const messageRoot = ref<HTMLElement | null>(null);
let controller: AbortController | undefined;
const locked = computed(() => !session.account?.smart_access);
@@ -23,8 +25,11 @@ const prompts: Array<[string, string]> = [
];
async function load(): Promise<void> {
if (locked.value) return;
historyLoading.value = true;
historyError.value = "";
try { messages.value = await reviewApi.assistantMessages(); }
catch (reason) { ui.showToast(reason instanceof Error ? reason.message : "对话读取失败"); }
catch (reason) { historyError.value = reason instanceof Error ? reason.message : "对话读取失败"; }
finally { historyLoading.value = false; }
}
async function send(): Promise<void> {
const value = question.value.trim();
@@ -58,7 +63,7 @@ onMounted(load);
<div class="assistant-panel">
<div v-if="locked" class="notice notice-warning"><strong>复盘助手仅对会员开放</strong><span>开通会员后可读取市场统计策略跟踪与个人复盘记录进行统一复盘分析</span></div>
<div class="assistant-content" :class="{ 'locked-content': locked }" :aria-disabled="locked">
<div ref="messageRoot" class="assistant-messages"><article v-for="message in messages" :key="message.id" :class="message.role"><span>{{ message.role === 'user' ? '我' : '复盘助手' }}</span><p>{{ message.content || (loading ? "正在整理复盘材料" : "") }}</p></article><div v-if="!messages.length" class="assistant-empty">可以从市场策略或自己的交易记录开始复盘</div></div>
<div ref="messageRoot" class="assistant-messages"><div v-if="historyLoading" class="assistant-empty">正在读取复盘助手对话</div><div v-else-if="historyError" class="assistant-empty"><strong>复盘助手对话暂不可用</strong><p>{{ historyError }}</p></div><template v-else><article v-for="message in messages" :key="message.id" :class="message.role"><span>{{ message.role === 'user' ? '我' : '复盘助手' }}</span><p>{{ message.content || (loading ? "正在整理复盘材料" : "") }}</p></article><div v-if="!messages.length" class="assistant-empty">可以从市场策略或自己的交易记录开始复盘</div></template></div>
<div class="assistant-prompts"><button v-for="item in prompts" :key="item[0]" type="button" :disabled="locked" @click="usePrompt(item[1])">{{ item[0] }}</button></div>
<form class="assistant-compose" @submit.prevent="send"><textarea v-model="question" class="textarea" maxlength="2000" placeholder="询问市场位置、主线、策略表现或自己的交易模式" :disabled="locked" required></textarea><div><button v-if="loading" class="btn" type="button" @click="stop">停止</button><button v-else class="btn btn-primary" type="submit" :disabled="locked">发送</button><button class="btn btn-ghost" type="button" :disabled="locked" @click="clear">清空对话</button></div></form>
</div>