mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 16:44:15 +08:00
feat(kline): add configurable external stock link
设置页新增「个股详情外链」: 可填 URL 模板(占位符 {code}/{market}/{symbol}),
信息条第一行右侧显示外链图标(新标签打开)。留空关闭。
- 新建 lib/stock-external-link.ts: load/save/build + scheme 白名单(http/https) + symbol 形状守卫
- storage.ts 新增 stockExternalTemplate kv
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { Settings2, RadioTower, Star } from 'lucide-react'
|
||||
import { Settings2, RadioTower, Star, ExternalLink } from 'lucide-react'
|
||||
import type { KlineRow, FinancialMetricRecord } from '@/lib/api'
|
||||
import { fmtPrice, fmtBigNum, fmtVolume } from '@/lib/format'
|
||||
import { ListColumnCustomizer } from '@/components/ListColumnCustomizer'
|
||||
import { WatchlistAddMenu } from '@/components/WatchlistAddMenu'
|
||||
import { INFO_GROUPS, type ColumnConfig } from '@/lib/stock-info-fields'
|
||||
import { buildStockExternalUrl, loadStockExternalTemplate } from '@/lib/stock-external-link'
|
||||
|
||||
const BULL = '#C74040'
|
||||
const BEAR = '#2D9B65'
|
||||
@@ -235,6 +236,8 @@ export function StockInfoBar({
|
||||
)
|
||||
}
|
||||
|
||||
const extUrl = buildStockExternalUrl(loadStockExternalTemplate(), symbol)
|
||||
|
||||
return (
|
||||
<div className="px-2 pb-3 font-mono text-[12px] select-none space-y-1">
|
||||
{/* Row 1: code, name, price, change, change% */}
|
||||
@@ -250,8 +253,19 @@ export function StockInfoBar({
|
||||
<span style={{ color: clr }} className="tabular-nums">
|
||||
{isUp ? '+' : ''}{fmtPrice(chgPct)}%
|
||||
</span>
|
||||
{/* 右侧操作按钮:加自选 + 加监控 + 信息条配置 */}
|
||||
{/* 右侧操作按钮:外链 + 加自选 + 加监控 + 信息条配置 */}
|
||||
<div className="ml-auto self-center flex items-center gap-1">
|
||||
{extUrl && (
|
||||
<a
|
||||
href={extUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={extUrl}
|
||||
className="p-1 rounded-btn text-muted hover:text-foreground hover:bg-elevated transition-colors"
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
)}
|
||||
{inWatchlist && onRemoveFromWatchlist ? (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 个股详情外链 — 可配置 URL 模板 + 证券代码。
|
||||
*
|
||||
* 占位符保持独立, 用户自行排列 (市场前缀/后缀因站而异):
|
||||
* {code} 纯 6 位代码, 如 000001
|
||||
* {market} 市场前缀小写, 如 sz / sh / bj
|
||||
* {symbol} 完整 symbol (含交易所后缀), 如 000001.SZ
|
||||
* 模板留空 = 关闭外链。
|
||||
*/
|
||||
import { storage } from '@/lib/storage'
|
||||
|
||||
/** 形状守卫: 只放行 6位数字 + 沪深北后缀 的 symbol (信息条场景下即股票) */
|
||||
const STOCK_SYMBOL_RE = /^(\d{6})\.(SH|SZ|BJ)$/
|
||||
|
||||
/** 未设置或留空 → 返回 '' 即关闭外链 */
|
||||
export function loadStockExternalTemplate(): string {
|
||||
return storage.stockExternalTemplate.get('')
|
||||
}
|
||||
|
||||
export function saveStockExternalTemplate(tpl: string): void {
|
||||
storage.stockExternalTemplate.set(tpl.trim())
|
||||
}
|
||||
|
||||
export function buildStockExternalUrl(template: string, symbol: string): string | null {
|
||||
if (!template) return null
|
||||
// scheme 白名单: 只放行 http/https, 挡掉 javascript:/data: 等危险 scheme
|
||||
if (!/^https?:\/\//i.test(template.trim())) return null
|
||||
const m = symbol.match(STOCK_SYMBOL_RE)
|
||||
if (!m) return null
|
||||
const code = m[1]
|
||||
const market = m[2].toLowerCase()
|
||||
return template
|
||||
.replaceAll('{code}', code)
|
||||
.replaceAll('{market}', market)
|
||||
.replaceAll('{symbol}', symbol)
|
||||
}
|
||||
@@ -44,6 +44,9 @@ export const storage = {
|
||||
/** 个股详情多日分时周期 */
|
||||
stockPreviewIntradayDays: kv<number>('stock_preview_intraday_days'),
|
||||
|
||||
/** 个股详情外链 URL 模板 (支持 {code}/{market}/{symbol}; 留空关闭) */
|
||||
stockExternalTemplate: kv<string>('stock_external_template'),
|
||||
|
||||
/** 策略结果列表列配置 */
|
||||
screenerResultColumns: kv<unknown[]>('screener_result_columns'),
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { Settings2, Trash2, RefreshCw, Bell, Volume2, Info } from 'lucide-react'
|
||||
import { Settings2, Trash2, RefreshCw, Bell, Volume2, Info, ExternalLink } from 'lucide-react'
|
||||
import { usePreferences, useVersion } from '@/lib/useSharedQueries'
|
||||
import { api } from '@/lib/api'
|
||||
import { QK } from '@/lib/queryKeys'
|
||||
@@ -15,6 +15,7 @@ import { SOUND_OPTIONS, previewSound } from '@/lib/notificationSound'
|
||||
import {
|
||||
listZhVoices, previewVoice, activateVoice, getCurrentVoiceURI,
|
||||
} from '@/lib/voiceBroadcast'
|
||||
import { loadStockExternalTemplate, saveStockExternalTemplate } from '@/lib/stock-external-link'
|
||||
|
||||
export function SettingsSystemPanel() {
|
||||
const qc = useQueryClient()
|
||||
@@ -23,6 +24,7 @@ export function SettingsSystemPanel() {
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const screenerAutoRun = prefs?.screener_auto_run ?? true
|
||||
const [extTpl, setExtTpl] = useState(() => loadStockExternalTemplate())
|
||||
const [clearing, setClearing] = useState(false)
|
||||
const [toastEnabled, setToastEnabled] = useState(() => {
|
||||
try { return localStorage.getItem('alert_toast_enabled') !== '0' } catch { return true }
|
||||
@@ -288,6 +290,30 @@ export function SettingsSystemPanel() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-card border border-border bg-surface p-5 mt-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<ExternalLink className="h-4 w-4 text-accent" />
|
||||
<h3 className="text-sm font-medium text-foreground">个股详情外链</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-4 py-2">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm text-foreground">详情页 URL 模板</div>
|
||||
<div className="text-[11px] text-muted truncate">{"支持 {code} {market} {symbol} · 留空关闭外链"}</div>
|
||||
</div>
|
||||
<input
|
||||
value={extTpl}
|
||||
onChange={(e) => {
|
||||
setExtTpl(e.target.value)
|
||||
saveStockExternalTemplate(e.target.value)
|
||||
}}
|
||||
placeholder="https://..."
|
||||
spellCheck={false}
|
||||
className="w-[26rem] max-w-[60%] h-8 px-2.5 rounded-btn border border-border bg-base text-xs font-mono text-foreground focus:border-accent/50 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-card border border-border bg-surface p-5 mt-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Trash2 className="h-4 w-4 text-accent" />
|
||||
|
||||
Reference in New Issue
Block a user