From 75db6901c056ea4941c882a8416fb9b3ef57113d Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Sun, 28 Dec 2025 13:42:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9concept?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/textUtils.js | 189 ++++++++++++++++++ src/views/Community/components/HeroPanel.js | 3 +- .../Concept/components/ForceGraphView.js | 3 +- src/views/Concept/components/HierarchyView.js | 3 +- src/views/Concept/index.js | 3 +- .../EventDetail/components/RelatedConcepts.js | 5 +- src/views/StockOverview/index.js | 3 +- 7 files changed, 202 insertions(+), 7 deletions(-) diff --git a/src/utils/textUtils.js b/src/utils/textUtils.js index 01d5c4d5..331f76be 100644 --- a/src/utils/textUtils.js +++ b/src/utils/textUtils.js @@ -2,6 +2,195 @@ * 文本处理工具函数 */ +/** + * 简单的 MD5 实现(与 Python hashlib.md5 兼容) + * 用于生成概念链接的哈希值 + */ +const md5 = (string) => { + function rotateLeft(lValue, iShiftBits) { + return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); + } + + function addUnsigned(lX, lY) { + const lX8 = lX & 0x80000000; + const lY8 = lY & 0x80000000; + const lX4 = lX & 0x40000000; + const lY4 = lY & 0x40000000; + const lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF); + if (lX4 & lY4) return lResult ^ 0x80000000 ^ lX8 ^ lY8; + if (lX4 | lY4) { + if (lResult & 0x40000000) return lResult ^ 0xC0000000 ^ lX8 ^ lY8; + return lResult ^ 0x40000000 ^ lX8 ^ lY8; + } + return lResult ^ lX8 ^ lY8; + } + + function f(x, y, z) { return (x & y) | (~x & z); } + function g(x, y, z) { return (x & z) | (y & ~z); } + function h(x, y, z) { return x ^ y ^ z; } + function i(x, y, z) { return y ^ (x | ~z); } + + function ff(a, b, c, d, x, s, ac) { + a = addUnsigned(a, addUnsigned(addUnsigned(f(b, c, d), x), ac)); + return addUnsigned(rotateLeft(a, s), b); + } + function gg(a, b, c, d, x, s, ac) { + a = addUnsigned(a, addUnsigned(addUnsigned(g(b, c, d), x), ac)); + return addUnsigned(rotateLeft(a, s), b); + } + function hh(a, b, c, d, x, s, ac) { + a = addUnsigned(a, addUnsigned(addUnsigned(h(b, c, d), x), ac)); + return addUnsigned(rotateLeft(a, s), b); + } + function ii(a, b, c, d, x, s, ac) { + a = addUnsigned(a, addUnsigned(addUnsigned(i(b, c, d), x), ac)); + return addUnsigned(rotateLeft(a, s), b); + } + + function convertToWordArray(str) { + let lWordCount; + const lMessageLength = str.length; + const lNumberOfWordsTemp1 = lMessageLength + 8; + const lNumberOfWordsTemp2 = (lNumberOfWordsTemp1 - (lNumberOfWordsTemp1 % 64)) / 64; + const lNumberOfWords = (lNumberOfWordsTemp2 + 1) * 16; + const lWordArray = Array(lNumberOfWords - 1); + let lBytePosition = 0; + let lByteCount = 0; + while (lByteCount < lMessageLength) { + lWordCount = (lByteCount - (lByteCount % 4)) / 4; + lBytePosition = (lByteCount % 4) * 8; + lWordArray[lWordCount] = lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition); + lByteCount++; + } + lWordCount = (lByteCount - (lByteCount % 4)) / 4; + lBytePosition = (lByteCount % 4) * 8; + lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition); + lWordArray[lNumberOfWords - 2] = lMessageLength << 3; + lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29; + return lWordArray; + } + + function wordToHex(lValue) { + let wordToHexValue = '', wordToHexValueTemp = '', lByte, lCount; + for (lCount = 0; lCount <= 3; lCount++) { + lByte = (lValue >>> (lCount * 8)) & 255; + wordToHexValueTemp = '0' + lByte.toString(16); + wordToHexValue = wordToHexValue + wordToHexValueTemp.substr(wordToHexValueTemp.length - 2, 2); + } + return wordToHexValue; + } + + function utf8Encode(str) { + str = str.replace(/\r\n/g, '\n'); + let utftext = ''; + for (let n = 0; n < str.length; n++) { + const c = str.charCodeAt(n); + if (c < 128) { + utftext += String.fromCharCode(c); + } else if (c > 127 && c < 2048) { + utftext += String.fromCharCode((c >> 6) | 192); + utftext += String.fromCharCode((c & 63) | 128); + } else { + utftext += String.fromCharCode((c >> 12) | 224); + utftext += String.fromCharCode(((c >> 6) & 63) | 128); + utftext += String.fromCharCode((c & 63) | 128); + } + } + return utftext; + } + + const x = convertToWordArray(utf8Encode(string)); + let a = 0x67452301, b = 0xEFCDAB89, c = 0x98BADCFE, d = 0x10325476; + const S11 = 7, S12 = 12, S13 = 17, S14 = 22; + const S21 = 5, S22 = 9, S23 = 14, S24 = 20; + const S31 = 4, S32 = 11, S33 = 16, S34 = 23; + const S41 = 6, S42 = 10, S43 = 15, S44 = 21; + + for (let k = 0; k < x.length; k += 16) { + const AA = a, BB = b, CC = c, DD = d; + a = ff(a, b, c, d, x[k], S11, 0xD76AA478); + d = ff(d, a, b, c, x[k + 1], S12, 0xE8C7B756); + c = ff(c, d, a, b, x[k + 2], S13, 0x242070DB); + b = ff(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE); + a = ff(a, b, c, d, x[k + 4], S11, 0xF57C0FAF); + d = ff(d, a, b, c, x[k + 5], S12, 0x4787C62A); + c = ff(c, d, a, b, x[k + 6], S13, 0xA8304613); + b = ff(b, c, d, a, x[k + 7], S14, 0xFD469501); + a = ff(a, b, c, d, x[k + 8], S11, 0x698098D8); + d = ff(d, a, b, c, x[k + 9], S12, 0x8B44F7AF); + c = ff(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1); + b = ff(b, c, d, a, x[k + 11], S14, 0x895CD7BE); + a = ff(a, b, c, d, x[k + 12], S11, 0x6B901122); + d = ff(d, a, b, c, x[k + 13], S12, 0xFD987193); + c = ff(c, d, a, b, x[k + 14], S13, 0xA679438E); + b = ff(b, c, d, a, x[k + 15], S14, 0x49B40821); + a = gg(a, b, c, d, x[k + 1], S21, 0xF61E2562); + d = gg(d, a, b, c, x[k + 6], S22, 0xC040B340); + c = gg(c, d, a, b, x[k + 11], S23, 0x265E5A51); + b = gg(b, c, d, a, x[k], S24, 0xE9B6C7AA); + a = gg(a, b, c, d, x[k + 5], S21, 0xD62F105D); + d = gg(d, a, b, c, x[k + 10], S22, 0x2441453); + c = gg(c, d, a, b, x[k + 15], S23, 0xD8A1E681); + b = gg(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8); + a = gg(a, b, c, d, x[k + 9], S21, 0x21E1CDE6); + d = gg(d, a, b, c, x[k + 14], S22, 0xC33707D6); + c = gg(c, d, a, b, x[k + 3], S23, 0xF4D50D87); + b = gg(b, c, d, a, x[k + 8], S24, 0x455A14ED); + a = gg(a, b, c, d, x[k + 13], S21, 0xA9E3E905); + d = gg(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8); + c = gg(c, d, a, b, x[k + 7], S23, 0x676F02D9); + b = gg(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A); + a = hh(a, b, c, d, x[k + 5], S31, 0xFFFA3942); + d = hh(d, a, b, c, x[k + 8], S32, 0x8771F681); + c = hh(c, d, a, b, x[k + 11], S33, 0x6D9D6122); + b = hh(b, c, d, a, x[k + 14], S34, 0xFDE5380C); + a = hh(a, b, c, d, x[k + 1], S31, 0xA4BEEA44); + d = hh(d, a, b, c, x[k + 4], S32, 0x4BDECFA9); + c = hh(c, d, a, b, x[k + 7], S33, 0xF6BB4B60); + b = hh(b, c, d, a, x[k + 10], S34, 0xBEBFBC70); + a = hh(a, b, c, d, x[k + 13], S31, 0x289B7EC6); + d = hh(d, a, b, c, x[k], S32, 0xEAA127FA); + c = hh(c, d, a, b, x[k + 3], S33, 0xD4EF3085); + b = hh(b, c, d, a, x[k + 6], S34, 0x4881D05); + a = hh(a, b, c, d, x[k + 9], S31, 0xD9D4D039); + d = hh(d, a, b, c, x[k + 12], S32, 0xE6DB99E5); + c = hh(c, d, a, b, x[k + 15], S33, 0x1FA27CF8); + b = hh(b, c, d, a, x[k + 2], S34, 0xC4AC5665); + a = ii(a, b, c, d, x[k], S41, 0xF4292244); + d = ii(d, a, b, c, x[k + 7], S42, 0x432AFF97); + c = ii(c, d, a, b, x[k + 14], S43, 0xAB9423A7); + b = ii(b, c, d, a, x[k + 5], S44, 0xFC93A039); + a = ii(a, b, c, d, x[k + 12], S41, 0x655B59C3); + d = ii(d, a, b, c, x[k + 3], S42, 0x8F0CCC92); + c = ii(c, d, a, b, x[k + 10], S43, 0xFFEFF47D); + b = ii(b, c, d, a, x[k + 1], S44, 0x85845DD1); + a = ii(a, b, c, d, x[k + 8], S41, 0x6FA87E4F); + d = ii(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0); + c = ii(c, d, a, b, x[k + 6], S43, 0xA3014314); + b = ii(b, c, d, a, x[k + 13], S44, 0x4E0811A1); + a = ii(a, b, c, d, x[k + 4], S41, 0xF7537E82); + d = ii(d, a, b, c, x[k + 11], S42, 0xBD3AF235); + c = ii(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB); + b = ii(b, c, d, a, x[k + 9], S44, 0xEB86D391); + a = addUnsigned(a, AA); + b = addUnsigned(b, BB); + c = addUnsigned(c, CC); + d = addUnsigned(d, DD); + } + return (wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d)).toLowerCase(); +}; + +/** + * 根据概念名称生成概念详情页面的 URL + * @param {string} conceptName - 概念名称 + * @returns {string} 概念详情页面的完整 URL + */ +export const getConceptHtmlUrl = (conceptName) => { + if (!conceptName) return ''; + const conceptHash = md5(conceptName); + return `https://valuefrontier.cn/htmls/concept/${conceptHash}/`; +}; + /** * 处理包含
标签的文本,转换为适合在React中显示的格式 * @param {string} text - 包含
标签的文本 diff --git a/src/views/Community/components/HeroPanel.js b/src/views/Community/components/HeroPanel.js index 5bab3bb4..aee424f1 100644 --- a/src/views/Community/components/HeroPanel.js +++ b/src/views/Community/components/HeroPanel.js @@ -29,6 +29,7 @@ import { AlertCircle, Clock, TrendingUp, Info, RefreshCw } from 'lucide-react'; import ReactECharts from 'echarts-for-react'; import { logger } from '@utils/logger'; import { getApiBase } from '@utils/apiConfig'; +import { getConceptHtmlUrl } from '@utils/textUtils'; import { useIndexQuote } from '@hooks/useIndexQuote'; import conceptStaticService from '@services/conceptStaticService'; import { GLASS_BLUR } from '@/constants/glassConfig'; @@ -450,7 +451,7 @@ const FlowingConcepts = () => { }; const handleClick = (name) => { - window.open(`https://valuefrontier.cn/htmls/${name}.html`, '_blank'); + window.open(getConceptHtmlUrl(name), '_blank'); }; if (loading) { diff --git a/src/views/Concept/components/ForceGraphView.js b/src/views/Concept/components/ForceGraphView.js index b25eb17b..e62d8904 100644 --- a/src/views/Concept/components/ForceGraphView.js +++ b/src/views/Concept/components/ForceGraphView.js @@ -44,6 +44,7 @@ import { ArrowLeft, } from 'lucide-react'; import { logger } from '../../../utils/logger'; +import { getConceptHtmlUrl } from '../../../utils/textUtils'; import { GLASS_BLUR } from '@/constants/glassConfig'; // 极光动画 - 黑金色主题 @@ -942,7 +943,7 @@ const ForceGraphView = ({ if (data.level === 'concept') { // 跳转到概念详情页 - const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(params.name)}.html`; + const htmlPath = getConceptHtmlUrl(params.name); window.open(htmlPath, '_blank'); return; } diff --git a/src/views/Concept/components/HierarchyView.js b/src/views/Concept/components/HierarchyView.js index 1c569fe1..bee38174 100644 --- a/src/views/Concept/components/HierarchyView.js +++ b/src/views/Concept/components/HierarchyView.js @@ -62,6 +62,7 @@ import { ExternalLink, } from 'lucide-react'; import { logger } from '../../../utils/logger'; +import { getConceptHtmlUrl } from '../../../utils/textUtils'; import { GLASS_BLUR } from '@/constants/glassConfig'; // 一级分类图标映射 @@ -685,7 +686,7 @@ const HierarchyView = ({ ]); } else if (item.level === 'concept') { // 跳转到概念详情页 - const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(item.name)}.html`; + const htmlPath = getConceptHtmlUrl(item.name); window.open(htmlPath, '_blank'); } }, [currentLv1, currentLv2]); diff --git a/src/views/Concept/index.js b/src/views/Concept/index.js index 7e313707..8cfd780f 100644 --- a/src/views/Concept/index.js +++ b/src/views/Concept/index.js @@ -1,6 +1,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { logger } from '../../utils/logger'; +import { getConceptHtmlUrl } from '../../utils/textUtils'; import defaultEventImage from '../../assets/img/default-event.jpg'; import { Box, @@ -674,7 +675,7 @@ const ConceptCenter = () => { trackConceptClicked(concept, position); } - const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(conceptName)}.html`; + const htmlPath = getConceptHtmlUrl(conceptName); window.open(htmlPath, '_blank'); }; diff --git a/src/views/EventDetail/components/RelatedConcepts.js b/src/views/EventDetail/components/RelatedConcepts.js index ebfe01cc..5fc59a89 100644 --- a/src/views/EventDetail/components/RelatedConcepts.js +++ b/src/views/EventDetail/components/RelatedConcepts.js @@ -34,6 +34,7 @@ import dayjs from 'dayjs'; import tradingDayUtils from '../../../utils/tradingDayUtils'; // 引入交易日工具 import { logger } from '../../../utils/logger'; import { getApiBase } from '../../../utils/apiConfig'; +import { getConceptHtmlUrl } from '../../../utils/textUtils'; import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme'; // 增强版 ConceptCard 组件 - 展示更多数据细节 @@ -61,7 +62,7 @@ const ConceptCard = ({ concept, tradingDate, onViewDetails }) => { // 处理概念点击 const handleConceptClick = () => { - window.open(`https://valuefrontier.cn/htmls/${encodeURIComponent(concept.concept)}.html`, '_blank'); + window.open(getConceptHtmlUrl(concept.concept), '_blank'); }; return ( @@ -758,7 +759,7 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad size="lg" flex={1} onClick={() => { - window.open(`https://valuefrontier.cn/htmls/${encodeURIComponent(selectedConcept.concept)}.html`, '_blank'); + window.open(getConceptHtmlUrl(selectedConcept.concept), '_blank'); }} leftIcon={} > diff --git a/src/views/StockOverview/index.js b/src/views/StockOverview/index.js index 85e51abb..bd0e3e28 100644 --- a/src/views/StockOverview/index.js +++ b/src/views/StockOverview/index.js @@ -55,6 +55,7 @@ import HotspotOverview from './components/HotspotOverview'; import FlexScreen from './components/FlexScreen'; import { echarts } from '@lib/echarts'; import { logger } from '../../utils/logger'; +import { getConceptHtmlUrl } from '../../utils/textUtils'; import tradingDays from '../../data/tradingDays.json'; import { useStockOverviewEvents } from './hooks/useStockOverviewEvents'; import { GLASS_BLUR } from '@/constants/glassConfig'; @@ -534,7 +535,7 @@ const StockOverview = () => { // 🎯 追踪概念点击 trackConceptClicked(concept, rank); - const htmlPath = `/htmls/${concept.concept_name}.html`; + const htmlPath = getConceptHtmlUrl(concept.concept_name); window.open(htmlPath, '_blank'); };