mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-12-19 05:05:44 +08:00
fix: 字体、终端样式
This commit is contained in:
parent
8002dc5bc5
commit
0f3251f35b
@ -46,9 +46,9 @@
|
|||||||
"@react-aria/visually-hidden": "^3.8.19",
|
"@react-aria/visually-hidden": "^3.8.19",
|
||||||
"@reduxjs/toolkit": "^2.5.1",
|
"@reduxjs/toolkit": "^2.5.1",
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
|
"@xterm/addon-canvas": "^0.7.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"ahooks": "^3.8.4",
|
"ahooks": "^3.8.4",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
|||||||
BIN
napcat.webui/public/fonts/AaCute.woff
Normal file
BIN
napcat.webui/public/fonts/AaCute.woff
Normal file
Binary file not shown.
BIN
napcat.webui/public/fonts/JetBrainsMono-Italic.ttf
Normal file
BIN
napcat.webui/public/fonts/JetBrainsMono-Italic.ttf
Normal file
Binary file not shown.
@ -27,7 +27,7 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
|||||||
<CardBody className="items-center md:gap-1 p-1 md:p-2">
|
<CardBody className="items-center md:gap-1 p-1 md:p-2">
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'font-outfit flex-1',
|
'flex-1',
|
||||||
size === 'md' ? 'text-2xl md:text-3xl' : 'text-xl md:text-2xl',
|
size === 'md' ? 'text-2xl md:text-3xl' : 'text-xl md:text-2xl',
|
||||||
title({
|
title({
|
||||||
color: size === 'md' ? 'pink' : 'yellow',
|
color: size === 'md' ? 'pink' : 'yellow',
|
||||||
|
|||||||
@ -58,7 +58,7 @@ export default function FileTable({
|
|||||||
onDownload
|
onDownload
|
||||||
}: FileTableProps) {
|
}: FileTableProps) {
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const pages = Math.ceil(files.length / PAGE_SIZE)
|
const pages = Math.ceil(files.length / PAGE_SIZE) || 1
|
||||||
const start = (page - 1) * PAGE_SIZE
|
const start = (page - 1) * PAGE_SIZE
|
||||||
const end = start + PAGE_SIZE
|
const end = start + PAGE_SIZE
|
||||||
const displayFiles = files.slice(start, end)
|
const displayFiles = files.slice(start, end)
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export default function Hitokoto() {
|
|||||||
<div className="text-danger-400">一言加载失败:{error.message}</div>
|
<div className="text-danger-400">一言加载失败:{error.message}</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="font-noto-serif">{data?.hitokoto}</div>
|
<div>{data?.hitokoto}</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
—— <span className="text-default-400">{data?.from}</span>{' '}
|
—— <span className="text-default-400">{data?.from}</span>{' '}
|
||||||
{data?.from_who}
|
{data?.from_who}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export default function HoverTiltedCard({
|
|||||||
rotateAmplitude = 14,
|
rotateAmplitude = 14,
|
||||||
showTooltip = false,
|
showTooltip = false,
|
||||||
overlayContent = (
|
overlayContent = (
|
||||||
<div className="text-center font-ubuntu mt-6 px-4 py-0.5 shadow-lg rounded-full bg-danger-600 text-default-100 bg-opacity-80">
|
<div className="text-center mt-6 px-4 py-0.5 shadow-lg rounded-full bg-danger-600 text-default-100 bg-opacity-80">
|
||||||
NapCat
|
NapCat
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -138,7 +138,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
shadow="sm"
|
shadow="sm"
|
||||||
className="my-4 bg-opacity-50 backdrop-blur-md overflow-visible z-20"
|
className="my-4 bg-opacity-50 backdrop-blur-md overflow-visible z-20"
|
||||||
>
|
>
|
||||||
<CardHeader className="font-noto-serif font-bold text-lg gap-1 pb-0">
|
<CardHeader className="font-bold text-lg gap-1 pb-0">
|
||||||
<span className="mr-2">请求体</span>
|
<span className="mr-2">请求体</span>
|
||||||
<Button
|
<Button
|
||||||
color="warning"
|
color="warning"
|
||||||
@ -186,7 +186,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
className="my-4 relative bg-opacity-50 backdrop-blur-md"
|
className="my-4 relative bg-opacity-50 backdrop-blur-md"
|
||||||
>
|
>
|
||||||
<PageLoading loading={isFetching} />
|
<PageLoading loading={isFetching} />
|
||||||
<CardHeader className="font-noto-serif font-bold text-lg gap-1 pb-0">
|
<CardHeader className="font-bold text-lg gap-1 pb-0">
|
||||||
<span className="mr-2">响应</span>
|
<span className="mr-2">响应</span>
|
||||||
<Button
|
<Button
|
||||||
color="warning"
|
color="warning"
|
||||||
|
|||||||
@ -67,7 +67,7 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
|||||||
onPress={() => onSelect(apiName as OneBotHttpApiPath)}
|
onPress={() => onSelect(apiName as OneBotHttpApiPath)}
|
||||||
>
|
>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<h2 className="font-ubuntu font-bold">{api.description}</h2>
|
<h2 className="font-bold">{api.description}</h2>
|
||||||
<div
|
<div
|
||||||
className={clsx('text-sm text-danger-200', {
|
className={clsx('text-sm text-danger-200', {
|
||||||
'!text-danger-400': apiName === selectedApi
|
'!text-danger-400': apiName === selectedApi
|
||||||
|
|||||||
@ -23,9 +23,7 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
|||||||
<PageLoading loading={loading} />
|
<PageLoading loading={loading} />
|
||||||
{error ? (
|
{error ? (
|
||||||
<CardBody className="items-center gap-1 justify-center">
|
<CardBody className="items-center gap-1 justify-center">
|
||||||
<div className="font-outfit flex-1 text-content1-foreground">
|
<div className="flex-1 text-content1-foreground">Error</div>
|
||||||
Error
|
|
||||||
</div>
|
|
||||||
<div className="whitespace-nowrap text-nowrap flex-shrink-0">
|
<div className="whitespace-nowrap text-nowrap flex-shrink-0">
|
||||||
{error.message}
|
{error.message}
|
||||||
</div>
|
</div>
|
||||||
@ -51,10 +49,8 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-col justify-center">
|
<div className="flex-col justify-center">
|
||||||
<div className="font-outfit text-lg truncate">{data?.nick}</div>
|
<div className="text-lg truncate">{data?.nick}</div>
|
||||||
<div className="font-ubuntu text-danger-500 text-sm">
|
<div className="text-danger-500 text-sm">{data?.uin}</div>
|
||||||
{data?.uin}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -47,11 +47,11 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
|||||||
style={{ overflow: 'hidden' }}
|
style={{ overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
<motion.div className="w-64 flex flex-col items-stretch h-full transition-transform duration-300 ease-in-out z-30 relative float-right">
|
<motion.div className="w-64 flex flex-col items-stretch h-full transition-transform duration-300 ease-in-out z-30 relative float-right">
|
||||||
<div className="flex justify-center items-center mt-2 gap-2">
|
<div className="flex justify-center items-center my-2 gap-2">
|
||||||
<Image radius="none" height={40} src={logo} className="mb-2" />
|
<Image radius="none" height={40} src={logo} className="mb-2" />
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex items-center hm-medium',
|
'flex items-center font-bold',
|
||||||
'!text-2xl shiny-text'
|
'!text-2xl shiny-text'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -10,23 +10,24 @@ interface TerminalInstanceProps {
|
|||||||
|
|
||||||
export function TerminalInstance({ id }: TerminalInstanceProps) {
|
export function TerminalInstance({ id }: TerminalInstanceProps) {
|
||||||
const termRef = useRef<XTermRef>(null)
|
const termRef = useRef<XTermRef>(null)
|
||||||
|
const connected = useRef(false)
|
||||||
|
|
||||||
|
const handleData = (data: string) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data)
|
||||||
|
if (parsed.data) {
|
||||||
|
termRef.current?.write(parsed.data)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
termRef.current?.write(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleData = (data: string) => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(data)
|
|
||||||
if (parsed.data) {
|
|
||||||
termRef.current?.write(parsed.data)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
termRef.current?.write(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TerminalManager.connectTerminal(id, handleData)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
TerminalManager.disconnectTerminal(id, handleData)
|
if (connected.current) {
|
||||||
|
TerminalManager.disconnectTerminal(id, handleData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
@ -34,5 +35,22 @@ export function TerminalInstance({ id }: TerminalInstanceProps) {
|
|||||||
TerminalManager.sendInput(id, data)
|
TerminalManager.sendInput(id, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <XTerm ref={termRef} onInput={handleInput} className="w-full h-full" />
|
const handleResize = (cols: number, rows: number) => {
|
||||||
|
if (!connected.current) {
|
||||||
|
connected.current = true
|
||||||
|
console.log('instance', rows, cols)
|
||||||
|
TerminalManager.connectTerminal(id, handleData, { rows, cols })
|
||||||
|
} else {
|
||||||
|
TerminalManager.sendResize(id, cols, rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<XTerm
|
||||||
|
ref={termRef}
|
||||||
|
onInput={handleInput}
|
||||||
|
onResize={handleResize} // 使用 fitAddon 改变后触发的 resize 回调
|
||||||
|
className="w-full h-full"
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
|
import { CanvasAddon } from '@xterm/addon-canvas'
|
||||||
import { FitAddon } from '@xterm/addon-fit'
|
import { FitAddon } from '@xterm/addon-fit'
|
||||||
import { WebLinksAddon } from '@xterm/addon-web-links'
|
import { WebLinksAddon } from '@xterm/addon-web-links'
|
||||||
|
// import { WebglAddon } from '@xterm/addon-webgl'
|
||||||
import { Terminal } from '@xterm/xterm'
|
import { Terminal } from '@xterm/xterm'
|
||||||
import '@xterm/xterm/css/xterm.css'
|
import '@xterm/xterm/css/xterm.css'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
@ -7,8 +9,6 @@ import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
|
|||||||
|
|
||||||
import { useTheme } from '@/hooks/use-theme'
|
import { useTheme } from '@/hooks/use-theme'
|
||||||
|
|
||||||
import { gradientText } from '@/utils/terminal'
|
|
||||||
|
|
||||||
export type XTermRef = {
|
export type XTermRef = {
|
||||||
write: (
|
write: (
|
||||||
...args: Parameters<Terminal['write']>
|
...args: Parameters<Terminal['write']>
|
||||||
@ -19,26 +19,26 @@ export type XTermRef = {
|
|||||||
) => ReturnType<Terminal['writeln']>
|
) => ReturnType<Terminal['writeln']>
|
||||||
writelnAsync: (data: Parameters<Terminal['writeln']>[0]) => Promise<void>
|
writelnAsync: (data: Parameters<Terminal['writeln']>[0]) => Promise<void>
|
||||||
clear: () => void
|
clear: () => void
|
||||||
|
terminalRef: React.RefObject<Terminal | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface XTermProps
|
export interface XTermProps
|
||||||
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onInput'> {
|
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onInput' | 'onResize'> {
|
||||||
onInput?: (data: string) => void
|
onInput?: (data: string) => void
|
||||||
onKey?: (key: string, event: KeyboardEvent) => void
|
onKey?: (key: string, event: KeyboardEvent) => void
|
||||||
|
onResize?: (cols: number, rows: number) => void // 新增属性
|
||||||
}
|
}
|
||||||
|
|
||||||
const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
||||||
const domRef = useRef<HTMLDivElement>(null)
|
const domRef = useRef<HTMLDivElement>(null)
|
||||||
const terminalRef = useRef<Terminal | null>(null)
|
const terminalRef = useRef<Terminal | null>(null)
|
||||||
const { className, onInput, onKey, ...rest } = props
|
const { className, onInput, onKey, onResize, ...rest } = props
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!domRef.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const terminal = new Terminal({
|
const terminal = new Terminal({
|
||||||
allowTransparency: true,
|
allowTransparency: true,
|
||||||
fontFamily: '"JetBrains Mono", "Aa偷吃可爱长大的", "Noto Serif SC", monospace',
|
fontFamily:
|
||||||
|
'"JetBrains Mono", "Aa偷吃可爱长大的", "Noto Serif SC", monospace',
|
||||||
cursorInactiveStyle: 'outline',
|
cursorInactiveStyle: 'outline',
|
||||||
drawBoldTextInBrightColors: false,
|
drawBoldTextInBrightColors: false,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@ -48,25 +48,15 @@ const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
|||||||
const fitAddon = new FitAddon()
|
const fitAddon = new FitAddon()
|
||||||
terminal.loadAddon(
|
terminal.loadAddon(
|
||||||
new WebLinksAddon((event, uri) => {
|
new WebLinksAddon((event, uri) => {
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey || event.metaKey) {
|
||||||
window.open(uri, '_blank')
|
window.open(uri, '_blank')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
terminal.loadAddon(fitAddon)
|
terminal.loadAddon(fitAddon)
|
||||||
terminal.open(domRef.current)
|
terminal.open(domRef.current!)
|
||||||
|
|
||||||
terminal.writeln(
|
|
||||||
gradientText(
|
|
||||||
'Welcome to NapCat WebUI',
|
|
||||||
[255, 0, 0],
|
|
||||||
[0, 255, 0],
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
terminal.loadAddon(new CanvasAddon())
|
||||||
terminal.onData((data) => {
|
terminal.onData((data) => {
|
||||||
if (onInput) {
|
if (onInput) {
|
||||||
onInput(data)
|
onInput(data)
|
||||||
@ -81,6 +71,12 @@ const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
|||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
fitAddon.fit()
|
fitAddon.fit()
|
||||||
|
// 获取当前终端尺寸
|
||||||
|
const cols = terminal.cols
|
||||||
|
const rows = terminal.rows
|
||||||
|
if (onResize) {
|
||||||
|
onResize(cols, rows)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 字体加载完成后重新调整终端大小
|
// 字体加载完成后重新调整终端大小
|
||||||
@ -100,21 +96,49 @@ const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (terminalRef.current) {
|
if (terminalRef.current) {
|
||||||
terminalRef.current.options.theme = {
|
if (theme === 'dark') {
|
||||||
background: theme === 'dark' ? '#00000000' : '#ffffff00',
|
terminalRef.current.options.theme = {
|
||||||
foreground: theme === 'dark' ? '#fff' : '#000',
|
background: '#00000000',
|
||||||
selectionBackground:
|
black: '#000000',
|
||||||
theme === 'dark'
|
red: '#cd3131',
|
||||||
? 'rgba(179, 0, 0, 0.3)'
|
green: '#0dbc79',
|
||||||
: 'rgba(255, 167, 167, 0.3)',
|
yellow: '#e5e510',
|
||||||
cursor: theme === 'dark' ? '#fff' : '#000',
|
blue: '#2472c8',
|
||||||
cursorAccent: theme === 'dark' ? '#000' : '#fff',
|
cyan: '#11a8cd',
|
||||||
black: theme === 'dark' ? '#fff' : '#000'
|
white: '#e5e5e5',
|
||||||
|
brightBlack: '#666666',
|
||||||
|
brightRed: '#f14c4c',
|
||||||
|
brightGreen: '#23d18b',
|
||||||
|
brightYellow: '#f5f543',
|
||||||
|
brightBlue: '#3b8eea',
|
||||||
|
brightCyan: '#29b8db',
|
||||||
|
brightWhite: '#e5e5e5',
|
||||||
|
foreground: '#cccccc',
|
||||||
|
selectionBackground: '#3a3d41',
|
||||||
|
cursor: '#ffffff'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
terminalRef.current.options.theme = {
|
||||||
|
background: '#ffffff00',
|
||||||
|
black: '#000000',
|
||||||
|
red: '#aa3731',
|
||||||
|
green: '#448c27',
|
||||||
|
yellow: '#cb9000',
|
||||||
|
blue: '#325cc0',
|
||||||
|
cyan: '#0083b2',
|
||||||
|
white: '#7f7f7f',
|
||||||
|
brightBlack: '#777777',
|
||||||
|
brightRed: '#f05050',
|
||||||
|
brightGreen: '#60cb00',
|
||||||
|
brightYellow: '#ffbc5d',
|
||||||
|
brightBlue: '#007acc',
|
||||||
|
brightCyan: '#00aacb',
|
||||||
|
brightWhite: '#b0b0b0',
|
||||||
|
foreground: '#000000',
|
||||||
|
selectionBackground: '#bfdbfe',
|
||||||
|
cursor: '#007acc'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
terminalRef.current.options.fontWeight =
|
|
||||||
theme === 'dark' ? 'normal' : '600'
|
|
||||||
terminalRef.current.options.fontWeightBold =
|
|
||||||
theme === 'dark' ? 'bold' : '900'
|
|
||||||
}
|
}
|
||||||
}, [theme])
|
}, [theme])
|
||||||
|
|
||||||
@ -139,7 +163,8 @@ const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
|||||||
},
|
},
|
||||||
clear: () => {
|
clear: () => {
|
||||||
terminalRef.current?.clear()
|
terminalRef.current?.clear()
|
||||||
}
|
},
|
||||||
|
terminalRef: terminalRef
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export const siteConfig = {
|
|||||||
href: '/config'
|
href: '/config'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'NapCat日志',
|
label: '猫猫日志',
|
||||||
icon: (
|
icon: (
|
||||||
<div className="w-5 h-5">
|
<div className="w-5 h-5">
|
||||||
<LogIcon />
|
<LogIcon />
|
||||||
|
|||||||
@ -41,9 +41,16 @@ class TerminalManager {
|
|||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
connectTerminal(id: string, callback: TerminalCallback): WebSocket {
|
connectTerminal(
|
||||||
|
id: string,
|
||||||
|
callback: TerminalCallback,
|
||||||
|
config?: {
|
||||||
|
cols?: number
|
||||||
|
rows?: number
|
||||||
|
}
|
||||||
|
): WebSocket {
|
||||||
let conn = this.connections.get(id)
|
let conn = this.connections.get(id)
|
||||||
|
const { cols = 80, rows = 24 } = config || {}
|
||||||
if (!conn) {
|
if (!conn) {
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
url.protocol = url.protocol.replace('http', 'ws')
|
url.protocol = url.protocol.replace('http', 'ws')
|
||||||
@ -74,6 +81,7 @@ class TerminalManager {
|
|||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
if (conn) conn.isConnected = true
|
if (conn) conn.isConnected = true
|
||||||
|
this.sendResize(id, cols, rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
@ -111,6 +119,13 @@ class TerminalManager {
|
|||||||
conn.ws.send(JSON.stringify({ type: 'input', data }))
|
conn.ws.send(JSON.stringify({ type: 'input', data }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendResize(id: string, cols: number, rows: number) {
|
||||||
|
const conn = this.connections.get(id)
|
||||||
|
if (conn?.ws.readyState === WebSocket.OPEN) {
|
||||||
|
conn.ws.send(JSON.stringify({ type: 'resize', cols, rows }))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const terminalManager = new TerminalManager()
|
const terminalManager = new TerminalManager()
|
||||||
|
|||||||
Binary file not shown.
@ -98,7 +98,7 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'h-10 flex items-center hm-medium text-xl backdrop-blur-lg rounded-full',
|
'h-10 flex items-center font-bold text-xl backdrop-blur-lg rounded-full',
|
||||||
'dark:bg-background dark:shadow-danger-100',
|
'dark:bg-background dark:shadow-danger-100',
|
||||||
'bg-background !bg-opacity-50',
|
'bg-background !bg-opacity-50',
|
||||||
'shadow-sm shadow-danger-50',
|
'shadow-sm shadow-danger-50',
|
||||||
|
|||||||
@ -100,7 +100,7 @@ export default function TerminalPage() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 p-4">
|
<div className="flex flex-col gap-2 p-4 h-[calc(100vh-6rem)] md:h-[calc(100vh-4rem)]">
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
|
|||||||
@ -1,17 +1,13 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Aa偷吃可爱长大的';
|
font-family: 'Aa偷吃可爱长大的';
|
||||||
src: url('../fonts/AaCute.ttf') format('truetype');
|
src: url('/fonts/AaCute.woff') format('woff');
|
||||||
font-weight: normal;
|
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'JetBrains Mono';
|
font-family: 'JetBrains Mono';
|
||||||
src: url('../fonts/JetBrainsMono.ttf') format('truetype');
|
src: url('/fonts/JetBrainsMono.ttf') format('truetype');
|
||||||
font-weight: normal;
|
}
|
||||||
font-style: normal;
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
src: url('/fonts/JetBrainsMono-Italic.ttf') format('truetype');
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
.xterm {
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
font-smooth: always;
|
|
||||||
}
|
|
||||||
@ -6,35 +6,14 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
font-family:
|
font-family:
|
||||||
PingFang SC,
|
|
||||||
'Aa偷吃可爱长大的',
|
'Aa偷吃可爱长大的',
|
||||||
|
PingFang SC,
|
||||||
Helvetica Neue,
|
Helvetica Neue,
|
||||||
Microsoft YaHei,
|
Microsoft YaHei,
|
||||||
sans-serif !important;
|
sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.hm-medium {
|
|
||||||
font-family:
|
|
||||||
PingFang SC,
|
|
||||||
'Aa偷吃可爱长大的',
|
|
||||||
Helvetica Neue,
|
|
||||||
Microsoft YaHei,
|
|
||||||
sans-serif !important;
|
|
||||||
@apply font-bold;
|
|
||||||
}
|
|
||||||
.font-ubuntu {
|
|
||||||
font-family: 'Aa偷吃可爱长大的', sans-serif;
|
|
||||||
}
|
|
||||||
.font-outfit {
|
|
||||||
font-family: 'Aa偷吃可爱长大的', sans-serif;
|
|
||||||
}
|
|
||||||
.font-libre {
|
|
||||||
font-family: 'Aa偷吃可爱长大的', serif;
|
|
||||||
}
|
|
||||||
.font-noto-serif {
|
|
||||||
font-family: 'Aa偷吃可爱长大的', serif;
|
|
||||||
}
|
|
||||||
.hide-scrollbar::-webkit-scrollbar {
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
width: 0 !important;
|
width: 0 !important;
|
||||||
height: 0 !important;
|
height: 0 !important;
|
||||||
@ -105,7 +84,7 @@ body {
|
|||||||
.context-view.monaco-menu-container * {
|
.context-view.monaco-menu-container * {
|
||||||
font-family:
|
font-family:
|
||||||
PingFang SC,
|
PingFang SC,
|
||||||
'Harmony',
|
'Aa偷吃可爱长大的',
|
||||||
Helvetica Neue,
|
Helvetica Neue,
|
||||||
Microsoft YaHei,
|
Microsoft YaHei,
|
||||||
sans-serif !important;
|
sans-serif !important;
|
||||||
@ -117,3 +96,10 @@ body {
|
|||||||
.ql-editor img {
|
.ql-editor img {
|
||||||
@apply inline-block;
|
@apply inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.xterm {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
font-smooth: always;
|
||||||
|
}
|
||||||
|
|||||||
@ -6,7 +6,11 @@ import { terminalManager } from '../terminal/terminal_manager';
|
|||||||
|
|
||||||
// 日志记录
|
// 日志记录
|
||||||
export const LogHandler: RequestHandler = async (req, res) => {
|
export const LogHandler: RequestHandler = async (req, res) => {
|
||||||
const filename = req.query.id as string;
|
const filename = req.query['id'];
|
||||||
|
if (!filename || typeof filename !== 'string') {
|
||||||
|
return sendError(res, 'ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
if (filename.includes('..')) {
|
if (filename.includes('..')) {
|
||||||
return sendError(res, 'ID不合法');
|
return sendError(res, 'ID不合法');
|
||||||
}
|
}
|
||||||
@ -40,7 +44,8 @@ export const LogRealTimeHandler: RequestHandler = async (req, res) => {
|
|||||||
// 终端相关处理器
|
// 终端相关处理器
|
||||||
export const CreateTerminalHandler: RequestHandler = async (req, res) => {
|
export const CreateTerminalHandler: RequestHandler = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = terminalManager.createTerminal();
|
const { cols, rows } = req.body;
|
||||||
|
const { id } = terminalManager.createTerminal(cols, rows);
|
||||||
return sendSuccess(res, { id });
|
return sendSuccess(res, { id });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create terminal:', error);
|
console.error('Failed to create terminal:', error);
|
||||||
@ -54,7 +59,10 @@ export const GetTerminalListHandler: RequestHandler = (_, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CloseTerminalHandler: RequestHandler = (req, res) => {
|
export const CloseTerminalHandler: RequestHandler = (req, res) => {
|
||||||
const id = req.params.id;
|
const id = req.params['id'];
|
||||||
|
if (!id) {
|
||||||
|
return sendError(res, 'ID不能为空');
|
||||||
|
}
|
||||||
terminalManager.closeTerminal(id);
|
terminalManager.closeTerminal(id);
|
||||||
return sendSuccess(res, {});
|
return sendSuccess(res, {});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,6 +13,8 @@ interface TerminalInstance {
|
|||||||
sockets: Set<WebSocket>;
|
sockets: Set<WebSocket>;
|
||||||
// 新增标识,用于防止重复关闭
|
// 新增标识,用于防止重复关闭
|
||||||
isClosing: boolean;
|
isClosing: boolean;
|
||||||
|
// 新增:存储终端历史输出
|
||||||
|
buffer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TerminalManager {
|
class TerminalManager {
|
||||||
@ -67,21 +69,24 @@ class TerminalManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataHandler = (data: string) => {
|
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
|
||||||
ws.send(JSON.stringify({ type: 'output', data }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
instance.sockets.add(ws);
|
instance.sockets.add(ws);
|
||||||
instance.lastAccess = Date.now();
|
instance.lastAccess = Date.now();
|
||||||
|
|
||||||
|
// 新增:发送当前终端内容给新连接
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'output', data: instance.buffer }));
|
||||||
|
}
|
||||||
|
|
||||||
ws.on('message', (data) => {
|
ws.on('message', (data) => {
|
||||||
if (instance) {
|
if (instance) {
|
||||||
const result = JSON.parse(data.toString());
|
const result = JSON.parse(data.toString());
|
||||||
if (result.type === 'input') {
|
if (result.type === 'input') {
|
||||||
instance.pty.write(result.data);
|
instance.pty.write(result.data);
|
||||||
}
|
}
|
||||||
|
// 新增:处理 resize 消息
|
||||||
|
if (result.type === 'resize') {
|
||||||
|
instance.pty.resize(result.cols, result.rows);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -103,18 +108,17 @@ class TerminalManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改:移除参数 id,使用 crypto.randomUUID 生成终端 id
|
// 修改:新增 cols 和 rows 参数,同步 xterm 尺寸,防止错位
|
||||||
createTerminal() {
|
createTerminal(cols: number, rows: number) {
|
||||||
const id = randomUUID();
|
const id = randomUUID();
|
||||||
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
||||||
const pty = ptySpawn(shell, [], {
|
const pty = ptySpawn(shell, [], {
|
||||||
name: 'xterm-256color',
|
name: 'xterm-256color',
|
||||||
cols: 80,
|
cols, // 使用客户端传入的 cols
|
||||||
rows: 24,
|
rows, // 使用客户端传入的 rows
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
// 统一编码设置
|
|
||||||
LANG: os.platform() === 'win32' ? 'chcp 65001' : 'zh_CN.UTF-8',
|
LANG: os.platform() === 'win32' ? 'chcp 65001' : 'zh_CN.UTF-8',
|
||||||
TERM: 'xterm-256color',
|
TERM: 'xterm-256color',
|
||||||
},
|
},
|
||||||
@ -125,9 +129,13 @@ class TerminalManager {
|
|||||||
lastAccess: Date.now(),
|
lastAccess: Date.now(),
|
||||||
sockets: new Set(),
|
sockets: new Set(),
|
||||||
isClosing: false,
|
isClosing: false,
|
||||||
|
buffer: '', // 初始化终端内容缓存
|
||||||
};
|
};
|
||||||
|
|
||||||
pty.onData((data: any) => {
|
pty.onData((data: any) => {
|
||||||
|
// 追加数据到 buffer
|
||||||
|
instance.buffer += data;
|
||||||
|
// 发送数据给已连接的 websocket
|
||||||
instance.sockets.forEach((ws) => {
|
instance.sockets.forEach((ws) => {
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({ type: 'output', data }));
|
ws.send(JSON.stringify({ type: 'output', data }));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user