您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
给 Tradingview 增加同花顺同步、拼音搜索等功能
// ==UserScript== // @name Tradingview A股助手 // @namespace https://github.com/xiaopc/tradingview-ashare // @description 给 Tradingview 增加同花顺同步、拼音搜索等功能 // @version 0.7.6 // @author xiaopc // @supportURL https://github.com/xiaopc/tradingview-ashare/issues // @match https://*.tradingview.com/chart/* // @icon https://static.tradingview.com/static/images/favicon.ico // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect t.10jqka.com.cn // @connect www.iwencai.com // @connect qt.gtimg.cn // @connect smartbox.gtimg.cn // @require https://unpkg.com/[email protected]/dist/preact.min.umd.js // @require https://unpkg.com/[email protected]/hooks/dist/hooks.umd.js // @require https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/htm/3.1.0/htm.umd.min.js // @require https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/lodash.js/4.17.21/lodash.min.js // @require https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/lscache/1.3.0/lscache.min.js // ==/UserScript== // config // * 显示智能分组 const SHOW_WENCAI_PLATE = true; const tvhelperCss = ` /* @import "https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/bulma/0.9.3/css/bulma-rtl.min.css"; */ .card { background-color: #fff; border-radius : .25rem; box-shadow : 0 .5em 1em -.125em rgba(10, 10, 10, .1), 0 0 0 1px rgba(10, 10, 10, .02); color : #4a4a4a; max-width : 100%; position : relative } .card-content:first-child, .card-footer:first-child, .card-header:first-child { border-top-left-radius : .25rem; border-top-right-radius: .25rem } .card-content:last-child, .card-footer:last-child, .card-header:last-child { border-bottom-left-radius : .25rem; border-bottom-right-radius: .25rem } .card-header { background-color: transparent; align-items : stretch; box-shadow : 0 .125em .25em rgba(10, 10, 10, .1); display : flex } .card-header-title { align-items: center; color : #363636; display : flex; flex-grow : 1; font-weight: 700; padding : .75rem 1rem } .card-header-title.is-centered { justify-content: center } .card-header-icon { margin : 0; padding : 0; align-items : center; display : flex; justify-content: center; padding : .5rem 1rem } .card-content { background-color: transparent; padding : 1rem } .card-footer { background-color: transparent; border-top : 1px solid #ededed; align-items : stretch; display : flex } .card-footer-item { align-items : center; display : flex; flex-basis : 0; flex-grow : 1; flex-shrink : 0; justify-content: center; padding : .75rem } .card-footer-item:not(:last-child) { border-left: 1px solid #ededed } .card .media:not(:last-child) { margin-bottom: 1.5rem } .notification { background-color: #f5f5f5; border-radius : .375em; position : relative; padding : 1rem 2.25rem 1rem 1.25rem; margin : 1rem 0 } .notification.is-warning { background-color: #ffe08a; color : rgba(0,0,0,.7) } .menu { font-size: 1rem } .menu-list { line-height: 1.25; list-style : none; margin : -.5rem -.75rem 0 -.75rem } .menu-list a { border-radius : 2px; color : #4a4a4a; display : block; padding : .3em .75em; line-height : 1; align-items : center; justify-content: space-between; display : flex; } .menu-list a:hover { background-color: #f5f5f5; color : #363636 } .menu-list a.is-active { background-color: #eff5fb } .menu-list li ul { border-right : 1px solid #dbdbdb; margin : .75em; padding-right: .75em } .menu-label { color : #7a7a7a; font-size : .75em; letter-spacing : .1em; text-transform : uppercase; display : flex; justify-content: space-between; } .menu-label:not(:first-child) { margin-top: 1em } .menu-label:not(:last-child) { margin-bottom: 1em } .tag:not(body) { align-items : center; background-color: #f5f5f5; border-radius : 4px; color : #4a4a4a; display : inline-flex; font-size : .75rem; height : 2em; justify-content : center; line-height : 1.5; padding-left : .75em; padding-right : .75em; white-space : nowrap } .tag:not(body) .delete { margin-right: .25rem; margin-left : -.375rem } .tag:not(body).is-white { background-color: #fff; color : #0a0a0a } .tag:not(body).is-black { background-color: #0a0a0a; color : #fff } .tag:not(body).is-light { background-color: #f5f5f5; color : rgba(0, 0, 0, .7) } .tag:not(body).is-dark { background-color: #363636; color : #fff } .tag:not(body).is-primary { background-color: #00d1b2; color : #fff } .tag:not(body).is-primary.is-light { background-color: #ebfffc; color : #00947e } .tag:not(body).is-link { background-color: #485fc7; color : #fff } .tag:not(body).is-link.is-light { background-color: #eff1fa; color : #3850b7 } .tag:not(body).is-info { background-color: #3e8ed0; color : #fff } .tag:not(body).is-info.is-light { background-color: #eff5fb; color : #296fa8 } .tag:not(body).is-success { background-color: #48c78e; color : #fff } .tag:not(body).is-success.is-light { background-color: #effaf5; color : #257953 } .tag:not(body).is-warning { background-color: #ffe08a; color : rgba(0, 0, 0, .7) } .tag:not(body).is-warning.is-light { background-color: #fffaeb; color : #946c00 } .tag:not(body).is-danger { background-color: #f14668; color : #fff } .tag:not(body).is-danger.is-light { background-color: #feecf0; color : #cc0f35 } .b-icon { align-items : center; display : inline-flex; justify-content: center; height : 1rem; width : 1rem; fill : #7a7a7a; border-radius : 4px; } .b-icon:hover { background-color: #eff5fb } .b-icon.is-medium { height : 1.2rem; width : 1.2rem; padding: .3rem; } #tvhelper { position : absolute; display : block; width : 13rem; height : 18rem; min-height: 3rem; max-height: 95vh; right : 2.2rem; bottom : 0; margin : 0.8rem; padding : 0; overflow : auto; resize : vertical; } #tvhelper>.card .card-header { position : sticky; top : 0; background: inherit; } #tvhelper>.card::-webkit-scrollbar { width : 6px; height : 6px; background-color: transparent; z-index : 999; } #tvhelper>.card::-webkit-scrollbar-track, #tvhelper>.card::-webkit-scrollbar-corner { background-color: transparent; } #tvhelper>.card::-webkit-scrollbar-thumb { border-radius : 3px; background-color: #f0f3fa; } #tvhelper>.card { height : 100%; overflow-y: auto; overflow-y: overlay; overflow-x: hidden; margin : 0; padding : 0; } #tvhelper>.card .card-content aside { display: block; } #tvhelper>.card .card-content ul.menu-list span.symbol-name, #tvhelper>.card .card-content p.menu-label span.plate-name{ display : inline-block; max-width : 7rem; white-space : nowrap; overflow : hidden; text-overflow: ellipsis; display : flex; flex-grow : 1; } #tvhelper-tooltip { position: absolute; display : none; width : 33rem; height : 18rem; margin : 0.8rem; right : 16rem; bottom : 0rem; } #tvhelper-tooltip.is-active { display: block; } #tvhelper-tooltip img { width: 100%; } .disabled { opacity: 0.6; } span.tv-data-mode--delayed--for-symbol-list { margin-left: -6px; transform : scale(0.6) translate(10px, -10px) }`; const svgSprite = `<svg width="0" height="0" class="hidden"><symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="refresh-outline"><title>Refresh</title><path d="M320 146s24.36-12-64-12a160 160 0 10160 160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32"></path><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M256 58l80 80-80 80"></path></symbol><symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 460 460" id="search-circle"><title>Search Circle</title><path d="m225,33c-105.87,0 -192,86.13 -192,192s86.13,192 192,192s192,-86.13 192,-192s-86.13,-192 -192,-192zm91.31,283.31a16,16 0 0 1 -22.62,0l-42.84,-42.83a88.08,88.08 0 1 1 22.63,-22.63l42.83,42.84a16,16 0 0 1 0,22.62z" id="svg_1"/><circle cx="201" cy="201" id="svg_2" r="56"/></symbol><symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 460 460" id="search-circle-outline"><title>Search Circle</title><path d="m230,54a176,176 0 1 0 176,176a176,176 0 0 0 -176,-176z" fill="none" id="svg_1" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path d="m206,134a72,72 0 1 0 72,72a72,72 0 0 0 -72,-72z" fill="none" id="svg_2" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path d="m257.64,257.64l52.36,52.36" fill="none" id="svg_3" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32"/></symbol><symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="eye-outline"><title>Eye</title><path d="M255.66 112c-77.94 0-157.89 45.11-220.83 135.33a16 16 0 00-.27 17.77C82.92 340.8 161.8 400 255.66 400c92.84 0 173.34-59.38 221.79-135.25a16.14 16.14 0 000-17.47C428.89 172.28 347.8 112 255.66 112z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></path><circle cx="256" cy="256" r="80" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"></circle></symbol><symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="eye-off-outline"><title>Eye Off</title><path d="M432 448a15.92 15.92 0 01-11.31-4.69l-352-352a16 16 0 0122.62-22.62l352 352A16 16 0 01432 448zM255.66 384c-41.49 0-81.5-12.28-118.92-36.5-34.07-22-64.74-53.51-88.7-91v-.08c19.94-28.57 41.78-52.73 65.24-72.21a2 2 0 00.14-2.94L93.5 161.38a2 2 0 00-2.71-.12c-24.92 21-48.05 46.76-69.08 76.92a31.92 31.92 0 00-.64 35.54c26.41 41.33 60.4 76.14 98.28 100.65C162 402 207.9 416 255.66 416a239.13 239.13 0 0075.8-12.58 2 2 0 00.77-3.31l-21.58-21.58a4 4 0 00-3.83-1 204.8 204.8 0 01-51.16 6.47zM490.84 238.6c-26.46-40.92-60.79-75.68-99.27-100.53C349 110.55 302 96 255.66 96a227.34 227.34 0 00-74.89 12.83 2 2 0 00-.75 3.31l21.55 21.55a4 4 0 003.88 1 192.82 192.82 0 0150.21-6.69c40.69 0 80.58 12.43 118.55 37 34.71 22.4 65.74 53.88 89.76 91a.13.13 0 010 .16 310.72 310.72 0 01-64.12 72.73 2 2 0 00-.15 2.95l19.9 19.89a2 2 0 002.7.13 343.49 343.49 0 0068.64-78.48 32.2 32.2 0 00-.1-34.78z"></path><path d="M256 160a95.88 95.88 0 00-21.37 2.4 2 2 0 00-1 3.38l112.59 112.56a2 2 0 003.38-1A96 96 0 00256 160zM165.78 233.66a2 2 0 00-3.38 1 96 96 0 00115 115 2 2 0 001-3.38z"></path></symbol><symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="caret-up-outline"><title>Caret Up</title><path d="M414 321.94L274.22 158.82a24 24 0 00-36.44 0L98 321.94c-13.34 15.57-2.28 39.62 18.22 39.62h279.6c20.5 0 31.56-24.05 18.18-39.62z"></path></symbol></svg>`; (function(window) { 'use strict'; const marketMap = {sz: 'SZSE', sh: 'SSE', hk: 'HKEX', hsi: 'HSI', ny: 'NYSE', oq: 'NASDAQ', am: 'AMEX'}; // nq: 三板 const currencyMap = {sz: 'CNY', sh: 'CNY', hk: 'HKD', hsi: 'HKD', ny: 'USD', oq: 'USD', am: 'USD'}; // utils const cEl = function (tag) { return document.createElement(tag) }; const gID = function (id) { return document.getElementById(id) }; const deU = function (str) { return JSON.parse(`["${str}"]`)[0] }; // gtimg const gtRealtimeFetcher = async (ids) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: 'https://qt.gtimg.cn/q=' + ids.join(','), responseType: 'arraybuffer', onload: function (response) { const responseText = new TextDecoder('gbk').decode(response.response); resolve(_.fromPairs(responseText.split('\n').filter(l => l.length > 2).map(l => { let [key, val] = l.split('='); return [key.slice(2), val.slice(1, -2)]; }))); }, onerror: function (err) { reject(err); } }); }); }; const gtSuggestRaw = async (text) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://smartbox.gtimg.cn/s3/?v=2&q=${encodeURIComponent(text)}&t=all&c=1`, onload: function (response) { const line = deU(response.responseText.split('\n').filter(l => l.startsWith('v_hint'))[0].slice(8, -1)); if (line.startsWith('N')) { resolve([]); } else { resolve(_.flatten([line.split('^')]).map(l => l.split('~'))); } }, onerror: function (err) { reject(err); } }); }); }; const fetchDataToDict = function (data, keys) { return _.zipObject(Object.keys(data), Object.values(data).map(i => _.zipObject(keys, i.split('~')))); }; const getRealtimeBasic = async (...args) => { const keys = ['_', 'name', 'code', 'last', 'prev_close', 'open', 'volume', 's', 'b', 'buy1', 'buy1_vol', 'buy2', 'buy2_vol', 'buy3', 'buy3_vol', 'buy4', 'buy4_vol', 'buy5', 'buy5_vol', 'sell1', 'sell1_vol', 'sell2', 'sell2_vol', 'sell3', 'sell3_vol', 'sell4', 'sell4_vol', 'sell5', 'sell5_vol', 'latest_deal', 'time', 'change', 'change_rate', 'high', 'low', 'p_v_m', '_volume', 'turnover', 'turn_rate', 'pe', 'status']; let ids = [...args]; ids = ids.map(i => (i.startsWith('ny') || i.startsWith('oq') || i.startsWith('am')) ? 'us' + i.slice(2) : i); const data = await gtRealtimeFetcher(ids); return fetchDataToDict(data, keys); }; const gtSuggest = async (text) => { const arr = await gtSuggestRaw(text); const typeMap = {GP: 'stock', 'GP-A': 'stock', 'GP-A-KCB': 'stock', ZS: 'index', ETF: 'fund', LOF: 'fund', 'QDII-LOF': 'fund'}; // KJ: 'fund' return arr.map(i => { const [type, description] = [typeMap[i[4]], i[2]]; if (type == undefined) return null; let [exchange, symbol] = [i[0], i[1]]; if (symbol.includes('.')) { [symbol, exchange] = symbol.split('.'); if (exchange == 'n') exchange = 'ny'; } else if (exchange == 'hk' && type == typeMap.GP) { symbol = Number(symbol).toString(); } else if (exchange == 'hk' && type == typeMap.ZS) { exchange = 'hsi'; } if (marketMap[exchange] == undefined) return null; return { "symbol": symbol, "description": description, "type": type, "exchange": marketMap[exchange], "currency_code": currencyMap[exchange], "provider_id": "ice", "country": currencyMap[exchange].slice(0, 2) }; }).filter(i => !!i); }; // tonghuashun const getThsSelfRaw = async () => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: "https://t.10jqka.com.cn/newcircle/group/getSelfStockWithMarket", responseType: 'json', onload: function (response) { resolve(response.response); }, onerror: function (err) { reject(err); } }); }); }; const getWencaiPlateRaw = async () => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://www.iwencai.com/unifiedwap/self-stock/plate/list", data: 'stocks=0&ths=0', responseType: 'json', onload: function (response) { resolve(response.response); }, onerror: function (err) { reject(err); } }); }); }; const parseMarketCode = (obj, mark = 'mark', stock = 'stock') => { if (obj[mark] == '17' || obj[mark] == '20') return 'sh' + obj[stock]; if (obj[mark] == '33' || obj[mark] == '36' || obj[mark] == '32') return 'sz' + obj[stock]; if (obj[mark] == '16') return 'sh' + obj[stock].replace(/^1B/, '00'); if (obj[mark] == '120' && obj[stock].startsWith('00')) return 'sh' + obj[stock]; if (obj[mark] == '177') return 'hk0' + obj[stock].slice(2); if (obj[mark] == '169') return 'ny' + obj[stock]; if (obj[mark] == '185') return 'oq' + obj[stock]; return null; }; const getThsSelf = async () => { const obj = await getThsSelfRaw(); if (obj.errorCode != 0) return obj.errorMsg; return obj.result.map(obj => parseMarketCode(obj, 'marketid', 'code')).filter(c => !!c); }; const getWencaiPlate = async () => { const obj = await getWencaiPlateRaw(); if (!obj.success) return []; // TODO: show error return obj.data.map(g => { const stocks = g.list.map(obj => parseMarketCode(obj)).filter(c => !!c); return { id: g.sn, name: g.ln, items: stocks }; }); }; // tradingview const { fetch: originalFetch, _exposed_chartWidgetCollection: tvChart } = window; const originalSetSymbol = tvChart?.setSymbol; const toTvSymbol = (id) => { const [market, code] = [marketMap[id.slice(0, 2)], id.slice(2)]; return market + ':' + (market == 'HKEX' ? Number(code).toString() : code); }; const fromTvSymbol = (symbol) => { if (!symbol) return null; let [market, code] = symbol.split(':'); if (market == 'HKEX') code = _.padStart(code, 5, '0'); market = _.findKey(marketMap, (m) => m == market); if (market == undefined) return null; return market + code; }; let latestSearchKw = null, latestSearchRes = null; const updateTvSymbol = (id) => { if (typeof tvChart?.setSymbol != 'function') return; tvChart.setSymbol(toTvSymbol(id), null, tvChart._subscribedChartWidget); }; const hookedTvSearch = async (...args) => { const [resource, config] = args; if (!resource.startsWith('https://symbol-search')) return await originalFetch(resource, config); const kw = new URL(resource).searchParams.get('text'); latestSearchKw = kw; const symbols = await gtSuggest(kw); latestSearchRes = symbols; return { ok: true, status: 200, json: () => ({symbols: symbols, symbols_remaining: 0}) }; }; // render app const {h, render} = preact; const {useState, useEffect, useMemo} = preactHooks; const html = htm.bind(h); function App (props) { // data const [plateData, setPlateData] = useState([]); const [marketData, setMarketData] = useState({}); const [marketCache, setMarketCache] = useState({}); // ui const [onRefresh, setOnRefresh] = useState(false); const [isLogin, setIsLogin] = useState(true); // hook const [enableSearchHook, setEnableSearchHook] = useState(false); const [curSymbolTv, setCurSymbolTv] = useState(null); useEffect(() =>{ window.fetch = enableSearchHook ? hookedTvSearch : originalFetch; }, [enableSearchHook]); useEffect(() => { if (typeof originalSetSymbol != 'function') return; // const tvSymbols = tvChart.chartsSymbols(); // if (Object.values(tvSymbols).length > 0) { // setCurSymbolTv(fromTvSymbol(Object.values(tvSymbols)[0].symbol)); // } tvChart.setSymbol = (...args) => { setCurSymbolTv(fromTvSymbol(args[0])); if (latestSearchKw == args[0] && latestSearchRes.length > 0) { return originalSetSymbol.bind(tvChart)(latestSearchRes[0].symbol, null, tvChart._subscribedChartWidget); } else { return originalSetSymbol.bind(tvChart)(...args); } }; }, []); const cachePlateData = (data) => { lscache.set('plateData', data, 1e15); }; const updatePlateData = async () => { if (onRefresh) return; setOnRefresh(true); // start update const selfData = await getThsSelf(); if (typeof selfData == 'string') { setIsLogin(false); setOnRefresh(false); return; } setIsLogin(true); const newPlateData = await getWencaiPlate(); const filteredPlateData = SHOW_WENCAI_PLATE ? newPlateData : newPlateData.filter(g => Number(g.id) > 0); const newData = [{id: 0, name: '自选股', items: selfData, open: true}, ...filteredPlateData]; let saveData = [], insertedIds = []; plateData.forEach(g => { const ol = newData.filter(o => o.id == g.id); if (ol.length == 0) return; const d = g; [d.name, d.items] = [ol[0].name, ol[0].items]; saveData.push(d); insertedIds.push(d.id); }); saveData = _.concat(saveData, newData.filter(o => !insertedIds.includes(o.id))); setPlateData(saveData); cachePlateData(saveData); // end update setOnRefresh(false); }; useEffect(() =>{ const cache = lscache.get('plateData'); if (cache) { setPlateData(cache); return; } updatePlateData(); }, []); let interval; const getNow = (div = 0) => Math.floor(new Date().getTime() / (div == 0 ? 1 : div)); const updateMarketData = async () => { let now = getNow(); const stocks = _.uniq(_.flatten(plateData.filter((_, i) => getPlateOpen(i)).map(g => g.items))); const noDataStocks = _.difference(stocks, Object.keys(marketCache)); const needUpdateStocks = Object.keys(marketCache).filter(i => stocks.includes(i)).sort((a, b) => marketCache[a] - marketCache[b]); const pass = _.slice([...noDataStocks, ...needUpdateStocks], 0, 20); if (pass.length == 0) { // clearInterval(interval); return; } const passData = await getRealtimeBasic(...pass); const passStocks = Object.keys(passData); const passCache = _.zipObject(passStocks, _.fill(Array(passStocks.length), getNow())); setMarketData({...marketData, ...passData}); setMarketCache({...marketCache, ...passCache}); }; useEffect(() => { if (plateData.length == 0) return; interval = setInterval(updateMarketData, 5000); return () => { clearInterval(interval) }; }, [plateData, marketData]); const showIntraday = _.debounce((e) => { if (e.type != "mouseover") { tooltipElement.classList.remove('is-active'); return; } const id = e.srcElement.dataset.id; if (!id.startsWith('sz') && !id.match(/^sh[^0]/)) return; tooltipElement.innerHTML = `<img src="https://image.sinajs.cn/newchart/min/n/${id}.gif?_=${getNow(100000)}" referrerpolicy="no-referrer">`; tooltipElement.classList.add('is-active'); }, 1000); function Item (props) { const id = (props.id.startsWith('ny') || props.id.startsWith('oq')) ? 'us' + props.id.slice(2) : props.id; const marketItem = marketData ? marketData[id] : null; const name = marketItem ? marketItem.name : id; const suspend = marketItem?.status == 'S'; const percent = marketItem ? (suspend ? '停牌' : marketItem.change_rate) : '-'; let spanClass = ''; if (percent > 0) spanClass = 'is-success'; else if (percent < 0) spanClass = 'is-danger'; return html` <li> <a onclick=${updateTvSymbol.bind(null, props.id)} class="${props.id == curSymbolTv ? 'is-active' : ''}"> <span class="symbol-name">${name}</span> <span class="tag is-info is-light ${spanClass}" data-id=${id} onmouseover=${showIntraday} onmouseout=${showIntraday} >${percent}%</span> </a> </li>` } function raisePlate (index) { if (index < 1) return; let newPlate = [...plateData]; [newPlate[index - 1], newPlate[index]] = [newPlate[index], newPlate[index - 1]]; setPlateData(newPlate); cachePlateData(newPlate); } function getPlateOpen (index) { const plate = plateData[index]; return Object.keys(plate).includes('open') && plate.open; } function flipPlate (index) { let newPlate = [...plateData]; newPlate[index].open = !getPlateOpen(index); setPlateData(newPlate); cachePlateData(newPlate); } function Plate (props) { const {group, groupid} = props; const visible = getPlateOpen(groupid); return html` <p class="menu-label"> <span class="plate-name">${group.name}</span> <span> <svg class="b-icon" onclick=${flipPlate.bind(null, groupid)}> <use xlink:href="#eye${visible ? '' : '-off'}-outline"/> </svg> <svg class="b-icon" onclick=${raisePlate.bind(null, groupid)}><use xlink:href="#caret-up-outline"/></svg> </span> </p> <ul class="menu-list" style="display: ${visible ? 'block' : 'none'};"> ${group.items.map(i => html`<${Item} id="${i}" />`)} </ul>` } return html` <div class="card"> <header class="card-header"> <p class="card-header-title">同花顺小窗</p> <span class="card-header-icon"> <svg class="b-icon is-medium" onclick=${() => setEnableSearchHook(!enableSearchHook)}> <use xlink:href="#search-circle${enableSearchHook ? '' : '-outline'}"/> </svg> <svg class="b-icon is-medium ${onRefresh ? 'disabled' : ''}" onclick=${updatePlateData}> <use xlink:href="#refresh-outline"/> </svg> </span> </header> <div class="card-content"> <div class="notification is-warning" style="display: ${!isLogin ? 'block' : 'none'};"> 未登录(不可用), <a href="https://www.10jqka.com.cn/" title="若无法加载自选板块,请登录(不可用)后点击同花顺主页的“问财”" rel="noopener noreferrer" target="_blank">到同花顺官网登录(不可用)</a> </div> <aside class="menu"> ${plateData.map((g, gi) => html`<${Plate} group=${g} groupid=${gi} /`)} </aside> </div> </div>` } const container = cEl('div'), svgElement = cEl('div'), tooltipElement = cEl('div'); container.id = 'tvhelper'; container.className = tooltipElement.className = 'card'; tooltipElement.id = 'tvhelper-tooltip'; svgElement.innerHTML = svgSprite; document.body.appendChild(svgElement); document.body.appendChild(container); document.body.appendChild(tooltipElement); render(html`<${App} />`, container); GM_addStyle(tvhelperCss); })(unsafeWindow ?? window);
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址