Initial UNC file watcher web console

This commit is contained in:
Codex
2026-04-17 17:08:49 +08:00
commit 1d283d540c
10 changed files with 1931 additions and 0 deletions
+361
View File
@@ -0,0 +1,361 @@
package main
import "html/template"
var pageTemplate = template.Must(template.New("page").Parse(`<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UNC 文件监听控制台</title>
<style>
:root {
--bg: #f3efe7;
--panel: #fffaf2;
--panel-strong: #f7efe1;
--ink: #1f2520;
--muted: #6b716a;
--accent: #205c43;
--accent-soft: #dcebdd;
--warn: #8a3b2f;
--warn-soft: #f7ddd8;
--line: #d8cfbf;
--shadow: rgba(31, 37, 32, 0.08);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background:
radial-gradient(circle at top left, rgba(32, 92, 67, 0.10), transparent 28%),
linear-gradient(180deg, #f8f4ec 0%, var(--bg) 100%);
color: var(--ink);
}
.shell {
width: min(1180px, calc(100% - 32px));
margin: 24px auto 40px;
}
.hero {
background: linear-gradient(135deg, #214d3a 0%, #2f6a54 100%);
color: #f8f5ee;
border-radius: 24px;
padding: 28px;
box-shadow: 0 20px 40px var(--shadow);
}
.hero h1 {
margin: 0 0 10px;
font-size: clamp(28px, 5vw, 42px);
line-height: 1.05;
}
.hero p {
margin: 0;
max-width: 860px;
color: rgba(248, 245, 238, 0.85);
line-height: 1.7;
}
.grid {
display: grid;
grid-template-columns: 1.35fr 0.65fr;
gap: 18px;
margin-top: 18px;
}
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 22px;
padding: 22px;
box-shadow: 0 14px 30px var(--shadow);
}
.panel h2 {
margin: 0 0 16px;
font-size: 20px;
}
.section {
border: 1px solid var(--line);
border-radius: 18px;
padding: 16px;
background: #fffdf8;
}
.section + .section {
margin-top: 14px;
}
.section h3 {
margin: 0 0 12px;
font-size: 16px;
}
.status-line {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 12px;
}
.pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-radius: 999px;
background: var(--panel-strong);
border: 1px solid var(--line);
font-weight: 600;
}
.pill.running {
background: var(--accent-soft);
color: var(--accent);
border-color: rgba(32, 92, 67, 0.2);
}
.note {
margin: 10px 0 0;
color: var(--muted);
line-height: 1.7;
}
.flash {
padding: 12px 14px;
border-radius: 16px;
margin-bottom: 14px;
line-height: 1.6;
border: 1px solid transparent;
}
.flash.ok {
background: var(--accent-soft);
color: var(--accent);
border-color: rgba(32, 92, 67, 0.18);
}
.flash.err {
background: var(--warn-soft);
color: var(--warn);
border-color: rgba(138, 59, 47, 0.18);
}
form {
display: grid;
gap: 14px;
}
.fields {
display: grid;
gap: 14px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.fields .wide {
grid-column: 1 / -1;
}
label {
display: grid;
gap: 8px;
font-size: 14px;
font-weight: 600;
}
label span {
color: var(--muted);
font-weight: 500;
font-size: 13px;
}
input, select {
width: 100%;
padding: 12px 14px;
border-radius: 14px;
border: 1px solid var(--line);
background: #fffdf8;
color: var(--ink);
font: inherit;
}
.checkbox {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
margin-bottom: 12px;
}
.checkbox input {
width: 18px;
height: 18px;
margin: 0;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 6px;
}
button {
appearance: none;
border: 0;
border-radius: 999px;
padding: 12px 20px;
font: inherit;
font-weight: 700;
cursor: pointer;
}
.primary {
background: var(--accent);
color: #f7f3ec;
}
.secondary {
background: #e7dfd0;
color: #493c2c;
}
.logs {
margin-top: 18px;
}
.logbox {
border-radius: 18px;
border: 1px solid var(--line);
background: #1f2520;
color: #dbe5dc;
padding: 16px;
min-height: 320px;
max-height: 560px;
overflow: auto;
font-family: Consolas, "Courier New", monospace;
font-size: 13px;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
}
.empty {
color: #9cab9e;
}
code {
font-family: Consolas, "Courier New", monospace;
}
@media (max-width: 900px) {
.grid {
grid-template-columns: 1fr;
}
.fields {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<main class="shell">
<section class="hero">
<h1>UNC 文件监听控制台</h1>
<p>支持两类规则同时运行。压缩包规则专门处理 <code>.tar.gz</code> 并结合 zip / unzip 目录去重;普通文件规则按扩展名列表把文件从 UNC 路径复制或剪切到本机目录,并支持按“同名+大小”或“同名+哈希”去重。</p>
</section>
<section class="grid">
<div class="panel">
<h2>监听配置</h2>
{{if .SubmitMessage}}<div class="flash ok">{{.SubmitMessage}}</div>{{end}}
{{if .SubmitError}}<div class="flash err">{{.SubmitError}}</div>{{end}}
{{if .LastError}}<div class="flash err">{{.LastError}}</div>{{end}}
<form method="post" action="/start">
<div class="section">
<label class="checkbox">
<input type="checkbox" name="archive_enabled" {{if .Form.ArchiveEnabled}}checked{{end}}>
启用压缩包规则
</label>
<div class="fields">
<label class="wide">压缩包 UNC 源路径
<span>例如 \\server\share\incoming,仅处理 .tar.gz</span>
<input name="archive_source" value="{{.Form.ArchiveSourceDir}}" placeholder="\\server\share\incoming">
</label>
<label>zip 路径
<span>需要复制或剪切到这里</span>
<input name="archive_zip" value="{{.Form.ArchiveZipDir}}" placeholder="D:\zip">
</label>
<label>unzip 路径
<span>已有同名解压目录则跳过</span>
<input name="archive_unzip" value="{{.Form.ArchiveUnzipDir}}" placeholder="D:\unzip">
</label>
</div>
</div>
<div class="section">
<label class="checkbox">
<input type="checkbox" name="generic_enabled" {{if .Form.GenericEnabled}}checked{{end}}>
启用普通文件规则
</label>
<div class="fields">
<label class="wide">普通文件 UNC 源路径
<span>例如 \\server\share\others</span>
<input name="generic_source" value="{{.Form.GenericSourceDir}}" placeholder="\\server\share\others">
</label>
<label>本机目标路径
<span>只处理到这个目录,不检查 unzip</span>
<input name="generic_target" value="{{.Form.GenericTargetDir}}" placeholder="D:\other_files">
</label>
<label>扩展名列表
<span>例如 .csv,.txt,.pdf 或 xlsx,docx</span>
<input name="generic_exts" value="{{.Form.GenericExts}}" placeholder=".csv,.txt,.pdf">
</label>
<label>去重方式
<span>目标目录已有同名文件时使用</span>
<select name="generic_dedupe">
<option value="name_size" {{if eq .Form.GenericDedupe "name_size"}}selected{{end}}>同名且大小相同才跳过</option>
<option value="name_hash" {{if eq .Form.GenericDedupe "name_hash"}}selected{{end}}>同名且哈希相同才跳过</option>
</select>
</label>
</div>
</div>
<div class="section">
<h3>公共设置</h3>
<div class="fields">
<label>扫描间隔
<span>支持 Go duration,如 15s、1m</span>
<input name="interval" value="{{.Form.Interval}}" placeholder="15s">
</label>
<label>稳定时长
<span>最后修改时间至少早于当前多久</span>
<input name="stable_for" value="{{.Form.StableFor}}" placeholder="30s">
</label>
<label>稳定扫描次数
<span>连续几次扫描未变化才处理</span>
<input name="stable_scans" value="{{.Form.StableScans}}" placeholder="2">
</label>
<label>处理方式
<span>copy 复制,move 剪切</span>
<select name="mode">
<option value="copy" {{if eq .Form.Mode "copy"}}selected{{end}}>copy</option>
<option value="move" {{if eq .Form.Mode "move"}}selected{{end}}>move</option>
</select>
</label>
</div>
<label class="checkbox" style="margin-top:12px;">
<input type="checkbox" name="recursive" {{if .Form.Recursive}}checked{{end}}>
递归扫描源目录
</label>
</div>
<p class="note">页面配置会自动保存到 <code>{{.SettingsPath}}</code>,下次打开页面会自动带出。</p>
<div class="actions">
<button class="primary" type="submit">启动或重启监听</button>
</div>
</form>
</div>
<div class="panel">
<h2>运行状态</h2>
<div class="status-line">
<div class="pill {{if .Running}}running{{end}}">
{{if .Running}}运行中{{else}}未运行{{end}}
</div>
{{if .StartedAt}}
<div class="pill">启动时间 {{.StartedAt}}</div>
{{end}}
</div>
{{if .LastMessage}}
<p class="note">{{.LastMessage}}</p>
{{end}}
<p class="note">如果 UNC 目录中的文件由后台服务持续写入,建议把“稳定时长”和“稳定扫描次数”设得更保守一些,例如 <code>stable_for=1m</code>、<code>stable_scans=3</code>。</p>
<p class="note">普通文件规则里如果目标目录出现同名但不同大小,或者同名但哈希不同,程序会记录为冲突并跳过,不会覆盖已有文件。</p>
<form method="post" action="/stop">
<div class="actions">
<button class="secondary" type="submit">停止监听</button>
</div>
</form>
</div>
</section>
<section class="panel logs">
<h2>最近日志</h2>
<div class="logbox">{{if .Logs}}{{range .Logs}}{{.}}
{{end}}{{else}}<span class="empty">暂无日志。启动监听后,这里会显示文件发现、跳过、复制、剪切和错误信息。</span>{{end}}</div>
</section>
</main>
</body>
</html>`))