使用React js完成web前端

This commit is contained in:
LIm1tZz7 committed 2026-09-27 22:08:23 +08:00
1 parent 9e8c5f1ef0
commit 7600f0f9ed
130 files changed
+12708 -788

No files matched your search

+483
View File
@@ -0,0 +1,483 @@
#Requires -Version 5.1
[CmdletBinding()]
param(
[Parameter(Position = 0)][string]$Command = 'web',
[Parameter(Position = 1)][string]$Action = '',
[Parameter(Position = 2)][string]$Key = '',
[Parameter(Position = 3)][string]$Value = '',
[Parameter(ValueFromRemainingArguments = $true)][string[]]$Rest,
[string]$Port = '',
[string]$HostName = '',
[string]$User = '',
[string]$Password = '',
[string]$PasswordHash = '',
[string]$StaticDir = '',
[string]$CookieSecure = '',
[string]$BodyLimit = '',
[string]$SessionIdle = '',
[string]$SessionMax = '',
[string]$LoginMaxAttempts = '',
[string]$LoginWindow = '',
[string]$RowsPerPage = '',
[string]$MaxPageSize = '',
[string]$MaxRows = '',
[string]$MaxSqlLength = '',
[string]$DataDir = '',
[string]$PipeName = '',
[string]$StoragePageSize = '',
[string]$StorageBtreeOrder = '',
[string]$StorageBufferPool = '',
[string]$StorageLog = '',
[string]$Seed = '',
[switch]$NoBrowser,
[switch]$SkipBuild
)
$ErrorActionPreference = 'Stop'
$ScriptVersion = '0.2.0'
$root = Split-Path -Parent $PSScriptRoot
# ChuSQL 启动器 / CLI 前端:解析命令与设置,
# 启动 Rust 存储进程与 Haskell Web 服务,
# 把子进程日志回显到本窗口。
function Say([string]$msg) { Write-Host $msg }
function Step([string]$msg) { Write-Host ''; Write-Host "==> $msg" -ForegroundColor Cyan }
function Warn([string]$msg) { Write-Host $msg -ForegroundColor Yellow }
function Fail([string]$msg) { Write-Host "!! $msg" -ForegroundColor Red; exit 1 }
$SettingKeys = @(
'port', 'host', 'user', 'password', 'password-hash', 'static-dir', 'cookie-secure', 'body-limit',
'session-idle', 'session-max', 'login-max-attempts', 'login-window',
'rows-per-page', 'max-page-size', 'max-rows', 'max-sql-length',
'data-dir', 'pipe-name',
'storage-page-size', 'storage-btree-order', 'storage-buffer-pool', 'storage-log',
'seed'
)
$Settings = [ordered]@{
'port' = @{ Env = 'CHUSQL_WEB_PORT'; Default = '7777'; Kind = 'int' }
'host' = @{ Env = 'CHUSQL_WEB_HOST'; Default = '127.0.0.1'; Kind = 'text' }
'user' = @{ Env = 'CHUSQL_WEB_USER'; Default = 'root'; Kind = 'text' }
'password' = @{ Env = 'CHUSQL_WEB_PASSWORD'; Default = ''; Kind = 'secret' }
'password-hash' = @{ Env = 'CHUSQL_WEB_PASSWORD_HASH'; Default = ''; Kind = 'secret' }
'static-dir' = @{ Env = 'CHUSQL_WEB_STATIC'; Default = 'static'; Kind = 'text' }
'cookie-secure' = @{ Env = 'CHUSQL_WEB_COOKIE_SECURE'; Default = '0'; Kind = 'bool' }
'body-limit' = @{ Env = 'CHUSQL_WEB_BODY_LIMIT'; Default = '65536'; Kind = 'int' }
'session-idle' = @{ Env = 'CHUSQL_WEB_SESSION_IDLE'; Default = '28800'; Kind = 'int' }
'session-max' = @{ Env = 'CHUSQL_WEB_SESSION_MAX'; Default = '86400'; Kind = 'int' }
'login-max-attempts' = @{ Env = 'CHUSQL_WEB_LOGIN_MAX_ATTEMPTS'; Default = '5'; Kind = 'int' }
'login-window' = @{ Env = 'CHUSQL_WEB_LOGIN_WINDOW'; Default = '300'; Kind = 'int' }
'rows-per-page' = @{ Env = 'CHUSQL_WEB_PAGE_SIZE'; Default = '25'; Kind = 'int' }
'max-page-size' = @{ Env = 'CHUSQL_WEB_MAX_PAGE_SIZE'; Default = '500'; Kind = 'int' }
'max-rows' = @{ Env = 'CHUSQL_WEB_MAX_ROWS'; Default = '1000'; Kind = 'int' }
'max-sql-length' = @{ Env = 'CHUSQL_WEB_MAX_SQL_LENGTH'; Default = '20000'; Kind = 'int' }
'data-dir' = @{ Env = 'CHUSQL_DATA_DIR'; Default = '<repo>\localdata'; Kind = 'text' }
'pipe-name' = @{ Env = 'CHUSQL_PIPE'; Default = ''; Kind = 'text' }
'storage-page-size' = @{ Env = 'CHUSQL_PAGE_SIZE'; Default = '4096'; Kind = 'int' }
'storage-btree-order' = @{ Env = 'CHUSQL_BTREE_ORDER'; Default = '4'; Kind = 'int' }
'storage-buffer-pool' = @{ Env = 'CHUSQL_BUFFER_POOL_SIZE'; Default = '1024'; Kind = 'int' }
'storage-log' = @{ Env = 'CHUSQL_LOG'; Default = 'info'; Kind = 'text' }
'seed' = @{ Env = 'CHUSQL_WEB_SEED'; Default = '1'; Kind = 'bool' }
}
$FlagValues = @{
'port' = $Port
'host' = $HostName
'user' = $User
'password' = $Password
'password-hash' = $PasswordHash
'static-dir' = $StaticDir
'cookie-secure' = $CookieSecure
'body-limit' = $BodyLimit
'session-idle' = $SessionIdle
'session-max' = $SessionMax
'login-max-attempts' = $LoginMaxAttempts
'login-window' = $LoginWindow
'rows-per-page' = $RowsPerPage
'max-page-size' = $MaxPageSize
'max-rows' = $MaxRows
'max-sql-length' = $MaxSqlLength
'data-dir' = $DataDir
'pipe-name' = $PipeName
'storage-page-size' = $StoragePageSize
'storage-btree-order' = $StorageBtreeOrder
'storage-buffer-pool' = $StorageBufferPool
'storage-log' = $StorageLog
'seed' = $Seed
}
$SettingsPath = if ($env:CHUSQL_SETTINGS_FILE) { $env:CHUSQL_SETTINGS_FILE } else { Join-Path $PSScriptRoot 'chusql.settings.json' }
function Read-SavedSettings {
if (-not (Test-Path $SettingsPath)) { return @{} }
try {
$raw = Get-Content -Path $SettingsPath -Raw -Encoding UTF8
if (-not $raw.Trim()) { return @{} }
$obj = $raw | ConvertFrom-Json
$map = @{}
foreach ($k in $SettingKeys) {
if ($obj.PSObject.Properties.Name -contains $k) { $map[$k] = [string]$obj.$k }
}
return $map
} catch {
Warn "settings file $SettingsPath is not readable JSON; ignoring it ($($_.Exception.Message))"
return @{}
}
}
function Save-SavedSettings($map) {
$ordered = [ordered]@{}
foreach ($k in $SettingKeys) { if ($map.ContainsKey($k)) { $ordered[$k] = $map[$k] } }
($ordered | ConvertTo-Json) | Set-Content -Path $SettingsPath -Encoding UTF8
}
function Get-EffectiveSettings {
$saved = Read-SavedSettings
$result = [ordered]@{}
foreach ($k in $SettingKeys) {
$def = $Settings[$k].Default
$flag = $FlagValues[$k]
$envName = $Settings[$k].Env
$envValue = [System.Environment]::GetEnvironmentVariable($envName)
if ($flag) {
$result[$k] = @{ Value = $flag; Source = 'command line' }
} elseif ($saved.ContainsKey($k)) {
$result[$k] = @{ Value = $saved[$k]; Source = 'settings file' }
} elseif ($envValue) {
$result[$k] = @{ Value = $envValue; Source = 'environment' }
} else {
$result[$k] = @{ Value = $def; Source = 'default' }
}
}
return $result
}
function Assert-Value([string]$key, [string]$value) {
if ($value -eq '' -or $value -like '<*') { return }
switch ($Settings[$key].Kind) {
'int' {
$n = 0
if (-not [int]::TryParse($value, [ref]$n)) { Fail "setting '$key' needs an integer, got: $value" }
}
'bool' {
if ($value.ToLower() -notin @('0', '1', 'true', 'false', 'yes', 'no', 'on', 'off')) {
Fail "setting '$key' needs a boolean (0/1/true/false), got: $value"
}
}
}
}
function Assert-Effective($eff) {
foreach ($k in $SettingKeys) { Assert-Value $k $eff[$k].Value }
}
function Show-Settings($eff) {
$note = ''
if (-not (Test-Path $SettingsPath)) { $note = ' (not created yet)' }
Say ''
Say 'effective settings (command line > settings file > environment > default)'
Say (" settings file: {0}{1}" -f $SettingsPath, $note)
Say ''
foreach ($k in $SettingKeys) {
$shown = if ($Settings[$k].Kind -eq 'secret' -and $eff[$k].Value) { '********' } else { $eff[$k].Value }
Say (" {0,-20} {1,-24} [{2}]" -f $k, $shown, $eff[$k].Source)
}
Say ''
}
function Show-Help {
Say ("ChuSQL launcher / CLI (version {0})" -f $ScriptVersion)
Say ''
Say 'usage: chusql.ps1 [command] [options]'
Say ''
Say 'commands:'
Say ' web start the browser console (default)'
Say ' cli CLI mode (not implemented yet; starts web instead)'
Say ' config show effective settings'
Say ' config set KEY VAL save a setting'
Say ' config unset KEY drop a saved setting'
Say ' config reset drop all saved settings'
Say ' config path print the settings file path'
Say ' version | help'
Say ''
Say 'options (this run only):'
Say ' -Port N HTTP port default 7777'
Say ' -HostName S listen address default 127.0.0.1'
Say ' -User S account name default root'
Say ' -Password S password (plain; hashed at start) default chusql (demo)'
Say ' -PasswordHash S password hash (pbkdf2) default none'
Say ' -StaticDir D static asset directory default static'
Say ' -CookieSecure B add Secure to the session cookie default false'
Say ' -BodyLimit N max request body bytes default 65536'
Say ' -SessionIdle N session idle timeout, sec default 28800'
Say ' -SessionMax N session max age, sec default 86400'
Say ' -LoginMaxAttempts N failed logins before 429 default 5'
Say ' -LoginWindow N lockout window, sec default 300'
Say ' -RowsPerPage N rows per page by default default 25'
Say ' -MaxPageSize N rows per page ceiling default 500'
Say ' -MaxRows N rows returned per query default 1000'
Say ' -MaxSqlLength N max SQL characters default 20000'
Say ' -DataDir D data directory default <repo>\localdata'
Say ' -PipeName S named pipe name default chusql-joint-<timestamp>'
Say ' -StoragePageSize N storage page size (Rust) default 4096'
Say ' -StorageBtreeOrder N B+tree order (Rust) default 4'
Say ' -StorageBufferPool N buffer pool pages (Rust) default 1024'
Say ' -StorageLog S storage log level (Rust) default info'
Say ' -Seed B load demo data when the db is empty default 1 (on)'
Say ' -NoBrowser do not open the browser'
Say ' -SkipBuild skip cargo/stack builds'
Say ''
Say 'examples:'
Say ' chusql.ps1 web -Port 9000 # temporary port'
Say ' chusql.ps1 config set port 9000 # make it sticky'
Say ' chusql.ps1 config # see what is in effect'
Say ' chusql.ps1 cli # CLI placeholder -> starts web'
Say ' chusql.ps1 web -Seed 0 # start with an empty database'
Say ''
Say 'the password can also be changed from the web console (Settings -> Account);'
Say 'that writes password-hash into the settings file and drops the plain password.'
}
function Invoke-Config([string]$cmd) {
switch ($cmd.ToLower()) {
'' {
Show-Settings (Get-EffectiveSettings)
}
'list' { Show-Settings (Get-EffectiveSettings) }
'path' { Say $SettingsPath }
'set' {
if (-not $Key) { Fail 'usage: chusql.ps1 config set KEY VALUE (chusql.ps1 config set -h lists keys)' }
if ($Key -in @('-h', '--help', 'help')) { Say ('keys: ' + ($SettingKeys -join ', ')); return }
$k = $Key.ToLower()
if ($SettingKeys -notcontains $k) { Fail "unknown setting '$Key' (keys: $($SettingKeys -join ', '))" }
if (-not $Value) { Fail "config set $Key needs a value" }
Assert-Value $k $Value
$saved = Read-SavedSettings
$saved[$k] = $Value
Save-SavedSettings $saved
$shown = if ($Settings[$k].Kind -eq 'secret') { '********' } else { $Value }
Say "saved $k = $shown ($SettingsPath)"
}
'unset' {
if (-not $Key) { Fail 'usage: chusql.ps1 config unset KEY' }
$k = $Key.ToLower()
$saved = Read-SavedSettings
if (-not $saved.ContainsKey($k)) { Warn "$k was not saved"; return }
$saved.Remove($k)
Save-SavedSettings $saved
Say "removed $k"
}
'reset' {
if (Test-Path $SettingsPath) { Remove-Item $SettingsPath -Force }
Say "settings cleared ($SettingsPath)"
}
default { Fail "unknown config action '$cmd' (use: set / unset / reset / path)" }
}
}
function Test-PortFree([int]$candidate) {
$listener = $null
try {
$listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $candidate)
$listener.Start()
return $true
} catch {
return $false
} finally {
if ($listener) { $listener.Stop() }
}
}
function Find-WebExe {
$installRoot = (& stack path --local-install-root 2>$null | Select-Object -Last 1)
if ($installRoot) {
$candidate = Join-Path $installRoot 'bin\chusql-web.exe'
if (Test-Path $candidate) { return $candidate }
}
$found = Get-ChildItem -Path (Join-Path $root 'chusql-web\.stack-work\dist') -Recurse -Filter 'chusql-web.exe' -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($found) { return $found.FullName }
return $null
}
switch ($Command.ToLower()) {
'help' { Show-Help; exit 0 }
'-h' { Show-Help; exit 0 }
'--help' { Show-Help; exit 0 }
'version' { Say ("chusql launcher {0}" -f $ScriptVersion); exit 0 }
'config' { Invoke-Config $Action; exit 0 }
'set' { Invoke-Config 'set'; exit 0 }
'unset' { Invoke-Config 'unset'; exit 0 }
'web' { }
'cli' { }
default { Fail "unknown command '$Command' (try: chusql.ps1 help)" }
}
if ($Rest -and $Rest.Count -gt 0) { Fail ("unexpected extra arguments: " + ($Rest -join ' ')) }
if ($Command.ToLower() -eq 'cli') {
Say 'CLI interface is under development.'
Say 'Starting the web console instead (planned: see projectplan P4).'
Say ''
}
$eff = Get-EffectiveSettings
Assert-Effective $eff
$listenPortWanted = [int]$eff['port'].Value
$listenHost = $eff['host'].Value
$account = $eff['user'].Value
$dataDir = $eff['data-dir'].Value
if ($dataDir -like '<*') { $dataDir = Join-Path $root 'localdata' }
$pipeName = $eff['pipe-name'].Value
$stamp = [DateTime]::Now.ToString('yyyyMMdd-HHmmss')
if (-not $pipeName -or $pipeName -like '<*') { $pipeName = "chusql-joint-$stamp" }
$accountPassword = $eff['password'].Value
Say ("==================== ChuSQL launcher {0} ====================" -f $ScriptVersion)
if (-not $SkipBuild) {
Step 'Building the Rust storage server (cargo build --release)'
Push-Location (Join-Path $root 'chusql-storage')
try {
& cargo build --release
if ($LASTEXITCODE -ne 0) { Fail 'cargo build --release failed' }
} finally { Pop-Location }
}
$storageExe = Join-Path $root 'chusql-storage\target\release\chusql-storage.exe'
if (-not (Test-Path $storageExe)) { $storageExe = Join-Path $root 'chusql-storage\target\release\chusql-storage' }
if (-not (Test-Path $storageExe)) { Fail 'storage binary not built; run: cd chusql-storage; cargo build --release' }
if (-not $SkipBuild) {
Step 'Building the Haskell web server (stack build)'
Push-Location (Join-Path $root 'chusql-web')
try {
& stack build
if ($LASTEXITCODE -ne 0) { Fail 'stack build failed' }
} finally { Pop-Location }
}
Push-Location (Join-Path $root 'chusql-web')
try { $webExe = Find-WebExe } finally { Pop-Location }
if (-not $webExe) { Fail 'chusql-web.exe not found; run: cd chusql-web; stack build' }
$listenPort = $listenPortWanted
if (-not (Test-PortFree $listenPort)) {
$picked = 0
for ($p = $listenPort + 1; $p -le $listenPort + 2000; $p++) {
if (Test-PortFree $p) { $picked = $p; break }
}
if ($picked -eq 0) { Fail "port $listenPort is unusable and no free port was found in $listenPort..$($listenPort + 2000)" }
Warn "port $listenPort is taken or reserved by Windows; using $picked instead"
$listenPort = $picked
}
$logDir = Join-Path $dataDir 'logs'
New-Item -ItemType Directory -Force -Path $dataDir, $logDir | Out-Null
$env:CHUSQL_PIPE = $pipeName
$env:CHUSQL_DATA_DIR = $dataDir
$env:CHUSQL_WEB_HOST = $listenHost
$env:CHUSQL_WEB_PORT = "$listenPort"
$env:CHUSQL_WEB_USER = $account
foreach ($k in $SettingKeys) {
if ($k -in @('port', 'host', 'user', 'data-dir', 'pipe-name')) { continue }
$envName = $Settings[$k].Env
$v = $eff[$k].Value
if ($v -and -not ($v -like '<*')) { Set-Item -Path ("Env:\" + $envName) -Value $v }
else { Remove-Item -Path ("Env:\" + $envName) -ErrorAction SilentlyContinue }
}
$storageLog = Join-Path $logDir 'storage.log'
$storageErrLog = Join-Path $logDir 'storage.err.log'
$webLog = Join-Path $logDir 'web.log'
$webErrLog = Join-Path $logDir 'web.err.log'
$storageProc = $null
$webProc = $null
function Stop-Children {
foreach ($p in @($webProc, $storageProc)) {
if ($p -and -not $p.HasExited) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue }
}
}
$SeenLines = @{}
# 把子进程新增日志回显到本窗口
function Show-NewLogLines([string]$tag, [string]$path, [string]$color) {
if (-not (Test-Path $path)) { return }
$lines = @(Get-Content -Path $path -Encoding UTF8 -ErrorAction SilentlyContinue)
$seen = 0
if ($SeenLines.ContainsKey($path)) { $seen = $SeenLines[$path] }
if ($lines.Count -le $seen) { return }
for ($i = $seen; $i -lt $lines.Count; $i++) {
Write-Host ("[{0}] " -f $tag) -NoNewline -ForegroundColor $color
Write-Host $lines[$i]
}
$SeenLines[$path] = $lines.Count
}
function Show-ChildLogs {
Show-NewLogLines 'storage' $storageLog 'DarkCyan'
Show-NewLogLines 'storage:err' $storageErrLog 'DarkYellow'
Show-NewLogLines 'web' $webLog 'DarkGreen'
Show-NewLogLines 'web:err' $webErrLog 'DarkYellow'
}
try {
Step 'Starting the Rust storage process'
$storageProc = Start-Process -FilePath $storageExe -WorkingDirectory $dataDir -PassThru -WindowStyle Hidden `
-RedirectStandardOutput $storageLog -RedirectStandardError $storageErrLog
Say ("storage pid {0} pipe {1} ({2})" -f $storageProc.Id, $pipeName, (Split-Path -Leaf $storageExe))
Step 'Starting the Haskell web server'
$webArgs = @('--port', "$listenPort", '--host', $listenHost, '--user', $account)
$webProc = Start-Process -FilePath $webExe -WorkingDirectory (Join-Path $root 'chusql-web') -PassThru -WindowStyle Hidden `
-ArgumentList $webArgs -RedirectStandardOutput $webLog -RedirectStandardError $webErrLog
Say ("web pid {0}" -f $webProc.Id)
$url = "http://${listenHost}:$listenPort/"
$statusUrl = "http://${listenHost}:$listenPort/api/status"
$ready = $false
$deadline = (Get-Date).AddSeconds(40)
while ((Get-Date) -lt $deadline) {
Show-ChildLogs
if ($webProc.HasExited) { Show-ChildLogs; Fail "the web server exited early; see $webLog" }
try {
$status = Invoke-RestMethod -Uri $statusUrl -TimeoutSec 3
if ($status.storage -eq 'up') { $ready = $true; break }
} catch { }
Start-Sleep -Milliseconds 400
}
if (-not $ready) { Show-ChildLogs; Fail "the web server did not report storage up within 40s; see $webLog" }
$shownPassword = if ($accountPassword) { '(saved in settings / environment)' } else { 'chusql (built-in demo password)' }
Say ''
Say '==================== ready ===================='
Say (" URL {0}" -f $url)
Say (" account {0}" -f $account)
Say (" password {0}" -f $shownPassword)
Say (" storage {0}" -f $storageExe)
Say (" data dir {0}" -f $dataDir)
Say (" logs {0}" -f $logDir)
if (Test-Path $SettingsPath) { Say (" settings {0}" -f $SettingsPath) }
Say '==============================================='
Say ''
if (-not $NoBrowser) { Start-Process $url | Out-Null }
Say 'Streaming child logs below. Press Ctrl+C to stop everything.'
while (-not $webProc.HasExited) {
Show-ChildLogs
Start-Sleep -Milliseconds 400
}
Show-ChildLogs
} finally {
Step 'Stopping'
Stop-Children
Say 'stopped. data and logs were kept.'
}