初始提交: ScriptForge 脚本快速转运行链接服务
Some checks failed
Release / build-and-release (push) Failing after 1m31s
Some checks failed
Release / build-and-release (push) Failing after 1m31s
- Go 后端 (Gin + GORM + SQLite) 提供 API 和纯文本脚本服务 - Vite + React + TypeScript + Tailwind 前端 - 单二进制部署 (Go embed 前端静态文件) - Gitea Actions CI/CD: 打标签自动构建多平台 Release - 支持 bash/zsh/sh/fish/python3/node/ruby/php 8种运行环境 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ScriptForge - 脚本快速转运行链接</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>" />
|
||||
</head>
|
||||
<body class="bg-gray-950 text-gray-100 min-h-screen">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
2722
frontend/package-lock.json
generated
Normal file
2722
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
frontend/package.json
Normal file
26
frontend/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "scriptforge-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.26.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.4"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
31
frontend/src/App.tsx
Normal file
31
frontend/src/App.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Routes, Route, Link } from 'react-router-dom'
|
||||
import Home from './pages/Home'
|
||||
import ScriptDetail from './pages/ScriptDetail'
|
||||
import DeleteScript from './pages/DeleteScript'
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<header className="border-b border-gray-800">
|
||||
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<Link to="/" className="text-xl font-bold tracking-tight hover:text-blue-400 transition-colors">
|
||||
<span className="text-blue-500">⚡</span> ScriptForge
|
||||
</Link>
|
||||
<span className="text-sm text-gray-500">脚本快速转运行链接</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 max-w-4xl mx-auto px-4 py-8 w-full">
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/s/:id" element={<ScriptDetail />} />
|
||||
<Route path="/s/:id/delete" element={<DeleteScript />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-gray-800 text-center text-xs text-gray-600 py-4">
|
||||
ScriptForge
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
35
frontend/src/components/CommandCard.tsx
Normal file
35
frontend/src/components/CommandCard.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
interface Props {
|
||||
command: string
|
||||
}
|
||||
|
||||
export default function CommandCard({ command }: Props) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(command).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 border border-blue-500/30 rounded-lg overflow-hidden">
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-blue-500/10 border-b border-blue-500/20">
|
||||
<span className="text-xs text-blue-400 font-medium">运行命令</span>
|
||||
</div>
|
||||
<div className="p-4 flex items-center gap-3">
|
||||
<code className="flex-1 text-sm font-mono text-green-400 break-all select-all">
|
||||
{command}
|
||||
</code>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="shrink-0 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
{copied ? '已复制' : '复制'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
71
frontend/src/components/ResultCard.tsx
Normal file
71
frontend/src/components/ResultCard.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { CreateScriptResponse } from '../types'
|
||||
import CommandCard from './CommandCard'
|
||||
|
||||
interface Props {
|
||||
result: CreateScriptResponse
|
||||
onReset: () => void
|
||||
}
|
||||
|
||||
export default function ResultCard({ result, onReset }: Props) {
|
||||
const detailUrl = `${window.location.origin}/s/${result.id}`
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl mb-2">✅</div>
|
||||
<h2 className="text-xl font-bold">运行链接已生成</h2>
|
||||
</div>
|
||||
|
||||
<CommandCard command={result.command} />
|
||||
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-4 space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">脚本 ID</span>
|
||||
<span className="font-mono text-blue-400">{result.id}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">运行环境</span>
|
||||
<span>{result.runtime}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">过期时间</span>
|
||||
<span>{new Date(result.expires_at).toLocaleString('zh-CN')}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">详情页</span>
|
||||
<a
|
||||
href={detailUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-400 hover:underline font-mono text-xs"
|
||||
>
|
||||
{detailUrl}
|
||||
</a>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-gray-700">
|
||||
<div className="text-xs text-gray-500 mb-1">管理令牌(请妥善保存,仅此一次)</div>
|
||||
<div className="font-mono text-xs bg-gray-900 px-3 py-2 rounded break-all select-all">
|
||||
{result.admin_token}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 justify-center">
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg text-sm transition-colors"
|
||||
>
|
||||
创建另一个
|
||||
</button>
|
||||
<a
|
||||
href={detailUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-sm transition-colors"
|
||||
>
|
||||
查看详情
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
76
frontend/src/components/ScriptForm.tsx
Normal file
76
frontend/src/components/ScriptForm.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useState } from 'react'
|
||||
import { RUNTIME_OPTIONS, EXPIRES_OPTIONS, RuntimeOption, ExpiresIn } from '../types'
|
||||
|
||||
interface Props {
|
||||
onSubmit: (content: string, runtime: RuntimeOption, expiresIn: ExpiresIn) => void
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export default function ScriptForm({ onSubmit, loading }: Props) {
|
||||
const [content, setContent] = useState('')
|
||||
const [runtime, setRuntime] = useState<RuntimeOption>('bash')
|
||||
const [expiresIn, setExpiresIn] = useState<ExpiresIn>('24h')
|
||||
|
||||
const canSubmit = content.trim().length > 0 && content.length <= 16384 && !loading
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">运行环境</label>
|
||||
<select
|
||||
value={runtime}
|
||||
onChange={(e) => setRuntime(e.target.value as RuntimeOption)}
|
||||
className="px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
{RUNTIME_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">过期时间</label>
|
||||
<select
|
||||
value={expiresIn}
|
||||
onChange={(e) => setExpiresIn(e.target.value as ExpiresIn)}
|
||||
className="px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
{EXPIRES_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="在此粘贴你的脚本..."
|
||||
rows={12}
|
||||
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-sm font-mono focus:outline-none focus:border-blue-500 resize-y"
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500">
|
||||
{content.length.toLocaleString()} / 16,384 字符
|
||||
{content.length > 16384 && (
|
||||
<span className="text-red-400 ml-1">超出限制!</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => onSubmit(content, runtime, expiresIn)}
|
||||
disabled={!canSubmit}
|
||||
className="px-6 py-2.5 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 disabled:text-gray-500 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
{loading ? '生成中...' : '生成运行链接'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
23
frontend/src/components/ScriptViewer.tsx
Normal file
23
frontend/src/components/ScriptViewer.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
interface Props {
|
||||
content: string
|
||||
runtime: string
|
||||
}
|
||||
|
||||
export default function ScriptViewer({ content, runtime }: Props) {
|
||||
return (
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-lg overflow-hidden">
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-gray-800/50 border-b border-gray-700">
|
||||
<span className="text-xs text-gray-400 font-mono">script.{runtime === 'node' ? 'js' : runtime === 'python3' ? 'py' : runtime}</span>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(content)}
|
||||
className="text-xs text-gray-500 hover:text-blue-400 transition-colors"
|
||||
>
|
||||
复制内容
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-4 text-sm font-mono overflow-x-auto leading-relaxed text-gray-200">
|
||||
{content}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
frontend/src/index.css
Normal file
3
frontend/src/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
44
frontend/src/lib/api.ts
Normal file
44
frontend/src/lib/api.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { CreateScriptResponse, ScriptDetail, RuntimeOption, ExpiresIn } from '../types'
|
||||
|
||||
const BASE = ''
|
||||
|
||||
async function request<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
const res = await fetch(BASE + url, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...options,
|
||||
})
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({ error: res.statusText }))
|
||||
throw new Error(body.error || `HTTP ${res.status}`)
|
||||
}
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function createScript(params: {
|
||||
content: string
|
||||
runtime: RuntimeOption
|
||||
expires_in: ExpiresIn
|
||||
}): Promise<CreateScriptResponse> {
|
||||
return request('/api/scripts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
}
|
||||
|
||||
export async function getScript(id: string): Promise<ScriptDetail> {
|
||||
return request(`/api/scripts/${id}`)
|
||||
}
|
||||
|
||||
export async function deleteScript(id: string, token: string): Promise<void> {
|
||||
await fetch(`${BASE}/api/scripts/${id}?token=${encodeURIComponent(token)}`, {
|
||||
method: 'DELETE',
|
||||
}).then((res) => {
|
||||
if (!res.ok && res.status !== 204) {
|
||||
throw new Error('delete failed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getCommandUrl(id: string): string {
|
||||
return `${window.location.origin}/raw/${id}`
|
||||
}
|
||||
13
frontend/src/main.tsx
Normal file
13
frontend/src/main.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
72
frontend/src/pages/DeleteScript.tsx
Normal file
72
frontend/src/pages/DeleteScript.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useState } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { deleteScript } from '../lib/api'
|
||||
|
||||
export default function DeleteScript() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const [token, setToken] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [deleted, setDeleted] = useState(false)
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!id || !token.trim()) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
await deleteScript(id, token.trim())
|
||||
setDeleted(true)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : '删除失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (deleted) {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<div className="text-6xl mb-4">🗑️</div>
|
||||
<h2 className="text-xl font-bold mb-2">脚本已删除</h2>
|
||||
<Link to="/" className="text-blue-400 hover:underline">
|
||||
创建新脚本
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto py-12">
|
||||
<h1 className="text-xl font-bold mb-4">删除脚本</h1>
|
||||
<p className="text-gray-400 text-sm mb-6">
|
||||
需要提供创建时返回的 admin_token 才能删除脚本 <code className="text-blue-400">{id}</code>
|
||||
</p>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
placeholder="粘贴 admin_token"
|
||||
className="w-full px-4 py-2.5 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-blue-500 mb-4"
|
||||
/>
|
||||
|
||||
{error && <p className="text-red-400 text-sm mb-4">{error}</p>}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={loading || !token.trim()}
|
||||
className="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 disabled:bg-gray-700 disabled:text-gray-500 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
{loading ? '删除中...' : '确认删除'}
|
||||
</button>
|
||||
<Link
|
||||
to={`/s/${id}`}
|
||||
className="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg text-sm transition-colors"
|
||||
>
|
||||
取消
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
53
frontend/src/pages/Home.tsx
Normal file
53
frontend/src/pages/Home.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import ScriptForm from '../components/ScriptForm'
|
||||
import ResultCard from '../components/ResultCard'
|
||||
import { createScript } from '../lib/api'
|
||||
import { CreateScriptResponse, RuntimeOption, ExpiresIn } from '../types'
|
||||
|
||||
export default function Home() {
|
||||
const [result, setResult] = useState<CreateScriptResponse | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleSubmit = useCallback(async (content: string, runtime: RuntimeOption, expiresIn: ExpiresIn) => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setResult(null)
|
||||
try {
|
||||
const res = await createScript({ content, runtime, expires_in: expiresIn })
|
||||
setResult(res)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : '创建失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setResult(null)
|
||||
setError(null)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!result ? (
|
||||
<>
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-3xl font-bold mb-2">粘贴脚本,生成运行链接</h1>
|
||||
<p className="text-gray-400">
|
||||
选择运行环境,立即生成可分享的 <code className="text-blue-400">curl | bash</code> 命令
|
||||
</p>
|
||||
</div>
|
||||
<ScriptForm onSubmit={handleSubmit} loading={loading} />
|
||||
{error && (
|
||||
<div className="mt-4 p-3 bg-red-900/50 border border-red-700 rounded-lg text-red-300 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<ResultCard result={result} onReset={handleReset} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
79
frontend/src/pages/ScriptDetail.tsx
Normal file
79
frontend/src/pages/ScriptDetail.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import ScriptViewer from '../components/ScriptViewer'
|
||||
import CommandCard from '../components/CommandCard'
|
||||
import { getScript } from '../lib/api'
|
||||
import { ScriptDetail as ScriptDetailType } from '../types'
|
||||
|
||||
export default function ScriptDetail() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const [script, setScript] = useState<ScriptDetailType | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getScript(id)
|
||||
.then(setScript)
|
||||
.catch((e) => setError(e instanceof Error ? e.message : '加载失败'))
|
||||
.finally(() => setLoading(false))
|
||||
}, [id])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center py-20">
|
||||
<div className="animate-pulse text-gray-500">加载中...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !script) {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<div className="text-6xl mb-4">😕</div>
|
||||
<h2 className="text-xl font-bold mb-2">脚本不存在或已过期</h2>
|
||||
<p className="text-gray-400 mb-6">{error}</p>
|
||||
<Link to="/" className="text-blue-400 hover:underline">
|
||||
创建新脚本
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const command = `curl ${window.location.origin}/raw/${script.id} | ${script.runtime}`
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6">
|
||||
<Link to="/" className="text-sm text-gray-500 hover:text-blue-400 transition-colors">
|
||||
← 返回首页
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<h1 className="text-2xl font-bold font-mono">{script.id}</h1>
|
||||
<span className="px-2 py-0.5 bg-blue-600/20 text-blue-400 text-xs rounded-full border border-blue-600/30">
|
||||
{script.runtime}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(script.expires_at).toLocaleString('zh-CN')} 过期
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ScriptViewer content={script.content} runtime={script.runtime} />
|
||||
|
||||
<div className="mt-8">
|
||||
<CommandCard command={command} />
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<Link
|
||||
to={`/s/${script.id}/delete`}
|
||||
className="text-xs text-gray-600 hover:text-red-400 transition-colors"
|
||||
>
|
||||
删除此脚本
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
39
frontend/src/types.ts
Normal file
39
frontend/src/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export interface CreateScriptResponse {
|
||||
id: string
|
||||
admin_token: string
|
||||
url: string
|
||||
command: string
|
||||
runtime: string
|
||||
expires_at: string
|
||||
}
|
||||
|
||||
export interface ScriptDetail {
|
||||
id: string
|
||||
runtime: string
|
||||
content: string
|
||||
content_length: number
|
||||
created_at: string
|
||||
expires_at: string
|
||||
expired: boolean
|
||||
}
|
||||
|
||||
export type RuntimeOption = 'bash' | 'zsh' | 'sh' | 'fish' | 'python3' | 'node' | 'ruby' | 'php'
|
||||
export type ExpiresIn = '1h' | '24h' | '7d' | '30d'
|
||||
|
||||
export const RUNTIME_OPTIONS: { value: RuntimeOption; label: string }[] = [
|
||||
{ value: 'bash', label: 'Bash' },
|
||||
{ value: 'zsh', label: 'Zsh' },
|
||||
{ value: 'sh', label: 'Sh' },
|
||||
{ value: 'fish', label: 'Fish' },
|
||||
{ value: 'python3', label: 'Python 3' },
|
||||
{ value: 'node', label: 'Node.js' },
|
||||
{ value: 'ruby', label: 'Ruby' },
|
||||
{ value: 'php', label: 'PHP' },
|
||||
]
|
||||
|
||||
export const EXPIRES_OPTIONS: { value: ExpiresIn; label: string }[] = [
|
||||
{ value: '1h', label: '1 小时' },
|
||||
{ value: '24h', label: '24 小时' },
|
||||
{ value: '7d', label: '7 天' },
|
||||
{ value: '30d', label: '30 天' },
|
||||
]
|
||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
8
frontend/tailwind.config.js
Normal file
8
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
20
frontend/tsconfig.json
Normal file
20
frontend/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
13
frontend/vite.config.ts
Normal file
13
frontend/vite.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': { target: 'http://localhost:8080' },
|
||||
'/raw': { target: 'http://localhost:8080' },
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user