feat(web): Index/model/space/tool ui upgrade

This commit is contained in:
zhaoying
2026-03-23 11:37:04 +08:00
parent 0775fad5f0
commit 4dbb2bf2e2
47 changed files with 1094 additions and 1123 deletions

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 5</title>
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="工作台-首页" transform="translate(-1285, -192)" stroke="#171719">
<g id="新手指引" transform="translate(1100, 63)">
<g id="编组-44" transform="translate(12, 121)">
<g id="编组-37" transform="translate(99, 7)">
<g id="编组-5" transform="translate(74, 1)">
<polyline id="路径" points="10 6 12 8 10 10"></polyline>
<line x1="12" y1="8" x2="2" y2="8" id="路径-2"></line>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 962 B

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 33</title>
<defs>
<filter x="-2.2%" y="-5.4%" width="104.5%" height="112.7%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="4" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0.0901960784 0 0 0 0 0.0901960784 0 0 0 0 0.0980392157 0 0 0 0.16 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
</defs>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="记忆管理" transform="translate(-804, -283)" stroke="#FF5D34">
<g id="1备份-2" filter="url(#filter-1)" transform="translate(252, 108)">
<g id="编组-33" transform="translate(552, 175)">
<g id="编组-32" transform="translate(3.5, 4)">
<line x1="-1.80133686e-14" y1="2.22222222" x2="11" y2="2.22222222" id="路径-29"></line>
<polyline id="路径-30" stroke-linejoin="round" points="3.3 2.2221179 3.3 0 7.7 0 7.7 2.22222222"></polyline>
<path d="M1.65,2.23587458 L1.65,9 C1.65,9.55228475 2.09771525,10 2.65,10 L8.35,10 C8.90228475,10 9.35,9.55228475 9.35,9 L9.35,2.22222222 L9.35,2.22222222" id="路径-31" stroke-linejoin="round"></path>
<line x1="4.4" y1="4.45203738" x2="4.4" y2="7.78537071" id="路径-32"></line>
<line x1="6.6" y1="4.45203738" x2="6.6" y2="7.78537071" id="路径-32"></line>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>互联网</title>
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="平台管理-工具管理--工具市场" transform="translate(-1348, -329)" stroke="#5F6266">
<g id="卡片1备份-2" transform="translate(1044, 176)">
<g id="编组-5" transform="translate(16, 152)">
<g id="互联网" transform="translate(288, 1)">
<g id="编组-11" transform="translate(2, 2)">
<circle id="椭圆形" cx="6" cy="6" r="5.5"></circle>
<line x1="0.857142857" y1="3.85714286" x2="11.1428571" y2="3.85714286" id="路径-6"></line>
<path d="M4.66132498,0.645258919 C4.17108437,1.90189554 3.85714286,3.83362782 3.85714286,6 C3.85714286,8.18013114 4.17508482,10.1226325 4.67068706,11.3786026 M7.26566129,11.533836 C7.79752775,10.2860801 8.14285714,8.27208968 8.14285714,6 C8.14285714,3.77681214 7.81223272,1.80073299 7.2997423,0.54778953" id="形状"></path>
<line x1="0.843017916" y1="7.71428571" x2="11.1287322" y2="7.71428571" id="路径-6"></line>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="10px" height="10px" viewBox="0 0 10 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>形状结合@2x</title>
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="工作台-模型管理-模型广场" transform="translate(-999, -169)" stroke="#A8A9AA" stroke-width="1.2">
<g id="卡片1备份" transform="translate(648, 146)">
<g id="编组-5" transform="translate(344, 16)">
<path d="M7,12 L17,12 M12,7 L12,17" id="形状结合"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 723 B

View File

@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 34</title> <title>编组 34</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-1056, -232)"> <g id="工作台-首页" transform="translate(-1040, -196)">
<g id="核心数据" transform="translate(256, 216)"> <g id="核心数据" transform="translate(252, 180)">
<g id="编组-19备份-3" transform="translate(648, 0)"> <g id="编组-19备份-3" transform="translate(636, 0)">
<g id="编组-34" transform="translate(152, 16)"> <g id="编组-34" transform="translate(152, 16)">
<rect id="矩形" fill="#9C6FFF" x="0" y="0" width="32" height="32" rx="8"></rect> <rect id="矩形" fill="#9C6FFF" x="0" y="0" width="32" height="32" rx="8"></rect>
<g id="icon-GIS_运行管理" transform="translate(7, 8)" fill="#FFFFFF" fill-rule="nonzero"> <g id="icon-GIS_运行管理" transform="translate(7, 8)" fill="#FFFFFF" fill-rule="nonzero">
<path d="M7.16762406,14.1962904 C6.34809084,14.1646209 5.54326395,13.9741465 4.80020028,13.636004 C3.71131238,13.1260904 3.00897969,12.3104053 2.71316514,11.2234143 L2.87831314,11.2234143 C3.04376149,11.2238267 3.19695148,11.1385171 3.28050226,10.9994401 C3.36405304,10.8603631 3.3653563,10.6885064 3.28392388,10.5482427 L2.07888794,8.54040242 C1.99542259,8.40040876 1.84176478,8.31425552 1.67554571,8.31425552 C1.50932664,8.31425552 1.35566883,8.40040876 1.27220348,8.54040242 L0.0617231013,10.5500101 C-0.0210158141,10.6903056 -0.0205423886,10.8629501 0.0629647773,11.0028132 C0.146471943,11.1426763 0.300303569,11.2284717 0.466426438,11.227833 L0.804889093,11.227833 C1.23681463,13.5308399 2.77123916,14.6911806 3.98534917,15.2567694 C4.98325891,15.7122175 6.06611522,15.9645167 7.16762406,15.998225 C7.68009348,15.9996891 8.09703053,15.596793 8.09953973,15.0976976 C8.100628,14.8580057 8.00249628,14.627997 7.82730865,14.4596427 C7.65206093,14.2900046 7.4147074,14.1952508 7.16762406,14.1962904 L7.16762406,14.1962904 Z M17.9430772,5.00547242 C17.8606071,4.86230637 17.7051137,4.77374302 17.5365591,4.77393451 L17.1980964,4.77393451 C16.7661709,2.47092761 15.230839,1.31058684 14.0176364,0.742346841 C13.0197435,0.28685171 11.9368771,0.0345501233 10.8353615,0.000895273713 C10.4982732,-0.0093928287 10.18217,0.159876081 10.0105024,0.442595865 C9.83883478,0.725315649 9.83883478,1.076638 10.0105024,1.35935779 C10.18217,1.64207757 10.4982732,1.81134648 10.8353615,1.80105838 C11.6548676,1.83493183 12.4595539,2.02529996 13.2036927,2.36134478 C14.2925806,2.87214217 14.9949133,3.68694353 15.2907278,4.77393451 L15.1255798,4.77393451 C14.9574006,4.77106293 14.8007624,4.85694697 14.7160555,4.99847492 C14.6313487,5.14000288 14.6318043,5.3150681 14.7172468,5.456176 L15.9213754,7.46401624 C16.0048407,7.60400991 16.1584985,7.69016315 16.3247176,7.69016315 C16.4909367,7.69016315 16.6445945,7.60400991 16.7280598,7.46401624 L17.9303735,5.4588272 C18.018434,5.32105318 18.023291,5.14771891 17.9430772,5.00547242 L17.9430772,5.00547242 Z M16.1563937,7.99424995 L10.1375658,7.99424995 C9.50717772,7.99229725 8.99410285,8.48763753 8.99060389,9.10157256 L8.99060389,14.8926716 C8.99410285,15.5066067 9.50717772,16.0019469 10.1375658,15.9999942 L16.1563937,15.9999942 C16.7867818,16.0019469 17.2998566,15.5066067 17.3033556,14.8926716 L17.3033556,9.10157256 C17.2998566,8.48763753 16.7867818,7.99229725 16.1563937,7.99424995 L16.1563937,7.99424995 Z M15.4431721,9.79530257 L15.4431721,14.1962904 L10.854417,14.1962904 L10.854417,9.79530257 L15.4431721,9.79530257 Z M9.00058536,1.10732836 C8.99708639,0.493393328 8.48401152,-0.0019469461 7.85362344,5.75355807e-06 L1.83207335,5.75355807e-06 C1.20203901,-0.00145886726 0.689517223,0.493739212 0.686018831,1.10732836 L0.686018831,6.89842744 C0.686499539,7.19390032 0.807543503,7.47707233 1.02249671,7.68558868 C1.23744991,7.89410503 1.5286856,8.0108668 1.83207335,8.01018014 L7.85090122,8.01018014 C8.15491658,8.01180822 8.44708623,7.89546249 8.66282574,7.68684408 C8.87856526,7.47822567 9.00010748,7.1945153 9.00058536,6.89842744 L9.00058536,1.10732836 Z M7.13677223,1.80105838 L7.13677223,6.20558116 L2.54983195,6.20558116 L2.54983195,1.80105838 L7.13677223,1.80105838 Z" id="形状"></path> <path d="M7.16762406,14.1962904 C6.34809084,14.1646209 5.54326395,13.9741465 4.80020028,13.636004 C3.71131238,13.1260904 3.00897969,12.3104053 2.71316514,11.2234143 L2.87831314,11.2234143 C3.04376149,11.2238267 3.19695148,11.1385171 3.28050226,10.9994401 C3.36405304,10.8603631 3.3653563,10.6885064 3.28392388,10.5482427 L2.07888794,8.54040242 C1.99542259,8.40040876 1.84176478,8.31425552 1.67554571,8.31425552 C1.50932664,8.31425552 1.35566883,8.40040876 1.27220348,8.54040242 L0.0617231013,10.5500101 C-0.0210158141,10.6903056 -0.0205423886,10.8629501 0.0629647773,11.0028132 C0.146471943,11.1426763 0.300303569,11.2284717 0.466426438,11.227833 L0.804889093,11.227833 C1.23681463,13.5308399 2.77123916,14.6911806 3.98534917,15.2567694 C4.98325891,15.7122175 6.06611522,15.9645167 7.16762406,15.998225 C7.68009348,15.9996891 8.09703053,15.596793 8.09953973,15.0976976 C8.100628,14.8580057 8.00249628,14.627997 7.82730865,14.4596427 C7.65206093,14.2900046 7.4147074,14.1952508 7.16762406,14.1962904 L7.16762406,14.1962904 Z M17.9430772,5.00547242 C17.8606071,4.86230637 17.7051137,4.77374302 17.5365591,4.77393451 L17.1980964,4.77393451 C16.7661709,2.47092761 15.230839,1.31058684 14.0176364,0.742346841 C13.0197435,0.28685171 11.9368771,0.0345501233 10.8353615,0.000895273713 C10.4982732,-0.0093928287 10.18217,0.159876081 10.0105024,0.442595865 C9.83883478,0.725315649 9.83883478,1.076638 10.0105024,1.35935779 C10.18217,1.64207757 10.4982732,1.81134648 10.8353615,1.80105838 C11.6548676,1.83493183 12.4595539,2.02529996 13.2036927,2.36134478 C14.2925806,2.87214217 14.9949133,3.68694353 15.2907278,4.77393451 L15.1255798,4.77393451 C14.9574006,4.77106293 14.8007624,4.85694697 14.7160555,4.99847492 C14.6313487,5.14000288 14.6318043,5.3150681 14.7172468,5.456176 L15.9213754,7.46401624 C16.0048407,7.60400991 16.1584985,7.69016315 16.3247176,7.69016315 C16.4909367,7.69016315 16.6445945,7.60400991 16.7280598,7.46401624 L17.9303735,5.4588272 C18.018434,5.32105318 18.023291,5.14771891 17.9430772,5.00547242 L17.9430772,5.00547242 Z M16.1563937,7.99424995 L10.1375658,7.99424995 C9.50717772,7.99229725 8.99410285,8.48763753 8.99060389,9.10157256 L8.99060389,14.8926716 C8.99410285,15.5066067 9.50717772,16.0019469 10.1375658,15.9999942 L16.1563937,15.9999942 C16.7867818,16.0019469 17.2998566,15.5066067 17.3033556,14.8926716 L17.3033556,9.10157256 C17.2998566,8.48763753 16.7867818,7.99229725 16.1563937,7.99424995 L16.1563937,7.99424995 Z M15.4431721,9.79530257 L15.4431721,14.1962904 L10.854417,14.1962904 L10.854417,9.79530257 L15.4431721,9.79530257 Z M9.00058536,1.10732836 C8.99708639,0.493393328 8.48401152,-0.0019469461 7.85362344,5.75355757e-06 L1.83207335,5.75355757e-06 C1.20203901,-0.00145886726 0.689517223,0.493739212 0.686018831,1.10732836 L0.686018831,6.89842744 C0.686499539,7.19390032 0.807543503,7.47707233 1.02249671,7.68558868 C1.23744991,7.89410503 1.5286856,8.0108668 1.83207335,8.01018014 L7.85090122,8.01018014 C8.15491658,8.01180822 8.44708623,7.89546249 8.66282574,7.68684408 C8.87856526,7.47822567 9.00010748,7.1945153 9.00058536,6.89842744 L9.00058536,1.10732836 Z M7.13677223,1.80105838 L7.13677223,6.20558116 L2.54983195,6.20558116 L2.54983195,1.80105838 L7.13677223,1.80105838 Z" id="形状"></path>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>箭头_向上</title> <title>箭头_向上</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-928, -367)" fill="#FF5D34" fill-rule="nonzero"> <g id="工作台-首页" transform="translate(-912, -331)" fill="#FF5D34" fill-rule="nonzero">
<g id="核心数据" transform="translate(256, 216)"> <g id="核心数据" transform="translate(252, 180)">
<g id="编组-19备份-3" transform="translate(648, 0)"> <g id="编组-19备份-3" transform="translate(636, 0)">
<g id="标签1" transform="translate(16, 148)"> <g id="标签1" transform="translate(16, 148)">
<g id="箭头_向上" transform="translate(15, 10) scale(1, -1) translate(-15, -10)translate(8, 3)"> <g id="箭头_向上" transform="translate(15, 10) scale(1, -1) translate(-15, -10)translate(8, 3)">
<polygon id="路径" points="7.58333789 4.21666211 7.58333789 12.4416621 6.41666211 12.4416621 6.41666211 4.21666211 4.60833789 6.025 3.79166211 5.20832422 7 2 10.2083379 5.20832422 9.39166211 6.025"></polygon> <polygon id="路径" points="7.58333789 4.21666211 7.58333789 12.4416621 6.41666211 12.4416621 6.41666211 4.21666211 4.60833789 6.025 3.79166211 5.20832422 7 2 10.2083379 5.20832422 9.39166211 6.025"></polygon>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 30</title> <title>编组 30</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> <g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="首页" transform="translate(-1006, -326)" stroke="#FF5D34" stroke-width="1.2"> <g id="工作台-首页" transform="translate(-990, -290)" stroke="#FF5D34" stroke-width="1.2">
<g id="核心数据" transform="translate(256, 216)"> <g id="核心数据" transform="translate(252, 180)">
<g id="编组-19备份-3" transform="translate(648, 0)"> <g id="编组-19备份-3" transform="translate(636, 0)">
<g id="编组-30" transform="translate(108, 116) scale(1, -1) translate(-108, -116)translate(102, 110)"> <g id="编组-30" transform="translate(108, 116) scale(1, -1) translate(-108, -116)translate(102, 110)">
<g id="编组-31" transform="translate(2.5, 2.5)"> <g id="编组-31" transform="translate(2.5, 2.5)">
<polyline id="路径" points="0 3 3.5 0 7 3"></polyline> <polyline id="路径" points="0 3 3.5 0 7 3"></polyline>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>箭头_向上</title> <title>箭头_向上</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-712, -367)" fill="#369F21" fill-rule="nonzero"> <g id="工作台-首页" transform="translate(-700, -331)" fill="#369F21" fill-rule="nonzero">
<g id="核心数据" transform="translate(256, 216)"> <g id="核心数据" transform="translate(252, 180)">
<g id="编组-19备份-2" transform="translate(432, 0)"> <g id="编组-19备份-2" transform="translate(424, 0)">
<g id="标签1" transform="translate(16, 148)"> <g id="标签1" transform="translate(16, 148)">
<g id="箭头_向上" transform="translate(8, 3)"> <g id="箭头_向上" transform="translate(8, 3)">
<polygon id="路径" points="7.58333789 4.21666211 7.58333789 12.4416621 6.41666211 12.4416621 6.41666211 4.21666211 4.60833789 6.025 3.79166211 5.20832422 7 2 10.2083379 5.20832422 9.39166211 6.025"></polygon> <polygon id="路径" points="7.58333789 4.21666211 7.58333789 12.4416621 6.41666211 12.4416621 6.41666211 4.21666211 4.60833789 6.025 3.79166211 5.20832422 7 2 10.2083379 5.20832422 9.39166211 6.025"></polygon>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 30</title> <title>编组 30</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> <g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="首页" transform="translate(-488, -326)" stroke="#369F21" stroke-width="1.2"> <g id="工作台-首页" transform="translate(-480, -290)" stroke="#369F21" stroke-width="1.2">
<g id="核心数据" transform="translate(256, 216)"> <g id="核心数据" transform="translate(252, 180)">
<g id="编组-19备份" transform="translate(216, 0)"> <g id="编组-19备份" transform="translate(212, 0)">
<g id="编组-30" transform="translate(16, 110)"> <g id="编组-30" transform="translate(16, 110)">
<g id="编组-31" transform="translate(2.5, 2.5)"> <g id="编组-31" transform="translate(2.5, 2.5)">
<polyline id="路径" points="0 3 3.5 0 7 3"></polyline> <polyline id="路径" points="0 3 3.5 0 7 3"></polyline>

Before

Width:  |  Height:  |  Size: 1013 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 17</title> <title>编组 17</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-1252, -772)"> <g id="工作台-首页" transform="translate(-1136, -700)">
<g id="快捷操作" transform="translate(1120, 502)"> <g id="快捷操作" transform="translate(1100, 554)">
<g id="编组-11" transform="translate(2, 38)"> <g id="编组-11" transform="translate(2, 34)">
<g id="编组-17" transform="translate(130, 232)"> <g id="编组-17" transform="translate(34, 112)">
<rect id="矩形" stroke="#DFE4ED" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="8"></rect> <rect id="矩形" stroke="#EBEBEB" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="12"></rect>
<g id="使用帮助" transform="translate(8, 8)" stroke="#212332" stroke-width="1.7"> <g id="使用帮助" transform="translate(8, 8)" stroke="#171719" stroke-width="1.7">
<g id="编组-16" transform="translate(2.7, 1.2)"> <g id="编组-16" transform="translate(2.7, 1.2)">
<rect id="矩形" x="0" y="3.20000312" width="18.6000182" height="16.2000158" rx="3.5"></rect> <rect id="矩形" x="0" y="3.20000312" width="18.6000182" height="16.2000158" rx="3.5"></rect>
<path d="M6.43068654,1.74093758 L9.44000922,3.20000313 L9.44000922,3.20000313 L9.44000922,19.2000188 L4.24931901,16.6833205 C3.38736738,16.2654045 2.84000277,15.3917071 2.84000277,14.4337852 L2.84000277,3.99047285 C2.84000277,2.60976098 3.9592909,1.49047285 5.34000277,1.49047285 C5.71781927,1.49047285 6.09072193,1.57610626 6.43068654,1.74093758 Z" id="矩形" fill="#FFFFFF" stroke-linejoin="round"></path> <path d="M6.43068654,1.74093758 L9.44000922,3.20000313 L9.44000922,3.20000313 L9.44000922,19.2000188 L4.24931901,16.6833205 C3.38736738,16.2654045 2.84000277,15.3917071 2.84000277,14.4337852 L2.84000277,3.99047285 C2.84000277,2.60976098 3.9592909,1.49047285 5.34000277,1.49047285 C5.71781927,1.49047285 6.09072193,1.57610626 6.43068654,1.74093758 Z" id="矩形" fill="#FFFFFF" stroke-linejoin="round"></path>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

After

Width:  |  Height:  |  Size: 336 KiB

View File

@@ -1,26 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 25</title> <title>编组 25</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-1152, -556)"> <g id="工作台-首页" transform="translate(-1136, -600)">
<g id="快捷操作" transform="translate(1120, 502)"> <g id="快捷操作" transform="translate(1100, 554)">
<g id="编组-11" transform="translate(2, 38)"> <g id="编组-11" transform="translate(2, 34)">
<g id="编组-25" transform="translate(30, 16)"> <g id="编组-25" transform="translate(34, 12)">
<rect id="矩形" stroke="#DFE4ED" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="8"></rect> <rect id="矩形" stroke="#EBEBEB" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="12"></rect>
<g id="模型管理" transform="translate(9, 9)"> <g id="模型管理" transform="translate(9, 9)">
<rect id="矩形" stroke="#212332" stroke-width="1.7" stroke-linejoin="round" x="2.2" y="7.7" width="12.1" height="12.1"></rect> <rect id="矩形" stroke="#171719" stroke-width="1.7" stroke-linejoin="round" x="2.2" y="7.7" width="12.1" height="12.1"></rect>
<polygon id="矩形" stroke="#212332" stroke-width="1.7" stroke-linejoin="round" points="7.7 2.2 19.8 2.2 14.3 7.7 2.2 7.7"></polygon> <polygon id="矩形" stroke="#171719" stroke-width="1.7" stroke-linejoin="round" points="7.7 2.2 19.8 2.2 14.3 7.7 2.2 7.7"></polygon>
<polygon id="矩形" stroke="#212332" stroke-width="1.7" stroke-linejoin="round" points="7.7 14.3 19.8 14.3 14.3 19.8 2.2 19.8"></polygon> <polygon id="矩形" stroke="#171719" stroke-width="1.7" stroke-linejoin="round" points="7.7 14.3 19.8 14.3 14.3 19.8 2.2 19.8"></polygon>
<polygon id="矩形备份-9" stroke="#212332" stroke-width="1.7" stroke-linejoin="round" points="14.3 7.7 19.8 2.2 19.8 14.3 14.3 19.8"></polygon> <polygon id="矩形备份-9" stroke="#171719" stroke-width="1.7" stroke-linejoin="round" points="14.3 7.7 19.8 2.2 19.8 14.3 14.3 19.8"></polygon>
<line x1="7.7" y1="2.2" x2="7.7" y2="14.3" id="路径-10" stroke="#212332" stroke-width="1.7"></line> <line x1="7.7" y1="2.2" x2="7.7" y2="14.3" id="路径-10" stroke="#171719" stroke-width="1.7"></line>
<path d="M7.7,4.4 C8.91502645,4.4 9.9,3.41502645 9.9,2.2 C9.9,0.98497355 8.91502645,0 7.7,0 C6.48497355,0 5.5,0.98497355 5.5,2.2 C5.5,3.41502645 6.48497355,4.4 7.7,4.4 Z" id="椭圆形" fill="#212332"></path> <path d="M7.7,4.4 C8.91502645,4.4 9.9,3.41502645 9.9,2.2 C9.9,0.98497355 8.91502645,0 7.7,0 C6.48497355,0 5.5,0.98497355 5.5,2.2 C5.5,3.41502645 6.48497355,4.4 7.7,4.4 Z" id="椭圆形" fill="#171719"></path>
<path d="M8.25899503,16.5 C9.47402148,16.5 10.458995,15.5150264 10.458995,14.3 C10.458995,13.0849736 9.47402148,12.1 8.25899503,12.1 C7.04396858,12.1 6.05899503,13.0849736 6.05899503,14.3 C6.05899503,15.5150264 7.04396858,16.5 8.25899503,16.5 Z" id="椭圆形" fill="#212332"></path> <path d="M8.25899503,16.5 C9.47402148,16.5 10.458995,15.5150264 10.458995,14.3 C10.458995,13.0849736 9.47402148,12.1 8.25899503,12.1 C7.04396858,12.1 6.05899503,13.0849736 6.05899503,14.3 C6.05899503,15.5150264 7.04396858,16.5 8.25899503,16.5 Z" id="椭圆形" fill="#171719"></path>
<path d="M2.2,9.9 C3.41502645,9.9 4.4,8.91502645 4.4,7.7 C4.4,6.48497355 3.41502645,5.5 2.2,5.5 C0.98497355,5.5 0,6.48497355 0,7.7 C0,8.91502645 0.98497355,9.9 2.2,9.9 Z" id="椭圆形备份-2" fill="#212332"></path> <path d="M2.2,9.9 C3.41502645,9.9 4.4,8.91502645 4.4,7.7 C4.4,6.48497355 3.41502645,5.5 2.2,5.5 C0.98497355,5.5 0,6.48497355 0,7.7 C0,8.91502645 0.98497355,9.9 2.2,9.9 Z" id="椭圆形备份-2" fill="#171719"></path>
<path d="M2.2,22 C3.41502645,22 4.4,21.0150264 4.4,19.8 C4.4,18.5849736 3.41502645,17.6 2.2,17.6 C0.98497355,17.6 0,18.5849736 0,19.8 C0,21.0150264 0.98497355,22 2.2,22 Z" id="椭圆形备份-3" fill="#212332"></path> <path d="M2.2,22 C3.41502645,22 4.4,21.0150264 4.4,19.8 C4.4,18.5849736 3.41502645,17.6 2.2,17.6 C0.98497355,17.6 0,18.5849736 0,19.8 C0,21.0150264 0.98497355,22 2.2,22 Z" id="椭圆形备份-3" fill="#171719"></path>
<path d="M19.8,4.4 C21.0150264,4.4 22,3.41502645 22,2.2 C22,0.98497355 21.0150264,0 19.8,0 C18.5849736,0 17.6,0.98497355 17.6,2.2 C17.6,3.41502645 18.5849736,4.4 19.8,4.4 Z" id="椭圆形" fill="#212332"></path> <path d="M19.8,4.4 C21.0150264,4.4 22,3.41502645 22,2.2 C22,0.98497355 21.0150264,0 19.8,0 C18.5849736,0 17.6,0.98497355 17.6,2.2 C17.6,3.41502645 18.5849736,4.4 19.8,4.4 Z" id="椭圆形" fill="#171719"></path>
<path d="M19.8,16.5 C21.0150264,16.5 22,15.5150264 22,14.3 C22,13.0849736 21.0150264,12.1 19.8,12.1 C18.5849736,12.1 17.6,13.0849736 17.6,14.3 C17.6,15.5150264 18.5849736,16.5 19.8,16.5 Z" id="椭圆形" fill="#212332"></path> <path d="M19.8,16.5 C21.0150264,16.5 22,15.5150264 22,14.3 C22,13.0849736 21.0150264,12.1 19.8,12.1 C18.5849736,12.1 17.6,13.0849736 17.6,14.3 C17.6,15.5150264 18.5849736,16.5 19.8,16.5 Z" id="椭圆形" fill="#171719"></path>
<path d="M14.3,9.9 C15.5150264,9.9 16.5,8.91502645 16.5,7.7 C16.5,6.48497355 15.5150264,5.5 14.3,5.5 C13.0849736,5.5 12.1,6.48497355 12.1,7.7 C12.1,8.91502645 13.0849736,9.9 14.3,9.9 Z" id="椭圆形备份-2" fill="#212332"></path> <path d="M14.3,9.9 C15.5150264,9.9 16.5,8.91502645 16.5,7.7 C16.5,6.48497355 15.5150264,5.5 14.3,5.5 C13.0849736,5.5 12.1,6.48497355 12.1,7.7 C12.1,8.91502645 13.0849736,9.9 14.3,9.9 Z" id="椭圆形备份-2" fill="#171719"></path>
<path d="M14.3,22 C15.5150264,22 16.5,21.0150264 16.5,19.8 C16.5,18.5849736 15.5150264,17.6 14.3,17.6 C13.0849736,17.6 12.1,18.5849736 12.1,19.8 C12.1,21.0150264 13.0849736,22 14.3,22 Z" id="椭圆形备份-3" fill="#212332"></path> <path d="M14.3,22 C15.5150264,22 16.5,21.0150264 16.5,19.8 C16.5,18.5849736 15.5150264,17.6 14.3,17.6 C13.0849736,17.6 12.1,18.5849736 12.1,19.8 C12.1,21.0150264 13.0849736,22 14.3,22 Z" id="椭圆形备份-3" fill="#171719"></path>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 14</title> <title>编组 14</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-408, -232)"> <g id="工作台-首页" transform="translate(-404, -196)">
<g id="核心数据" transform="translate(256, 216)"> <g id="核心数据" transform="translate(252, 180)">
<g id="编组-14" transform="translate(152, 16)"> <g id="编组-14" transform="translate(152, 16)">
<rect id="矩形" fill="#155EEF" x="0" y="0" width="32" height="32" rx="8"></rect> <rect id="矩形" fill="#155EEF" x="0" y="0" width="32" height="32" rx="8"></rect>
<g id="模型管理" transform="translate(8, 7)" fill="#FFFFFF" fill-rule="nonzero"> <g id="模型管理" transform="translate(8, 7)" fill="#FFFFFF" fill-rule="nonzero">

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 26</title> <title>编组 26</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-1254, -556)"> <g id="工作台-首页" transform="translate(-1244, -600)">
<g id="快捷操作" transform="translate(1120, 502)"> <g id="快捷操作" transform="translate(1100, 554)">
<g id="编组-11" transform="translate(2, 38)"> <g id="编组-11" transform="translate(2, 34)">
<g id="编组-26" transform="translate(132, 16)"> <g id="编组-26" transform="translate(142, 12)">
<rect id="矩形" stroke="#DFE4ED" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="8"></rect> <rect id="矩形" stroke="#EBEBEB" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="12"></rect>
<g id="空间管理" transform="translate(11, 10)" stroke="#212332" stroke-linejoin="round" stroke-width="1.7"> <g id="空间管理" transform="translate(11, 10)" stroke="#171719" stroke-linejoin="round" stroke-width="1.7">
<polygon id="路径" points="0 5 9 0 18 5 18 15 9 20 0 15"></polygon> <polygon id="路径" points="0 5 9 0 18 5 18 15 9 20 0 15"></polygon>
<polygon id="路径-8" points="9.00139153 1 1 15 17 15"></polygon> <polygon id="路径-8" points="9.00139153 1 1 15 17 15"></polygon>
<polygon id="路径-8" transform="translate(9, 11) scale(1, -1) translate(-9, -11)" points="9.00065228 8 5.25 14 12.75 14"></polygon> <polygon id="路径-8" transform="translate(9, 11) scale(1, -1) translate(-9, -11)" points="9.00065228 8 5.25 14 12.75 14"></polygon>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -7,10 +7,10 @@
<stop stop-color="#369F21" offset="100%"></stop> <stop stop-color="#369F21" offset="100%"></stop>
</linearGradient> </linearGradient>
</defs> </defs>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-624, -232)"> <g id="工作台-首页" transform="translate(-616, -196)">
<g id="核心数据" transform="translate(256, 216)"> <g id="核心数据" transform="translate(252, 180)">
<g id="编组-19备份" transform="translate(216, 0)"> <g id="编组-19备份" transform="translate(212, 0)">
<g id="编组-32" transform="translate(152, 16)"> <g id="编组-32" transform="translate(152, 16)">
<rect id="矩形" fill="url(#linearGradient-1)" x="0" y="0" width="32" height="32" rx="8"></rect> <rect id="矩形" fill="url(#linearGradient-1)" x="0" y="0" width="32" height="32" rx="8"></rect>
<g id="空间-(1)" transform="translate(5, 7)" fill="#FFFFFF" fill-rule="nonzero"> <g id="空间-(1)" transform="translate(5, 7)" fill="#FFFFFF" fill-rule="nonzero">

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 24</title> <title>编组 24</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-1152, -664)"> <g id="工作台-首页" transform="translate(-1352, -600)">
<g id="快捷操作" transform="translate(1120, 502)"> <g id="快捷操作" transform="translate(1100, 554)">
<g id="编组-11" transform="translate(2, 38)"> <g id="编组-11" transform="translate(2, 34)">
<g id="编组-24" transform="translate(30, 124)"> <g id="编组-24" transform="translate(250, 12)">
<rect id="矩形" stroke="#DFE4ED" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="8"></rect> <rect id="矩形" stroke="#EBEBEB" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="12"></rect>
<g id="用户管理" transform="translate(9, 10)" stroke="#212332" stroke-width="1.7"> <g id="用户管理" transform="translate(9, 10)" stroke="#171719" stroke-width="1.7">
<path d="M10.4752216,9.8347602 C12.0826337,9.8347602 13.5411901,10.4629457 14.5971671,11.4807313 C15.6415902,12.4873807 16.2916977,13.8763311 16.2916977,15.41013 L16.2916987,17.2469287 C16.292495,17.768016 16.0699552,18.2385306 15.715897,18.5800613 C15.3496472,18.9333521 14.8431448,19.15 14.2862956,19.15 L2.85772417,19.15 C2.30025073,19.15 1.79383233,18.9336648 1.42754075,18.5806207 C1.07279965,18.2387094 0.85,17.7674542 0.85,17.2456298 L0.85,15.41013 C0.85,13.8763309 1.50010692,12.4873802 2.54452956,11.4807307 C3.60050579,10.4629455 5.05906118,9.8347602 6.6664729,9.8347602 Z" id="路径" fill-rule="nonzero"></path> <path d="M10.4752216,9.8347602 C12.0826337,9.8347602 13.5411901,10.4629457 14.5971671,11.4807313 C15.6415902,12.4873807 16.2916977,13.8763311 16.2916977,15.41013 L16.2916987,17.2469287 C16.292495,17.768016 16.0699552,18.2385306 15.715897,18.5800613 C15.3496472,18.9333521 14.8431448,19.15 14.2862956,19.15 L2.85772417,19.15 C2.30025073,19.15 1.79383233,18.9336648 1.42754075,18.5806207 C1.07279965,18.2387094 0.85,17.7674542 0.85,17.2456298 L0.85,15.41013 C0.85,13.8763309 1.50010692,12.4873802 2.54452956,11.4807307 C3.60050579,10.4629455 5.05906118,9.8347602 6.6664729,9.8347602 Z" id="路径" fill-rule="nonzero"></path>
<path d="M8.51095419,0.85 C9.31202689,0.85 10.0374296,1.17449643 10.5625442,1.69913767 C11.0873448,2.22346508 11.4120985,2.94775006 11.4120985,3.74776253 C11.4120985,4.54882217 11.0868662,5.27413048 10.5618329,5.79931671 C10.0365783,6.32472447 9.31123944,6.65 8.51095419,6.65 C7.71104886,6.65 6.98645948,6.32499629 6.46178132,5.80015797 C5.93701763,5.27523409 5.61209853,4.5502916 5.61209853,3.75 C5.61209853,2.94883778 5.93633819,2.22374798 6.46093583,1.69899021 C6.98544108,1.17432485 7.71016945,0.85 8.51095419,0.85 Z" id="路径" fill-rule="nonzero"></path> <path d="M8.51095419,0.85 C9.31202689,0.85 10.0374296,1.17449643 10.5625442,1.69913767 C11.0873448,2.22346508 11.4120985,2.94775006 11.4120985,3.74776253 C11.4120985,4.54882217 11.0868662,5.27413048 10.5618329,5.79931671 C10.0365783,6.32472447 9.31123944,6.65 8.51095419,6.65 C7.71104886,6.65 6.98645948,6.32499629 6.46178132,5.80015797 C5.93701763,5.27523409 5.61209853,4.5502916 5.61209853,3.75 C5.61209853,2.94883778 5.93633819,2.22374798 6.46093583,1.69899021 C6.98544108,1.17432485 7.71016945,0.85 8.51095419,0.85 Z" id="路径" fill-rule="nonzero"></path>
<path d="M16.5,8 C17.8807119,8 19,6.88071187 19,5.5 C19,4.11928813 17.8807119,3 16.5,3" id="路径" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M16.5,8 C17.8807119,8 19,6.88071187 19,5.5 C19,4.11928813 17.8807119,3 16.5,3" id="路径" stroke-linecap="round" stroke-linejoin="round"></path>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 33</title> <title>编组 33</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-840, -232)"> <g id="工作台-首页" transform="translate(-828, -196)">
<g id="核心数据" transform="translate(256, 216)"> <g id="核心数据" transform="translate(252, 180)">
<g id="编组-19备份-2" transform="translate(432, 0)"> <g id="编组-19备份-2" transform="translate(424, 0)">
<g id="编组-33" transform="translate(152, 16)"> <g id="编组-33" transform="translate(152, 16)">
<rect id="矩形" fill="#4DA8FF" x="0" y="0" width="32" height="32" rx="8"></rect> <rect id="矩形" fill="#4DA8FF" x="0" y="0" width="32" height="32" rx="8"></rect>
<g id="用户总数总计" transform="translate(6, 8)" fill="#FFFFFF" fill-rule="nonzero"> <g id="用户总数总计" transform="translate(6, 8)" fill="#FFFFFF" fill-rule="nonzero">

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -127,23 +127,23 @@ const ChatInput: FC<ChatInputProps> = ({
className={clsx( className={clsx(
"rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/pdf_disabled.svg')]", "rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/pdf_disabled.svg')]",
file.type.includes('pdf') file.type.includes('pdf')
? "rb:bg-[url('src/assets/images/file/pdf.svg')]" ? "rb:bg-[url('@/assets/images/file/pdf.svg')]"
: (file.type.includes('excel') || file.type.includes('spreadsheetml.sheet')) : (file.type.includes('excel') || file.type.includes('spreadsheetml.sheet'))
? "rb:bg-[url('src/assets/images/file/excel.svg')]" ? "rb:bg-[url('@/assets/images/file/excel.svg')]"
: file.type.includes('csv') : file.type.includes('csv')
? "rb:bg-[url('src/assets/images/file/csv.svg')]" ? "rb:bg-[url('@/assets/images/file/csv.svg')]"
: file.type.includes('html') : file.type.includes('html')
? "rb:bg-[url('src/assets/images/file/html.svg')]" ? "rb:bg-[url('@/assets/images/file/html.svg')]"
: file.type.includes('json') : file.type.includes('json')
? "rb:bg-[url('src/assets/images/file/json.svg')]" ? "rb:bg-[url('@/assets/images/file/json.svg')]"
: file.type.includes('ppt') : file.type.includes('ppt')
? "rb:bg-[url('src/assets/images/file/ppt.svg')]" ? "rb:bg-[url('@/assets/images/file/ppt.svg')]"
: file.type.includes('text') : file.type.includes('text')
? "rb:bg-[url('src/assets/images/file/txt.svg')]" ? "rb:bg-[url('@/assets/images/file/txt.svg')]"
: file.type.includes('markdown') : file.type.includes('markdown')
? "rb:bg-[url('src/assets/images/file/md.svg')]" ? "rb:bg-[url('@/assets/images/file/md.svg')]"
: (file.type.includes('doc') || file.type.includes('docx') || file.type.includes('word') || file.type.includes('wordprocessingml.document')) : (file.type.includes('doc') || file.type.includes('docx') || file.type.includes('word') || file.type.includes('wordprocessingml.document'))
? "rb:bg-[url('src/assets/images/file/word.svg')]" ? "rb:bg-[url('@/assets/images/file/word.svg')]"
: null : null
)} )}
></div> ></div>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-02 15:21:14 * @Date: 2026-02-02 15:21:14
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-24 14:59:53 * @Last Modified time: 2026-03-20 20:24:43
*/ */
/** /**
* RbCard Component * RbCard Component
@@ -23,6 +23,7 @@ import clsx from 'clsx';
/** Props interface for RbCard component */ /** Props interface for RbCard component */
interface RbCardProps extends CardProps { interface RbCardProps extends CardProps {
isNeedTooltip?: boolean;
children?: ReactNode; children?: ReactNode;
/** Custom avatar component */ /** Custom avatar component */
avatarText?: string; avatarText?: string;
@@ -32,16 +33,22 @@ interface RbCardProps extends CardProps {
/** Click handler */ /** Click handler */
onClick?: () => void; onClick?: () => void;
footer?: ReactNode; footer?: ReactNode;
headerClassName?: string;
titleClassName?: string;
} }
/** Custom card component with flexible styling and header options */ /** Custom card component with flexible styling and header options */
const RbCard: FC<RbCardProps> = ({ const RbCard: FC<RbCardProps> = ({
isNeedTooltip = true,
title, title,
children, children,
avatarText, avatarText,
avatarClassName, avatarClassName,
avatarUrl, avatarUrl,
footer, footer,
headerClassName,
titleClassName,
className,
...props ...props
}) => { }) => {
return ( return (
@@ -54,17 +61,23 @@ const RbCard: FC<RbCardProps> = ({
: avatarText : avatarText
? <Flex align="center" justify="center" className={clsx(avatarClassName, "rb:size-11 rb:rounded-lg rb:text-[24px] rb:text-[#ffffff] rb:bg-[#155EEF]")}>{avatarText}</Flex> : null ? <Flex align="center" justify="center" className={clsx(avatarClassName, "rb:size-11 rb:rounded-lg rb:text-[24px] rb:text-[#ffffff] rb:bg-[#155EEF]")}>{avatarText}</Flex> : null
} }
<Tooltip title={title}> {isNeedTooltip
<div className="rb:flex-1 rb:leading-5.5 rb:min-w-0 rb:whitespace-break-spaces rb:wrap-break-word rb:line-clamp-2"> ? <Tooltip title={title}>
<div className={`rb:flex-1 rb:leading-5.5 rb:min-w-0 rb:whitespace-break-spaces rb:wrap-break-word rb:line-clamp-2 ${titleClassName}`}>
{title}
</div>
</Tooltip>
: <div className="rb:flex-1 rb:leading-5.5 rb:min-w-0 rb:whitespace-break-spaces rb:wrap-break-word rb:line-clamp-2">
{title} {title}
</div> </div>
</Tooltip> }
</Flex>} </Flex>}
classNames={{ classNames={{
header: 'rb:text-[16px] rb:p-[16px_16px_8px_16px]! rb:border-0!', header: `rb:text-[16px] rb:p-[16px_16px_8px_16px]! rb:border-0! ${headerClassName}`,
body: 'rb:p-4! rb:bg-white!', body: 'rb:p-4! rb:pt-2! rb:bg-white!',
}} }}
className="rb:hover:shadow-[0px_2px_8px_0px_rgba(23,23,25,0.16)]! rb:group" className={`rb:hover:shadow-[0px_2px_8px_0px_rgba(23,23,25,0.16)]! rb:group ${className}`}
> >
{children} {children}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-02 15:24:23 * @Date: 2026-02-02 15:24:23
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-10 14:59:39 * @Last Modified time: 2026-03-23 11:35:33
*/ */
/** /**
* SearchInput Component * SearchInput Component
@@ -22,7 +22,7 @@ import { Input, type InputProps } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
/** Props interface for SearchInput component */ /** Props interface for SearchInput component */
interface SearchInputProps { interface SearchInputProps extends InputProps {
/** Placeholder text */ /** Placeholder text */
placeholder?: string; placeholder?: string;
/** Callback fired when search value changes */ /** Callback fired when search value changes */

View File

@@ -467,6 +467,7 @@ export const en = {
notAllSpaces: 'Cannot be all spaces', notAllSpaces: 'Cannot be all spaces',
download: 'Download', download: 'Download',
view: 'View', view: 'View',
updated_at: 'Updated At',
}, },
model: { model: {
searchPlaceholder: 'search model…', searchPlaceholder: 'search model…',
@@ -1918,6 +1919,9 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
refreshFailed: 'Refresh Failed', refreshFailed: 'Refresh Failed',
// Market related // Market related
mcpMarket: 'MCP Market',
availableMcp: 'Available MCP Services',
descEmpty: 'There is currently no introduction available …',
marketSelectTitle: 'Select an MCP Market', marketSelectTitle: 'Select an MCP Market',
marketSelectDesc: 'Choose a market source from the left, configure the connection to browse MCP services', marketSelectDesc: 'Choose a market source from the left, configure the connection to browse MCP services',
marketRefreshSuccess: 'List refreshed', marketRefreshSuccess: 'List refreshed',
@@ -1966,6 +1970,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
timeout: 'Timeout (seconds)', timeout: 'Timeout (seconds)',
sseReadTimeout: 'SSE Read Timeout (seconds)', sseReadTimeout: 'SSE Read Timeout (seconds)',
saveAndTest: 'Save and Test', saveAndTest: 'Save and Test',
testLink: 'Connection Test',
noTags: 'There are no tags here…',
timeFormat: 'Time Formatting', timeFormat: 'Time Formatting',
timeZoneConversion: 'Time Zone Conversion', timeZoneConversion: 'Time Zone Conversion',

View File

@@ -1104,6 +1104,7 @@ export const zh = {
notAllSpaces: '不能是纯空格', notAllSpaces: '不能是纯空格',
download: '下载', download: '下载',
view: '查看', view: '查看',
updated_at: '更新时间',
}, },
model: { model: {
searchPlaceholder: '搜索模型…', searchPlaceholder: '搜索模型…',
@@ -1914,6 +1915,9 @@ export const zh = {
refreshFailed: '刷新失败', refreshFailed: '刷新失败',
// Market 相关 // Market 相关
mcpMarket: 'MCP Market',
availableMcp: '可用的 MCP 服务',
descEmpty: '暂无介绍…',
marketSelectTitle: '选择一个 MCP 市场', marketSelectTitle: '选择一个 MCP 市场',
marketSelectDesc: '从左侧选择一个市场源,配置连接后即可浏览该市场的 MCP 服务', marketSelectDesc: '从左侧选择一个市场源,配置连接后即可浏览该市场的 MCP 服务',
marketRefreshSuccess: '列表已刷新', marketRefreshSuccess: '列表已刷新',
@@ -1962,6 +1966,8 @@ export const zh = {
timeout: '超时时间(秒)', timeout: '超时时间(秒)',
sseReadTimeout: 'SSE 读取超时时间(秒)', sseReadTimeout: 'SSE 读取超时时间(秒)',
saveAndTest: '保存并测试', saveAndTest: '保存并测试',
testLink: '连接测试',
noTags: '暂无标签…',
timeFormat: '时间格式化', timeFormat: '时间格式化',
timeZoneConversion: '时区转换', timeZoneConversion: '时区转换',

View File

@@ -299,7 +299,7 @@ const Conversation: FC = () => {
<Flex className="rb:w-full rb:p-[-16px]!"> <Flex className="rb:w-full rb:p-[-16px]!">
<div className="rb:w-80 rb:h-screen rb:bg-[#F6F6F6] rb:overflow-hidden"> <div className="rb:w-80 rb:h-screen rb:bg-[#F6F6F6] rb:overflow-hidden">
<Flex align="center" gap={8} className="rb:p-5!"> <Flex align="center" gap={8} className="rb:p-5!">
<div className="rb:size-6 rb:bg-cover rb:bg-[url('src/assets/images/conversation/redbear.png')]"></div> <div className="rb:size-6 rb:bg-cover rb:bg-[url('@/assets/images/conversation/redbear.png')]"></div>
<div className="rb:text-[16px] rb:leading-5 rb:font-[Gilroy-Extrabold] rb:font-extrabold">{t('memoryConversation.chatTitle')}</div> <div className="rb:text-[16px] rb:leading-5 rb:font-[Gilroy-Extrabold] rb:font-extrabold">{t('memoryConversation.chatTitle')}</div>
</Flex> </Flex>

View File

@@ -9,10 +9,8 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import guideBgImg from '@/assets/images/index/guide_bg@2x.png'
import { Button, Tour } from 'antd'; import { Button, Tour } from 'antd';
import type { TourProps } from 'antd'; import type { TourProps } from 'antd';
import arrowRight from '@/assets/images/index/arrow_right_blue.svg'
const GuideCard: React.FC = () => { const GuideCard: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -67,22 +65,18 @@ const GuideCard: React.FC = () => {
return ( return (
<> <>
<div className='rb:w-full rb:p-4' style={{ backgroundImage: `url(${guideBgImg})`, backgroundSize: '100% 100%' }}> <div className='rb:w-full rb:bg-white rb:rounded-xl rb:pb-3'>
<div className='rb:flex rb:justify-start rb:text-white rb:text-base rb:font-semibold' > <div className='rb:font-[MiSans-Bold] rb:font-bold rb:leading-5 rb:p-3 rb:bg-cover rb:rounded-tl-xl rb:rounded-tr-xl rb:bg-[url("@/assets/images/index/guide_bg@2x.png")]' >
{ t('index.getStarted')} { t('index.getStarted')}
</div> </div>
<div className='rb:flex rb:text-xs rb:text-white rb:leading-[18px] rb:mt-3'> <div className='rb:leading-4.5 rb:text-[12px] rb:-mt-2 rb:pl-3 rb:pr-1.75'>
{ t('index.startedDesc')} { t('index.startedDesc')}
</div> </div>
<div className='rb:flex rb:w-full rb:items-center rb:justify-between rb:gap-3 rb:mt-4'> <div className="rb:mt-2 rb:pl-3 rb:pr-4">
<Button ref={startButtonRef} className='rb:gap-2 rb:w-full rb:flex rb:items-center rb:text-[#155EEF]' onClick={handleStartGuide}> <Button ref={startButtonRef} block className='rb:gap-1 rb:flex rb:items-center' onClick={handleStartGuide}>
<span className='rb:text-xs'>{ t('index.viewGuide')}</span> <span className='rb:text-xs'>{t('index.viewGuide')}</span>
<img src={arrowRight} className='rb:size-4' /> <div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/common/arrow_right_dark.svg')]"></div>
</Button> </Button>
{/* <Button className='rb:gap-2 rb:flex rb:items-center rb:text-[#155EEF]'>
<span className='rb:text-xs'>{ t('index.watchVideo')}</span>
<img src={arrowRight} className='rb:size-4' />
</Button> */}
</div> </div>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { type FC } from 'react'; import { type FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Flex } from 'antd'
import modelIcon from '@/assets/images/index/model_mgt.svg' import modelIcon from '@/assets/images/index/model_mgt.svg'
import spaceIcon from '@/assets/images/index/space_mgt.svg' import spaceIcon from '@/assets/images/index/space_mgt.svg'
@@ -89,25 +90,30 @@ const QuickActions: FC<QuickActionsProps> = ({ onNavigate }) => {
]; ];
return ( return (
<div className='rb:w-full rb:p-4 rb:bg-[#FBFDFF] rb:border-1 rb:border-[#DFE4ED] rb:rounded-xl'> <div className='rb:w-full rb:bg-white rb:rounded-xl rb:mt-2.5 rb:py-3'>
<div className='rb:flex rb:justify-start rb:text-base rb:font-medium rb:text-[#212332]'> <div className='rb:font-[MiSans-Bold] rb:font-bold rb:leading-5 rb:px-4 rb:pb-3.5'>
{ t('quickActions.title') } { t('quickActions.title') }
</div> </div>
<div className="rb:grid rb:grid-cols-3 md:rb:grid-cols-4 rb:gap-4 rb:mt-4"> <div className="rb:grid rb:grid-cols-3 md:rb:grid-cols-4 rb:gap-4">
{quickActions.map((action) => (
{quickActions.map((action) => ( <Flex
<div key={action.key} key={action.key}
className="rb:flex rb:flex-col rb:items-center rb:text-center rb:cursor-pointer rb:group" vertical
onClick={action.onClick} gap={8}
> align="center"
<img src={action.icon} className='rb:size-10 rb:mx-auto' /> justify="center"
<div className="rb:mt-2 rb:text-xs rb:max-w-[74px] rb:text-[#5B6167] rb:text-center rb:leading-[14px]"> className="rb:cursor-pointer"
{action.title} onClick={action.onClick}
</div> >
</div> <img src={action.icon} className='rb:size-10 rb:mx-auto' />
))} <div className="rb:text-[12px] rb:max-w-18.25 rb:text-[#5B6167] rb:text-center rb:leading-3.5">
</div> {action.title}
</div>); </div>
</Flex>
))}
</div>
</div>
);
}; };
export default QuickActions; export default QuickActions;

View File

@@ -1,20 +1,15 @@
import { type FC } from 'react' import { type FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import totalModels from '@/assets/images/index/models.svg'; import { Flex } from 'antd';
import totalSpaces from '@/assets/images/index/spaces.svg'; import clsx from 'clsx';
import totalUsers from '@/assets/images/index/users.svg';
import totalApps from '@/assets/images/index/apps.svg';
import arrowUpDb from '@/assets/images/index/arrow_up_d.svg'
import arrowDownDb from '@/assets/images/index/arrow_down_d.svg'
import arrowUp from '@/assets/images/index/arrow_up.svg'
import arrowDown from '@/assets/images/index/arrow_down.svg'
import styles from './index.module.css'
import { type DataResponse } from '@/api/common' import { type DataResponse } from '@/api/common'
import Tag from '@/components/Tag'
const list = [ const list = [
{ {
key: 'models', key: 'models',
icon: totalModels, icon: 'rb:bg-[url("@/assets/images/index/models.svg")]',
value: '24', value: '24',
// trendValue: '12.5%', // trendValue: '12.5%',
trend: 'up', trend: 'up',
@@ -25,7 +20,7 @@ const list = [
}, },
{ {
key: 'spaces', key: 'spaces',
icon: totalSpaces, icon: 'rb:bg-[url("@/assets/images/index/spaces.svg")]',
value: '156', value: '156',
trendValue: '+8', trendValue: '+8',
trend: 'down', trend: 'down',
@@ -36,7 +31,7 @@ const list = [
}, },
{ {
key: 'users', key: 'users',
icon: totalUsers, icon: 'rb:bg-[url("@/assets/images/index/users.svg")]',
value: '1,248', value: '1,248',
trendValue: '+42', trendValue: '+42',
trend: 'up', trend: 'up',
@@ -47,7 +42,7 @@ const list = [
}, },
{ {
key: 'running_apps', key: 'running_apps',
icon: totalApps, icon: 'rb:bg-[url("@/assets/images/index/apps.svg")]',
value: '12.8k', value: '12.8k',
trendValue: '98.7%', trendValue: '98.7%',
trend: 'up', trend: 'up',
@@ -60,70 +55,106 @@ const list = [
const TopCardList: FC<{data?: DataResponse}> = ({ data }) => { const TopCardList: FC<{data?: DataResponse}> = ({ data }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className="rb:grid rb:grid-cols-4 rb:gap-[16px]"> <div className="rb:grid rb:grid-cols-4 rb:gap-3 rb:mt-3">
{list.map((item) => { {list.map((item) => {
return ( return (
<div <div
key={item.key} key={item.key}
className={styles.card} className="rb:bg-white rb:rounded-xl rb:p-4"
style={{ >
background: item.background, <Flex justify="space-between" align="center" gap={24}>
}} <div className="rb:text-[12pxx] rb:font-medium rb:flex-1">{t(`dashboard.${'total_' + item.key}`)}</div>
> <div className={`rb:size-8 rb:bg-cover ${item.icon}`}></div>
<div className={styles.header}> </Flex>
<div className="rb:text-xs rb:font-medium rb:text-[#212332] rb:w-[96px]">{t(`dashboard.${'total_' + item.key}`)}</div>
<div className={styles.avatar}><img src={item.icon} /></div>
</div>
<div className={styles.content}> <div className="rb:mt-5 rb:font-[MiSans-Bold] rb:font-bold rb:text-[24px] rb:leading-8">
{item.key === 'spaces' && String(data?.active_workspaces)} {item.key === 'spaces' && String(data?.active_workspaces)}
{item.key === 'running_apps' && String(data?.[`${item.key}` as keyof DataResponse] || item.value || 0)} {item.key === 'running_apps' && String(data?.[`${item.key}` as keyof DataResponse] || item.value || 0)}
{item.key !== 'spaces' && item.key !== 'running_apps' && String(data?.[`total_${item.key}` as keyof DataResponse] || item.value || 0)} {item.key !== 'spaces' && item.key !== 'running_apps' && String(data?.[`total_${item.key}` as keyof DataResponse] || item.value || 0)}
</div> </div>
<div className='rb:flex rb:flex-col rb:items-start'> <div className='rb:flex rb:flex-col rb:items-start rb:mt-2'>
{item.key === 'models' ? ( {item.key === 'models'
<div className='rb:text-xs rb:leading-4 rb:text-[#5F6266] rb:w-[130px]'> ? (
{t(`dashboard.${'desc_' + item.key}`, { account: data?.total_llm, nums: data?.total_embedding })} <div className='rb:text-xs rb:leading-4 rb:text-[#5F6266] rb:w-32.25'>
{t(`dashboard.${'desc_' + item.key}`, { account: data?.total_llm, nums: data?.total_embedding })}
</div> </div>
) : (<> )
: (<>
<div className='rb:flex rb:items-center rb:text-xs rb:leading-4 rb:gap-1'> <div className='rb:flex rb:items-center rb:text-xs rb:leading-4 rb:gap-1'>
{item.key === 'spaces' && (<> {item.key === 'spaces' && (<>
<img src={Number(data?.new_workspaces_this_week || 0) >= 0 ? arrowUpDb : arrowDownDb} className='rb:size-3'/> <div className={clsx("rb:size-3 rb:bg-cover rb:mr-0.5", {
<span className={Number(data?.new_workspaces_this_week || 0) >= 0 ? 'rb:text-[#369F21]' : 'rb:text-[#FF5D34]'}>{Number(data?.new_workspaces_this_week || 0) >= 0 ? '+' : '-'}{Math.abs(Number(data?.new_workspaces_this_week || 0))}</span> "rb:bg-[url('@/assets/images/index/arrow_up_d.svg')]": Number(data?.new_workspaces_this_week || 0) >= 0,
"rb:bg-[url('@/assets/images/index/arrow_down_d.svg')]": Number(data?.new_workspaces_this_week || 0) < 0,
})}></div>
<span className={clsx('rb:font-medium', {
"rb:text-[#369F21]": Number(data?.new_workspaces_this_week || 0) >= 0,
"rb:text-[#FF5D34]": Number(data?.new_workspaces_this_week || 0) < 0,
})}>{Number(data?.new_workspaces_this_week || 0) >= 0 ? '+' : '-'}{Math.abs(Number(data?.new_workspaces_this_week || 0))}</span>
</>)} </>)}
{item.key === 'users' && (<> {item.key === 'users' && (<>
<img src={Number(data?.new_users_this_week || 0) >= 0 ? arrowUpDb : arrowDownDb} className='rb:size-3'/> <div className={clsx("rb:size-3 rb:bg-cover rb:mr-0.5", {
<span className={Number(data?.new_users_this_week || 0) >= 0 ? 'rb:text-[#369F21]' : 'rb:text-[#FF5D34]'}>{Number(data?.new_users_this_week || 0) >= 0 ? '+' : '-'}{Math.abs(Number(data?.new_users_this_week || 0))}</span> "rb:bg-[url('@/assets/images/index/arrow_up_d.svg')]": Number(data?.new_users_this_week || 0) >= 0,
"rb:bg-[url('@/assets/images/index/arrow_down_d.svg')]": Number(data?.new_users_this_week || 0) < 0,
})}></div>
<span className={clsx('rb:font-medium', {
"rb:text-[#369F21]": Number(data?.new_users_this_week || 0) >= 0,
"rb:text-[#FF5D34]": Number(data?.new_users_this_week || 0) < 0,
})}>{Number(data?.new_users_this_week || 0) >= 0 ? '+' : '-'}{Math.abs(Number(data?.new_users_this_week || 0))}</span>
</>)} </>)}
{item.key === 'running_apps' && (<> {item.key === 'running_apps' && (<>
<img src={Number(data?.new_apps_this_week || 0) >= 0 ? arrowUpDb : arrowDownDb} className='rb:size-3'/> <div className={clsx("rb:size-3 rb:bg-cover rb:mr-0.5", {
<span className={Number(data?.new_apps_this_week || 0) >= 0 ? 'rb:text-[#369F21]' : 'rb:text-[#FF5D34]'}>{Number(data?.new_apps_this_week || 0) >= 0 ? '+' : '-'}{Math.abs(Number(data?.new_apps_this_week || 0))}</span> "rb:bg-[url('@/assets/images/index/arrow_up_d.svg')]": Number(data?.new_apps_this_week || 0) >= 0,
"rb:bg-[url('@/assets/images/index/arrow_down_d.svg')]": Number(data?.new_apps_this_week || 0) < 0,
})}></div>
<span className={clsx('rb:font-medium', {
"rb:text-[#369F21]": Number(data?.new_apps_this_week || 0) >= 0,
"rb:text-[#FF5D34]": Number(data?.new_apps_this_week || 0) < 0,
})}>{Number(data?.new_apps_this_week || 0) >= 0 ? '+' : '-'}{Math.abs(Number(data?.new_apps_this_week || 0))}</span>
</>)} </>)}
</div> </div>
<div className='rb:text-xs rb:leading-4 rb:text-[#5F6266]'> <div className='rb:text-[12px] rb:leading-4 rb:text-[#5F6266]'>
{t(`dashboard.${'desc_' + item.key}`)} {t(`dashboard.${'desc_' + item.key}`)}
</div> </div>
</>)} </>)
}
</div> </div>
{item.key === 'models' && (<div className={`rb:flex rb:max-w-40 rb:text-xs rb:mt-2 rb:items-center rb:gap-1 rb:border-1 rb:rounded rb:px-2 rb:py-0.5 ${Number(data?.model_week_growth_rate || 0) >= 0 ? 'rb:text-[#369F21] rb:border-[#369F21] rb:bg-[rgba(54, 159, 33, 0.25)]' : 'rb:text-[#FF5D34] rb:border-[#FF5D34] rb:bg-[rgba(255, 93, 52, 0.25)]'}`}> {item.key === 'models' && (<Tag color={Number(data?.model_week_growth_rate || 0) >= 0 ? "success" : "warning"} className="rb:mt-2">
<img src={Number(data?.model_week_growth_rate || 0) >= 0 ? arrowUp : arrowDown} className='rb:size-3'/> <Flex align="center">
<span>{Math.abs(Number(data?.model_week_growth_rate || 0))}% {t('dashboard.thisWeek')}</span> <div className={clsx("rb:size-3.5 rb:bg-cover rb:mr-0.5", {
</div>)} "rb:bg-[url('@/assets/images/index/arrow_up.svg')]": Number(data?.model_week_growth_rate || 0) >= 0,
{item.key === 'spaces' && (<div className={`rb:flex rb:max-w-40 rb:text-xs rb:mt-2 rb:items-center rb:gap-1 rb:border-1 rb:rounded rb:px-2 rb:py-0.5 ${Number(data?.workspace_week_growth_rate || 0) >= 0 ? 'rb:text-[#369F21] rb:border-[#369F21] rb:bg-[rgba(54, 159, 33, 0.25)]' : 'rb:text-[#FF5D34] rb:border-[#FF5D34] rb:bg-[rgba(255, 93, 52, 0.25)]'}`}> "rb:bg-[url('@/assets/images/index/arrow_down.svg')]": Number(data?.model_week_growth_rate || 0) < 0,
<img src={Number(data?.workspace_week_growth_rate || 0) >= 0 ? arrowUp : arrowDown} className='rb:size-3'/> })}></div>
<span>{Math.abs(Number(data?.workspace_week_growth_rate || 0))}% {t('dashboard.thisWeek')}</span> <span>{Math.abs(Number(data?.model_week_growth_rate || 0))}% {t('dashboard.thisWeek')}</span>
</div>)} </Flex>
{item.key === 'users' && (<div className={`rb:flex rb:max-w-40 rb:text-xs rb:mt-2 rb:items-center rb:gap-1 rb:border-1 rb:rounded rb:px-2 rb:py-0.5 ${Number(data?.user_week_growth_rate || 0) >= 0 ? 'rb:text-[#369F21] rb:border-[#369F21] rb:bg-[rgba(54, 159, 33, 0.25)]' : 'rb:text-[#FF5D34] rb:border-[#FF5D34] rb:bg-[rgba(255, 93, 52, 0.25)]'}`}> </Tag>)}
<img src={Number(data?.user_week_growth_rate || 0) >= 0 ? arrowUp : arrowDown} className='rb:size-3'/> {item.key === 'spaces' && (<Tag color={Number(data?.workspace_week_growth_rate || 0) >= 0 ? "success" : "warning"} className="rb:mt-2">
<span>{Math.abs(Number(data?.user_week_growth_rate || 0))}% {t('dashboard.thisWeek')}</span> <Flex align="center">
</div>)} <div className={clsx("rb:size-3.5 rb:bg-cover rb:mr-0.5", {
{item.key === 'running_apps' && (<div className={`rb:flex rb:max-w-40 rb:text-xs rb:mt-2 rb:items-center rb:gap-1 rb:border-1 rb:rounded rb:px-2 rb:py-0.5 ${Number(data?.app_week_growth_rate || 0) >= 0 ? 'rb:text-[#369F21] rb:border-[#369F21] rb:bg-[rgba(54, 159, 33, 0.25)]' : 'rb:text-[#FF5D34] rb:border-[#FF5D34] rb:bg-[rgba(255, 93, 52, 0.25)]'}`}> "rb:bg-[url('@/assets/images/index/arrow_up.svg')]": Number(data?.workspace_week_growth_rate || 0) >= 0,
<img src={Number(data?.app_week_growth_rate || 0) >= 0 ? arrowUp : arrowDown} className='rb:size-3'/> "rb:bg-[url('@/assets/images/index/arrow_down.svg')]": Number(data?.workspace_week_growth_rate || 0) < 0,
<span>{Math.abs(Number(data?.app_week_growth_rate || 0))}% {t('dashboard.thisWeek')}</span> })}></div>
</div>)} <span>{Math.abs(Number(data?.workspace_week_growth_rate || 0))}% {t('dashboard.thisWeek')}</span>
</Flex>
</Tag>)}
{item.key === 'users' && (<Tag color={Number(data?.user_week_growth_rate || 0) >= 0 ? "success" : "warning"} className="rb:mt-2">
<Flex align="center">
<div className={clsx("rb:size-3.5 rb:bg-cover rb:mr-0.5", {
"rb:bg-[url('@/assets/images/index/arrow_up.svg')]": Number(data?.user_week_growth_rate || 0) >= 0,
"rb:bg-[url('@/assets/images/index/arrow_down.svg')]": Number(data?.user_week_growth_rate || 0) < 0,
})}></div>
<span>{Math.abs(Number(data?.user_week_growth_rate || 0))}% {t('dashboard.thisWeek')}</span>
</Flex>
</Tag>)}
{item.key === 'running_apps' && (<Tag color={Number(data?.app_week_growth_rate || 0) >= 0 ? "success" : "warning"} className="rb:mt-2">
<Flex align="center">
<div className={clsx("rb:size-3.5 rb:bg-cover rb:mr-0.5", {
"rb:bg-[url('@/assets/images/index/arrow_up.svg')]": Number(data?.app_week_growth_rate || 0) >= 0,
"rb:bg-[url('@/assets/images/index/arrow_down.svg')]": Number(data?.app_week_growth_rate || 0) < 0,
})}></div>
<span>{Math.abs(Number(data?.app_week_growth_rate || 0))}% {t('dashboard.thisWeek')}</span>
</Flex>
</Tag>)}
</div> </div>
) )
})} })}

View File

@@ -8,11 +8,11 @@
*/ */
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Divider } from 'antd'; import { Flex } from 'antd';
// import arrowRight from '@/assets/images/index/arrow_right.svg'
import { getVersion, type versionResponse } from '@/api/common' import { getVersion, type versionResponse } from '@/api/common'
const GuideCard: React.FC = () => { const VersionCard: React.FC = () => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [versionInfo, setVersionInfo] = useState<versionResponse | null>(null); const [versionInfo, setVersionInfo] = useState<versionResponse | null>(null);
@@ -23,13 +23,6 @@ const GuideCard: React.FC = () => {
return currentLang === 'zh' ? versionInfo.introduction : (versionInfo.introduction_en || versionInfo.introduction); return currentLang === 'zh' ? versionInfo.introduction : (versionInfo.introduction_en || versionInfo.introduction);
}; };
// 解析换行符和HTML的方法
const parseContent = (text: string) => {
if (!text) return '';
// 将 \n 转换为 <br/> 标签
return text.replace(/\\n/g, '<br/>');
};
useEffect(() => { useEffect(() => {
const fetchVersion = async () => { const fetchVersion = async () => {
try { try {
@@ -44,58 +37,36 @@ const GuideCard: React.FC = () => {
}, []); }, []);
return ( return (
<div className='rb:w-full rb:p-4 rb:border-1 rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded-xl'> <div className='rb:w-full rb:p-3 rb:bg-white rb:rounded-xl rb:mt-3'>
<div className='rb:flex rb:items-center rb:justify-start rb:text-[#5B6167] rb:text-base rb:font-semibold rb:gap-2'> <Flex gap={4} className="rb:mb-3">
{ t('index.latestUpdate')} <span className="rb:font-[MiSans-Bold] rb:font-bold rb:leading-5">{t('index.latestUpdate')}</span>
<span className='rb:text-xs rb:text-[#1890FF]'> <span className='rb:text-[12px] rb:text-white rb:leading-4.25 rb:pt-px rb:pl-2 rb:pr-1.75 rb:bg-[#171719] rb:rounded-lg rb:rounded-bl-none '>
{versionInfo?.version} {versionInfo?.version}
</span> </span>
</div> </Flex>
<div className='rb:flex rb:flex-col rb:max-h-[400px] rb:overflow-y-auto rb:text-[#5B6167]'> {versionInfo && (() => {
{versionInfo && (() => { const introduction = getIntroduction();
const introduction = getIntroduction(); return introduction ? (<>
return introduction ? (<> <div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:mt-1 rb:mb-2">
<div className='rb:flex rb:items-center rb:gap-2 rb:text-sm rb:text-[#5B6167] rb:leading-5 '> {t('version.releaseDate')}: {introduction.releaseDate} | {t('version.name')}: {introduction.codeName}
</div>
<span className='rb:text-xs rb:text-[#5B6167]'> <div className="rb:max-h-76 rb:overflow-y-auto">
{t('version.releaseDate')}: {introduction.releaseDate} <p
</span> className='rb:text-[12px] rb:leading-4.5'
<Divider type='vertical' /> dangerouslySetInnerHTML={{ __html: introduction.upgradePosition }}
<span className='rb:text-xs rb:text-[#5B6167]'> />
{t('version.name')}: {introduction.codeName} {introduction.coreUpgrades?.map((item: string, index: number) => (
</span> <p
</div> key={index}
<p className='rb:text-[12px] rb:leading-4.5 rb:mt-2'
className='rb:text-sm rb:text-[#5B6167] rb:leading-5 rb:mt-2' dangerouslySetInnerHTML={{ __html: item }}
dangerouslySetInnerHTML={{ __html: introduction.upgradePosition }} />
/> ))}
{introduction.coreUpgrades?.map((item: string, index: number) => ( </div>
<p </>) : null;
key={index} })()}
className='rb:text-sm rb:text-[#5B6167] rb:leading-5'
dangerouslySetInnerHTML={{ __html: item }}
/>
))}
</>) : null;
})()}
{/* {loading ? (
t('index.loading')
) : (
versionInfo?.introduction || t('index.latestUpdateDesc')
)} */}
</div>
{/* <div className='rb:flex rb:w-full rb:items-center rb:justify-between rb:gap-3 rb:mt-4'>
<Button className='rb:gap-2 rb:flex rb:items-center rb:text-[#212332] '>
<span className='rb:text-xs'>{ t('index.viewDetails')}</span>
<img src={arrowRight} className='rb:size-4' />
</Button>
<Button className='rb:gap-2 rb:flex rb:items-center rb:text-[#212332]'>
<span className='rb:text-xs'>{ t('index.changeLog')}</span>
<img src={arrowRight} className='rb:size-4' />
</Button>
</div> */}
</div> </div>
); );
}; };
export default GuideCard; export default VersionCard;

View File

@@ -1,12 +1,12 @@
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Space, Button } from 'antd'; import { Space, Button, Row, Col, Flex } from 'antd';
import TopCardList from './components/TopCardList'; import TopCardList from './components/TopCardList';
import GuideCard from './components/GuideCard'; import GuideCard from './components/GuideCard';
import VersionCard from './components/VersionCard'; import VersionCard from './components/VersionCard';
import QuickActions from './components/QuickActions'; import QuickActions from './components/QuickActions';
import bgImg from '@/assets/images/index/index_bg@2x.png'
import Table, { type TableRef } from '@/components/Table' import Table, { type TableRef } from '@/components/Table'
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { formatDateTime } from '@/utils/format'; import { formatDateTime } from '@/utils/format';
@@ -42,16 +42,15 @@ const Index = () => {
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
}, },
{ {
title: t('space.spaceIcon'), title: t('space.spaceIcon'),
dataIndex: 'icon', dataIndex: 'icon',
key: 'icon', key: 'icon',
render:(value: string, record: any) => { render:(value: string, record: any) => {
return value ? ( return value ? (
<img src={value} alt="icon" className='rb:w-[24px] rb:h-[24px]' /> <img src={value} alt="icon" className='rb:size-6' />
) : ( ) : (
<div className='rb:w-[24px] rb:h-[24px] rb:bg-blue-500 rb:text-white rb:rounded rb:flex rb:items-center rb:justify-center rb:text-xs rb:font-medium'> <div className='rb:size-6 rb:bg-[#155EEF] rb:text-white rb:rounded rb:flex rb:items-center rb:justify-center rb:text-xs rb:font-medium'>
{record.name?.charAt(0)?.toUpperCase() || '?'} {record.name?.charAt(0)?.toUpperCase() || '?'}
</div> </div>
) )
@@ -84,7 +83,7 @@ const Index = () => {
width: 100, width: 100,
render: (_, record) => ( render: (_, record) => (
<Space size="middle"> <Space size="middle">
<Button onClick={() => handleJump(record.id)} color="primary" variant="text">{t('space.enterSpace')}</Button> <Button type="link" onClick={() => handleJump(record.id)}>{t('space.enterSpace')}</Button>
</Space> </Space>
), ),
}, },
@@ -99,44 +98,40 @@ const Index = () => {
return ( return (
<div className="rb:pb-[24px]"> <Row gutter={12}>
<div className="rb:mt-[16px] rb:flex rb:gap-4"> <Col flex="1">
<div className='rb:flex-1'> <Flex vertical>
<div className='rb:flex-col rb:w-full rb:h-[120px] rb:mb-4 rb:p-6 rb:leading-[30px]' style={{backgroundImage: `url(${bgImg})`, backgroundSize: '100% 100%'}}> <div className='rb:w-full rb:h-26 rb:p-4 rb:bg-cover rb:bg-[url("@/assets/images/index/index_bg@2x.png")]'>
<div className='rb:flex rb:text-[22px] rb:text-[#0041C3] rb:font-semibold'> <div className="rb:font-[MiSans-Bold] rb:font-bold rb:text-white rb:text-[18px] rb:leading-7">
{ t('index.spaceTitle' )} {t('index.spaceTitle')}
</div> </div>
<div className='rb:flex rb:mt-2 rb:text-xs rb:leading-[18px] rb:text-[#5F6266] rb:max-w-[560px]'> <div className='rb:mt-2 rb:text-[12px] rb:leading-4.5 rb:text-white rb:max-w-139.75'>
{ t('index.spaceSubTitle' )} {t('index.spaceSubTitle')}
</div> </div>
</div> </div>
{/* 统计卡片 */} {/* 统计卡片 */}
<TopCardList data={dashboardData} /> <TopCardList data={dashboardData} />
<div className="rb:rounded rb:max-h-[calc(100%-100px)] rb:overflow-y-auto rb:mt-4"> <div className="rb:rounded-xl rb:bg-white rb:pt-3 rb:px-3 rb:overflow-y-hidden rb:my-3 rb:flex-1">
<Table <Table
ref={tableRef} ref={tableRef}
apiUrl={tableApi} apiUrl={tableApi}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
bordered={false}
scrollY="100%"
className="rb:-mb-3!"
// scroll={{ y: 'calc(100vh - 340px)' }}
/> />
</div> </div>
</div> </Flex>
<div className='rb:flex-0 rb:min-w-80'> </Col>
{/* 引导 */} <Col flex="328px">
<GuideCard /> {/* 引导 */}
<div className='rb:w-full rb:mt-4 '> <GuideCard />
<VersionCard /> <VersionCard />
</div> <QuickActions onNavigate={navigate} />
{/* 快捷操作 */} </Col>
<div className='rb:w-full rb:mt-4'> </Row>
<QuickActions onNavigate={navigate} />
</div>
</div>
</div>
</div>
); );
} }

View File

@@ -1,8 +1,8 @@
/* /*
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:50:00 * @Date: 2026-02-03 16:50:00
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:50:00 * @Last Modified time: 2026-03-20 18:50:41
*/ */
/** /**
* Group Model View * Group Model View
@@ -12,14 +12,15 @@
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import clsx from 'clsx' import clsx from 'clsx'
import { Button } from 'antd' import { Button, Flex, Tooltip, Space } from 'antd'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { ProviderModelItem, ModelListItem, DescriptionItem, BaseRef } from './types' import type { ProviderModelItem, ModelListItem, DescriptionItem, BaseRef } from './types'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard'
import { getModelNewList } from '@/api/models' import { getModelNewList } from '@/api/models'
import PageEmpty from '@/components/Empty/PageEmpty'; import PageEmpty from '@/components/Empty/PageEmpty';
import { formatDateTime } from '@/utils/format'; import { formatDateTime } from '@/utils/format';
import Tag from '@/components/Tag'
/** /**
* Group model list component * Group model list component
@@ -50,11 +51,6 @@ const Group = forwardRef <BaseRef,{ query: any; handleEdit: (data: ModelListItem
label: t(`modelNew.type`), label: t(`modelNew.type`),
children: data.type ? t(`modelNew.${data.type}`) : '-', children: data.type ? t(`modelNew.${data.type}`) : '-',
}, },
{
key: 'is_active',
label: t(`modelNew.status`),
children: data.is_active ? t(`common.statusEnabled`) : t(`common.statusDisabled`),
},
{ {
key: 'created_at', key: 'created_at',
label: t(`modelNew.created_at`), label: t(`modelNew.created_at`),
@@ -73,31 +69,36 @@ const Group = forwardRef <BaseRef,{ query: any; handleEdit: (data: ModelListItem
{list.length === 0 {list.length === 0
? <PageEmpty /> ? <PageEmpty />
:( :(
<div className="rb:grid rb:grid-cols-4 rb:gap-4"> <div className="rb:grid rb:grid-cols-4 rb:gap-3">
{list.map(item => ( {list.map(item => (
<RbCard <RbCard
key={item.id} key={item.id}
title={item.name}
avatarUrl={item.logo} avatarUrl={item.logo}
avatar={ avatarText={item.name[0]}
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]"> title={<Flex vertical gap={6}>
{item.name[0]} <Tooltip title={item.name}>
</div> <div className="rb:wrap-break-word rb:line-clamp-1">{item.name}</div>
} </Tooltip>
<Space>
<Tag color={item.is_active ? 'success' : 'error'}>{item.is_active ? t(`common.statusEnabled`) : t(`common.statusDisabled`)}</Tag>
</Space>
</Flex>}
isNeedTooltip={false}
footer={<Button className="rb:h-9!" type="primary" ghost block onClick={() => handleEdit(item)}>{t('modelNew.configureBtn')}</Button>}
> >
{formatData(item)?.map((description: DescriptionItem) => ( <Flex vertical gap={8}>
<div {formatData(item)?.map((description: DescriptionItem) => (
key={description.key} <div
className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3" key={description.key}
> className="rb:flex rb:justify-between rb:text-[14px] rb:leading-5"
<span className="rb:whitespace-nowrap">{(description.label as string)}</span> >
<span className={clsx({ <span className="rb:whitespace-nowrap rb:text-[#5B6167]">{(description.label as string)}</span>
"rb:text-[#212332]": description.key !== 'is_active', <span className={clsx({
"rb:text-[#369F21] rb:font-medium": description.key === 'is_active' && item.is_active, "rb:font-medium": description.key === 'type',
})}>{(description.children as string)}</span> })}>{(description.children as string)}</span>
</div> </div>
))} ))}
<Button className="rb:mt-2" type="primary" ghost block onClick={() => handleEdit(item)}>{t('modelNew.configureBtn')}</Button> </Flex>
</RbCard> </RbCard>
))} ))}
</div> </div>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:50:10 * @Date: 2026-02-03 16:50:10
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-27 10:20:51 * @Last Modified time: 2026-03-20 18:51:27
*/ */
/** /**
* Model List View * Model List View
@@ -11,11 +11,11 @@
*/ */
import { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { Button, Flex, Row, Col } from 'antd' import { Button, Flex, Row, Col, Tooltip, Space } from 'antd'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { ProviderModelItem, KeyConfigModalRef, ModelListDetailRef, ModelListItem, BaseRef } from './types' import type { ProviderModelItem, KeyConfigModalRef, ModelListDetailRef, ModelListItem, BaseRef } from './types'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard'
import { getModelNewList } from '@/api/models' import { getModelNewList } from '@/api/models'
import PageEmpty from '@/components/Empty/PageEmpty'; import PageEmpty from '@/components/Empty/PageEmpty';
import Tag from '@/components/Tag'; import Tag from '@/components/Tag';
@@ -69,26 +69,26 @@ const ModelList = forwardRef<BaseRef, { query: any; handleEdit: (vo?: ModelListI
{list.map(item => ( {list.map(item => (
<RbCard <RbCard
key={item.provider} key={item.provider}
title={t(`modelNew.${item.provider}`)}
avatarUrl={getListLogoUrl(item.provider, item.logo)} avatarUrl={getListLogoUrl(item.provider, item.logo)}
avatar={ avatarText={item.provider[0].toUpperCase()}
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]"> title={<Flex vertical gap={6}>
{item.provider[0].toUpperCase()} <Tooltip title={t(`modelNew.${item.provider}`)}>
</div> <div className="rb:wrap-break-word rb:line-clamp-1">{t(`modelNew.${item.provider}`)}</div>
} </Tooltip>
bodyClassName="rb:relative rb:pb-[64px]! rb:h-[calc(100%-64px)]!" <Space>
<Flex gap={8} wrap>{item.tags.map(tag => <Tag key={tag}>{t(`modelNew.${tag}`)}</Tag>)}</Flex>
</Space>
</Flex>}
isNeedTooltip={false}
footer={<Row gutter={9} className="rb:pt-2!">
<Col span={12}>
<Button className="rb:h-9!" block onClick={() => handleShowModel(item)}>{t('modelNew.showModel')}</Button>
</Col>
<Col span={12}>
<Button className="rb:h-9!" type="primary" ghost block onClick={() => handleKeyConfig(item)}>{t('modelNew.keyConfig')}</Button>
</Col>
</Row>}
> >
<Flex gap={8} wrap>{item.tags.map(tag => <Tag key={tag}>{t(`modelNew.${tag}`)}</Tag>)}</Flex>
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
<Row gutter={12}>
<Col span={12}>
<Button block onClick={() => handleShowModel(item)}>{t('modelNew.showModel')}</Button>
</Col>
<Col span={12}>
<Button type="primary" ghost block onClick={() => handleKeyConfig(item)}>{t('modelNew.keyConfig')}</Button>
</Col>
</Row>
</div>
</RbCard> </RbCard>
))} ))}
</div> </div>

View File

@@ -1,8 +1,8 @@
/* /*
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:50:14 * @Date: 2026-02-03 16:50:14
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:50:14 * @Last Modified time: 2026-03-23 11:33:44
*/ */
/** /**
* Model Square View * Model Square View
@@ -10,17 +10,17 @@
* Allows adding models and viewing details * Allows adding models and viewing details
*/ */
import { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { Button, Space, App, Divider, Flex, Tooltip } from 'antd' import { Button, Space, App, Flex, Tooltip } from 'antd'
import { UsergroupAddOutlined } from '@ant-design/icons'; import { UsergroupAddOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import clsx from 'clsx';
import type { ModelPlaza, ModelPlazaItem, ModelSquareDetailRef, BaseRef } from './types' import type { ModelPlaza, ModelPlazaItem, BaseRef } from './types'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard'
import { getModelPlaza, addModelPlaza } from '@/api/models' import { getModelPlaza, addModelPlaza } from '@/api/models'
import PageEmpty from '@/components/Empty/PageEmpty'; import PageEmpty from '@/components/Empty/PageEmpty';
import Tag from '@/components/Tag'; import Tag from '@/components/Tag';
import ModelSquareDetail from './components/ModelSquareDetail'
import { getLogoUrl } from './utils' import { getLogoUrl } from './utils'
/** /**
@@ -29,7 +29,6 @@ import { getLogoUrl } from './utils'
const ModelSquare = forwardRef <BaseRef, { query: any; }>(({ query }, ref) => { const ModelSquare = forwardRef <BaseRef, { query: any; }>(({ query }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { message } = App.useApp() const { message } = App.useApp()
const modelSquareDetailRef = useRef<ModelSquareDetailRef>(null)
const [list, setList] = useState<ModelPlaza[]>([]) const [list, setList] = useState<ModelPlaza[]>([])
useEffect(() => { useEffect(() => {
getList() getList()
@@ -38,14 +37,12 @@ const ModelSquare = forwardRef <BaseRef, { query: any; }>(({ query }, ref) => {
const getList = () => { const getList = () => {
getModelPlaza(query) getModelPlaza(query)
.then(res => { .then(res => {
setList((res as ModelPlaza[]) || []) const response = res as ModelPlaza[]
setList(response || [])
setActiveProvider(response[0]?.provider || null)
}) })
} }
/** Open model detail drawer */
const handleMore = (vo: ModelPlaza) => {
modelSquareDetailRef.current?.handleOpen(vo)
}
/** Add model to workspace */ /** Add model to workspace */
const handleAdd = (item: ModelPlazaItem) => { const handleAdd = (item: ModelPlazaItem) => {
addModelPlaza(item.id) addModelPlaza(item.id)
@@ -59,61 +56,86 @@ const ModelSquare = forwardRef <BaseRef, { query: any; }>(({ query }, ref) => {
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getList, getList,
})); }));
const [activeProvider, setActiveProvider] = useState<string | null>(null)
return ( return (
<> <>
{list.length === 0 {list.length === 0
? <PageEmpty /> ? <PageEmpty />
: list.map(vo => ( : <>
<div key={vo.provider}> <Space size={8} className="rb:mb-3!">
<div className="rb:flex rb:justify-between rb:items-center rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md"> {list.map(vo => (
<div className="rb:font-medium">{t(`modelNew.${vo.provider}`)}</div> <div
<Button type="link" onClick={() => handleMore(vo)}>{t('modelNew.viewAll')}({t(`modelNew.modelCount`, { count: vo.models.length })})&gt;</Button> key={vo.provider}
</div> className={clsx('rb:border rb:border-[#171719] rb:rounded-full rb:px-2 rb:py-1 rb:cursor-pointer', {
'rb:text-white rb:bg-[#171719]': activeProvider === vo.provider,
<div className="rb:grid rb:grid-cols-3 rb:gap-4"> 'rb:text-[#171719]': activeProvider === vo.provider,
{vo.models.slice(0, 6).map(item => ( })}
<RbCard onClick={() => setActiveProvider(vo.provider)}
key={item.id} >{t(`modelNew.${vo.provider}`)}</div>
title={item.name} ))}
subTitle={<Space size={8}> </Space>
<Tag className="rb:mt-1">{t(`modelNew.${item.type}`)}</Tag> {list.filter(vo => vo.provider === activeProvider).map(vo => (
{item.is_official && <Tag color="success" className="rb:mt-1">{t(`modelNew.official`)}</Tag>} <div key={vo.provider} className="rb:max-h-[calc(100%-50px)] rb:overflow-y-auto">
</Space>} <div className="rb:grid rb:grid-cols-3 rb:gap-4">
avatarUrl={getLogoUrl(item.logo)} {vo.models.map(item => (
avatar={ <RbCard
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]"> key={item.id}
{item.name[0]} avatarUrl={getLogoUrl(item.logo)}
</div> avatarText={item.name[0]}
} title={
bodyClassName="rb:relative rb:pb-[80px]! rb:h-[calc(100%-64px)]!" <Flex justify="space-between" gap={16}>
> <Flex vertical gap={6}>
<Tooltip title={item.description}> <Tooltip title={item.name}>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:font-regular rb:wrap-break-word rb:line-clamp-2 rb:mt-3">{item.description}</div> <div className="rb:wrap-break-word rb:line-clamp-1">{item.name}</div>
</Tooltip> </Tooltip>
<Flex gap={8} wrap className="rb:mt-3!">{item.tags.map((tag, tagIndex) => <Tag key={tagIndex}>{tag}</Tag>)}</Flex> <Space size={8} className="rb:mt-1!">
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6"> <Tag>{t(`modelNew.${item.type}`)}</Tag>
<Divider size="middle" /> {item.is_official && <Tag color="success">{t(`modelNew.official`)}</Tag>}
<Flex justify="space-between"> </Space>
<Space size={8}><UsergroupAddOutlined /> {item.add_count}</Space> </Flex>
<Space> <Button
{item.is_added size="small"
? <Button type="primary" disabled>{t('modelNew.added')}</Button> disabled={item.is_added || item.is_deprecated}
: <Button type="primary" ghost disabled={item.is_deprecated} onClick={() => handleAdd(item)}>{item.is_deprecated ? t('modelNew.deprecated') : `+ ${t('common.add')}`}</Button> onClick={() => handleAdd(item)}
} >{item.is_deprecated ? t('modelNew.deprecated') : '+'}</Button>
</Space>
</Flex> </Flex>
</div> }
</RbCard> isNeedTooltip={false}
))} footer={<Flex justify="space-between" align="center" className="rb:text-[#5B6167] rb:text-[12px]">
</div> @{t(`modelNew.${vo.provider}`)}
</div> <Space size={4}><UsergroupAddOutlined /> {item.add_count}</Space>
)) </Flex>}
} >
<Tooltip title={item.description}>
<div className="rb:h-10 rb:leading-5 rb:wrap-break-word rb:line-clamp-2">{item.description}</div>
</Tooltip>
<ModelSquareDetail <Flex gap={8} wrap align="center" className="rb:mt-2!">
ref={modelSquareDetailRef} <Flex gap={6}>
refresh={getList} {item.tags?.slice(0, 2).map((type, i) => (
/> <div key={i} className="rb:bg-[#F6F6F6] rb:rounded-md rb:py-px rb:px-1 rb:text-[12px] rb:leading-4.5">{type}</div>
))}
</Flex>
{item.tags.length > 2 && (
<Tooltip
title={<Flex wrap gap={6}>{item.tags?.slice(2, item.tags.length).map((type, i) => (
<div key={i} className="rb:bg-[#F6F6F6] rb:rounded-md rb:py-px rb:px-1 rb:text-[12px] rb:leading-4.5 rb:text-[#171719]">{type}</div>
))}</Flex>}
color="white"
placement="bottom"
>
<div className="rb:bg-[#F6F6F6] rb:rounded-md rb:py-px rb:px-1 rb:text-[12px] rb:leading-4.5">+{item.tags.length - 2}</div>
</Tooltip>
)}
</Flex>
</RbCard>
))}
</div>
</div>
))}
</>
}
</> </>
) )
}) })

View File

@@ -1,130 +0,0 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:49:49
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-04 11:50:31
*/
/**
* Model Square Detail Drawer
* Displays all models from a specific provider in the model square
* Allows adding models and editing custom models
*/
import { useState, useImperativeHandle, forwardRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Space, App, Flex, Tooltip, Divider } from 'antd'
import { UsergroupAddOutlined } from '@ant-design/icons';
import type { ModelPlaza, ModelPlazaItem, ModelSquareDetailRef } from '../types';
import RbDrawer from '@/components/RbDrawer';
import { getModelPlaza, addModelPlaza } from '@/api/models'
import RbCard from '@/components/RbCard/Card'
import Tag from '@/components/Tag';
import PageEmpty from '@/components/Empty/PageEmpty';
import { getLogoUrl } from '../utils'
/**
* Component props
*/
interface ModelSquareDetailProps {
/** Callback to refresh parent list */
refresh: () => void;
}
/**
* Model square detail drawer component
*/
const ModelSquareDetail = forwardRef<ModelSquareDetailRef, ModelSquareDetailProps>(({ refresh }, ref) => {
const { t } = useTranslation();
const { message } = App.useApp()
const [model, setModel] = useState<ModelPlaza>({} as ModelPlaza)
const [open, setOpen] = useState(false);
const [list, setList] = useState<ModelPlazaItem[]>([])
/** Open drawer with model plaza data */
const handleOpen = (vo: ModelPlaza) => {
setModel(vo)
setOpen(true)
getList(vo)
}
/** Close drawer */
const handleClose = () => {
setOpen(false)
refresh()
}
/** Fetch model list for provider */
const getList = (vo: ModelPlaza) => {
getModelPlaza({ provider: vo.provider })
.then(res => {
const response = res as ModelPlaza[]
setList(response.length > 0 ? response[0].models : [])
})
}
/** Add model to workspace */
const handleAdd = (item: ModelPlazaItem) => {
addModelPlaza(item.id)
.then(() => {
message.success(`${item.name}${t('modelNew.addSuccess')}`)
getList(model)
})
}
/** Expose methods to parent component */
useImperativeHandle(ref, () => ({
handleOpen,
}));
return (
<RbDrawer
title={<>{t(`modelNew.${model.provider}`)} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})</>}
open={open}
onClose={handleClose}
>
<div className="rb:h-full rb:overflow-y-auto">
{list.length === 0
? <PageEmpty />
: <div className="rb:grid rb:grid-cols-2 rb:gap-4">
{list.map(item => (
<RbCard
key={item.id}
title={item.name}
subTitle={<Space size={8} className="rb:mt-1!">
<Tag>{t(`modelNew.${item.type}`)}</Tag>
{item.is_official && <Tag color="success">{t(`modelNew.official`)}</Tag>}
{item.capability?.filter(item => item !== 'video').map(vo => <Tag key={vo}>{t(`modelNew.${vo}`)}</Tag>)}
</Space>}
avatarUrl={getLogoUrl(item.logo)}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]}
</div>
}
bodyClassName="rb:relative rb:pb-[80px]! rb:h-[calc(100%-64px)]!"
>
<Tooltip title={item.description}>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:font-regular rb:wrap-break-word rb:line-clamp-2 rb:mt-3">{item.description}</div>
</Tooltip>
<Flex gap={8} wrap className="rb:mt-3!">{item.tags.map((tag, tagIndex) => <Tag key={tagIndex}>{tag}</Tag>)}</Flex>
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
<Divider size="middle" />
<Flex justify="space-between">
<Space size={8}><UsergroupAddOutlined /> {item.add_count}</Space>
<Space>
{item.is_added
? <Button type="primary" disabled>{t('modelNew.added')}</Button>
: <Button type="primary" ghost disabled={item.is_deprecated} onClick={() => handleAdd(item)}>{item.is_deprecated ? t('modelNew.deprecated') : `+ ${t('common.add')}`}</Button>
}
</Space>
</Flex>
</div>
</RbCard>
))}
</div>
}
</div>
</RbDrawer>
);
});
export default ModelSquareDetail;

View File

@@ -1,8 +1,8 @@
/* /*
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:50:05 * @Date: 2026-02-03 16:50:05
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:50:05 * @Last Modified time: 2026-03-20 19:02:31
*/ */
/** /**
* Model Management Main Page * Model Management Main Page
@@ -84,7 +84,7 @@ const tabKeys = ['group', 'list', 'square']
} }
return ( return (
<> <Flex vertical gap={16}>
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<PageTabs <PageTabs
value={activeTab} value={activeTab}
@@ -100,19 +100,19 @@ const tabKeys = ['group', 'list', 'square']
url={modelTypeUrl} url={modelTypeUrl}
hasAll={false} hasAll={false}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))} format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
className="rb:w-30" className="rb:w-40"
allowClear={true} allowClear={true}
placeholder={t('modelNew.type')} placeholder={t('modelNew.type')}
/> />
</Form.Item> </Form.Item>
} }
{(activeTab === 'list' || activeTab === 'square') && {activeTab === 'list' &&
<Form.Item name="provider" noStyle> <Form.Item name="provider" noStyle>
<CustomSelect <CustomSelect
url={modelProviderUrl} url={modelProviderUrl}
hasAll={false} hasAll={false}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))} format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
className="rb:w-30" className="rb:w-40"
allowClear={true} allowClear={true}
placeholder={t('modelNew.provider')} placeholder={t('modelNew.provider')}
/> />
@@ -123,7 +123,6 @@ const tabKeys = ['group', 'list', 'square']
<SearchInput <SearchInput
maxLength={50} maxLength={50}
placeholder={t(`modelNew.${activeTab}SearchPlaceholder`)} placeholder={t(`modelNew.${activeTab}SearchPlaceholder`)}
className="rb:w-70!"
/> />
</Form.Item> </Form.Item>
} }
@@ -133,7 +132,7 @@ const tabKeys = ['group', 'list', 'square']
</Form> </Form>
</Flex> </Flex>
<div className="rb:w-full rb:h-[calc(100%-48px)] rb:my-4"> <div className="rb:w-full rb:h-[calc(100vh-125px)] rb:overflow-y-auto">
{activeTab === 'group' && <GroupModel ref={groupRef} query={query} handleEdit={handleEdit} />} {activeTab === 'group' && <GroupModel ref={groupRef} query={query} handleEdit={handleEdit} />}
{activeTab === 'list' && <ModelList ref={modelListRef} query={query} handleEdit={handleEdit} />} {activeTab === 'list' && <ModelList ref={modelListRef} query={query} handleEdit={handleEdit} />}
{activeTab === 'square' && <ModelSquare query={query} />} {activeTab === 'square' && <ModelSquare query={query} />}
@@ -146,7 +145,7 @@ const tabKeys = ['group', 'list', 'square']
ref={customModelModalRef} ref={customModelModalRef}
refresh={handleRefresh} refresh={handleRefresh}
/> />
</> </Flex>
) )
} }

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:50:18 * @Date: 2026-02-03 16:50:18
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-07 16:14:25 * @Last Modified time: 2026-03-20 20:21:45
*/ */
/** /**
* Type definitions for Model Management * Type definitions for Model Management
@@ -270,14 +270,6 @@ export interface ModelPlazaItem {
is_omni?: boolean; is_omni?: boolean;
} }
/**
* Model square detail ref interface
*/
export interface ModelSquareDetailRef {
/** Open detail drawer with model plaza data */
handleOpen: (vo: ModelPlaza) => void;
}
/** /**
* Custom model form data * Custom model form data
*/ */

View File

@@ -2,23 +2,25 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 14:10:15 * @Date: 2026-02-03 14:10:15
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-06 10:56:44 * @Last Modified time: 2026-03-20 16:36:02
*/ */
import { type FC, useState, useRef, type MouseEvent } from 'react'; import { type FC, useState, useRef } from 'react';
import type { MenuInfo } from 'rc-menu/lib/interface';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Row, Col, Button, Flex, Divider, Space, App, Tooltip } from 'antd' import { Row, Col, Flex, Space, App, Tooltip, Dropdown } from 'antd'
import SearchInput from '@/components/SearchInput'; import SearchInput from '@/components/SearchInput';
import OntologyModal from './components/OntologyModal' import OntologyModal from './components/OntologyModal'
import type { OntologyModalRef, OntologyItem, Query, OntologyImportModalRef, OntologyExportModalRef } from './types' import type { OntologyModalRef, OntologyItem, Query, OntologyImportModalRef, OntologyExportModalRef } from './types'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard'
import Tag from '@/components/Tag' import Tag from '@/components/Tag'
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList' import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
import { getOntologyScenesUrl, deleteOntologyScene } from '@/api/ontology' import { getOntologyScenesUrl, deleteOntologyScene } from '@/api/ontology'
import { formatDateTime } from '@/utils/format' import { formatDateTime } from '@/utils/format'
import OntologyImportModal from './components/OntologyImportModal' import OntologyImportModal from './components/OntologyImportModal'
import OntologyExportModal from './components/OntologyExportModal' import OntologyExportModal from './components/OntologyExportModal'
import RbButton from '@/components/RbButton'
/** /**
* Ontology management page component * Ontology management page component
@@ -51,20 +53,18 @@ const Ontology: FC = () => {
* @param record - The ontology item to edit * @param record - The ontology item to edit
* @param e - Mouse event to prevent propagation * @param e - Mouse event to prevent propagation
*/ */
const handleEdit = (record: OntologyItem, e: MouseEvent) => { const handleEdit = (record: OntologyItem, e: MenuInfo) => {
e.preventDefault(); e.domEvent.stopPropagation();
e.stopPropagation();
entityModalRef.current?.handleOpen(record) entityModalRef.current?.handleOpen(record)
} }
/** /**
* Delete an ontology scene with confirmation * Delete an ontology scene with confirmation
* @param item - The ontology item to delete * @param item - The ontology item to delete
* @param e - Mouse event to prevent propagation * @param e - Menu click info
*/ */
const handleDelete = (item: OntologyItem, e: MouseEvent) => { const handleDelete = (item: OntologyItem, e: MenuInfo) => {
e.preventDefault(); e.domEvent.stopPropagation();
e.stopPropagation();
modal.confirm({ modal.confirm({
title: t('common.confirmDeleteDesc', { name: item.scene_name }), title: t('common.confirmDeleteDesc', { name: item.scene_name }),
okText: t('common.delete'), okText: t('common.delete'),
@@ -111,28 +111,23 @@ const Ontology: FC = () => {
return ( return (
<> <>
<Row gutter={16} className="rb:mb-4"> <Flex align="center" justify="space-between" className="rb:mb-4!">
<Col span={8}> <SearchInput
<SearchInput placeholder={t('ontology.searchPlaceholder')}
placeholder={t('ontology.searchPlaceholder')} onSearch={(value) => setQuery({ scene_name: value })}
onSearch={(value) => setQuery({ scene_name: value })} />
className="rb:w-full!" <Space size={12}>
/> <RbButton ghost type="primary" onClick={handleExport}>
</Col> {t('ontology.export')}
<Col span={16} className="rb:text-right"> </RbButton>
<Space size={12}> <RbButton ghost type="primary" onClick={handleImport}>
<Button onClick={handleExport}> {t('ontology.import')}
{t('ontology.export')} </RbButton>
</Button> <RbButton type="primary" onClick={handleCreate}>
<Button onClick={handleImport}> + {t('ontology.create')}
{t('ontology.import')} </RbButton>
</Button> </Space>
<Button type="primary" onClick={handleCreate}> </Flex>
+ {t('ontology.create')}
</Button>
</Space>
</Col>
</Row>
<PageScrollList<OntologyItem, Query> <PageScrollList<OntologyItem, Query>
ref={scrollListRef} ref={scrollListRef}
@@ -141,58 +136,70 @@ const Ontology: FC = () => {
column={3} column={3}
renderItem={(item) =>( renderItem={(item) =>(
<RbCard <RbCard
title={item.scene_name} title={
extra={<Tag>{item.type_num} {t('ontology.typeCount')}</Tag>} <Flex justify="space-between">
onClick={() => handleJump(item)} <Flex gap={4} vertical>
className="rb:cursor-pointer rb:relative" {item.scene_name}
> <Space size={8}>
{item.is_system_default && <Tag>{item.type_num} {t('ontology.typeCount')}</Tag>
<div className="rb:absolute rb:-right-px rb:-top-px rb:bg-[#FF5D34] rb:rounded-[0px_7px_0px_8px] rb:text-[12px] rb:text-white rb:font-regular rb:leading-4 rb:py-0.5 rb:px-1"> {item.is_system_default && <Tag color="warning">{t('common.default')}</Tag>}
{t('common.default')} </Space>
</div> </Flex>
<Dropdown
menu={{
items: [
{
key: 'edit',
icon: <div className="rb:size-6 rb:bg-cover rb:cursor-pointer rb:bg-[url('@/assets/images/common/edit.svg')]" />,
label: t('common.edit'),
onClick: (e: MenuInfo) => handleEdit(item, e),
},
{
key: 'delete',
icon: <div className="rb:size-6 rb:bg-cover rb:cursor-pointer rb:bg-[url('@/assets/images/common/delete.svg')]" />,
label: t('common.delete'),
onClick: (e: MenuInfo) => handleDelete(item, e),
},
]
}}
placement="bottomRight"
>
<div className="rb:cursor-pointer rb:size-6 rb:bg-[url('@/assets/images/common/more.svg')] rb:hover:bg-[url('@/assets/images/common/more_hover.svg')]"></div>
</Dropdown>
</Flex>
} }
<div isNeedTooltip={false}
className="rb:flex rb:gap-2 rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3" headerClassName="rb:pb-0!"
> onClick={() => handleJump(item)}
<span className="rb:whitespace-nowrap">{t(`ontology.scene_description`)}</span> className="rb:cursor-pointer!"
<Tooltip title={item.scene_description} placement="topRight"> >
<span className="rb:font-medium rb:flex-1 rb:text-right rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{item.scene_description}</span> <Tooltip title={item.scene_description}>
</Tooltip> <div className="rb:h-10 rb:wrap-break-word rb:line-clamp-2 rb:leading-5">{item.scene_description}</div>
</div> </Tooltip>
{(['created_at', 'updated_at'] as const).map(key => (
<div <Flex gap={8} wrap align="center" className="rb:mt-2!">
key={key} <Flex gap={8} className="rb:flex-1 rb:overflow-hidden rb:wrap-break-word! rb:line-clamp-1!">
className="rb:flex rb:gap-2 rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
>
<span className="rb:whitespace-nowrap">{t(`ontology.${key}`)}</span>
<span className="rb:font-medium">{formatDateTime(item[key])}</span>
</div>
))}
<Divider size="middle" />
<Flex gap={8} wrap align="center">
<div className="rb:text-[#5B6167] rb:leading-4.5">{t('ontology.entityTypes')}: </div>
<div className="rb:flex-1 rb:overflow-hidden rb:wrap-break-word! rb:line-clamp-1!">
{item.entity_type?.map((type, i) => ( {item.entity_type?.map((type, i) => (
<Tag key={i} color={i % 2 ? 'processing' : 'success'} className="rb:ml-2">{type}</Tag> <span key={i} className="rb:bg-[#F6F6F6] rb:rounded-md rb:py-px rb:px-1 rb:text-[12px] rb:leading-4.5">{type}</span>
))} ))}
</div> </Flex>
{item.type_num > 3 && ( {item.type_num > 3 && (
<Tag color="default">+{item.type_num - 3}</Tag> <span className="rb:bg-[#F6F6F6] rb:rounded-full rb:py-px rb:px-1 rb:text-[12px] rb:leading-4.5">+{item.type_num - 3}</span>
)} )}
</Flex> </Flex>
<div className="rb:mt-4 rb:h-5 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167] rb:flex rb:items-center rb:justify-end"> <Row className="rb:mt-4!">
{!item.is_system_default && <Space size={16}> {(['created_at', 'updated_at'] as const).map(key => (
<div <Col
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]" key={key}
onClick={(e) => handleEdit(item, e)} span={12}
></div> className="rb:text-[#5B6167] rb:text-[12px]! rb:leading-4.5"
<div >
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]" <div>{t(`ontology.${key}`)}</div>
onClick={(e) => handleDelete(item, e)} <div>{formatDateTime(item[key])}</div>
></div> </Col>
</Space>} ))}
</div> </Row>
</RbCard> </RbCard>
)} )}
/> />

View File

@@ -2,17 +2,17 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 14:10:20 * @Date: 2026-02-03 14:10:20
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-06 11:26:49 * @Last Modified time: 2026-03-20 16:35:14
*/ */
import { type FC, useEffect, useState, useRef } from 'react' import { type FC, useEffect, useState, useRef } from 'react'
import { useParams } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { App, Row, Col, Tooltip, Space, Button } from 'antd' import { App, Row, Col, Tooltip, Space, Button, Flex } from 'antd'
import PageHeader from '../components/PageHeader' import PageHeader from '@/components/Layout/PageHeader'
import { getOntologyClassList, deleteOntologyClass } from '@/api/ontology' import { getOntologyClassList, deleteOntologyClass } from '@/api/ontology'
import type { OntologyClassData, OntologyClassModalRef, OntologyClassExtractModalRef, OntologyClassItem } from '@/views/Ontology/types' import type { OntologyClassData, OntologyClassModalRef, OntologyClassExtractModalRef, OntologyClassItem } from '@/views/Ontology/types'
import RbCard from '@/components/RbCard/Card'; import RbCard from '@/components/RbCard';
import OntologyClassModal from '../components/OntologyClassModal' import OntologyClassModal from '../components/OntologyClassModal'
import SearchInput from '@/components/SearchInput'; import SearchInput from '@/components/SearchInput';
import OntologyClassExtractModal from '../components/OntologyClassExtractModal' import OntologyClassExtractModal from '../components/OntologyClassExtractModal'
@@ -26,6 +26,7 @@ import Tag from '@/components/Tag'
const Detail: FC = () => { const Detail: FC = () => {
// Hooks // Hooks
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate()
const { id } = useParams() const { id } = useParams()
const { modal, message } = App.useApp() const { modal, message } = App.useApp()
@@ -100,19 +101,29 @@ const Detail: FC = () => {
return ( return (
<> <>
<PageHeader <PageHeader
name={<Space> title={<Space>
{data.scene_name} {data.scene_name}
{data.is_system_default ? <Tag color="warning">{t('common.default')}</Tag> : undefined} {data.is_system_default ? <Tag color="warning">{t('common.default')}</Tag> : undefined}
<Tooltip title={data.scene_description}>
<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/common/question.svg')]"></div>
</Tooltip>
</Space>}
extra={<Space size={12}>
{data.is_system_default ? undefined : (<Space>
<Button type="primary" ghost className="rb:h-6! rb:px-2! rb:leading-5.5!" onClick={handleAdd}>+ {t('ontology.addClass')}</Button>
<Button className="rb:h-6! rb:px-2! rb:leading-5.5!" type="primary" onClick={handleExtract}>+ {t('ontology.extract')}</Button>
</Space>)}
<Flex align="center" className="rb:leading-5 rb:text-[14px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={() => navigate(-1)}>
<div
className="rb:mr-2 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/logout.svg')]"
></div>
{t('common.return')}
</Flex>
</Space>} </Space>}
subTitle={<Tooltip title={data.scene_description}><div className="rb:h-4 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{data.scene_description}</div></Tooltip>}
extra={data.is_system_default ? undefined : (<Space>
<Button type="primary" ghost className="rb:h-6! rb:px-2! rb:leading-5.5!" onClick={handleAdd}>+ {t('ontology.addClass')}</Button>
<Button className="rb:h-6! rb:px-2! rb:leading-5.5!" type="primary" onClick={handleExtract}>+ {t('ontology.extract')}</Button>
</Space>)}
/> />
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4"> <div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:pb-3 rb:px-3">
<Row gutter={16} className="rb:mb-4"> <Row gutter={12} className="rb:mb-4">
<Col span={6} offset={18}> <Col span={6} offset={18}>
<SearchInput <SearchInput
placeholder={t('ontology.classSearchPlaceholder')} placeholder={t('ontology.classSearchPlaceholder')}
@@ -128,13 +139,12 @@ const Detail: FC = () => {
<RbCard <RbCard
title={item.class_name} title={item.class_name}
extra={data.is_system_default ? undefined : (<div extra={data.is_system_default ? undefined : (<div
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]" className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/common/delete.svg')] rb:hover:bg-[url('@/assets/images/common/delete_hover.svg')]"
onClick={() => handleDelete(item)} onClick={() => handleDelete(item)}
></div>)} ></div>)}
className="rb:bg-transparent!"
> >
<Tooltip title={item.class_description}> <Tooltip title={item.class_description}>
<div className="rb:h-8.5 rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-2">{item.class_description}</div> <div className="rb:h-10 rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:wrap-break-word rb:line-clamp-2">{item.class_description}</div>
</Tooltip> </Tooltip>
</RbCard> </RbCard>
</Col> </Col>

View File

@@ -1,8 +1,8 @@
/* /*
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-05 10:43:49 * @Date: 2026-02-05 10:43:49
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-05 10:43:49 * @Last Modified time: 2026-03-20 20:28:44
*/ */
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { Button, Tooltip } from 'antd'; import { Button, Tooltip } from 'antd';
@@ -10,9 +10,10 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import type { Skill } from './types' import type { Skill } from './types'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard'
import { getSkillListUrl } from '@/api/skill' import { getSkillListUrl } from '@/api/skill'
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList' import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
import { formatDateTime } from '@/utils/format'
/** /**
* Skills List Page Component * Skills List Page Component
@@ -66,14 +67,15 @@ const Skills: React.FC = () => {
return ( return (
<RbCard <RbCard
title={item.name} title={item.name}
avatar={<div className="rb:w-12 rb:h-12 rb:text-center rb:font-semibold rb:text-[28px] rb:leading-12 rb:rounded-lg rb:text-[#FBFDFF] rb:bg-[#155EEF] rb:mr-2">{item.name[0]}</div>}
className="rb:cursor-pointer" className="rb:cursor-pointer"
titleClassName="rb:line-clamp-1!"
onClick={() => handleView(item)} onClick={() => handleView(item)}
> >
{/* Skill description with tooltip */} {/* Skill description with tooltip */}
<Tooltip title={item.description}> <Tooltip title={item.description}>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1">{item.description}</div> <div className="rb:h-10 rb:leading-5 rb:wrap-break-word rb:line-clamp-2">{item.description}</div>
</Tooltip> </Tooltip>
<div className="rb:text-[#5B6167] rb:leading-4.5 rb:text-[12px] rb:mt-4">{t('common.updated_at')}: {formatDateTime(item.updated_at)}</div>
</RbCard> </RbCard>
); );
}} }}

View File

@@ -1,8 +1,8 @@
/* /*
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 17:48:59 * @Date: 2026-02-03 17:48:59
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:48:59 * @Last Modified time: 2026-03-20 18:49:51
*/ */
/** /**
* Space Management Page * Space Management Page
@@ -11,13 +11,12 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { List, Button } from 'antd'; import { List, Button, Flex, Space as AntSpace, Tooltip } from 'antd';
import type { Space, SpaceModalRef } from './types'; import type { Space, SpaceModalRef } from './types';
import SpaceModal from './components/SpaceModal'; import SpaceModal from './components/SpaceModal';
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard'
import { getWorkspaces, switchWorkspace } from '@/api/workspaces' import { getWorkspaces, switchWorkspace } from '@/api/workspaces'
import BodyWrapper from '@/components/Empty/BodyWrapper' import BodyWrapper from '@/components/Empty/BodyWrapper'
import Tag from '@/components/Tag' import Tag from '@/components/Tag'
@@ -76,20 +75,21 @@ const SpaceManagement: React.FC = () => {
<List.Item key={item.id}> <List.Item key={item.id}>
<RbCard <RbCard
avatarUrl={item.icon} avatarUrl={item.icon}
avatar={<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]"> avatarText={item.name[0]}
{item.name[0]} title={<Flex vertical gap={6}>
</div>} <Tooltip title={item.name}>
title={item.name} <div className="rb:wrap-break-word rb:line-clamp-1">{item.name}</div>
subTitle={<Tag className="rb:mt-1 rb:font-regular!" color={item.storage_type === 'rag' ? 'processing' : 'warning'}>{t(`space.${item.storage_type || 'neo4j'}`)}</Tag>} </Tooltip>
> <AntSpace>
<div className={clsx("rb:absolute rb:-top-px rb:-right-px rb:p-[2px_9px] rb:text-[#FFFFFF] rb:leading-4 rb:text-[12px] rb:font-regular rb:rounded-[0px_12px_0px_12px]", { <Tag color={item.storage_type === 'rag' ? 'processing' : 'warning'}>{t(`space.${item.storage_type || 'neo4j'}`)}</Tag>
'rb:bg-[#369F21]': item.is_active, <Tag color={item.is_active ? 'success' : 'error'}>{item.is_active ? t('space.associated') : t('space.notAssociated')}</Tag>
'rb:bg-[#A8A9AA]': !item.is_active, </AntSpace>
})}>{item.is_active ? t('space.associated') : t('space.notAssociated')}</div> </Flex>}
isNeedTooltip={false}
<Button type="primary" ghost block className="rb:mt-10" onClick={() => handleJump(item.id)}> footer={<Button type="primary" ghost block className="rb:mt-2 rb:h-9!" onClick={() => handleJump(item.id)}>
{t('space.enterSpace')} {t('space.enterSpace')}
</Button> </Button>}
>
</RbCard> </RbCard>
</List.Item> </List.Item>
)} )}

View File

@@ -1,37 +1,40 @@
import React, { useState, useRef, useEffect, type ReactNode } from 'react'; import { useState, useRef, useEffect, forwardRef, useImperativeHandle, type ReactNode } from 'react';
import { import {
Button,
Row, Row,
Col, Col,
App, App,
List, List,
Space Space,
Flex,
Tooltip,
Dropdown,
} from 'antd'; } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import type { ToolItem, Query, CustomToolModalRef } from './types'; import type { ToolItem, CustomToolModalRef, CustomRef } from './types';
import CustomToolModal from './components/CustomToolModal'; import CustomToolModal from './components/CustomToolModal';
import SearchInput from '@/components/SearchInput'
import BodyWrapper from '@/components/Empty/BodyWrapper' import BodyWrapper from '@/components/Empty/BodyWrapper'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard'
import { getTools, deleteTool } from '@/api/tools' import { getTools, deleteTool } from '@/api/tools'
import { formatDateTime } from '@/utils/format'
const Custom: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ getStatusTag }) => { const Custom = forwardRef<CustomRef, { getStatusTag: (status: string) => ReactNode; keyword?: string | undefined }>(({ getStatusTag, keyword }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { message, modal } = App.useApp() const { message, modal } = App.useApp()
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState<ToolItem[]>([]); const [data, setData] = useState<ToolItem[]>([]);
const [query, setQuery] = useState<Query>({ name: undefined, tool_type: 'custom' });
const customToolModalRef = useRef<CustomToolModalRef>(null); const customToolModalRef = useRef<CustomToolModalRef>(null);
useEffect(() => { useEffect(() => {
getData() getData()
}, [query.name]) }, [keyword])
const getData = () => { const getData = () => {
setLoading(true) setLoading(true)
getTools(query) getTools({
tool_type: 'custom',
name: keyword
})
.then((res) => { .then((res) => {
setData(res as ToolItem[]) setData(res as ToolItem[])
}) })
@@ -39,15 +42,14 @@ const Custom: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ get
setLoading(false) setLoading(false)
}) })
} }
const handleSearch = (value?: string) => {
setQuery(prev => ({ ...prev, name: value }))
}
// 打开添加服务弹窗 // 打开添加服务弹窗
const handleEdit = (data?: ToolItem) => { const handleEdit = (data?: ToolItem) => {
customToolModalRef.current?.handleOpen(data); customToolModalRef.current?.handleOpen(data);
}; };
useImperativeHandle(ref, () => ({ handleEdit }));
// 删除服务 // 删除服务
const handleDeleteService = (item: ToolItem) => { const handleDeleteService = (item: ToolItem) => {
modal.confirm({ modal.confirm({
@@ -65,71 +67,80 @@ const Custom: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ get
}; };
return ( return (
<div> <>
<Row gutter={16} className='rb:mb-4 rb:w-full'>
<Col span={8}>
<SearchInput
placeholder={t('tool.customSearchPlaceholder')}
onSearch={handleSearch}
style={{width: '100%'}}
/>
</Col>
<Col span={16} className="rb:text-right">
<Button type="primary" onClick={() => {handleEdit()}}>{t('tool.addCustom')}</Button>
</Col>
</Row>
<BodyWrapper loading={loading} empty={data.length === 0}> <BodyWrapper loading={loading} empty={data.length === 0}>
<List <List
grid={{ gutter: 16, column: 2 }} grid={{ gutter: 16, column: 3 }}
dataSource={data} dataSource={data}
renderItem={(item) => ( renderItem={(item) => (
<List.Item key={item.id}> <List.Item key={item.id}>
<RbCard <RbCard
// avatar={
// <div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
// {item.name[0]}
// </div>
// }
title={ title={
<div> <Flex justify="space-between" gap={16}>
{item.name}<br/> <Space size={8} className="rb:flex-1!">
{/* <div className="rb:mt-1 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167]">xx个工具</div> */} <Tooltip title={item.name}>
</div> <div className="rb:wrap-break-word rb:line-clamp-1">{item.name}</div>
} </Tooltip>
extra={getStatusTag(item.status)} {getStatusTag(item.status)}
>
<div>
{['auth_type', 'tags', 'created_at'].map(key => (
<div
key={key}
className="rb:flex rb:gap-4 rb:justify-start rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-32">{t(`tool.${key}`)}</div>
<div className='rb:flex-1 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-inline rb:text-left rb:py-px rb:rounded rb:font-medium'>
{key === 'created_at' && item[key]
? dayjs(item[key]).format('YYYY-MM-DD HH:mm:ss')
: key === 'auth_type'
? t(`tool.${(item.config_data as any)?.[key]}`)
: key === 'tags'
? (item[key] as string[]).join('、')
: (item.config_data as any)?.[key] || '-'
}
</div>
</div>
))}
<div className="rb:mt-4 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167] rb:flex rb:items-center rb:justify-end">
<Space size={16}>
<div
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
onClick={() => handleEdit(item)}
></div>
<div
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
onClick={() => handleDeleteService(item)}
></div>
</Space> </Space>
</div> <Dropdown
</div> menu={{
items: [
{
key: 'edit',
icon: <div className="rb:size-4 rb:bg-cover rb:cursor-pointer rb:bg-[url('@/assets/images/common/edit.svg')]" />,
label: t('common.edit'),
onClick: () => handleEdit(item),
},
{
key: 'delete',
className: 'rb:text-[#FF5D34]!',
icon: <div className="rb:size-4 rb:bg-cover rb:cursor-pointer rb:bg-[url('@/assets/images/common/delete_red.svg')]" />,
label: t('common.delete'),
onClick: () => handleDeleteService(item),
},
]
}}
placement="bottomRight"
>
<div className="rb:cursor-pointer rb:size-6 rb:bg-[url('@/assets/images/common/more.svg')] rb:hover:bg-[url('@/assets/images/common/more_hover.svg')]"></div>
</Dropdown>
</Flex>
}
isNeedTooltip={false}
>
{item.tags?.length > 0
? <Flex gap={8} wrap align="center">
<Flex gap={6}>
{item.tags?.slice(0, 2).map((type, i) => (
<div key={i} className="rb:bg-[#F6F6F6] rb:rounded-md rb:py-px rb:px-1 rb:text-[12px] rb:leading-4.5">{type}</div>
))}
</Flex>
{item.tags.length > 2 && (
<Tooltip
title={<Flex wrap gap={6}>{item.tags?.slice(2, item.tags.length).map((type, i) => (
<div key={i} className="rb:bg-[#F6F6F6] rb:rounded-md rb:py-px rb:px-1 rb:text-[12px] rb:leading-4.5 rb:text-[#171719]">{type}</div>
))}</Flex>}
color="white"
placement="bottom"
>
<div className="rb:bg-[#F6F6F6] rb:rounded-md rb:py-px rb:px-1 rb:text-[12px] rb:leading-4.5">+{item.tags.length - 2}</div>
</Tooltip>
)}
</Flex>
: <div className="rb:text-[#A8A9AA] rb:leading-5">{t('tool.noTags')}</div>
}
<Row className="rb:bg-[#F6F6F6] rb:rounded-lg rb:py-2! rb:px-3! rb:leading-5 rb:mt-4!">
<Col span={12}>
<div className="rb:text-[#5B6167] rb:mb-1">{t('tool.auth_type')}</div>
{(item.config_data as any)?.auth_type}
</Col>
<Col span={12}>
<div className="rb:text-[#5B6167] rb:mb-1">{t('tool.created_at')}</div>
{formatDateTime(item.created_at)}
</Col>
</Row>
</RbCard> </RbCard>
</List.Item> </List.Item>
)} )}
@@ -142,8 +153,8 @@ const Custom: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ get
ref={customToolModalRef} ref={customToolModalRef}
refresh={getData} refresh={getData}
/> />
</div> </>
); );
}; });
export default Custom; export default Custom;

View File

@@ -1,30 +1,28 @@
import React, { useState, useRef, useEffect, type ReactNode } from 'react'; import React, { useState, useRef, useEffect, type ReactNode } from 'react';
import { import {
List,
Flex,
Space,
Tooltip,
Row, Row,
Col, Col,
Tag,
List,
Flex
} from 'antd'; } from 'antd';
import { EyeOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import dayjs, { type Dayjs } from 'dayjs' import dayjs, { type Dayjs } from 'dayjs'
import type { Query, ToolItem, TimeToolModalRef, JsonToolModalRef, InnerToolModalRef } from './types'; import type { ToolItem, TimeToolModalRef, JsonToolModalRef, InnerToolModalRef } from './types';
import SearchInput from '@/components/SearchInput'
import BodyWrapper from '@/components/Empty/BodyWrapper' import BodyWrapper from '@/components/Empty/BodyWrapper'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard'
import TimeToolModal from './components/TimeToolModal' import TimeToolModal from './components/TimeToolModal'
import JsonToolModal from './components/JsonToolModal' import JsonToolModal from './components/JsonToolModal'
import InnerToolModal from './components/InnerToolModal' import InnerToolModal from './components/InnerToolModal'
import { getTools } from '@/api/tools' import { getTools } from '@/api/tools'
import { InnerConfigData } from './constant' import { InnerConfigData } from './constant'
const Inner: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ getStatusTag }) => { const Inner: React.FC<{ getStatusTag: (status: string) => ReactNode; keyword?: string | undefined }> = ({ getStatusTag, keyword }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState<ToolItem[]>([]); const [data, setData] = useState<ToolItem[]>([]);
const [query, setQuery] = useState<Query>({ name: undefined, tool_type: 'builtin' });
const [curTime, setCurTime] = useState<Dayjs>(dayjs()) const [curTime, setCurTime] = useState<Dayjs>(dayjs())
const timeToolModalRef = useRef<TimeToolModalRef>(null) const timeToolModalRef = useRef<TimeToolModalRef>(null)
const jsonToolModalRef = useRef<JsonToolModalRef>(null) const jsonToolModalRef = useRef<JsonToolModalRef>(null)
@@ -38,11 +36,14 @@ const Inner: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ getS
return () => { return () => {
clearInterval(timer) clearInterval(timer)
} }
}, [query.name]) }, [keyword])
const getData = () => { const getData = () => {
setLoading(true) setLoading(true)
getTools(query) getTools({
tool_type: 'builtin',
name: keyword
})
.then((res) => { .then((res) => {
setData(res as ToolItem[]) setData(res as ToolItem[])
}) })
@@ -50,9 +51,6 @@ const Inner: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ getS
setLoading(false) setLoading(false)
}) })
} }
const handleSearch = (value?: string) => {
setQuery(prev => ({ ...prev, name: value }))
}
// 打开添加服务弹窗 // 打开添加服务弹窗
const handleEdit = (data: ToolItem) => { const handleEdit = (data: ToolItem) => {
@@ -71,78 +69,77 @@ const Inner: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ getS
return ( return (
<div> <div>
<Row gutter={16} className='rb:mb-4 rb:w-full'>
<Col span={8}>
<SearchInput
placeholder={t('tool.innerSearchPlaceholder')}
onSearch={handleSearch}
style={{width: '100%'}}
/>
</Col>
</Row>
<BodyWrapper loading={loading} empty={data.length === 0}> <BodyWrapper loading={loading} empty={data.length === 0}>
<List <List
grid={{ gutter: 16, column: 2 }} grid={{ gutter: 16, column: 3 }}
dataSource={data} dataSource={data}
renderItem={(item) => ( renderItem={(item) => (
<List.Item key={item.id} className='rb:h-full!'> <List.Item key={item.id} className='rb:h-full!'>
<RbCard <RbCard
// className={clsx({ title={
// 'rb:h-85.5!': item.config_data.tool_class === 'DateTimeTool' || item.config_data.tool_class === 'JsonTool' <Flex justify="space-between" gap={16}>
// })} <Space size={8}>
// avatar={ <Tooltip title={item.name}>
// <div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]"> <div className="rb:wrap-break-word rb:line-clamp-1">{item.name}</div>
// {item.name[0]} </Tooltip>
// </div> {getStatusTag(item.status)}
// } </Space>
title={item.name} <Flex align="center" justify="center" className="rb:size-5.5 rb:hover:bg-[#F6F6F6] rb:rounded-md">
extra={getStatusTag(item.status)} <div
bodyClassName='rb:h-[calc(100%-40px)]' className="rb:size-4 rb:bg-cover rb:cursor-pointer rb:bg-[url('@/assets/images/common/edit.svg')]"
> onClick={() => handleEdit(item)}
<div className="rb:h-full rb:flex rb:flex-col rb:justify-between"> />
<div className="rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167]">
{t(`tool.${item.config_data.tool_class}_features`)} <br />
<Flex gap={4} wrap className="rb:mt-2 rb:w-full">
{InnerConfigData[item.config_data.tool_class].features.map(vo => <Tag key={vo} color="default">{ t(`tool.${vo}`) }</Tag>) }
</Flex> </Flex>
</Flex>
}
isNeedTooltip={false}
>
<Tooltip title={t(`tool.${item.config_data.tool_class}_features`)}>
<div className="rb:h-10 rb:wrap-break-word rb:line-clamp-2 rb:leading-5">{t(`tool.${item.config_data.tool_class}_features`)}</div>
</Tooltip>
{item.config_data.tool_class === 'DateTimeTool' <Flex gap={8} wrap align="center" className="rb:mt-2! rb:mb-4!">
? <div className="rb:mt-3 rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md"> <Flex gap={6}>
{t('tool.currentTime')} {InnerConfigData[item.config_data.tool_class].features?.slice(0, 2).map((type, i) => (
<div className="rb:font-medium rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md rb:my-2"> <div key={i} className="rb:bg-[#F6F6F6] rb:rounded-md rb:py-px rb:px-1 rb:text-[12px] rb:leading-4.5">{type}</div>
{curTime.format('YYYY-MM-DD HH:mm:ss')} ))}
</div> </Flex>
{t('tool.timestamp')} {InnerConfigData[item.config_data.tool_class].features.length > 2 && (
<div className="rb:font-medium rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md rb:mt-2"> <Tooltip
{curTime.unix()} title={<Flex wrap gap={6}>{InnerConfigData[item.config_data.tool_class].features?.slice(2, InnerConfigData[item.config_data.tool_class].features.length).map((type, i) => (
</div> <div key={i} className="rb:bg-[#F6F6F6] rb:rounded-md rb:py-px rb:px-1 rb:text-[12px] rb:leading-4.5 rb:text-[#171719]">{type}</div>
</div> ))}</Flex>}
:item.config_data.tool_class === 'JsonTool' color="white"
? <div className="rb:mt-3 rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md"> placement="bottom"
{t('tool.jsonEg')} >
<div className="rb:font-medium rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md rb:my-2"> <div className="rb:bg-[#F6F6F6] rb:rounded-md rb:py-px rb:px-1 rb:text-[12px] rb:leading-4.5">+{InnerConfigData[item.config_data.tool_class].features.length - 2}</div>
{InnerConfigData[item.config_data.tool_class].eg} </Tooltip>
</div> )}
</div> </Flex>
: <div className="rb:mt-3 rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md">
{t('tool.configStatus')}
<div className="rb:font-medium rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md rb:my-2">
{t(`tool.${item.status}_desc`)}
</div>
</div>
}
</div>
<div className="rb:mt-4 rb:flex rb:items-center rb:justify-end"> <Row className="rb:bg-[#F6F6F6] rb:rounded-lg rb:py-2! rb:px-3! rb:leading-5">
{item.config_data.tool_class === 'DateTimeTool' || item.config_data.tool_class === 'JsonTool' ? {item.config_data.tool_class === 'DateTimeTool'
<EyeOutlined className="rb:text-5 rb:text-[#5B6167]! rb:hover:text-[#212332]!" onClick={() => handleEdit(item)} /> ? <>
: <div <Col span={12}>
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]" <div className="rb:text-[#5B6167] rb:mb-1">{t('tool.currentTime')}</div>
onClick={() => handleEdit(item)} {curTime.format('YYYY-MM-DD HH:mm:ss')}
></div> </Col>
} <Col span={12}>
</div> <div className="rb:text-[#5B6167] rb:mb-1">{t('tool.timestamp')}</div>
</div> {curTime.unix()}
</Col>
</>
: item.config_data.tool_class === 'JsonTool'
? <Col span={24}>
<div className="rb:text-[#5B6167] rb:mb-1">{t('tool.jsonEg')}</div>
{InnerConfigData[item.config_data.tool_class].eg}
</Col>
: <Col span={24}>
<div className="rb:text-[#5B6167] rb:mb-1">{t('configStatus')}</div>
{t(`tool.${item.status}_desc`)}
</Col>
}
</Row>
</RbCard> </RbCard>
</List.Item> </List.Item>
)} )}

View File

@@ -1,14 +1,20 @@
import React, { useState, useRef, useEffect, useCallback, type ReactNode } from 'react'; import React, { useState, useRef, useEffect, useCallback, type ReactNode } from 'react';
import { Input, Button, App, Card, Space, Skeleton, Tag } from 'antd'; import { Button, App, Space, Row, Col, Flex, Tooltip } from 'antd';
import { SearchOutlined, SettingOutlined, GlobalOutlined, SyncOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import InfiniteScroll from 'react-infinite-scroll-component'; import InfiniteScroll from 'react-infinite-scroll-component';
import clsx from 'clsx'
import MarketConfigModal, { type MarketConfigModalRef } from './components/MarketConfigModal'; import MarketConfigModal, { type MarketConfigModalRef } from './components/MarketConfigModal';
import McpServiceModal from './components/McpServiceModal'; import McpServiceModal from './components/McpServiceModal';
import type { McpServiceModalRef } from './types'; import type { McpServiceModalRef } from './types';
import pageEmptyIcon from '@/assets/images/empty/pageEmpty.png' import pageEmptyIcon from '@/assets/images/empty/pageEmpty.png'
import Empty from '@/components/Empty/index' import Empty from '@/components/Empty/index'
import { getMarketTools, getMarketConfig, getMarketMCPs, getMarketMCPDetail, getMarketMCPsActivated, getTools } from '@/api/tools'; import { getMarketTools, getMarketConfig, getMarketMCPs, getMarketMCPDetail, getMarketMCPsActivated, getTools } from '@/api/tools';
import SearchInput from '@/components/SearchInput';
import RbCard from '@/components/RbCard'
import Tag from '@/components/Tag'
import marketIcon from '@/assets/images/tool/market.png'
interface MarketSource { interface MarketSource {
id: string; id: string;
name: string; name: string;
@@ -97,6 +103,9 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
}); });
setCategories(Array.from(categoryMap.values())); setCategories(Array.from(categoryMap.values()));
if (response.items[0]?.id) {
handleSelectSource(response.items[0]?.id)
}
} }
} catch (error) { } catch (error) {
console.error('获取市场数据失败:', error); console.error('获取市场数据失败:', error);
@@ -223,6 +232,7 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
}; };
const handleSelectSource = async (sourceId: string) => { const handleSelectSource = async (sourceId: string) => {
if (sourceId === selectedSource) return
setSelectedSource(sourceId); setSelectedSource(sourceId);
setSearchKeyword(''); setSearchKeyword('');
setCurrentPage(1); setCurrentPage(1);
@@ -235,21 +245,6 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
await fetchMcpList(sourceId, 1); await fetchMcpList(sourceId, 1);
}; };
const handleRefresh = async (sourceId: string) => {
// 清除缓存,重新从第一页加载
setMcpCache(prev => {
const next = { ...prev };
delete next[sourceId];
return next;
});
setCurrentPage(1);
await fetchMcpList(sourceId, 1);
const source = marketSources.find(s => s.id === sourceId);
if (source) {
message.success(`${source.name} ${t('tool.marketRefreshSuccess')}`);
}
};
const handleOpenConfig = async (sourceId: string) => { const handleOpenConfig = async (sourceId: string) => {
const source = marketSources.find(s => s.id === sourceId); const source = marketSources.find(s => s.id === sourceId);
if (!source) return; if (!source) return;
@@ -329,13 +324,13 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
if (!selectedSource) { if (!selectedSource) {
return ( return (
<div className="rb:flex rb:flex-col rb:items-center rb:justify-center rb:h-full rb:text-center"> <div className="rb:flex rb:flex-col rb:items-center rb:justify-center rb:h-full rb:text-center">
<Empty <Empty
url={pageEmptyIcon} url={pageEmptyIcon}
title={t('tool.marketSelectTitle')} title={t('tool.marketSelectTitle')}
subTitle={t('tool.marketSelectDesc')} subTitle={t('tool.marketSelectDesc')}
size={200} size={200}
className="rb:h-full" className="rb:h-full"
/> />
</div> </div>
); );
@@ -348,230 +343,170 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
return ( return (
<> <>
<div className="rb:flex rb:justify-between rb:items-center rb:pb-0"> <Flex justify="space-between" align="center">
<div className="rb:flex rb:items-center rb:gap-4"> <Flex gap={12} align="center" className="rb:pl-1!">
<div className="rb:w-10 rb:h-10 rb:flex rb:items-center rb:justify-center rb:bg-gray-50 rb:rounded-xl rb:flex-shrink-0 rb:overflow-hidden"> <Flex align="center" justify="center" className="rb:size-12">
{source.logo_url ? ( {source.logo_url ? (
<img <img
src={source.logo_url} src={source.logo_url}
alt={source.name} alt={source.name}
className="rb:w-full rb:h-full rb:object-cover" className="rb:w-full rb:h-full rb:object-cover rb:rounded-xl"
referrerPolicy="no-referrer" referrerPolicy="no-referrer"
onError={(e) => { onError={(e) => {
e.currentTarget.style.display = 'none'; e.currentTarget.src = marketIcon
const parent = e.currentTarget.parentElement;
if (parent) {
parent.innerHTML = '🏪';
parent.style.fontSize = '48px';
}
}} }}
/> />
) : ( ) : (
<span className="rb:text-5xl">🏪</span> <div className="rb:size-12 rb:rounded-xl rb:bg-cover rb:bg-[url('@/assets/images/tool/market.png')]"></div>
)} )}
</Flex>
<div>
<div className="rb:font-[MiSans Bold] rb:font-bold rb:text-[16px] rb:leading-5.5">{source.name}</div>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5">{t('tool.availableMcp')} ({mcpTotal})</div>
</div> </div>
<div className="rb:flex rb:items-center rb:flex-1"> </Flex>
<h2 className="rb:text-xl rb:font-semibold rb:text-gray-900 rb:mb-2 rb:mr-2">{source.name}</h2>
MCP <span className="rb:text-gray-600 rb:font-normal">({mcpTotal})</span>
{/* <p className="rb:text-sm rb:text-gray-600 rb:leading-relaxed">{source.description}</p> */}
</div>
</div>
<div className="rb:flex rb:gap-3"> <Space size={12}>
<div className="rb:flex rb:gap-3 rb:items-center"> <SearchInput
{source.connected && ( placeholder={t('tool.marketSearchPlaceholder')}
<Button size="small" icon={<SyncOutlined />} onClick={() => handleRefresh(selectedSource)}> value={searchKeyword}
{t('tool.marketRefresh')} onSearch={(value: string) => handleSearchChange(value)}
</Button> allowClear
)} style={{ width: 200 }}
/>
<Input <Button type="primary" ghost onClick={() => handleOpenConfig(selectedSource)}>
prefix={<SearchOutlined />}
placeholder={t('tool.marketSearchPlaceholder')}
value={searchKeyword}
onChange={(e) => handleSearchChange(e.target.value)}
allowClear
style={{ width: 200 }}
/>
</div>
<Button icon={<SettingOutlined />} onClick={() => handleOpenConfig(selectedSource)}>
{t('tool.marketConfigBtn')} {t('tool.marketConfigBtn')}
</Button> </Button>
<Button type="primary" icon={<GlobalOutlined />} onClick={() => window.open(source.url, '_blank')}> <Button type="primary" onClick={() => window.open(source.url, '_blank')}>
{t('tool.marketVisit')} {t('tool.marketVisit')}
</Button> </Button>
</div> </Space>
</div> </Flex>
<div className="rb:mt-6"> <div className="rb:mt-4">
<div id="mcpScrollableDiv" className="rb:overflow-y-auto rb:h-[calc(100vh-260px)]"> <div id="mcpScrollableDiv" className="rb:overflow-y-auto rb:h-[calc(100vh-188px)]">
{!loading && mcpList.length === 0 ? ( {!loading && mcpList.length === 0 ? (
<Empty <Empty
url={pageEmptyIcon} url={pageEmptyIcon}
title={searchKeyword ? t('tool.marketNoSearchResult') : t('tool.marketNoData')} title={searchKeyword ? t('tool.marketNoSearchResult') : t('tool.marketNoData')}
subTitle={searchKeyword ? t('tool.marketNoSearchResultDesc') : t('tool.marketNoDataDesc')} subTitle={searchKeyword ? t('tool.marketNoSearchResultDesc') : t('tool.marketNoDataDesc')}
size={200} size={200}
className="rb:h-full" className="rb:h-full"
/> />
) : ( ) : (
<InfiniteScroll <InfiniteScroll
dataLength={mcpList.length} dataLength={mcpList.length}
next={loadMore} next={loadMore}
hasMore={hasMore} hasMore={hasMore}
loader={null} loader={null}
scrollableTarget="mcpScrollableDiv" scrollableTarget="mcpScrollableDiv"
> >
<div <Row gutter={[12,12]}>
className="rb:gap-4" {mcpList.map(mcp => (
style={{ <Col
display: 'grid', key={mcp.id}
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))' span={12}
}}
>
{mcpList.map(mcp => (
<div
key={mcp.id}
className="rb:bg-white rb:border rb:border-gray-200 rb:rounded-lg rb:p-4 rb:pb-2 rb:transition-all rb:duration-200 hover:rb:shadow-lg hover:rb:border-gray-300"
> >
<div className="rb:flex rb:justify-between rb:items-center rb:mb-3"> <RbCard
<div className="rb:w-12 rb:h-12 rb:flex rb:items-center rb:justify-center rb:bg-gray-50 rb:rounded-lg rb:overflow-hidden"> avatarUrl={mcp.logo_url || marketIcon}
{mcp.logo_url ? ( title={
<img <Flex justify="space-between" gap={16}>
src={mcp.logo_url} <Flex vertical gap={6}>
alt={getLocaleField(mcp, 'name')} <Tooltip title={getLocaleField(mcp, 'name')}>
className="rb:w-full rb:h-full rb:object-cover" <div className="rb:wrap-break-word rb:line-clamp-1">{getLocaleField(mcp, 'name')}</div>
referrerPolicy="no-referrer" </Tooltip>
onError={(e) => { <Flex gap={8} wrap className='rb:wrap-break-word rb:line-clamp-1'>
e.currentTarget.style.display = 'none'; {mcp.categories?.[0] && (
const parent = e.currentTarget.parentElement; <Tag>{mcp.categories[0]}</Tag>
if (parent) { )}
parent.innerHTML = '🔧'; {mcp.activated && <Tag color="success">{t('tool.marketActivated')}</Tag>}
parent.style.fontSize = '24px'; {mcp.inDatabase && <Tag>{t('tool.marketInDatabase')}</Tag>}
} </Flex>
}} </Flex>
/> <Button
) : ( disabled={mcp.inDatabase}
<span className="rb:text-3xl">🔧</span> size="small"
)} onClick={() => handleOpenMcpServiceModal(mcp)}
</div> >+</Button>
{mcp.categories?.[0] && ( </Flex>
<span className="rb:px-2 rb:py-1 rb:rounded rb:text-xs rb:font-medium rb:bg-blue-50 rb:text-blue-700"> }
{mcp.categories[0]} isNeedTooltip={false}
</span> footer={<Flex justify="space-between" align="center" className="rb:text-[#5B6167] rb:text-[12px] rb:mb-1!">
)} {mcp.publisher && <span>{mcp.publisher.startsWith('@') ? mcp.publisher : `@${mcp.publisher}`}</span>}
</div> {mcp.view_count && <Space size={4}>
<h3 className="rb:text-base rb:font-semibold rb:text-gray-900 rb:mb-1">{getLocaleField(mcp, 'name')}</h3> <div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/common/global_outline.svg')]"></div>
{mcp.publisher && ( {mcp.view_count.toLocaleString()}
<div className="rb:mb-2"> </Space>}
<span className="rb:text-xs rb:text-gray-500">{mcp.publisher.startsWith('@') ? mcp.publisher : `@${mcp.publisher}`}</span> </Flex>}
</div> >
)} {getLocaleField(mcp, 'description') ?
<p className="rb:text-sm rb:text-gray-600 rb:line-clamp-2 rb:mb-3 rb:min-h-10">{getLocaleField(mcp, 'description')}</p> <Tooltip title={getLocaleField(mcp, 'description')}>
<div className="rb:flex rb:gap-4 rb:mb-3 rb:pt-3 rb:border-t rb:border-gray-100"> <div className="rb:h-10 rb:leading-5 rb:wrap-break-word rb:line-clamp-2 rb:mt-2">{getLocaleField(mcp, 'description')}</div>
{mcp.view_count != null && ( </Tooltip>
<span className="rb:flex rb:items-center rb:gap-1 rb:text-xs rb:text-gray-500"> : <div className="rb:h-10 rb:leading-5 rb:text-[#A8A9AA] rb:mt-2">{t('tool.descEmpty')}</div>
<GlobalOutlined /> {mcp.view_count.toLocaleString()} }
</span> </RbCard>
)} </Col>
</div>
<div className={`rb:flex rb:items-center ${mcp.activated || mcp.inDatabase ? 'rb:justify-between' : 'rb:justify-end'}`}>
<div className="rb:flex rb:gap-2">
{mcp.activated && <Tag color="success">{t('tool.marketActivated')}</Tag>}
{mcp.inDatabase && <Tag color="blue">{t('tool.marketInDatabase')}</Tag>}
</div>
<Button disabled={mcp.inDatabase} type="primary" size="small" onClick={() => handleOpenMcpServiceModal(mcp)}>
+ {t('tool.marketAdd')}
</Button>
</div>
</div>
))} ))}
</div> </Row>
</InfiniteScroll> </InfiniteScroll>
)} )}
</div> </div>
</div> </div>
</> </>
); );
}; };
return ( return (
<div className="rb:flex rb:gap-4 rb:h-[calc(100vh-138px)]"> <Row gutter={16}>
{/* 左侧市场源列表 */} <Col flex="380px">
<div className="rb:w-80 rb:h-full rb:overflow-y-auto"> <Flex vertical gap={16}>
<Space size={12} direction="vertical" className="rb:w-full"> <div className="rb:font-[MiSans-Bold] rb:font-bold rb:text-[16px] rb:leading-5.5">{t('tool.mcpMarket')}</div>
{categories.map(cat => ( {categories.map(cat => (
<Card <Flex key={cat.id} vertical gap={8}>
key={cat.id} <div className="rb:text-[#5B6167] rb:text-[12px] rb:font-medium rb:leading-4.5">
type="inner" {cat.name}
title={ </div>
<div className="rb:flex rb:items-center rb:gap-2"> {marketSources
<span>{cat.name}</span> .filter(s => s.category === cat.id)
</div> .map(source => (
} <Flex
classNames={{ key={source.id}
body: "rb:p-[10px]!", align="center"
header: "rb:bg-[#F6F8FC]!" gap={8}
}} className={clsx('rb:bg-white rb:rounded-xl rb:py-2! rb:px-3! rb:cursor-pointer rb:transition-all', {
> 'rb:border rb:border-[#171719]': selectedSource === source.id,
<Space size={8} direction="vertical" className="rb:w-full"> 'rb:shadow-[0px_2px_6px_0px_rgba(23,23,25,0.1)]': selectedSource !== source.id
{marketSources })}
.filter(s => s.category === cat.id) onClick={() => handleSelectSource(source.id)}
.map(source => ( >
<div <div className="rb:size-7 rb:shrink-0 rb:flex rb:items-center rb:justify-center rb:overflow-hidden rb:rounded rb:bg-gray-100">
key={source.id} {source.logo_url ? (
className={`rb:bg-white rb:rounded-lg rb:p-2 rb:border rb:cursor-pointer rb:flex rb:items-center rb:gap-2 rb:transition-all ${ <img
selectedSource === source.id src={source.logo_url}
? 'rb:border-[#155EEF] rb:shadow-[0px_2px_4px_0px_rgba(33,35,50,0.15)]' alt={source.name}
: 'rb:border-[#DFE4ED] rb:hover:border-[#155EEF] rb:hover:shadow-[0px_2px_4px_0px_rgba(33,35,50,0.15)]' className="rb:w-full rb:h-full rb:object-cover rb:rounded-sm"
}`} referrerPolicy="no-referrer"
onClick={() => handleSelectSource(source.id)} onError={(e) => {
> e.currentTarget.src = marketIcon;
<div className="rb:w-5 rb:h-5 rb:flex-shrink-0 rb:flex rb:items-center rb:justify-center rb:overflow-hidden rb:rounded rb:bg-gray-100"> }}
{source.logo_url ? ( />
<img ) : (
src={source.logo_url} <div className="rb:size-7 rb:rounded-sm rb:bg-cover rb:bg-[url('@/assets/images/tool/market.png')]"></div>
alt={source.name}
className="rb:w-full rb:h-full rb:object-cover"
referrerPolicy="no-referrer"
onError={(e) => {
e.currentTarget.style.display = 'none';
const parent = e.currentTarget.parentElement;
if (parent) {
parent.innerHTML = '🏪';
parent.style.fontSize = '16px';
}
}}
/>
) : (
<span className="rb:text-base">🏪</span>
)}
</div>
<span className="rb:flex-1 rb:font-medium rb:text-[12px] rb:overflow-hidden rb:text-ellipsis rb:whitespace-nowrap">
{source.name}
</span>
{/* <span className="rb:text-xs rb:text-gray-500 rb:px-1.5 rb:py-0.5 rb:bg-gray-100 rb:rounded-full rb:flex-shrink-0">
{source.mcp_count}
</span> */}
{source.connected && (
<span className="rb:text-green-500 rb:text-[8px] rb:flex-shrink-0"></span>
)} )}
</div> </div>
))} <span className="rb:flex-1 rb:font-medium rb:overflow-hidden rb:text-ellipsis rb:whitespace-nowrap">
</Space> {source.name}
</Card> </span>
</Flex>
))}
</Flex>
))} ))}
</Space> </Flex>
</div> </Col>
<Col flex="1">
{/* 右侧内容区 */} {renderSourceDetail()}
<div className="rb:flex-1 rb:border-l rb:border-gray-200 rb:overflow-hidden"> </Col>
<div className="rb:h-full rb:overflow-y-auto rb:p-6">
{renderSourceDetail()}
</div>
</div>
{/* 配置弹窗 */} {/* 配置弹窗 */}
<MarketConfigModal <MarketConfigModal
ref={marketConfigModalRef} ref={marketConfigModalRef}
@@ -581,7 +516,7 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
ref={mcpServiceModalRef} ref={mcpServiceModalRef}
refresh={handleRefreshAfterAdd} refresh={handleRefreshAfterAdd}
/> />
</div> </Row>
); );
}; };

View File

@@ -1,37 +1,38 @@
import React, { useState, useRef, useEffect, type ReactNode } from 'react'; import { useState, useRef, useEffect, forwardRef, useImperativeHandle, type ReactNode } from 'react';
import { import {
Button,
Row,
Col,
App, App,
List, List,
Space, Space,
Tooltip,
Dropdown,
Flex,
} from 'antd'; } from 'antd';
import { LinkOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { ToolItem, Query, McpServiceModalRef } from './types'; import type { ToolItem, McpServiceModalRef, McpRef } from './types';
import McpServiceModal from './components/McpServiceModal'; import McpServiceModal from './components/McpServiceModal';
import SearchInput from '@/components/SearchInput'
import BodyWrapper from '@/components/Empty/BodyWrapper' import BodyWrapper from '@/components/Empty/BodyWrapper'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard'
import { getTools, deleteTool, testConnection } from '@/api/tools' import { getTools, deleteTool, testConnection } from '@/api/tools'
import { formatDateTime } from '@/utils/format'
const Mcp: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ getStatusTag }) => { const Mcp = forwardRef<McpRef, { getStatusTag: (status: string) => ReactNode; keyword?: string | undefined }>(({ getStatusTag, keyword }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { message, modal } = App.useApp() const { message, modal } = App.useApp()
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState<ToolItem[]>([]); const [data, setData] = useState<ToolItem[]>([]);
const [query, setQuery] = useState<Query>({ name: undefined, tool_type: 'mcp' });
const addServiceModalRef = useRef<McpServiceModalRef>(null); const addServiceModalRef = useRef<McpServiceModalRef>(null);
useEffect(() => { useEffect(() => {
getData() getData()
}, [query.name]) }, [keyword])
const getData = () => { const getData = () => {
setLoading(true) setLoading(true)
getTools(query) getTools({
tool_type: 'mcp',
name: keyword
})
.then((res) => { .then((res) => {
setData(res as ToolItem[]) setData(res as ToolItem[])
}) })
@@ -39,9 +40,8 @@ const Mcp: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ getSta
setLoading(false) setLoading(false)
}) })
} }
const handleSearch = (value?: string) => {
setQuery(prev => ({ ...prev, name: value })) useImperativeHandle(ref, () => ({ handleEdit, getData }));
}
// 打开添加服务弹窗 // 打开添加服务弹窗
const handleEdit = (data?: ToolItem) => { const handleEdit = (data?: ToolItem) => {
@@ -82,19 +82,7 @@ const Mcp: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ getSta
}; };
return ( return (
<div> <>
<Row gutter={16} className='rb:mb-4 rb:w-full'>
<Col span={8}>
<SearchInput
placeholder={t('tool.mcpSearchPlaceholder')}
onSearch={handleSearch}
style={{width: '100%'}}
/>
</Col>
<Col span={16} className="rb:text-right">
<Button type="primary" onClick={() => {handleEdit()}}>{t('tool.addService')}</Button>
</Col>
</Row>
<BodyWrapper loading={loading} empty={data?.length === 0}> <BodyWrapper loading={loading} empty={data?.length === 0}>
<List <List
grid={{ gutter: 16, column: 3 }} grid={{ gutter: 16, column: 3 }}
@@ -102,58 +90,58 @@ const Mcp: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ getSta
renderItem={(item) => ( renderItem={(item) => (
<List.Item key={item.id}> <List.Item key={item.id}>
<RbCard <RbCard
// avatar={ title={
// <div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]"> <Flex justify="space-between" gap={16}>
// {item.name[0]} <Space size={8} className="rb:flex-1!">
// </div> <Tooltip title={item.name}>
// } <div className="rb:wrap-break-word rb:line-clamp-1">{item.name}</div>
title={item.name} </Tooltip>
extra={getStatusTag(item.status)} {getStatusTag(item.status)}
>
<div>
{[
'server_url',
'last_health_check',
].map(key => {
const value = item.config_data?.[key as keyof typeof item.config_data];
let displayValue: React.ReactNode;
if (key === 'last_health_check') {
displayValue = value ? new Date(value as number).toLocaleString() : '-';
} else if (typeof value === 'string' || typeof value === 'number') {
displayValue = value;
} else {
displayValue = '-';
}
return (
<div
key={key}
className="rb:flex rb:gap-4 rb:justify-start rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-27.5">{t(`tool.${key}`)}</div>
<div className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-1">{displayValue}</div>
</div>
);
})}
<div className="rb:mt-4 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167] rb:flex rb:items-center rb:justify-end">
<Space size={16}>
<div
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
onClick={() => handleEdit(item)}
></div>
<Button type="text" icon={<LinkOutlined />} onClick={() => handleTestConnection(item)}></Button>
<div
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
onClick={() => handleDeleteService(item)}
></div>
</Space> </Space>
<Dropdown
menu={{
items: [
{
key: 'edit',
icon: <div className="rb:size-4 rb:bg-cover rb:cursor-pointer rb:bg-[url('@/assets/images/common/edit.svg')]" />,
label: t('common.edit'),
onClick: () => handleEdit(item),
},
{
key: 'link',
icon: <div className="rb:size-4 rb:bg-cover rb:cursor-pointer rb:bg-[url('@/assets/images/conversation/link.svg')]" />,
label: t('tool.testLink'),
onClick: () => handleTestConnection(item),
},
{
key: 'delete',
className: 'rb:text-[#FF5D34]!',
icon: <div className="rb:size-4 rb:bg-cover rb:cursor-pointer rb:bg-[url('@/assets/images/common/delete_red.svg')]" />,
label: t('common.delete'),
onClick: () => handleDeleteService(item),
},
]
}}
placement="bottomRight"
>
<div className="rb:cursor-pointer rb:size-6 rb:bg-[url('@/assets/images/common/more.svg')] rb:hover:bg-[url('@/assets/images/common/more_hover.svg')]"></div>
</Dropdown>
</Flex>
}
isNeedTooltip={false}
>
<Flex vertical gap={4} className="rb:bg-[#F6F6F6] rb:rounded-lg rb:py-2! rb:px-3! rb:text-[#5B6167] rb:leading-5">
{t(`tool.server_url`)}
<div className="rb:h-10 rb:break-all rb:line-clamp-2 rb:text-[#171719]">
{item.config_data?.server_url}
</div> </div>
</div> </Flex>
<div className="rb:text-[#5B6167] rb:leading-4.5 rb:text-[12px] rb:mt-4">{t('tool.last_health_check')}: {formatDateTime(item.config_data?.last_health_check)}</div>
</RbCard> </RbCard>
</List.Item> </List.Item>
)} )}
className="rb:h-[calc(100vh-178px)] rb:overflow-y-auto rb:overflow-x-hidden" className="rb:h-[calc(100vh-124px)] rb:overflow-y-auto rb:overflow-x-hidden"
/> />
</BodyWrapper> </BodyWrapper>
@@ -162,8 +150,8 @@ const Mcp: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ getSta
ref={addServiceModalRef} ref={addServiceModalRef}
refresh={getData} refresh={getData}
/> />
</div> </>
); );
}; });
export default Mcp; export default Mcp;

View File

@@ -94,7 +94,7 @@ const InnerToolModal = forwardRef<InnerToolModalRef, InnerToolModalProps>(({
confirmLoading={loading} confirmLoading={loading}
> >
{editVo?.config_data?.tool_class && config && <> {editVo?.config_data?.tool_class && config && <>
<RbAlert className="rb:mb-3"> <RbAlert className="rb:mb-3!">
<div> <div>
<div className="rb:text-[14px] rb:font-medium">{t('tool.configDesc')}</div> <div className="rb:text-[14px] rb:font-medium">{t('tool.configDesc')}</div>
<div className="rb:mt-2">{t(`tool.${editVo?.config_data?.tool_class}_config_desc`)}</div> <div className="rb:mt-2">{t(`tool.${editVo?.config_data?.tool_class}_config_desc`)}</div>

View File

@@ -6,8 +6,8 @@
* @LastEditors: yujiangping * @LastEditors: yujiangping
* @LastEditTime: 2026-03-06 15:11:31 * @LastEditTime: 2026-03-06 15:11:31
*/ */
import React, { useState } from 'react'; import React, { useState, useRef } from 'react';
import { Tabs } from 'antd'; import { type SegmentedProps, Flex, Space, Form, Button } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Mcp from './Mcp'; import Mcp from './Mcp';
@@ -15,20 +15,28 @@ import Inner from './Inner';
import Custom from './Custom'; import Custom from './Custom';
import Market from './Market'; import Market from './Market';
import Tag from '@/components/Tag' import Tag from '@/components/Tag'
import PageTabs from '@/components/PageTabs'
import SearchInput from '@/components/SearchInput'
import type { McpRef, CustomRef } from './types'
const tabKeys = ['mcp', 'inner', 'custom', 'market'] // const tabKeys = ['mcp', 'inner', 'custom', 'market'] //
const ToolManagement: React.FC = () => { const ToolManagement: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('mcp'); const [activeTab, setActiveTab] = useState<SegmentedProps['value']>('mcp');
const mcpRef = useRef<McpRef>(null);
const customRef = useRef<CustomRef>(null);
const [form] = Form.useForm();
const name = Form.useWatch(['name'], form)
const formatTabItems = () => { const formatTabItems = () => {
return tabKeys.map(key => ({ return tabKeys.map(value => ({
key, value,
label: t(`tool.${key}`), label: t(`tool.${value}`),
})) }))
} }
const handleChangeTab = (key: string) => { const handleChangeTab = (key: SegmentedProps['value']) => {
setActiveTab(key); setActiveTab(key);
form.resetFields()
} }
// 获取状态标签 // 获取状态标签
const getStatusTag = (status: string) => { const getStatusTag = (status: string) => {
@@ -45,17 +53,36 @@ const ToolManagement: React.FC = () => {
}; };
return ( return (
<div className="rb:-mt-4"> <>
<Tabs <Flex justify="space-between" className="rb:mb-4!">
activeKey={activeTab} <PageTabs
items={formatTabItems()} value={activeTab}
onChange={handleChangeTab} options={formatTabItems()}
/> onChange={handleChangeTab}
{activeTab === 'mcp' && <Mcp getStatusTag={getStatusTag} />} />
{activeTab === 'inner' && <Inner getStatusTag={getStatusTag} />}
{activeTab === 'custom' && <Custom getStatusTag={getStatusTag} />} {activeTab !== 'market' && <Form form={form}>
<Space size={12}>
<Form.Item name="name" noStyle>
<SearchInput
placeholder={t(`tool.${activeTab === 'mcp'
? 'mcpSearchPlaceholder'
: activeTab === 'custom'
? 'customSearchPlaceholder'
: 'innerSearchPlaceholder'
}`)}
/>
</Form.Item>
{activeTab === 'mcp' && <Button type="primary" onClick={() => mcpRef.current?.handleEdit()}>{t('tool.addService')}</Button>}
{activeTab === 'custom' && <Button type="primary" onClick={() => customRef.current?.handleEdit()}>{t('tool.addCustom')}</Button>}
</Space>
</Form>}
</Flex>
{activeTab === 'mcp' && <Mcp ref={mcpRef} keyword={name} getStatusTag={getStatusTag} />}
{activeTab === 'inner' && <Inner keyword={name} getStatusTag={getStatusTag} />}
{activeTab === 'custom' && <Custom ref={customRef} keyword={name} getStatusTag={getStatusTag} />}
{activeTab === 'market' && <Market getStatusTag={getStatusTag} />} {activeTab === 'market' && <Market getStatusTag={getStatusTag} />}
</div> </>
); );
}; };

View File

@@ -147,4 +147,11 @@ export interface MarketQuery {
page?: number; page?: number;
pagesize?: number; pagesize?: number;
keywords?: string; keywords?: string;
}
export interface McpRef {
handleEdit: (data?: ToolItem) => void;
getData: () => void;
}
export interface CustomRef {
handleEdit: (data?: ToolItem) => void;
} }