mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
dev: terminal
This commit is contained in:
@@ -10,6 +10,9 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@heroui/avatar": "2.2.7",
|
||||
"@heroui/breadcrumbs": "2.2.7",
|
||||
"@heroui/button": "2.2.10",
|
||||
|
@@ -1152,7 +1152,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="0ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1197,7 +1197,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="800ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1247,7 +1247,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="1600ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1297,7 +1297,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="2400ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1344,7 +1344,7 @@ export const WebUIIcon = (props: IconSvgProps) => (
|
||||
begin="3200ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1399,7 +1399,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="0ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1446,7 +1446,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="600ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1496,7 +1496,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="1200ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1543,7 +1543,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="1800ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1590,7 +1590,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="2400ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1637,7 +1637,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="3000ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1684,7 +1684,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="3600ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1731,7 +1731,7 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
begin="4200ms"
|
||||
></animate>
|
||||
<animate
|
||||
attributeName="fill-opacity"
|
||||
attributeName="fillOpacity"
|
||||
to="1"
|
||||
dur="800ms"
|
||||
calcMode="linear"
|
||||
@@ -1744,3 +1744,224 @@ export const BietiaopIcon = (props: IconSvgProps) => (
|
||||
</svg>
|
||||
</>
|
||||
)
|
||||
|
||||
export const FileIcon = (props: IconSvgProps) => (
|
||||
<svg
|
||||
version="1.1"
|
||||
id="_x36_"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512 512"
|
||||
xmlSpace="preserve"
|
||||
{...props}
|
||||
>
|
||||
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
||||
<g
|
||||
id="SVGRepo_tracerCarrier"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: '#D4B476' }}
|
||||
d="M441.853,393.794H70.147C31.566,393.794,0,362.228,0,323.647V106.969 c0-38.581,31.566-70.147,70.147-70.147h371.706c38.581,0,70.147,31.566,70.147,70.147v216.678 C512,362.228,480.434,393.794,441.853,393.794z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#D4B476' }}
|
||||
d="M199.884,249.574H70.147C31.566,249.574,0,218.008,0,179.427V70.147C0,31.566,31.566,0,70.147,0 h129.737c38.581,0,70.147,31.566,70.147,70.147v109.28C270.031,218.008,238.465,249.574,199.884,249.574z"
|
||||
></path>
|
||||
<polygon
|
||||
style={{ fill: '#F0EFEF' }}
|
||||
points="485.439,329.388 87.357,347.774 78.653,130.095 476.734,111.709 "
|
||||
></polygon>
|
||||
<defs>
|
||||
<filter
|
||||
id="Adobe_OpacityMaskFilter"
|
||||
filterUnits="userSpaceOnUse"
|
||||
x="34.381"
|
||||
y="60.216"
|
||||
width="416.68"
|
||||
height="259.557"
|
||||
>
|
||||
<feFlood
|
||||
style={{
|
||||
floodColor: 'white',
|
||||
floodOpacity: 1
|
||||
}}
|
||||
result="back"
|
||||
></feFlood>
|
||||
<feBlend in="SourceGraphic" in2="back" mode="normal"></feBlend>
|
||||
</filter>
|
||||
</defs>
|
||||
<mask
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="34.381"
|
||||
y="60.216"
|
||||
width="416.68"
|
||||
height="259.557"
|
||||
id="SVGID_1_"
|
||||
>
|
||||
<g style={{ filter: 'url(#Adobe_OpacityMaskFilter)' }}>
|
||||
<defs>
|
||||
<filter
|
||||
id="Adobe_OpacityMaskFilter_1_"
|
||||
filterUnits="userSpaceOnUse"
|
||||
x="34.381"
|
||||
y="60.216"
|
||||
width="416.68"
|
||||
height="259.557"
|
||||
>
|
||||
<feFlood
|
||||
style={{ floodColor: 'white', floodOpacity: 1 }}
|
||||
result="back"
|
||||
></feFlood>
|
||||
<feBlend in="SourceGraphic" in2="back" mode="normal"></feBlend>
|
||||
</filter>
|
||||
</defs>
|
||||
<mask
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="34.381"
|
||||
y="60.216"
|
||||
width="416.68"
|
||||
height="259.557"
|
||||
id="SVGID_1_"
|
||||
>
|
||||
<g style={{ filter: 'url(#Adobe_OpacityMaskFilter_1_)' }}> </g>
|
||||
</mask>
|
||||
<linearGradient
|
||||
id="SVGID_2_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="34.3814"
|
||||
y1="189.9944"
|
||||
x2="451.061"
|
||||
y2="189.9944"
|
||||
>
|
||||
<stop offset="0.57" style={{ stopColor: '#F6F6F6' }}></stop>
|
||||
<stop offset="0.6039" style={{ stopColor: '#F6F6F6' }}></stop>
|
||||
</linearGradient>
|
||||
<polygon
|
||||
style={{ mask: 'url(#SVGID_1_)', fill: 'url(#SVGID_2_)' }}
|
||||
points="451.061,277.073 54.598,319.773 34.381,102.916 430.845,60.216 "
|
||||
></polygon>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient
|
||||
id="SVGID_3_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="34.3814"
|
||||
y1="189.9944"
|
||||
x2="451.061"
|
||||
y2="189.9944"
|
||||
>
|
||||
<stop offset="0.57" style={{ stopColor: '#FFFFFF' }}></stop>
|
||||
<stop offset="0.6039" style={{ stopColor: '#F0F0F0' }}></stop>
|
||||
</linearGradient>
|
||||
<polygon
|
||||
style={{ fill: 'url(#SVGID_3_)' }}
|
||||
points="451.061,277.073 54.598,319.773 34.381,102.916 430.845,60.216 "
|
||||
></polygon>
|
||||
<path
|
||||
style={{ fill: '#69A092' }}
|
||||
d="M441.853,417.32H70.147C31.566,417.32,0,385.754,0,347.173V168.515h512v178.658 C512,385.754,480.434,417.32,441.853,417.32z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#D4B476' }}
|
||||
d="M441.853,429.594H70.147C31.566,429.594,0,398.028,0,359.447V189.995h512v169.453 C512,398.028,480.434,429.594,441.853,429.594z"
|
||||
></path>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: '#CBBC89' }}
|
||||
d="M41.051,330.321h28.918h7.581c0.686,0,1.357,0,2.012,0c0.655,0,1.171,0.126,1.545,0.375 c0.499,0.312,0.795,0.764,0.889,1.357c0.094,0.594,0.141,1.296,0.141,2.106c0,0.312,0.014,0.608,0.047,0.888 c0.031,0.281-0.016,0.547-0.14,0.796c-0.25,0.998-0.796,1.543-1.638,1.638c-0.843,0.094-1.888,0.14-3.136,0.14h-9.733H53.778 c-0.998,0-2.058-0.015-3.182-0.047c-1.122-0.03-1.965,0.173-2.526,0.608c-0.563,0.375-0.858,1.03-0.89,1.965 c-0.032,0.937-0.047,1.873-0.047,2.808v9.265c0,0.5-0.015,1.123-0.046,1.872c-0.033,0.749,0.014,1.373,0.139,1.871v0.748 c0.125,0.375,0.235,0.735,0.328,1.077c0.093,0.344,0.295,0.608,0.608,0.796c0.562,0.374,1.436,0.547,2.621,0.514 c1.184-0.03,2.214-0.047,3.088-0.047h16.845c0.561,0,1.171-0.014,1.825-0.046c0.655-0.031,1.31-0.031,1.965,0 c0.655,0.032,1.248,0.109,1.779,0.234c0.529,0.126,0.889,0.344,1.076,0.655c0.311,0.375,0.467,0.843,0.467,1.404 c0,0.56,0,1.186,0,1.871c0,0.375,0,0.719,0,1.03c0,0.313-0.062,0.594-0.186,0.842c-0.25,0.625-0.595,0.969-1.03,1.03 c-0.25,0.125-0.485,0.186-0.702,0.186c-0.219,0-0.483,0.033-0.796,0.094c-0.25,0.063-0.514,0.079-0.796,0.047 c-0.281-0.03-0.546-0.047-0.794-0.047h-3.277H54.433c-0.999,0-2.168-0.014-3.51-0.047c-1.342-0.03-2.292,0.172-2.853,0.609 c-0.314,0.25-0.547,0.64-0.702,1.169c-0.156,0.531-0.25,1.14-0.281,1.825c-0.033,0.687-0.033,1.389,0,2.106 c0.03,0.717,0.046,1.357,0.046,1.918v16.096c0,1.062,0.032,2.217,0.094,3.463c0.062,1.249-0.156,2.185-0.655,2.807 c-0.186,0.25-0.469,0.391-0.842,0.422c-0.374,0.032-0.749,0.109-1.123,0.234h-1.779c-0.875,0-1.684-0.03-2.433-0.093 c-0.749-0.062-1.279-0.375-1.59-0.937c-0.25-0.374-0.375-0.888-0.375-1.543c0-0.656,0-1.326,0-2.013v-7.488v-39.119v-10.107 c0-0.686-0.016-1.45-0.047-2.292c-0.032-0.842,0.107-1.512,0.422-2.012c0.249-0.374,0.715-0.685,1.404-0.935 c0.124-0.062,0.264-0.077,0.42-0.047C40.785,330.4,40.925,330.383,41.051,330.321z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#CBBC89' }}
|
||||
d="M96.895,330.508c0.436,0,0.935-0.014,1.496-0.046c0.563-0.032,1.108-0.032,1.639,0 c0.529,0.032,1.013,0.109,1.45,0.234c0.437,0.125,0.749,0.312,0.937,0.561c0.374,0.438,0.576,1.046,0.608,1.825 c0.03,0.78,0.047,1.576,0.047,2.386v9.079v36.124v10.949c0,0.811,0,1.669,0,2.574c0,0.905-0.156,1.606-0.468,2.105 c-0.25,0.314-0.547,0.5-0.889,0.563c-0.344,0.062-0.765,0.156-1.264,0.28h-1.777c-0.874,0-1.684-0.034-2.433-0.094 c-0.749-0.061-1.28-0.374-1.592-0.935c-0.25-0.374-0.375-0.89-0.375-1.545c0-0.655,0-1.325,0-2.012v-7.487V345.95v-10.107 c0-0.686-0.015-1.451-0.045-2.292c-0.034-0.843,0.108-1.514,0.42-2.013c0.249-0.436,0.748-0.748,1.497-0.936 c0.125-0.061,0.25-0.077,0.375-0.047C96.645,330.587,96.769,330.571,96.895,330.508z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#CBBC89' }}
|
||||
d="M126.093,330.321c0.436,0,0.935-0.015,1.498-0.047c0.561-0.03,1.107-0.03,1.638,0 c0.529,0.032,1.012,0.109,1.451,0.234c0.436,0.125,0.748,0.313,0.936,0.562c0.374,0.5,0.56,1.155,0.56,1.965 c0,0.811,0,1.654,0,2.528v9.826v30.042v8.609c0,0.687,0,1.436,0,2.245c0,0.813,0.094,1.468,0.281,1.967 c0.188,0.436,0.529,0.811,1.03,1.122c0.313,0.125,0.717,0.203,1.217,0.235c0.499,0.032,0.998,0.046,1.498,0.046h4.772h16.845 h5.24c0.436,0,0.89-0.014,1.359-0.046c0.467-0.032,0.888,0.016,1.263,0.139c0.81,0.25,1.34,0.594,1.591,1.03 c0.062,0.188,0.124,0.5,0.188,0.937c0.061,0.436,0.107,0.889,0.139,1.357c0.03,0.467,0.015,0.935-0.047,1.402 c-0.062,0.469-0.126,0.829-0.187,1.077c-0.126,0.437-0.406,0.782-0.843,1.029c-0.313,0.25-0.765,0.375-1.357,0.375 c-0.593,0-1.169,0-1.731,0h-6.177H133.58h-6.083c-0.874,0-1.623-0.047-2.246-0.139c-0.624-0.094-1.092-0.39-1.404-0.89 c-0.25-0.374-0.373-0.857-0.373-1.451c0-0.591,0-1.168,0-1.729v-6.74v-25.549v-20.308v-5.802c0-0.561,0.014-1.122,0.045-1.684 c0.032-0.561,0.141-0.998,0.328-1.31c0.249-0.374,0.717-0.685,1.404-0.935c0.124-0.062,0.265-0.077,0.422-0.047 C125.827,330.4,125.968,330.383,126.093,330.321z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#CBBC89' }}
|
||||
d="M187.883,330.321h30.602h8.049c0.686,0,1.372-0.015,2.059-0.047 c0.685-0.03,1.248,0.109,1.684,0.422c0.188,0.125,0.343,0.281,0.468,0.467c0.124,0.188,0.249,0.375,0.374,0.563 c0.125,0.436,0.187,1.138,0.187,2.105c0,0.967-0.062,1.701-0.187,2.199c-0.25,0.874-0.78,1.357-1.592,1.451 c-0.811,0.094-1.777,0.14-2.9,0.14h-9.359h-15.722c-0.998,0-2.169-0.015-3.51-0.047c-1.343-0.03-2.325,0.139-2.949,0.514 c-0.624,0.374-0.951,1.03-0.982,1.966c-0.032,0.936-0.048,1.904-0.048,2.901v9.546c0,0.688-0.015,1.498-0.045,2.433 c-0.034,0.937,0.014,1.686,0.139,2.246c0.188,0.749,0.562,1.249,1.123,1.498c0.249,0.125,0.686,0.25,1.31,0.373h0.935 c0.249,0.063,0.515,0.08,0.796,0.047c0.281-0.03,0.577-0.047,0.89-0.047h3.181h18.343c0.561,0,1.184-0.014,1.872-0.046 c0.685-0.031,1.387-0.031,2.104,0c0.717,0.032,1.372,0.093,1.967,0.187c0.591,0.094,1.044,0.233,1.357,0.421 c0.186,0.126,0.341,0.407,0.467,0.842c0.125,0.438,0.218,0.905,0.282,1.404c0.061,0.5,0.077,1.016,0.045,1.545 c-0.031,0.531-0.109,0.983-0.233,1.357c-0.126,0.561-0.406,0.967-0.842,1.217c-0.127,0.126-0.297,0.203-0.515,0.235 c-0.22,0.032-0.422,0.079-0.608,0.139h-0.563c-0.312,0.063-0.623,0.079-0.935,0.047c-0.313-0.03-0.624-0.047-0.935-0.047h-3.463 h-19.747c-0.747,0-1.482-0.014-2.199-0.047c-0.719-0.03-1.39,0-2.012,0.094c-0.624,0.094-1.171,0.267-1.639,0.515 c-0.467,0.25-0.794,0.687-0.982,1.31c-0.126,0.375-0.173,0.811-0.139,1.31c0.03,0.5,0.045,0.999,0.045,1.498v5.239v8.892 c0,0.873,0.032,1.669,0.094,2.386c0.062,0.718,0.312,1.233,0.749,1.544c0.435,0.313,0.935,0.483,1.497,0.515 c0.561,0.032,1.185,0.046,1.871,0.046h5.99h17.314h5.334c0.499,0,0.998-0.014,1.497-0.046c0.498-0.032,0.936,0.016,1.31,0.139 c0.749,0.25,1.248,0.594,1.498,1.03c0.123,0.188,0.217,0.5,0.281,0.937c0.061,0.436,0.108,0.889,0.14,1.357 c0.03,0.467,0.015,0.935-0.047,1.402c-0.062,0.469-0.126,0.829-0.187,1.077c-0.25,0.5-0.531,0.842-0.842,1.029 c-0.375,0.25-0.858,0.375-1.45,0.375c-0.594,0-1.171,0-1.731,0h-6.552h-24.894h-6.551c-0.874,0-1.623-0.047-2.245-0.139 c-0.624-0.094-1.093-0.39-1.404-0.89c-0.25-0.374-0.374-0.857-0.374-1.451c0-0.591,0-1.168,0-1.729v-6.551v-25.082v-20.964 v-5.802c0-0.561,0-1.122,0-1.684c0-0.561,0.124-0.998,0.374-1.31c0.249-0.435,0.716-0.749,1.404-0.935 c0.125-0.062,0.248-0.077,0.375-0.047C187.632,330.4,187.756,330.383,187.883,330.321z"
|
||||
></path>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: '#98806E' }}
|
||||
d="M41.051,330.321h28.918h7.581c0.686,0,1.357,0,2.012,0c0.655,0,1.171,0.126,1.545,0.375 c0.499,0.312,0.795,0.764,0.889,1.357c0.094,0.594,0.141,1.296,0.141,2.106c0,0.312,0.014,0.608,0.047,0.888 c0.031,0.281-0.016,0.547-0.14,0.796c-0.25,0.998-0.796,1.543-1.638,1.638c-0.843,0.094-1.888,0.14-3.136,0.14h-9.733H53.778 c-0.998,0-2.058-0.015-3.182-0.047c-1.122-0.03-1.965,0.173-2.526,0.608c-0.563,0.375-0.858,1.03-0.89,1.965 c-0.032,0.937-0.047,1.873-0.047,2.808v9.265c0,0.5-0.015,1.123-0.046,1.872c-0.033,0.749,0.014,1.373,0.139,1.871v0.748 c0.125,0.375,0.235,0.735,0.328,1.077c0.093,0.344,0.295,0.608,0.608,0.796c0.562,0.374,1.436,0.547,2.621,0.514 c1.184-0.03,2.214-0.047,3.088-0.047h16.845c0.561,0,1.171-0.014,1.825-0.046c0.655-0.031,1.31-0.031,1.965,0 c0.655,0.032,1.248,0.109,1.779,0.234c0.529,0.126,0.889,0.344,1.076,0.655c0.311,0.375,0.467,0.843,0.467,1.404 c0,0.56,0,1.186,0,1.871c0,0.375,0,0.719,0,1.03c0,0.313-0.062,0.594-0.186,0.842c-0.25,0.625-0.595,0.969-1.03,1.03 c-0.25,0.125-0.485,0.186-0.702,0.186c-0.219,0-0.483,0.033-0.796,0.094c-0.25,0.063-0.514,0.079-0.796,0.047 c-0.281-0.03-0.546-0.047-0.794-0.047h-3.277H54.433c-0.999,0-2.168-0.014-3.51-0.047c-1.342-0.03-2.292,0.172-2.853,0.609 c-0.314,0.25-0.547,0.64-0.702,1.169c-0.156,0.531-0.25,1.14-0.281,1.825c-0.033,0.687-0.033,1.389,0,2.106 c0.03,0.717,0.046,1.357,0.046,1.918v16.096c0,1.062,0.032,2.217,0.094,3.463c0.062,1.249-0.156,2.185-0.655,2.807 c-0.186,0.25-0.469,0.391-0.842,0.422c-0.374,0.032-0.749,0.109-1.123,0.234h-1.779c-0.875,0-1.684-0.03-2.433-0.093 c-0.749-0.062-1.279-0.375-1.59-0.937c-0.25-0.374-0.375-0.888-0.375-1.543c0-0.656,0-1.326,0-2.013v-7.488v-39.119v-10.107 c0-0.686-0.016-1.45-0.047-2.292c-0.032-0.842,0.107-1.512,0.422-2.012c0.249-0.374,0.715-0.685,1.404-0.935 c0.124-0.062,0.264-0.077,0.42-0.047C40.785,330.4,40.925,330.383,41.051,330.321z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#98806E' }}
|
||||
d="M96.895,330.508c0.436,0,0.935-0.014,1.496-0.046c0.563-0.032,1.108-0.032,1.639,0 c0.529,0.032,1.013,0.109,1.45,0.234c0.437,0.125,0.749,0.312,0.937,0.561c0.374,0.438,0.576,1.046,0.608,1.825 c0.03,0.78,0.047,1.576,0.047,2.386v9.079v36.124v10.949c0,0.811,0,1.669,0,2.574c0,0.905-0.156,1.606-0.468,2.105 c-0.25,0.314-0.547,0.5-0.889,0.563c-0.344,0.062-0.765,0.156-1.264,0.28h-1.777c-0.874,0-1.684-0.034-2.433-0.094 c-0.749-0.061-1.28-0.374-1.592-0.935c-0.25-0.374-0.375-0.89-0.375-1.545c0-0.655,0-1.325,0-2.012v-7.487V345.95v-10.107 c0-0.686-0.015-1.451-0.045-2.292c-0.034-0.843,0.108-1.514,0.42-2.013c0.249-0.436,0.748-0.748,1.497-0.936 c0.125-0.061,0.25-0.077,0.375-0.047C96.645,330.587,96.769,330.571,96.895,330.508z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#98806E' }}
|
||||
d="M126.093,330.321c0.436,0,0.935-0.015,1.498-0.047c0.561-0.03,1.107-0.03,1.638,0 c0.529,0.032,1.012,0.109,1.451,0.234c0.436,0.125,0.748,0.313,0.936,0.562c0.374,0.5,0.56,1.155,0.56,1.965 c0,0.811,0,1.654,0,2.528v9.826v30.042v8.609c0,0.687,0,1.436,0,2.245c0,0.813,0.094,1.468,0.281,1.967 c0.188,0.436,0.529,0.811,1.03,1.122c0.313,0.125,0.717,0.203,1.217,0.235c0.499,0.032,0.998,0.046,1.498,0.046h4.772h16.845 h5.24c0.436,0,0.89-0.014,1.359-0.046c0.467-0.032,0.888,0.016,1.263,0.139c0.81,0.25,1.34,0.594,1.591,1.03 c0.062,0.188,0.124,0.5,0.188,0.937c0.061,0.436,0.107,0.889,0.139,1.357c0.03,0.467,0.015,0.935-0.047,1.402 c-0.062,0.469-0.126,0.829-0.187,1.077c-0.126,0.437-0.406,0.782-0.843,1.029c-0.313,0.25-0.765,0.375-1.357,0.375 c-0.593,0-1.169,0-1.731,0h-6.177H133.58h-6.083c-0.874,0-1.623-0.047-2.246-0.139c-0.624-0.094-1.092-0.39-1.404-0.89 c-0.25-0.374-0.373-0.857-0.373-1.451c0-0.591,0-1.168,0-1.729v-6.74v-25.549v-20.308v-5.802c0-0.561,0.014-1.122,0.045-1.684 c0.032-0.561,0.141-0.998,0.328-1.31c0.249-0.374,0.717-0.685,1.404-0.935c0.124-0.062,0.265-0.077,0.422-0.047 C125.827,330.4,125.968,330.383,126.093,330.321z"
|
||||
></path>
|
||||
<path
|
||||
style={{ fill: '#98806E' }}
|
||||
d="M187.883,330.321h30.602h8.049c0.686,0,1.372-0.015,2.059-0.047 c0.685-0.03,1.248,0.109,1.684,0.422c0.188,0.125,0.343,0.281,0.468,0.467c0.124,0.188,0.249,0.375,0.374,0.563 c0.125,0.436,0.187,1.138,0.187,2.105c0,0.967-0.062,1.701-0.187,2.199c-0.25,0.874-0.78,1.357-1.592,1.451 c-0.811,0.094-1.777,0.14-2.9,0.14h-9.359h-15.722c-0.998,0-2.169-0.015-3.51-0.047c-1.343-0.03-2.325,0.139-2.949,0.514 c-0.624,0.374-0.951,1.03-0.982,1.966c-0.032,0.936-0.048,1.904-0.048,2.901v9.546c0,0.688-0.015,1.498-0.045,2.433 c-0.034,0.937,0.014,1.686,0.139,2.246c0.188,0.749,0.562,1.249,1.123,1.498c0.249,0.125,0.686,0.25,1.31,0.373h0.935 c0.249,0.063,0.515,0.08,0.796,0.047c0.281-0.03,0.577-0.047,0.89-0.047h3.181h18.343c0.561,0,1.184-0.014,1.872-0.046 c0.685-0.031,1.387-0.031,2.104,0c0.717,0.032,1.372,0.093,1.967,0.187c0.591,0.094,1.044,0.233,1.357,0.421 c0.186,0.126,0.341,0.407,0.467,0.842c0.125,0.438,0.218,0.905,0.282,1.404c0.061,0.5,0.077,1.016,0.045,1.545 c-0.031,0.531-0.109,0.983-0.233,1.357c-0.126,0.561-0.406,0.967-0.842,1.217c-0.127,0.126-0.297,0.203-0.515,0.235 c-0.22,0.032-0.422,0.079-0.608,0.139h-0.563c-0.312,0.063-0.623,0.079-0.935,0.047c-0.313-0.03-0.624-0.047-0.935-0.047h-3.463 h-19.747c-0.747,0-1.482-0.014-2.199-0.047c-0.719-0.03-1.39,0-2.012,0.094c-0.624,0.094-1.171,0.267-1.639,0.515 c-0.467,0.25-0.794,0.687-0.982,1.31c-0.126,0.375-0.173,0.811-0.139,1.31c0.03,0.5,0.045,0.999,0.045,1.498v5.239v8.892 c0,0.873,0.032,1.669,0.094,2.386c0.062,0.718,0.312,1.233,0.749,1.544c0.435,0.313,0.935,0.483,1.497,0.515 c0.561,0.032,1.185,0.046,1.871,0.046h5.99h17.314h5.334c0.499,0,0.998-0.014,1.497-0.046c0.498-0.032,0.936,0.016,1.31,0.139 c0.749,0.25,1.248,0.594,1.498,1.03c0.123,0.188,0.217,0.5,0.281,0.937c0.061,0.436,0.108,0.889,0.14,1.357 c0.03,0.467,0.015,0.935-0.047,1.402c-0.062,0.469-0.126,0.829-0.187,1.077c-0.25,0.5-0.531,0.842-0.842,1.029 c-0.375,0.25-0.858,0.375-1.45,0.375c-0.594,0-1.171,0-1.731,0h-6.552h-24.894h-6.551c-0.874,0-1.623-0.047-2.245-0.139 c-0.624-0.094-1.093-0.39-1.404-0.89c-0.25-0.374-0.374-0.857-0.374-1.451c0-0.591,0-1.168,0-1.729v-6.551v-25.082v-20.964 v-5.802c0-0.561,0-1.122,0-1.684c0-0.561,0.124-0.998,0.374-1.31c0.249-0.435,0.716-0.749,1.404-0.935 c0.125-0.062,0.248-0.077,0.375-0.047C187.632,330.4,187.756,330.383,187.883,330.321z"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
<polygon
|
||||
style={{ fill: '#BBAF98' }}
|
||||
points="276.167,208.741 0,302.069 0,186.053 512,186.053 512,302.069 "
|
||||
></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const LogIcon = (props: IconSvgProps) => (
|
||||
<svg
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
||||
<g
|
||||
id="SVGRepo_tracerCarrier"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<rect width="48" height="48" fill="white" fillOpacity="0.01"></rect>
|
||||
<rect
|
||||
x="13"
|
||||
y="10"
|
||||
width="28"
|
||||
height="34"
|
||||
fill="#2F88FF"
|
||||
stroke="#000000"
|
||||
strokeWidth="4"
|
||||
strokeLinejoin="round"
|
||||
></rect>
|
||||
<path
|
||||
d="M35 10V4H8C7.44772 4 7 4.44772 7 5V38H13"
|
||||
stroke="#000000"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M21 22H33"
|
||||
stroke="white"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M21 30H33"
|
||||
stroke="white"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
74
napcat.webui/src/components/sortable_tab.tsx
Normal file
74
napcat.webui/src/components/sortable_tab.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import clsx from 'clsx'
|
||||
import { useRef } from 'react'
|
||||
|
||||
import { Tab } from './tabs'
|
||||
|
||||
interface SortableTabProps {
|
||||
id: string
|
||||
value: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function SortableTab({
|
||||
id,
|
||||
value,
|
||||
children,
|
||||
className
|
||||
}: SortableTabProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging
|
||||
} = useSortable({ id })
|
||||
|
||||
const mouseDownTime = useRef<number>(0)
|
||||
const mouseDownPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? 1 : 0
|
||||
}
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
mouseDownTime.current = Date.now()
|
||||
mouseDownPos.current = { x: e.clientX, y: e.clientY }
|
||||
}
|
||||
|
||||
const handleMouseUp = (e: React.MouseEvent) => {
|
||||
const timeDiff = Date.now() - mouseDownTime.current
|
||||
const distanceX = Math.abs(e.clientX - mouseDownPos.current.x)
|
||||
const distanceY = Math.abs(e.clientY - mouseDownPos.current.y)
|
||||
|
||||
// 如果时间小于200ms且移动距离小于5px,认为是点击而不是拖拽
|
||||
if (timeDiff < 200 && distanceX < 5 && distanceY < 5) {
|
||||
listeners?.onClick?.(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tab
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
value={value}
|
||||
{...attributes}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
className={clsx(
|
||||
'cursor-move select-none border-b-2 transition-colors',
|
||||
isDragging
|
||||
? 'bg-default-100 border-primary'
|
||||
: 'hover:bg-default-100 border-transparent',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Tab>
|
||||
)
|
||||
}
|
83
napcat.webui/src/components/tabs/index.tsx
Normal file
83
napcat.webui/src/components/tabs/index.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import clsx from 'clsx'
|
||||
import { type ReactNode, createContext, forwardRef, useContext } from 'react'
|
||||
|
||||
interface TabsContextValue {
|
||||
activeKey: string
|
||||
onChange: (key: string) => void
|
||||
}
|
||||
|
||||
const TabsContext = createContext<TabsContextValue>({
|
||||
activeKey: '',
|
||||
onChange: () => {}
|
||||
})
|
||||
|
||||
interface TabsProps {
|
||||
activeKey: string
|
||||
onChange: (key: string) => void
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Tabs({ activeKey, onChange, children, className }: TabsProps) {
|
||||
return (
|
||||
<TabsContext.Provider value={{ activeKey, onChange }}>
|
||||
<div className={clsx('flex flex-col gap-2', className)}>{children}</div>
|
||||
</TabsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
interface TabListProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function TabList({ children, className }: TabListProps) {
|
||||
return (
|
||||
<div className={clsx('flex items-center gap-1', className)}>{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface TabProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
value: string
|
||||
className?: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const Tab = forwardRef<HTMLButtonElement, TabProps>(
|
||||
({ value, className, children, ...props }, ref) => {
|
||||
const { activeKey, onChange } = useContext(TabsContext)
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
onClick={() => onChange(value)}
|
||||
className={clsx(
|
||||
'px-4 py-2 rounded-t transition-colors',
|
||||
activeKey === value
|
||||
? 'bg-primary text-white'
|
||||
: 'hover:bg-default-100',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Tab.displayName = 'Tab'
|
||||
|
||||
interface TabPanelProps {
|
||||
value: string
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function TabPanel({ value, children, className }: TabPanelProps) {
|
||||
const { activeKey } = useContext(TabsContext)
|
||||
|
||||
if (value !== activeKey) return null
|
||||
|
||||
return <div className={clsx('flex-1', className)}>{children}</div>
|
||||
}
|
57
napcat.webui/src/components/terminal/terminal-instance.tsx
Normal file
57
napcat.webui/src/components/terminal/terminal-instance.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import WebUIManager from '@/controllers/webui_manager'
|
||||
|
||||
import XTerm, { XTermRef } from '../xterm'
|
||||
|
||||
interface TerminalInstanceProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
export function TerminalInstance({ id }: TerminalInstanceProps) {
|
||||
const termRef = useRef<XTermRef>(null)
|
||||
const wsRef = useRef<WebSocket>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const ws = WebUIManager.connectTerminal(id, (data) => {
|
||||
termRef.current?.write(data)
|
||||
})
|
||||
wsRef.current = ws
|
||||
|
||||
// 添加连接状态监听
|
||||
ws.onopen = () => {
|
||||
console.log('Terminal connected:', id)
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('Terminal connection error:', error)
|
||||
termRef.current?.write(
|
||||
'\r\n\x1b[31mConnection error. Please try reconnecting.\x1b[0m\r\n'
|
||||
)
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('Terminal disconnected:', id)
|
||||
termRef.current?.write('\r\n\x1b[31mConnection closed.\x1b[0m\r\n')
|
||||
}
|
||||
|
||||
return () => {
|
||||
ws.close()
|
||||
}
|
||||
}, [id])
|
||||
|
||||
const handleInput = (data: string) => {
|
||||
const ws = wsRef.current
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
ws.send(JSON.stringify({ type: 'input', data }))
|
||||
} catch (error) {
|
||||
console.error('Failed to send terminal input:', error)
|
||||
}
|
||||
} else {
|
||||
console.warn('WebSocket is not in OPEN state')
|
||||
}
|
||||
}
|
||||
|
||||
return <XTerm ref={termRef} onInput={handleInput} className="h-full" />
|
||||
}
|
12
napcat.webui/src/components/under_construction.tsx
Normal file
12
napcat.webui/src/components/under_construction.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export default function UnderConstruction() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full pt-4">
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<div className="text-6xl font-bold text-gray-500">🚧</div>
|
||||
<div className="text-2xl font-bold text-gray-500">
|
||||
Under Construction
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -22,11 +22,15 @@ export type XTermRef = {
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
const XTerm = forwardRef<XTermRef, React.HTMLAttributes<HTMLDivElement>>(
|
||||
(props, ref) => {
|
||||
export interface XTermProps
|
||||
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onInput'> {
|
||||
onInput?: (data: string) => void
|
||||
}
|
||||
|
||||
const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
||||
const domRef = useRef<HTMLDivElement>(null)
|
||||
const terminalRef = useRef<Terminal | null>(null)
|
||||
const { className, ...rest } = props
|
||||
const { className, onInput, ...rest } = props
|
||||
const { theme } = useTheme()
|
||||
useEffect(() => {
|
||||
if (!domRef.current) {
|
||||
@@ -64,6 +68,12 @@ const XTerm = forwardRef<XTermRef, React.HTMLAttributes<HTMLDivElement>>(
|
||||
)
|
||||
)
|
||||
|
||||
terminal.onData((data) => {
|
||||
if (onInput) {
|
||||
onInput(data)
|
||||
}
|
||||
})
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
fitAddon.fit()
|
||||
})
|
||||
@@ -147,7 +157,6 @@ const XTerm = forwardRef<XTermRef, React.HTMLAttributes<HTMLDivElement>>(
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
export default XTerm
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
BugIcon2,
|
||||
FileIcon,
|
||||
InfoIcon,
|
||||
LogIcon,
|
||||
RouteIcon,
|
||||
SettingsIcon,
|
||||
SignalTowerIcon,
|
||||
@@ -49,10 +51,10 @@ export const siteConfig = {
|
||||
href: '/config'
|
||||
},
|
||||
{
|
||||
label: '系统日志',
|
||||
label: 'NapCat日志',
|
||||
icon: (
|
||||
<div className="w-5 h-5">
|
||||
<TerminalIcon />
|
||||
<LogIcon />
|
||||
</div>
|
||||
),
|
||||
href: '/logs'
|
||||
@@ -75,6 +77,24 @@ export const siteConfig = {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '文件管理',
|
||||
icon: (
|
||||
<div className="w-5 h-5">
|
||||
<FileIcon />
|
||||
</div>
|
||||
),
|
||||
href: '/file_manager'
|
||||
},
|
||||
{
|
||||
label: '系统终端',
|
||||
icon: (
|
||||
<div className="w-5 h-5">
|
||||
<TerminalIcon />
|
||||
</div>
|
||||
),
|
||||
href: '/terminal'
|
||||
},
|
||||
{
|
||||
label: '关于我们',
|
||||
icon: (
|
||||
|
@@ -9,6 +9,14 @@ export interface Log {
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface TerminalSession {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface TerminalInfo {
|
||||
id: string
|
||||
}
|
||||
|
||||
export default class WebUIManager {
|
||||
public static async checkWebUiLogined() {
|
||||
const { data } =
|
||||
@@ -130,4 +138,94 @@ export default class WebUIManager {
|
||||
|
||||
return eventSource
|
||||
}
|
||||
|
||||
public static async createTerminal(
|
||||
cols: number,
|
||||
rows: number
|
||||
): Promise<TerminalSession> {
|
||||
const { data } = await serverRequest.post<ServerResponse<TerminalSession>>(
|
||||
'/Log/terminal/create',
|
||||
{ cols, rows }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async sendTerminalInput(
|
||||
id: string,
|
||||
input: string
|
||||
): Promise<void> {
|
||||
await serverRequest.post(`/Log/terminal/${id}/input`, { input })
|
||||
}
|
||||
|
||||
public static getTerminalStream(id: string, onData: (data: string) => void) {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) throw new Error('未登录')
|
||||
|
||||
const _token = JSON.parse(token)
|
||||
const eventSource = new EventSourcePolyfill(
|
||||
`/api/Log/terminal/${id}/stream`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${_token}`,
|
||||
Accept: 'text/event-stream'
|
||||
},
|
||||
withCredentials: true
|
||||
}
|
||||
)
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const { data } = JSON.parse(event.data)
|
||||
onData(data)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
return eventSource
|
||||
}
|
||||
|
||||
public static async closeTerminal(id: string): Promise<void> {
|
||||
await serverRequest.post(`/Log/terminal/${id}/close`)
|
||||
}
|
||||
|
||||
public static async getTerminalList(): Promise<TerminalInfo[]> {
|
||||
const { data } =
|
||||
await serverRequest.get<ServerResponse<TerminalInfo[]>>(
|
||||
'/Log/terminal/list'
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static connectTerminal(
|
||||
id: string,
|
||||
onData: (data: string) => void
|
||||
): WebSocket {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) throw new Error('未登录')
|
||||
|
||||
const _token = JSON.parse(token)
|
||||
const ws = new WebSocket(
|
||||
`ws://${window.location.host}/api/ws/terminal?id=${id}&token=${_token}`
|
||||
)
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const { data } = JSON.parse(event.data)
|
||||
onData(data)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket连接出错:', error)
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket连接关闭')
|
||||
}
|
||||
|
||||
return ws
|
||||
}
|
||||
}
|
||||
|
122
napcat.webui/src/pages/dashboard/terminal.tsx
Normal file
122
napcat.webui/src/pages/dashboard/terminal.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { DndContext, DragEndEvent, closestCenter } from '@dnd-kit/core'
|
||||
import {
|
||||
SortableContext,
|
||||
arrayMove,
|
||||
horizontalListSortingStrategy
|
||||
} from '@dnd-kit/sortable'
|
||||
import { Button } from '@heroui/button'
|
||||
import { useEffect, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { IoAdd, IoClose } from 'react-icons/io5'
|
||||
|
||||
import { SortableTab } from '@/components/sortable_tab'
|
||||
import { TabList, TabPanel, Tabs } from '@/components/tabs'
|
||||
import { TerminalInstance } from '@/components/terminal/terminal-instance'
|
||||
|
||||
import WebUIManager from '@/controllers/webui_manager'
|
||||
|
||||
interface TerminalTab {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export default function TerminalPage() {
|
||||
const [tabs, setTabs] = useState<TerminalTab[]>([])
|
||||
const [selectedTab, setSelectedTab] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
// 获取已存在的终端列表
|
||||
WebUIManager.getTerminalList().then((terminals) => {
|
||||
if (terminals.length === 0) return
|
||||
|
||||
const newTabs = terminals.map((terminal, index) => ({
|
||||
id: terminal.id,
|
||||
title: `Terminal ${index + 1}`
|
||||
}))
|
||||
|
||||
setTabs(newTabs)
|
||||
setSelectedTab(newTabs[0].id)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const createNewTerminal = async () => {
|
||||
try {
|
||||
const { id } = await WebUIManager.createTerminal(80, 24)
|
||||
const newTab = {
|
||||
id,
|
||||
title: `Terminal ${tabs.length + 1}`
|
||||
}
|
||||
|
||||
setTabs((prev) => [...prev, newTab])
|
||||
setSelectedTab(id)
|
||||
} catch (error) {
|
||||
console.error('Failed to create terminal:', error)
|
||||
toast.error('创建终端失败')
|
||||
}
|
||||
}
|
||||
|
||||
const closeTerminal = async (id: string) => {
|
||||
try {
|
||||
await WebUIManager.closeTerminal(id)
|
||||
setTabs((prev) => prev.filter((tab) => tab.id !== id))
|
||||
if (selectedTab === id) {
|
||||
setSelectedTab(tabs[0]?.id || '')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('关闭终端失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (active.id !== over?.id) {
|
||||
setTabs((items) => {
|
||||
const oldIndex = items.findIndex((item) => item.id === active.id)
|
||||
const newIndex = items.findIndex((item) => item.id === over?.id)
|
||||
return arrayMove(items, oldIndex, newIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-2 p-4">
|
||||
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<Tabs activeKey={selectedTab} onChange={setSelectedTab}>
|
||||
<div className="flex items-center gap-2">
|
||||
<TabList className="flex-1">
|
||||
<SortableContext
|
||||
items={tabs}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<SortableTab key={tab.id} id={tab.id} value={tab.id}>
|
||||
{tab.title}
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
size="sm"
|
||||
className="ml-2"
|
||||
onPress={() => closeTerminal(tab.id)}
|
||||
>
|
||||
<IoClose />
|
||||
</Button>
|
||||
</SortableTab>
|
||||
))}
|
||||
</SortableContext>
|
||||
</TabList>
|
||||
<Button
|
||||
isIconOnly
|
||||
onPress={createNewTerminal}
|
||||
startContent={<IoAdd />}
|
||||
/>
|
||||
</div>
|
||||
{tabs.map((tab) => (
|
||||
<TabPanel key={tab.id} value={tab.id} className="flex-1">
|
||||
<TerminalInstance id={tab.id} />
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
</DndContext>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import { Route, Routes, useLocation } from 'react-router-dom'
|
||||
|
||||
import UnderConstruction from '@/components/under_construction'
|
||||
|
||||
import DefaultLayout from '@/layouts/default'
|
||||
|
||||
import DashboardIndexPage from './dashboard'
|
||||
@@ -11,6 +13,7 @@ import HttpDebug from './dashboard/debug/http'
|
||||
import WSDebug from './dashboard/debug/websocket'
|
||||
import LogsPage from './dashboard/logs'
|
||||
import NetworkPage from './dashboard/network'
|
||||
import TerminalPage from './dashboard/terminal'
|
||||
|
||||
export default function IndexPage() {
|
||||
const location = useLocation()
|
||||
@@ -33,6 +36,8 @@ export default function IndexPage() {
|
||||
<Route path="ws" element={<WSDebug />} />
|
||||
<Route path="http" element={<HttpDebug />} />
|
||||
</Route>
|
||||
<Route element={<UnderConstruction />} path="/file_manager" />
|
||||
<Route element={<TerminalPage />} path="/terminal" />
|
||||
<Route element={<AboutPage />} path="/about" />
|
||||
</Routes>
|
||||
</motion.div>
|
||||
|
@@ -11,6 +11,7 @@ import { cors } from '@webapi/middleware/cors';
|
||||
import { createUrl } from '@webapi/utils/url';
|
||||
import { sendSuccess } from '@webapi/utils/response';
|
||||
import { join } from 'node:path';
|
||||
import { terminalManager } from '@webapi/terminal/terminal_manager';
|
||||
|
||||
// 实例化Express
|
||||
const app = express();
|
||||
@@ -45,6 +46,8 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
||||
// ------------挂载路由------------
|
||||
// 挂载静态路由(前端),路径为 [/前缀]/webui
|
||||
app.use('/webui', express.static(pathWrapper.staticPath));
|
||||
// 初始化WebSocket服务器
|
||||
terminalManager.initialize(app);
|
||||
// 挂载API接口
|
||||
app.use('/api', ALLRouter);
|
||||
// 所有剩下的请求都转到静态页面
|
||||
|
@@ -2,6 +2,7 @@ import type { RequestHandler } from 'express';
|
||||
import { sendError, sendSuccess } from '../utils/response';
|
||||
import { WebUiConfigWrapper } from '../helper/config';
|
||||
import { logSubscription } from '@/common/log';
|
||||
import { terminalManager } from '../terminal/terminal_manager';
|
||||
|
||||
// 日志记录
|
||||
export const LogHandler: RequestHandler = async (req, res) => {
|
||||
@@ -35,3 +36,65 @@ export const LogRealTimeHandler: RequestHandler = async (req, res) => {
|
||||
logSubscription.unsubscribe(listener);
|
||||
});
|
||||
};
|
||||
|
||||
// 终端相关处理器
|
||||
export const CreateTerminalHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const id = Math.random().toString(36).substring(2);
|
||||
terminalManager.createTerminal(id);
|
||||
return sendSuccess(res, { id });
|
||||
} catch (error) {
|
||||
console.error('Failed to create terminal:', error);
|
||||
return sendError(res, '创建终端失败');
|
||||
}
|
||||
};
|
||||
|
||||
export const GetTerminalListHandler: RequestHandler = (req, res) => {
|
||||
const list = terminalManager.getTerminalList();
|
||||
return sendSuccess(res, list);
|
||||
};
|
||||
|
||||
export const CloseTerminalHandler: RequestHandler = (req, res) => {
|
||||
const id = req.params.id;
|
||||
terminalManager.closeTerminal(id);
|
||||
return sendSuccess(res, {});
|
||||
};
|
||||
|
||||
// 终端数据交换
|
||||
export const TerminalHandler: RequestHandler = (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!terminalManager.getTerminal(id)) {
|
||||
return sendError(res, '终端不存在');
|
||||
}
|
||||
|
||||
if (req.body.input) {
|
||||
terminalManager.writeTerminal(id, req.body.input);
|
||||
}
|
||||
|
||||
return sendSuccess(res, {});
|
||||
};
|
||||
|
||||
// 终端数据流(SSE)
|
||||
export const TerminalStreamHandler: RequestHandler = (req, res) => {
|
||||
const id = req.params.id;
|
||||
const instance = terminalManager.getTerminal(id);
|
||||
|
||||
if (!instance) {
|
||||
return sendError(res, '终端不存在');
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
|
||||
const dataHandler = (data: string) => {
|
||||
if (!res.writableEnded) {
|
||||
res.write(`data: ${JSON.stringify({ type: 'output', data })}\n\n`);
|
||||
}
|
||||
};
|
||||
|
||||
const dispose = terminalManager.onTerminalData(id, dataHandler);
|
||||
|
||||
req.on('close', () => {
|
||||
dispose();
|
||||
});
|
||||
};
|
||||
|
@@ -1,13 +1,23 @@
|
||||
import { Router } from 'express';
|
||||
import { LogHandler, LogListHandler, LogRealTimeHandler } from '../api/Log';
|
||||
import {
|
||||
LogHandler,
|
||||
LogListHandler,
|
||||
LogRealTimeHandler,
|
||||
CreateTerminalHandler,
|
||||
GetTerminalListHandler,
|
||||
CloseTerminalHandler,
|
||||
} from '../api/Log';
|
||||
|
||||
const router = Router();
|
||||
// router:读取日志内容
|
||||
router.get('/GetLog', LogHandler);
|
||||
// router:读取日志列表
|
||||
router.get('/GetLogList', LogListHandler);
|
||||
|
||||
// router:实时日志
|
||||
// 日志相关路由
|
||||
router.get('/GetLog', LogHandler);
|
||||
router.get('/GetLogList', LogListHandler);
|
||||
router.get('/GetLogRealTime', LogRealTimeHandler);
|
||||
|
||||
// 终端相关路由
|
||||
router.get('/terminal/list', GetTerminalListHandler);
|
||||
router.post('/terminal/create', CreateTerminalHandler);
|
||||
router.post('/terminal/:id/close', CloseTerminalHandler);
|
||||
|
||||
export { router as LogRouter };
|
||||
|
155
src/webui/src/terminal/terminal_manager.ts
Normal file
155
src/webui/src/terminal/terminal_manager.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { WebUiConfig } from '@/webui';
|
||||
import { AuthHelper } from '../helper/SignToken';
|
||||
import { spawn, type ChildProcess } from 'child_process';
|
||||
import * as os from 'os';
|
||||
import { WebSocket, WebSocketServer } from 'ws';
|
||||
|
||||
interface TerminalInstance {
|
||||
process: ChildProcess;
|
||||
lastAccess: number;
|
||||
dataHandlers: Set<(data: string) => void>;
|
||||
}
|
||||
|
||||
class TerminalManager {
|
||||
private terminals: Map<string, TerminalInstance> = new Map();
|
||||
private wss: WebSocketServer | null = null;
|
||||
|
||||
initialize(server: any) {
|
||||
this.wss = new WebSocketServer({
|
||||
server,
|
||||
path: '/api/ws/terminal',
|
||||
});
|
||||
|
||||
this.wss.on('connection', async (ws, req) => {
|
||||
try {
|
||||
const url = new URL(req.url || '', 'ws://localhost');
|
||||
const token = url.searchParams.get('token');
|
||||
const terminalId = url.searchParams.get('id');
|
||||
|
||||
if (!token || !terminalId) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证 token
|
||||
// 解析token
|
||||
let Credential: WebUiCredentialJson;
|
||||
try {
|
||||
Credential = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
|
||||
} catch (e) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
const config = await WebUiConfig.GetWebUIConfig();
|
||||
const validate = AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
|
||||
|
||||
if (!validate) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = this.terminals.get(terminalId);
|
||||
if (!instance) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const dataHandler = (data: string) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'output', data }));
|
||||
}
|
||||
};
|
||||
instance.dataHandlers.add(dataHandler);
|
||||
|
||||
ws.on('message', (message) => {
|
||||
try {
|
||||
const data = JSON.parse(message.toString());
|
||||
if (data.type === 'input') {
|
||||
this.writeTerminal(terminalId, data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to process terminal input:', error);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
instance.dataHandlers.delete(dataHandler);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('WebSocket authentication failed:', err);
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createTerminal(id: string) {
|
||||
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
||||
const shellProcess = spawn(shell, [], {
|
||||
env: process.env,
|
||||
shell: true,
|
||||
});
|
||||
|
||||
const instance: TerminalInstance = {
|
||||
process: shellProcess,
|
||||
lastAccess: Date.now(),
|
||||
dataHandlers: new Set(),
|
||||
};
|
||||
|
||||
// 修改这里,使用 shellProcess 而不是 process
|
||||
shellProcess.stdout.on('data', (data) => {
|
||||
const str = data.toString();
|
||||
instance.dataHandlers.forEach((handler) => handler(str));
|
||||
});
|
||||
|
||||
shellProcess.stderr.on('data', (data) => {
|
||||
const str = data.toString();
|
||||
instance.dataHandlers.forEach((handler) => handler(str));
|
||||
});
|
||||
|
||||
this.terminals.set(id, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
getTerminal(id: string) {
|
||||
return this.terminals.get(id);
|
||||
}
|
||||
|
||||
closeTerminal(id: string) {
|
||||
const instance = this.terminals.get(id);
|
||||
if (instance) {
|
||||
instance.process.kill();
|
||||
this.terminals.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
onTerminalData(id: string, handler: (data: string) => void) {
|
||||
const instance = this.terminals.get(id);
|
||||
if (instance) {
|
||||
instance.dataHandlers.add(handler);
|
||||
return () => {
|
||||
instance.dataHandlers.delete(handler);
|
||||
};
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
|
||||
writeTerminal(id: string, data: string) {
|
||||
const instance = this.terminals.get(id);
|
||||
if (instance && instance.process.stdin) {
|
||||
instance.process.stdin.write(data, (error) => {
|
||||
if (error) {
|
||||
console.error('Failed to write to terminal:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getTerminalList() {
|
||||
return Array.from(this.terminals.keys()).map((id) => ({
|
||||
id,
|
||||
lastAccess: this.terminals.get(id)!.lastAccess,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export const terminalManager = new TerminalManager();
|
Reference in New Issue
Block a user