Compare commits

Author SHA1 Message Date
Blizzard 5003a44d2b Merge pull request 'fix: display stock codes alongside names across admin views' (#7) from dev into master
Veyra CI/CD / verify (push) Successful in 5m18s
Veyra CI/CD / deploy (push) Successful in 2m20s
2026-10-09 14:58:08 +00:00
Blizzard 9d56f6169a fix: display stock codes alongside names across admin views 2026-10-09 22:57:56 +08:00
Blizzard 7ad49c4064 Merge pull request 'fix: match padded stock names in security searches' (#6) from dev into master
Veyra CI/CD / verify (push) Successful in 4m33s
Veyra CI/CD / deploy (push) Canceled after 0s
2026-10-09 14:56:35 +00:00
Blizzard 1496d77bcc fix: match padded stock names in security searches 2026-10-09 22:56:22 +08:00
Blizzard 393f83bb9a Merge pull request 'fix: persist configurable model request deadlines' (#5) from dev into master
Veyra CI/CD / verify (push) Successful in 4m23s
Veyra CI/CD / deploy (push) Canceled after 0s
2026-10-09 14:53:53 +00:00
Blizzard d39fb0e0d2 fix: persist configurable model request deadlines 2026-10-09 22:53:40 +08:00
9 changed files with 62 additions and 10 deletions
+15 -3
View File
@@ -2,13 +2,25 @@
更新日期:2026-10-09。 更新日期:2026-10-09。
## AI 接入与本轮部署(2026-10-09,进行中) ## AI 接入与本轮部署(2026-10-09)
- 用户后来指定 GPT-6.1-Astra;实际供应商目录没有此 ID,请求 404。用户确认改用 gpt-6.1-sol,模型目录匹配且约 6.8 秒真实结构化连接测试通过;主配置已保存该模型,密钥保留。
- 用户明确要求 AI 分析/审核通过后自动发布、不逐条人工操作,并授权通过 CI/CD 部署到现有服务器。新增十五版迁移:审核策略配置与候选分析任务,所有表使用 veyra_、字符串 UUID。 - 用户明确要求 AI 分析/审核通过后自动发布、不逐条人工操作,并授权通过 CI/CD 部署到现有服务器。新增十五版迁移:审核策略配置与候选分析任务,所有表使用 veyra_、字符串 UUID。
- 接入 Responses / Chat Completions 结构化调用,密钥从现有 Vault 解密,禁止重定向携带密钥;主分析与独立复核严格验证证券、Schema、证据索引与文本边界。不改写价格、不生成用户计划或成交。 - 接入 Responses / Chat Completions 结构化调用,密钥从现有 Vault 解密,禁止重定向携带密钥;主分析与独立复核严格验证证券、Schema、证据索引与文本边界。不改写价格、不生成用户计划或成交。
- 后台增加 AI 审核和自动发布开关、任务状态、处理入口、分析与复核结果;审核模型可安全复用主配置。Worker 每 30 秒检查,五分钟租约、版本去重、失败最多三次并间隔五分钟;提交时重新核对配置、授权身份、策略与候选,结果和发布/审计/Outbox 同事务。 - 后台增加 AI 审核和自动发布开关、任务状态、处理入口、分析与复核结果;审核模型可安全复用主配置。Worker 每 30 秒检查,五分钟租约、版本去重、失败最多三次并间隔五分钟;提交时重新核对配置、授权身份、策略与候选,结果和发布/审计/Outbox 同事务。
- App 详情增加真实 AI 分析、复核与资料缺口;统一 Screen 隐藏纵横滚动条,保留滑动。完整 Go race、类型检查、管理端构建与 App 双端 Hermes 导出通过。已用现有 Vault 密钥实际调用 gpt-5.5:约 3.4 秒返回有效结构化 REJECT(明确缺少研究数据的连接测试),接口可用且没有发布测试候选。PR #3 合并 bf643b0,run 40 因源码下载截断失败,未部署;正在补下载重试与 tar 完整性校验。 - App 详情增加真实 AI 分析、复核与资料缺口;统一 Screen 隐藏纵横滚动条,保留滑动。完整 Go race、类型检查、管理端构建与 App 双端 Hermes 导出通过。已用现有 Vault 密钥实际调用 gpt-5.5:约 3.4 秒返回有效结构化 REJECT(明确缺少研究数据的连接测试),接口可用且没有发布测试候选。PR #3 合并 bf643b0,run 40 因源码下载截断失败,未部署;下载重试与 tar 完整性校验经 PR #4 合并 b47889c,run 41 的 verify/deploy 均成功,容器 healthy。
- iPhone 包新增 Expo 云端未签名设备 IPA 构建;只上传隔离 App 源码,不上传后端私有环境。首轮自定义步骤名称错误已修正为显式 pod install;已提交构建 e36bba44-4949-4069-a2fb-e1f491dac7f1,普通 Apple ID 安装仍需个人重签,尚未声称安装包完成或可直接安装。 - iPhone 包新增 Expo 云端未签名设备 IPA 构建;只上传隔离 App 源码,不上传后端私有环境。首轮自定义步骤名称错误已修正为显式 pod install;已提交构建 e36bba44-4949-4069-a2fb-e1f491dac7f1,构建已 FINISHED;设备 IPA 已下载到 Downloads/Veyra-unsigned.ipa(10,953,223 字节),zip 完整性、cn.sundynix.veyra、iPhoneOS/arm64、内嵌 Hermes 和公开 HTTPS 地址验证通过,无签名。SHA-256 19b6650dd73712fcf612f9b84b3cd7efdc8e5470d99b3f0d17bbaabe5972a9ac。IPA 与校验文件已上传 Gitea 预发布 v0.1.0-preview.20261009;普通 Apple ID 仍需个人重签,实际安装运行待用户完成。
- 实际主分析与复核均已启用 gpt-6.1-sol,现有加密密钥保留;AI 审核策略版本 2 已启用自动发布。核对实际审计:两条已有候选在 22:11 已经人工发布,一个计划在 22:17 已确认,均早于本轮 AI 启用;App 当前可见两条候选、一个计划、原有模拟账户现金 1000.00。不能把既有人工发布称作 AI 结果;暂无待审核候选,AI 任务列表为空,后续新候选自动处理。没有创建或改写用户资金、计划、成交。
- 部署后的配置校验发现旧数据库 timeout_seconds 上限仍为 30,已新增十六版迁移匹配调用端 120 秒上限,并使用实际数据库隔离 schema 测试 120 秒配置持久化;Go race 与 go vet 通过,正在通过 PR / CI 部署修复。
## 股票名称搜索修复(2026-10-09)
- 实际 HTTPS 查询复现:名称“诺普信”无结果,但代码 002215 和名称“诺 普 信”均返回该深交所股票。数据源在名称中填充了空格,原 SQL LIKE 连续匹配漏掉无空格查询,证券并未漏同步。
- 查询统一忽略名称与输入中的空格、制表符和全角空格,保持原始源名称;转义 LIKE 通配符为字面匹配,避免输入 % 或 _ 返回全部股票。管理端无搜索结果文案与无同步数据区分。真实 PostgreSQL 隔离 schema 的 race 回归覆盖名称/代码/空格和通配符,go vet、管理端类型检查/build/embed 通过;已通过 PR #6 合并,待 CI 上线后实查。
- 按用户补充要求,管理端行情列表/详情、候选队列、审核详情/确认、总览观察与信号卡将代码与名称并列展示,代码使用等宽字形;选股、回测、策略证券列表以及 App 候选/计划/持仓/成交原已有代码,继续保留。管理端统一 StockIdentity 组件,类型检查/build/embed 通过。
- 用户最新指示先不处理 App 安装,保留已有 IPA 产物;当前优先验证名称搜索、股票代码展示与模型运行配置。
## App Expo Go 真机入口(2026-10-09) ## App Expo Go 真机入口(2026-10-09)
@@ -0,0 +1,6 @@
-- Keep the persisted limit aligned with the model client's configurable deadline.
ALTER TABLE veyra_provider_config
DROP CONSTRAINT veyra_provider_config_timeout_seconds_check;
ALTER TABLE veyra_provider_config
ADD CONSTRAINT veyra_provider_config_timeout_seconds_check
CHECK (timeout_seconds BETWEEN 1 AND 120);
+4 -1
View File
@@ -43,11 +43,14 @@ func testProviderLifecycle(t *testing.T, ctx context.Context, pool *pgxpool.Pool
t.Fatal("stale configuration accepted") t.Fatal("stale configuration accepted")
} }
token := "test-private-provider-key" token := "test-private-provider-key"
input = service.ProviderInput{Name: "configured vendor", BaseURL: "https://provider.example/v1", ModelName: "configured-model", ModelCatalogURL: "https://provider.example/v1/models", TimeoutSeconds: 15, Enabled: true, APIKey: &token, ExpectedVersion: config.Version} input = service.ProviderInput{Name: "configured vendor", BaseURL: "https://provider.example/v1", ModelName: "configured-model", ModelCatalogURL: "https://provider.example/v1/models", TimeoutSeconds: 120, Enabled: true, APIKey: &token, ExpectedVersion: config.Version}
config, err = svc.Save(ctx, actor, "model_primary", uuid.NewString(), input) config, err = svc.Save(ctx, actor, "model_primary", uuid.NewString(), input)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if config.TimeoutSeconds != 120 {
t.Fatal("model deadline was not persisted")
}
data, _ := json.Marshal(config) data, _ := json.Marshal(config)
if bytes.Contains(data, []byte(token)) || bytes.Contains(data, []byte("encrypted_api_key")) { if bytes.Contains(data, []byte(token)) || bytes.Contains(data, []byte("encrypted_api_key")) {
t.Fatal("API exposes credential") t.Fatal("API exposes credential")
+16
View File
@@ -47,6 +47,22 @@ func testScreeningLifecycle(t *testing.T, ctx context.Context, pool *pgxpool.Poo
if err := repository.NewMarketRepository(pool).Save(ctx, provider, []model.Instrument{instrument}, time.Now().UTC(), ""); err != nil { if err := repository.NewMarketRepository(pool).Save(ctx, provider, []model.Instrument{instrument}, time.Now().UTC(), ""); err != nil {
t.Fatal(err) t.Fatal(err)
} }
searchRepo := repository.NewMarketRepository(pool)
if err := searchRepo.Save(ctx, provider, []model.Instrument{{Symbol: "002215", Exchange: "SZSE", Name: "诺 普 信", PriceCents: 1000, ObservedAt: observed}}, time.Now().UTC(), ""); err != nil {
t.Fatal(err)
}
for _, query := range []string{"诺普信", "诺 普 信", " 诺\t普 信 ", "002215", "002 215"} {
stocks, err := searchRepo.List(ctx, query)
if err != nil || len(stocks) != 1 || stocks[0].Symbol != "002215" || stocks[0].Name != "诺 普 信" {
t.Fatalf("padded security search %q: %v, %v", query, stocks, err)
}
}
for _, query := range []string{"not-a-security", "%", "_"} {
stocks, err := searchRepo.List(ctx, query)
if err != nil || len(stocks) != 0 {
t.Fatalf("literal security search %q matched: %v, %v", query, stocks, err)
}
}
repo := repository.NewScreeningRepository(pool) repo := repository.NewScreeningRepository(pool)
targets, err := repo.Targets(ctx, provider) targets, err := repo.Targets(ctx, provider)
if err != nil || len(targets) != 1 { if err != nil || len(targets) != 1 {
+5 -1
View File
@@ -7,6 +7,7 @@ import (
"git.sundynix.cn/Blizzard/Veyra/internal/model" "git.sundynix.cn/Blizzard/Veyra/internal/model"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"strings"
"time" "time"
) )
@@ -14,7 +15,10 @@ type MarketRepository struct{ pool *pgxpool.Pool }
func NewMarketRepository(pool *pgxpool.Pool) *MarketRepository { return &MarketRepository{pool} } func NewMarketRepository(pool *pgxpool.Pool) *MarketRepository { return &MarketRepository{pool} }
func (r *MarketRepository) List(ctx context.Context, query string) ([]model.Instrument, error) { func (r *MarketRepository) List(ctx context.Context, query string) ([]model.Instrument, error) {
rows, e := r.pool.Query(ctx, `SELECT id,symbol,exchange,name,price_cents,observed_at,received_at,source_id,source_version FROM veyra_instrument WHERE symbol LIKE $1 OR name LIKE $1 ORDER BY (price_cents>0) DESC,symbol LIMIT 100`, "%"+query+"%") // Vendors pad some short names with whitespace (for example "诺 普 信").
// Normalize only for matching; preserve the supplied name in stored data.
pattern := "%" + strings.NewReplacer(`\`, `\\`, "%", `\%`, "_", `\_`).Replace(query) + "%"
rows, e := r.pool.Query(ctx, `SELECT id,symbol,exchange,name,price_cents,observed_at,received_at,source_id,source_version FROM veyra_instrument WHERE symbol LIKE regexp_replace($1,'[[:space:] ]+','','g') OR regexp_replace(name,'[[:space:] ]+','','g') LIKE regexp_replace($1,'[[:space:] ]+','','g') ORDER BY (price_cents>0) DESC,symbol LIMIT 100`, pattern)
if e != nil { if e != nil {
return nil, mapError(e) return nil, mapError(e)
} }
+3 -2
View File
@@ -1,3 +1,4 @@
import {StockIdentity} from "./StockIdentity"
import {useEffect,useRef,useState,type FormEvent} from "react" import {useEffect,useRef,useState,type FormEvent} from "react"
import {ScreeningRunControl} from "./ScreeningRunControl" import {ScreeningRunControl} from "./ScreeningRunControl"
import {StockHistoryChart} from "./StockHistoryChart" import {StockHistoryChart} from "./StockHistoryChart"
@@ -35,10 +36,10 @@ export function CandidatesPanel({onSessionExpired,initialCandidateID}:{onSession
{([["观察逻辑","thesis"],["反证与风险","counter_evidence"],["失效条件","invalidation"]] as const).map(([label,key])=><label key={key} className="block space-y-2 text-sm"><span>{label}</span><textarea className={control} rows={3} required maxLength={key==="thesis"?2000:1000} value={content[key]} onChange={e=>set(key,e.target.value)}/></label>)} {([["观察逻辑","thesis"],["反证与风险","counter_evidence"],["失效条件","invalidation"]] as const).map(([label,key])=><label key={key} className="block space-y-2 text-sm"><span>{label}</span><textarea className={control} rows={3} required maxLength={key==="thesis"?2000:1000} value={content[key]} onChange={e=>set(key,e.target.value)}/></label>)}
<div className="space-y-3"><h3 className="text-sm font-medium">证据来源</h3>{content.evidence.map((evidence,i)=><div key={i} className="grid gap-3 rounded-lg border p-4 sm:grid-cols-2">{([["来源 HTTPS 链接","source_url"],["页码 / 段落 / 标题","locator"],["证据摘要","summary"]] as const).map(([label,key])=><label className="space-y-2 text-sm" key={key}><span>{label}</span><Input required type={key==="source_url"?"url":"text"} value={evidence[key]} onChange={e=>setContent(c=>({...c,evidence:c.evidence.map((item,index)=>index===i?{...item,[key]:e.target.value}:item)}))}/></label>)}{content.evidence.length>1&&<Button type="button" variant="ghost" onClick={()=>setContent(c=>({...c,evidence:c.evidence.filter((_,index)=>index!==i)}))}>移除此证据</Button>}</div>)}<Button type="button" variant="outline" disabled={content.evidence.length>=8} onClick={()=>setContent(c=>({...c,evidence:[...c.evidence,{source_url:"",locator:"",summary:""}]}))}>添加证据</Button></div> <div className="space-y-3"><h3 className="text-sm font-medium">证据来源</h3>{content.evidence.map((evidence,i)=><div key={i} className="grid gap-3 rounded-lg border p-4 sm:grid-cols-2">{([["来源 HTTPS 链接","source_url"],["页码 / 段落 / 标题","locator"],["证据摘要","summary"]] as const).map(([label,key])=><label className="space-y-2 text-sm" key={key}><span>{label}</span><Input required type={key==="source_url"?"url":"text"} value={evidence[key]} onChange={e=>setContent(c=>({...c,evidence:c.evidence.map((item,index)=>index===i?{...item,[key]:e.target.value}:item)}))}/></label>)}{content.evidence.length>1&&<Button type="button" variant="ghost" onClick={()=>setContent(c=>({...c,evidence:c.evidence.filter((_,index)=>index!==i)}))}>移除此证据</Button>}</div>)}<Button type="button" variant="outline" disabled={content.evidence.length>=8} onClick={()=>setContent(c=>({...c,evidence:[...c.evidence,{source_url:"",locator:"",summary:""}]}))}>添加证据</Button></div>
<div className="grid gap-4 sm:grid-cols-4">{([["参考区间下限","entry_low"],["参考区间上限","entry_high"],["风险价格","stop_loss"],["目标观察价格","target_price"]] as const).map(([label,key])=><label className="space-y-2 text-sm" key={key}><span>{label}</span><Input required inputMode="decimal" value={content.reference_plan[key]} onChange={e=>setContent(c=>({...c,reference_plan:{...c.reference_plan,[key]:e.target.value}}))}/></label>)}</div><p className="text-xs text-muted-foreground">金额最多两位小数;风险价格 &lt; 下限 ≤ 上限 &lt; 目标价格。观察时间限最近 24 小时,有效期最长 7 天。</p><div className="flex gap-3"><Button disabled={busy} type="submit">{busy?"保存中":"保存新草稿"}</Button><Button type="button" variant="outline" disabled={busy} onClick={()=>setEditing(false)}>取消</Button></div></form></CardContent></Card>} <div className="grid gap-4 sm:grid-cols-4">{([["参考区间下限","entry_low"],["参考区间上限","entry_high"],["风险价格","stop_loss"],["目标观察价格","target_price"]] as const).map(([label,key])=><label className="space-y-2 text-sm" key={key}><span>{label}</span><Input required inputMode="decimal" value={content.reference_plan[key]} onChange={e=>setContent(c=>({...c,reference_plan:{...c.reference_plan,[key]:e.target.value}}))}/></label>)}</div><p className="text-xs text-muted-foreground">金额最多两位小数;风险价格 &lt; 下限 ≤ 上限 &lt; 目标价格。观察时间限最近 24 小时,有效期最长 7 天。</p><div className="flex gap-3"><Button disabled={busy} type="submit">{busy?"保存中":"保存新草稿"}</Button><Button type="button" variant="outline" disabled={busy} onClick={()=>setEditing(false)}>取消</Button></div></form></CardContent></Card>}
<div className="grid items-start gap-6 xl:grid-cols-[.9fr_1.3fr]"><Card><CardHeader><div className="flex items-center justify-between"><CardTitle>候选队列</CardTitle><select aria-label="候选状态" className={`${control} max-w-32`} value={status} onChange={e=>setStatus(e.target.value)}><option value="">全部状态</option>{Object.entries(states).map(([key,label])=><option key={key} value={key}>{label}</option>)}</select></div><CardDescription>最近 100 条最新修订及当前发布版本。</CardDescription></CardHeader><CardContent className="space-y-2">{loading&&<p className="text-sm text-muted-foreground">正在读取…</p>}{!loading&&list.length===0&&<p className="py-8 text-center text-sm text-muted-foreground">尚无符合此状态的股票候选。点击上方自动筛选,命中结果会进入待审核;无需手工录入。</p>}{list.map(c=><button disabled={busy} key={c.id} onClick={()=>void select(c)} className={`w-full rounded-lg border p-4 text-left transition hover:bg-muted ${selected?.id===c.id?"border-primary bg-accent/40":""}`}><div className="flex items-center justify-between gap-2"><strong className="text-lg">{c.content.name}</strong><Badge variant="outline">{states[c.status]}</Badge></div><p className="mt-1 font-mono text-xs text-muted-foreground">{c.content.exchange} · {c.content.symbol}</p><div className="my-3 flex items-end gap-2"><span className="text-xs text-muted-foreground">参考区间</span><strong className="font-mono text-xl">¥{c.content.reference_plan.entry_low}–{c.content.reference_plan.entry_high}</strong></div><p className="text-xs text-muted-foreground">风险 ¥{c.content.reference_plan.stop_loss} · 目标观察 ¥{c.content.reference_plan.target_price}</p><p className="mt-3 text-sm">{c.content.title}</p><p className="mt-2 text-xs text-muted-foreground">修订 {c.revision} · 版本 {c.version} · {c.is_active_publication?"当前有效发布":c.status==="PUBLISHED"?"已过期或被替代":"模拟研究"}</p></button>)}</CardContent></Card> <div className="grid items-start gap-6 xl:grid-cols-[.9fr_1.3fr]"><Card><CardHeader><div className="flex items-center justify-between"><CardTitle>候选队列</CardTitle><select aria-label="候选状态" className={`${control} max-w-32`} value={status} onChange={e=>setStatus(e.target.value)}><option value="">全部状态</option>{Object.entries(states).map(([key,label])=><option key={key} value={key}>{label}</option>)}</select></div><CardDescription>最近 100 条最新修订及当前发布版本。</CardDescription></CardHeader><CardContent className="space-y-2">{loading&&<p className="text-sm text-muted-foreground">正在读取…</p>}{!loading&&list.length===0&&<p className="py-8 text-center text-sm text-muted-foreground">尚无符合此状态的股票候选。点击上方自动筛选,命中结果会进入待审核;无需手工录入。</p>}{list.map(c=><button disabled={busy} key={c.id} onClick={()=>void select(c)} className={`w-full rounded-lg border p-4 text-left transition hover:bg-muted ${selected?.id===c.id?"border-primary bg-accent/40":""}`}><div className="flex items-center justify-between gap-2"><strong className="text-lg"><StockIdentity name={c.content.name} symbol={c.content.symbol}/></strong><Badge variant="outline">{states[c.status]}</Badge></div><p className="mt-1 font-mono text-xs text-muted-foreground">{c.content.exchange} · {c.content.symbol}</p><div className="my-3 flex items-end gap-2"><span className="text-xs text-muted-foreground">参考区间</span><strong className="font-mono text-xl">¥{c.content.reference_plan.entry_low}–{c.content.reference_plan.entry_high}</strong></div><p className="text-xs text-muted-foreground">风险 ¥{c.content.reference_plan.stop_loss} · 目标观察 ¥{c.content.reference_plan.target_price}</p><p className="mt-3 text-sm">{c.content.title}</p><p className="mt-2 text-xs text-muted-foreground">修订 {c.revision} · 版本 {c.version} · {c.is_active_publication?"当前有效发布":c.status==="PUBLISHED"?"已过期或被替代":"模拟研究"}</p></button>)}</CardContent></Card>
<Card><CardHeader><CardTitle>{selected?`${selected.content.name} · ${selected.content.symbol}`:"股票依据与审核"}</CardTitle><CardDescription>{selected?`${selected.content.name} ${selected.content.exchange}:${selected.content.symbol} · 修订 ${selected.revision}`:"选择候选查看依据与审核记录。"}</CardDescription></CardHeader><CardContent className="space-y-5">{selected&&<><StockHistoryChart symbol={selected.content.symbol} exchange={selected.content.exchange} onSessionExpired={onSessionExpired}/><div className="flex flex-wrap gap-2"><Badge>{states[selected.status]}</Badge><Badge variant="outline">模拟候选</Badge><Badge variant="outline">版本 {selected.version}</Badge></div>{selected.screening&&<div className="rounded-lg border p-4 text-sm leading-7"><strong>自动日线筛选</strong><br/>{selected.screening.strategy_name} · 修订 {selected.screening.strategy_revision}<br/>日线 {selected.screening.bar_date} · {selected.screening.source_adapter} 配置版本 {selected.screening.source_version}<p className="break-all text-xs text-muted-foreground">输入摘要 {selected.screening.input_hash}</p></div>}<p className="text-xs text-muted-foreground">观察 {new Date(selected.observed_at).toLocaleString("zh-CN")} · 有效截止 {new Date(selected.valid_until).toLocaleString("zh-CN")}</p>{([["观察逻辑",selected.content.thesis],["反证与风险",selected.content.counter_evidence],["失效条件",selected.content.invalidation]]).map(([label,value])=><section key={label}><h3 className="mb-2 text-sm font-semibold">{label}</h3><p className="whitespace-pre-wrap text-sm leading-7 text-muted-foreground">{value}</p></section>)}<section><h3 className="mb-3 text-sm font-semibold">证据</h3>{selected.content.evidence.map((e,i)=><div key={i} className="mb-3 rounded-lg bg-muted p-4 text-sm"><a href={e.source_url} target="_blank" rel="noopener noreferrer" className="break-all text-primary underline">{e.locator}</a><p className="mt-2 whitespace-pre-wrap leading-6">{e.summary}</p></div>)}</section><div className="rounded-lg border p-4 text-sm leading-7">参考区间 ¥{selected.content.reference_plan.entry_low}–{selected.content.reference_plan.entry_high}<br/>风险价格 ¥{selected.content.reference_plan.stop_loss} · 目标观察 ¥{selected.content.reference_plan.target_price}</div> <Card><CardHeader><CardTitle>{selected?`${selected.content.name} · ${selected.content.symbol}`:"股票依据与审核"}</CardTitle><CardDescription>{selected?`${selected.content.name} ${selected.content.exchange}:${selected.content.symbol} · 修订 ${selected.revision}`:"选择候选查看依据与审核记录。"}</CardDescription></CardHeader><CardContent className="space-y-5">{selected&&<><StockHistoryChart symbol={selected.content.symbol} exchange={selected.content.exchange} onSessionExpired={onSessionExpired}/><div className="flex flex-wrap gap-2"><Badge>{states[selected.status]}</Badge><Badge variant="outline">模拟候选</Badge><Badge variant="outline">版本 {selected.version}</Badge></div>{selected.screening&&<div className="rounded-lg border p-4 text-sm leading-7"><strong>自动日线筛选</strong><br/>{selected.screening.strategy_name} · 修订 {selected.screening.strategy_revision}<br/>日线 {selected.screening.bar_date} · {selected.screening.source_adapter} 配置版本 {selected.screening.source_version}<p className="break-all text-xs text-muted-foreground">输入摘要 {selected.screening.input_hash}</p></div>}<p className="text-xs text-muted-foreground">观察 {new Date(selected.observed_at).toLocaleString("zh-CN")} · 有效截止 {new Date(selected.valid_until).toLocaleString("zh-CN")}</p>{([["观察逻辑",selected.content.thesis],["反证与风险",selected.content.counter_evidence],["失效条件",selected.content.invalidation]]).map(([label,value])=><section key={label}><h3 className="mb-2 text-sm font-semibold">{label}</h3><p className="whitespace-pre-wrap text-sm leading-7 text-muted-foreground">{value}</p></section>)}<section><h3 className="mb-3 text-sm font-semibold">证据</h3>{selected.content.evidence.map((e,i)=><div key={i} className="mb-3 rounded-lg bg-muted p-4 text-sm"><a href={e.source_url} target="_blank" rel="noopener noreferrer" className="break-all text-primary underline">{e.locator}</a><p className="mt-2 whitespace-pre-wrap leading-6">{e.summary}</p></div>)}</section><div className="rounded-lg border p-4 text-sm leading-7">参考区间 ¥{selected.content.reference_plan.entry_low}–{selected.content.reference_plan.entry_high}<br/>风险价格 ¥{selected.content.reference_plan.stop_loss} · 目标观察 ¥{selected.content.reference_plan.target_price}</div>
<div className="flex flex-wrap gap-2">{selected.is_latest&&selected.status!=="PENDING_REVIEW"&&<Button variant="outline" disabled={busy} onClick={()=>edit(selected)}>创建新修订</Button>}{(selected.is_latest&&selected.status==="DRAFT"?["submit"]:selected.is_latest&&selected.status==="PENDING_REVIEW"?["publish","return"]:selected.is_active_publication?["withdraw"]:[]).map(a=><Button key={a} disabled={busy} variant={a==="publish"?"default":"outline"} onClick={()=>{setAction(a);setReason("")}}>{actions[a]}</Button>)}</div> <div className="flex flex-wrap gap-2">{selected.is_latest&&selected.status!=="PENDING_REVIEW"&&<Button variant="outline" disabled={busy} onClick={()=>edit(selected)}>创建新修订</Button>}{(selected.is_latest&&selected.status==="DRAFT"?["submit"]:selected.is_latest&&selected.status==="PENDING_REVIEW"?["publish","return"]:selected.is_active_publication?["withdraw"]:[]).map(a=><Button key={a} disabled={busy} variant={a==="publish"?"default":"outline"} onClick={()=>{setAction(a);setReason("")}}>{actions[a]}</Button>)}</div>
{action&&<form onSubmit={e=>void confirm(e)} className="space-y-3 rounded-lg border border-primary/30 p-4"><p className="text-sm font-semibold">确认{actions[action]}:{selected.content.name},修订 {selected.revision},版本 {selected.version}</p><p className="text-xs text-muted-foreground">{action==="publish"?"发布后 App 可读取此模拟候选;旧发布版本将被替代。":action==="withdraw"?"撤回后此版本从有效候选列表移除,历史链接仍显示失效状态。":"此操作会写入不可覆盖的审核记录。"}</p><textarea required maxLength={1000} aria-label="审核意见" placeholder="填写具体理由或审核意见" rows={3} className={control} value={reason} onChange={e=>setReason(e.target.value)}/><div className="flex gap-2"><Button disabled={busy||!reason.trim()} type="submit">{busy?"提交中":"确认"}</Button><Button disabled={busy} type="button" variant="ghost" onClick={()=>setAction("")}>取消</Button></div></form>} {action&&<form onSubmit={e=>void confirm(e)} className="space-y-3 rounded-lg border border-primary/30 p-4"><p className="text-sm font-semibold">确认{actions[action]}:{selected.content.name}({selected.content.symbol}),修订 {selected.revision},版本 {selected.version}</p><p className="text-xs text-muted-foreground">{action==="publish"?"发布后 App 可读取此模拟候选;旧发布版本将被替代。":action==="withdraw"?"撤回后此版本从有效候选列表移除,历史链接仍显示失效状态。":"此操作会写入不可覆盖的审核记录。"}</p><textarea required maxLength={1000} aria-label="审核意见" placeholder="填写具体理由或审核意见" rows={3} className={control} value={reason} onChange={e=>setReason(e.target.value)}/><div className="flex gap-2"><Button disabled={busy||!reason.trim()} type="submit">{busy?"提交中":"确认"}</Button><Button disabled={busy} type="button" variant="ghost" onClick={()=>setAction("")}>取消</Button></div></form>}
{selected.ai_analysis&&<section className="rounded-xl bg-primary/5 p-4 space-y-3"><h3 className="font-semibold">AI 分析与复核</h3><p className="text-sm leading-7">{selected.ai_analysis.analysis?.summary}</p><p className="text-sm text-muted-foreground">风险:{selected.ai_analysis.analysis?.risks}</p><p className="text-sm leading-7">复核:{selected.ai_analysis.review?.summary}</p><p className="text-xs text-muted-foreground">{selected.ai_analysis.primary_model} / {selected.ai_analysis.review_model}</p></section>}<section><h3 className="mb-3 text-sm font-semibold">审核记录</h3>{selected.reviews?.length?selected.reviews.map(r=><div key={r.id} className="border-t py-3 text-sm"><strong>{actions[r.decision.toLowerCase()]??r.decision}</strong><span className="ml-3 text-xs text-muted-foreground">{new Date(r.created_at).toLocaleString("zh-CN")} · 输入版本 {r.input_version}</span><p className="mt-2 whitespace-pre-wrap text-muted-foreground">{r.reason}</p></div>):<p className="text-sm text-muted-foreground">尚无审核记录。</p>}</section></>}</CardContent></Card></div> {selected.ai_analysis&&<section className="rounded-xl bg-primary/5 p-4 space-y-3"><h3 className="font-semibold">AI 分析与复核</h3><p className="text-sm leading-7">{selected.ai_analysis.analysis?.summary}</p><p className="text-sm text-muted-foreground">风险:{selected.ai_analysis.analysis?.risks}</p><p className="text-sm leading-7">复核:{selected.ai_analysis.review?.summary}</p><p className="text-xs text-muted-foreground">{selected.ai_analysis.primary_model} / {selected.ai_analysis.review_model}</p></section>}<section><h3 className="mb-3 text-sm font-semibold">审核记录</h3>{selected.reviews?.length?selected.reviews.map(r=><div key={r.id} className="border-t py-3 text-sm"><strong>{actions[r.decision.toLowerCase()]??r.decision}</strong><span className="ml-3 text-xs text-muted-foreground">{new Date(r.created_at).toLocaleString("zh-CN")} · 输入版本 {r.input_version}</span><p className="mt-2 whitespace-pre-wrap text-muted-foreground">{r.reason}</p></div>):<p className="text-sm text-muted-foreground">尚无审核记录。</p>}</section></>}</CardContent></Card></div>
</div> </div>
} }
+2 -1
View File
@@ -1,3 +1,4 @@
import {StockIdentity} from "./StockIdentity"
import {StockHistoryChart} from "./StockHistoryChart" import {StockHistoryChart} from "./StockHistoryChart"
import {useEffect,useState} from "react" import {useEffect,useState} from "react"
import {ApiError,request} from "@/lib/auth" import {ApiError,request} from "@/lib/auth"
@@ -11,5 +12,5 @@ export function MarketPanel({onSessionExpired,initialSymbol}:{onSessionExpired:(
function fail(e:unknown){if(e instanceof ApiError&&e.status===401)onSessionExpired();else setError(e instanceof Error?e.message:"行情读取失败")} function fail(e:unknown){if(e instanceof ApiError&&e.status===401)onSessionExpired();else setError(e instanceof Error?e.message:"行情读取失败")}
async function load(){setBusy(true);setError("");try{const [stocks,runs]=await Promise.all([request<Instrument[]>(`/api/admin/v1/instruments?q=${encodeURIComponent(query)}`),request<Sync[]>("/api/admin/v1/market/syncs")]);setList(stocks);setTasks(runs)}catch(e){fail(e)}finally{setBusy(false)}} async function load(){setBusy(true);setError("");try{const [stocks,runs]=await Promise.all([request<Instrument[]>(`/api/admin/v1/instruments?q=${encodeURIComponent(query)}`),request<Sync[]>("/api/admin/v1/market/syncs")]);setList(stocks);setTasks(runs)}catch(e){fail(e)}finally{setBusy(false)}}
useEffect(()=>{void load()},[]) useEffect(()=>{void load()},[])
return <Card className="mt-6"><CardHeader><CardTitle>沪深股票池 · 行情与日线</CardTitle></CardHeader><CardContent className="space-y-5"><p className="text-sm leading-7 text-muted-foreground">启用公开行情适配器后,后端启动即同步沪深股票,此后每五分钟更新。此接口用于开发研究,价格按供应商时间展示,休市或同步失败时保留上次快照。不会把抓取时间当成实时行情时间。</p><form className="flex gap-3" onSubmit={e=>{e.preventDefault();void load()}}><input className="w-full rounded-lg border px-3 py-2 text-sm" placeholder="按股票代码或名称搜索" value={query} onChange={e=>setQuery(e.target.value)}/><Button disabled={busy}>查询 / 刷新</Button></form>{error&&<p className="text-destructive text-sm">{error}</p>}<div className="max-h-[560px] overflow-auto"><table className="w-full text-left text-sm"><thead><tr>{["股票","价格","行情时间"].map(v=><th key={v} className="p-3">{v}</th>)}</tr></thead><tbody>{list.map(v=><tr className="border-t" key={v.id}><td className="p-3"><button className="text-left text-base font-medium hover:text-primary" onClick={()=>setSelected(v)}>{v.name}</button><p className="text-xs text-muted-foreground">{v.exchange}:{v.symbol}</p></td><td className="font-mono text-lg">{v.price==="0.00"?"无报价":v.price+" 元"}</td><td className="text-xs">{new Date(v.observed_at).toLocaleString("zh-CN")}</td></tr>)}</tbody></table>{list.length===0&&<p className="p-3 text-muted-foreground">暂无同步数据,请检查系统设置中的行情源与同步记录。</p>}</div>{selected&&<div><div className="mb-4 flex items-end justify-between"><div><h2 className="text-xl font-semibold">{selected.name}</h2><p className="mt-1 font-mono text-xs text-muted-foreground">{selected.exchange}:{selected.symbol}</p></div><strong className="font-mono text-3xl">¥{selected.price}</strong></div><StockHistoryChart symbol={selected.symbol} exchange={selected.exchange} onSessionExpired={onSessionExpired}/></div>}<details><summary className="cursor-pointer text-sm">最近同步记录</summary><div className="mt-3 space-y-2">{tasks.length===0&&<p className="text-sm text-muted-foreground">还没有同步记录。</p>}{tasks.map(t=><div key={t.id} className="rounded-lg bg-muted p-3 text-xs">{new Date(t.finished_at).toLocaleString("zh-CN")} · {t.status==="COMPLETED"?`同步 ${t.instrument_count} 个证券`:t.message}</div>)}</div></details></CardContent></Card> return <Card className="mt-6"><CardHeader><CardTitle>沪深股票池 · 行情与日线</CardTitle></CardHeader><CardContent className="space-y-5"><p className="text-sm leading-7 text-muted-foreground">启用公开行情适配器后,后端启动即同步沪深股票,此后每五分钟更新。此接口用于开发研究,价格按供应商时间展示,休市或同步失败时保留上次快照。不会把抓取时间当成实时行情时间。</p><form className="flex gap-3" onSubmit={e=>{e.preventDefault();void load()}}><input className="w-full rounded-lg border px-3 py-2 text-sm" placeholder="按股票代码或名称搜索" value={query} onChange={e=>setQuery(e.target.value)}/><Button disabled={busy}>查询 / 刷新</Button></form>{error&&<p className="text-destructive text-sm">{error}</p>}<div className="max-h-[560px] overflow-auto"><table className="w-full text-left text-sm"><thead><tr>{["股票","价格","行情时间"].map(v=><th key={v} className="p-3">{v}</th>)}</tr></thead><tbody>{list.map(v=><tr className="border-t" key={v.id}><td className="p-3"><button className="text-left text-base font-medium hover:text-primary" onClick={()=>setSelected(v)}><StockIdentity name={v.name} symbol={v.symbol}/></button><p className="text-xs text-muted-foreground">{v.exchange}:{v.symbol}</p></td><td className="font-mono text-lg">{v.price==="0.00"?"无报价":v.price+" 元"}</td><td className="text-xs">{new Date(v.observed_at).toLocaleString("zh-CN")}</td></tr>)}</tbody></table>{list.length===0&&<p className="p-3 text-muted-foreground">{query.trim()?"未找到匹配股票,请尝试股票代码或其他名称。":"暂无同步数据,请检查系统设置中的行情源与同步记录。"}</p>}</div>{selected&&<div><div className="mb-4 flex items-end justify-between"><div><h2 className="text-xl font-semibold"><StockIdentity name={selected.name} symbol={selected.symbol}/></h2><p className="mt-1 font-mono text-xs text-muted-foreground">{selected.exchange}:{selected.symbol}</p></div><strong className="font-mono text-3xl">¥{selected.price}</strong></div><StockHistoryChart symbol={selected.symbol} exchange={selected.exchange} onSessionExpired={onSessionExpired}/></div>}<details><summary className="cursor-pointer text-sm">最近同步记录</summary><div className="mt-3 space-y-2">{tasks.length===0&&<p className="text-sm text-muted-foreground">还没有同步记录。</p>}{tasks.map(t=><div key={t.id} className="rounded-lg bg-muted p-3 text-xs">{new Date(t.finished_at).toLocaleString("zh-CN")} · {t.status==="COMPLETED"?`同步 ${t.instrument_count} 个证券`:t.message}</div>)}</div></details></CardContent></Card>
} }
@@ -0,0 +1,8 @@
type StockIdentityProps = { name: string; symbol: string }
export function StockIdentity({ name, symbol }: StockIdentityProps) {
return <span className="inline-flex flex-wrap items-baseline gap-x-2 gap-y-1">
<span>{name}</span>
<span className="font-mono text-[0.85em] font-semibold tabular-nums">{symbol}</span>
</span>
}
+3 -2
View File
@@ -1,3 +1,4 @@
import {StockIdentity} from "./StockIdentity"
import {useEffect,useRef,useState} from "react" import {useEffect,useRef,useState} from "react"
import {ArrowUpRight,RefreshCw} from "lucide-react" import {ArrowUpRight,RefreshCw} from "lucide-react"
import {request,ApiError} from "@/lib/auth" import {request,ApiError} from "@/lib/auth"
@@ -20,7 +21,7 @@ export function TradingOverview({onSessionExpired,onNavigate}:{onSessionExpired:
return <div className="space-y-6"><div className="flex items-center justify-between"><div className="flex gap-2"><Badge variant="outline">沪深 A 股</Badge><Badge variant="outline">公开行情 · 开发研究</Badge></div><Button variant="outline" disabled={loading} onClick={()=>void refresh()}><RefreshCw className={loading?"animate-spin":""}/>刷新数据</Button></div>{error&&<p role="alert" className="text-sm text-destructive">{error}</p>} return <div className="space-y-6"><div className="flex items-center justify-between"><div className="flex gap-2"><Badge variant="outline">沪深 A 股</Badge><Badge variant="outline">公开行情 · 开发研究</Badge></div><Button variant="outline" disabled={loading} onClick={()=>void refresh()}><RefreshCw className={loading?"animate-spin":""}/>刷新数据</Button></div>{error&&<p role="alert" className="text-sm text-destructive">{error}</p>}
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">{[{title:"已同步股票",value:success?.instrument_count??"—",note:success?`最近成功同步 ${new Date(success.finished_at).toLocaleTimeString("zh-CN")}`:"等待行情源同步",page:"股票行情"},{title:"待审核股票候选",value:data?pending.length:"—",note:"最近 100 条候选修订",page:"候选审核"},{title:"有效发布候选",value:data?published.length:"—",note:"最近 100 条候选修订",page:"候选审核"},{title:"已启用研究策略",value:data?data.strategies.filter(s=>s.status==="ACTIVE").length:"—",note:"最近 100 个策略 · 非收益指标",page:"策略中心"}].map(x=><button key={x.title} className="rounded-xl border bg-card p-5 text-left transition hover:border-primary/40" onClick={()=>onNavigate(x.page)}><div className="flex items-center justify-between"><span className="text-sm text-muted-foreground">{x.title}</span><ArrowUpRight size={15} className="text-muted-foreground"/></div><p className="my-3 font-mono text-4xl font-semibold tracking-tight">{x.value}</p><p className="text-xs text-muted-foreground">{x.note}</p></button>)}</div> <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">{[{title:"已同步股票",value:success?.instrument_count??"—",note:success?`最近成功同步 ${new Date(success.finished_at).toLocaleTimeString("zh-CN")}`:"等待行情源同步",page:"股票行情"},{title:"待审核股票候选",value:data?pending.length:"—",note:"最近 100 条候选修订",page:"候选审核"},{title:"有效发布候选",value:data?published.length:"—",note:"最近 100 条候选修订",page:"候选审核"},{title:"已启用研究策略",value:data?data.strategies.filter(s=>s.status==="ACTIVE").length:"—",note:"最近 100 个策略 · 非收益指标",page:"策略中心"}].map(x=><button key={x.title} className="rounded-xl border bg-card p-5 text-left transition hover:border-primary/40" onClick={()=>onNavigate(x.page)}><div className="flex items-center justify-between"><span className="text-sm text-muted-foreground">{x.title}</span><ArrowUpRight size={15} className="text-muted-foreground"/></div><p className="my-3 font-mono text-4xl font-semibold tracking-tight">{x.value}</p><p className="text-xs text-muted-foreground">{x.note}</p></button>)}</div>
<ScreeningRunControl onSessionExpired={onSessionExpired} onCompleted={()=>void refresh()}/> <ScreeningRunControl onSessionExpired={onSessionExpired} onCompleted={()=>void refresh()}/>
<div className="grid items-start gap-6 xl:grid-cols-[1.35fr_1fr]"><Card><CardHeader><div className="flex items-center justify-between"><CardTitle>股票观察</CardTitle><Button variant="ghost" size="sm" onClick={()=>onNavigate("股票行情")}>查看股票池<ArrowUpRight/></Button></div><p className="mt-2 text-xs text-muted-foreground">按证券代码排序的行情样本 · 非涨幅排名或投资推荐</p></CardHeader><CardContent>{selected&&<div className="mb-5"><div className="mb-4 flex items-end justify-between"><div><button className="text-xl font-semibold hover:text-primary" onClick={()=>onNavigate("股票行情",selected.symbol)}>{selected.name}</button><p className="mt-1 font-mono text-xs text-muted-foreground">{selected.exchange} · {selected.symbol}</p></div><div className="text-right"><p className="font-mono text-3xl font-semibold">¥{selected.price}</p><p className="mt-1 text-xs text-muted-foreground">报价 {new Date(selected.observed_at).toLocaleString("zh-CN")}</p></div></div><StockHistoryChart symbol={selected.symbol} exchange={selected.exchange} onSessionExpired={onSessionExpired}/></div>}<div className="overflow-auto"><table className="w-full text-left text-sm"><thead className="text-xs text-muted-foreground"><tr><th className="pb-3">股票 / 代码</th><th className="pb-3 text-right">最新报价</th><th className="pb-3 text-right">行情时间</th></tr></thead><tbody>{data?.stocks.slice(0,8).map(stock=><tr key={stock.id} className="border-t"><td className="py-3"><button className="text-left hover:text-primary" onClick={()=>setSelected(stock)}><strong>{stock.name}</strong><p className="mt-1 font-mono text-xs text-muted-foreground">{stock.exchange}:{stock.symbol}</p></button></td><td className="text-right font-mono">¥{stock.price}</td><td className="text-right text-xs text-muted-foreground">{new Date(stock.observed_at).toLocaleTimeString("zh-CN")}</td></tr>)}</tbody></table>{data?.stocks.length===0&&<p className="py-8 text-sm text-muted-foreground">暂无股票快照,请在行情源设置中检查同步配置。</p>}</div></CardContent></Card> <div className="grid items-start gap-6 xl:grid-cols-[1.35fr_1fr]"><Card><CardHeader><div className="flex items-center justify-between"><CardTitle>股票观察</CardTitle><Button variant="ghost" size="sm" onClick={()=>onNavigate("股票行情")}>查看股票池<ArrowUpRight/></Button></div><p className="mt-2 text-xs text-muted-foreground">按证券代码排序的行情样本 · 非涨幅排名或投资推荐</p></CardHeader><CardContent>{selected&&<div className="mb-5"><div className="mb-4 flex items-end justify-between"><div><button className="text-xl font-semibold hover:text-primary" onClick={()=>onNavigate("股票行情",selected.symbol)}><StockIdentity name={selected.name} symbol={selected.symbol}/></button><p className="mt-1 font-mono text-xs text-muted-foreground">{selected.exchange} · {selected.symbol}</p></div><div className="text-right"><p className="font-mono text-3xl font-semibold">¥{selected.price}</p><p className="mt-1 text-xs text-muted-foreground">报价 {new Date(selected.observed_at).toLocaleString("zh-CN")}</p></div></div><StockHistoryChart symbol={selected.symbol} exchange={selected.exchange} onSessionExpired={onSessionExpired}/></div>}<div className="overflow-auto"><table className="w-full text-left text-sm"><thead className="text-xs text-muted-foreground"><tr><th className="pb-3">股票 / 代码</th><th className="pb-3 text-right">最新报价</th><th className="pb-3 text-right">行情时间</th></tr></thead><tbody>{data?.stocks.slice(0,8).map(stock=><tr key={stock.id} className="border-t"><td className="py-3"><button className="text-left hover:text-primary" onClick={()=>setSelected(stock)}><strong><StockIdentity name={stock.name} symbol={stock.symbol}/></strong><p className="mt-1 font-mono text-xs text-muted-foreground">{stock.exchange}:{stock.symbol}</p></button></td><td className="text-right font-mono">¥{stock.price}</td><td className="text-right text-xs text-muted-foreground">{new Date(stock.observed_at).toLocaleTimeString("zh-CN")}</td></tr>)}</tbody></table>{data?.stocks.length===0&&<p className="py-8 text-sm text-muted-foreground">暂无股票快照,请在行情源设置中检查同步配置。</p>}</div></CardContent></Card>
<Card><CardHeader><div className="flex items-center justify-between"><CardTitle>待审核信号</CardTitle><Button variant="ghost" size="sm" onClick={()=>onNavigate("候选审核")}>进入审核<ArrowUpRight/></Button></div><p className="mt-2 text-xs text-muted-foreground">从股票、参考区间与信号依据开始审核</p></CardHeader><CardContent className="space-y-3">{pending.length===0&&<div className="rounded-xl border border-dashed p-6 text-sm leading-7 text-muted-foreground">当前没有待审核股票。点击“立即自动筛选”后,真实日线命中结果会自动进入这里;不符合规则的股票不会生成候选。</div>}{pending.slice(0,8).map(c=><button key={c.id} className="w-full rounded-xl border p-4 text-left hover:border-primary/40" onClick={()=>onNavigate("候选审核",undefined,c.id)}><div className="flex items-center justify-between"><strong className="text-lg">{c.content.name}</strong><Badge variant="outline">待审核</Badge></div><p className="mt-1 font-mono text-xs text-muted-foreground">{c.content.exchange} · {c.content.symbol}</p><p className="my-3 font-mono text-xl">¥{c.content.reference_plan.entry_low}–{c.content.reference_plan.entry_high}</p><p className="text-xs leading-6 text-muted-foreground">{c.screening?`${c.screening.strategy_name} · 日线 ${c.screening.bar_date}`:"人工补充研究"}<br/>有效至 {new Date(c.valid_until).toLocaleString("zh-CN")}</p></button>)}</CardContent></Card></div> <Card><CardHeader><div className="flex items-center justify-between"><CardTitle>待审核信号</CardTitle><Button variant="ghost" size="sm" onClick={()=>onNavigate("候选审核")}>进入审核<ArrowUpRight/></Button></div><p className="mt-2 text-xs text-muted-foreground">从股票、参考区间与信号依据开始审核</p></CardHeader><CardContent className="space-y-3">{pending.length===0&&<div className="rounded-xl border border-dashed p-6 text-sm leading-7 text-muted-foreground">当前没有待审核股票。点击“立即自动筛选”后,真实日线命中结果会自动进入这里;不符合规则的股票不会生成候选。</div>}{pending.slice(0,8).map(c=><button key={c.id} className="w-full rounded-xl border p-4 text-left hover:border-primary/40" onClick={()=>onNavigate("候选审核",undefined,c.id)}><div className="flex items-center justify-between"><strong className="text-lg"><StockIdentity name={c.content.name} symbol={c.content.symbol}/></strong><Badge variant="outline">待审核</Badge></div><p className="mt-1 font-mono text-xs text-muted-foreground">{c.content.exchange} · {c.content.symbol}</p><p className="my-3 font-mono text-xl">¥{c.content.reference_plan.entry_low}–{c.content.reference_plan.entry_high}</p><p className="text-xs leading-6 text-muted-foreground">{c.screening?`${c.screening.strategy_name} · 日线 ${c.screening.bar_date}`:"人工补充研究"}<br/>有效至 {new Date(c.valid_until).toLocaleString("zh-CN")}</p></button>)}</CardContent></Card></div>
<p className="text-xs leading-6 text-muted-foreground">价格按供应商观察时间展示,收盘后保留收盘快照。曲线来自实际未复权日线;当前未接入指数、涨跌幅、成交额与行业分类,因此不展示相应统计。</p></div> <p className="text-xs leading-6 text-muted-foreground">价格按供应商观察时间展示,收盘后保留收盘快照。曲线来自实际未复权日线;当前未接入指数、涨跌幅、成交额与行业分类,因此不展示相应统计。</p></div>
} }