feat(backend): implement task creation and admin web

This commit is contained in:
QiuSW
2026-07-26 14:03:32 +08:00
parent 2b265c92fc
commit c5d3b215ff
58 changed files with 8773 additions and 83 deletions
@@ -0,0 +1,19 @@
{{define "error"}}
<!doctype html>
<html lang="zh-CN">
<head>
<title>{{.Page.Title}} - 采购任务管理</title>
{{template "document-head" .}}
</head>
<body>
{{template "site-header" .}}
<main id="main-content" class="page narrow-page">
<section class="error-page" role="alert">
<h1>{{.Heading}}</h1>
<p>{{.Message}}</p>
<a class="button primary" href="/tasks">返回任务列表</a>
</section>
</main>
</body>
</html>
{{end}}
@@ -0,0 +1,22 @@
{{define "document-head"}}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<link rel="icon" href="data:,">
<link rel="stylesheet" href="/static/admin.css">
<script src="/static/admin.js" defer></script>
{{end}}
{{define "site-header"}}
<a class="skip-link" href="#main-content">跳到主要内容</a>
<header class="site-header">
<a class="brand" href="/tasks" aria-label="采购任务管理首页">
<span class="brand-mark" aria-hidden="true">采</span>
<span>采购任务管理</span>
</a>
<nav class="main-nav" aria-label="主导航">
<a href="/tasks" {{if .Page.TasksCurrent}}aria-current="page"{{end}}>任务列表</a>
<a href="/tasks/new" {{if .Page.NewCurrent}}aria-current="page"{{end}}>新建任务</a>
</nav>
</header>
{{end}}
@@ -0,0 +1,106 @@
{{define "task-detail"}}
<!doctype html>
<html lang="zh-CN">
<head>
<title>{{.Task.Title}} - 采购任务管理</title>
{{template "document-head" .}}
</head>
<body>
{{template "site-header" .}}
<main id="main-content" class="page">
<a class="back-link" href="/tasks">返回任务列表</a>
{{if .Notice}}
<div class="notice" role="status" tabindex="-1" data-notice>{{.Notice}}</div>
{{end}}
<div class="title-row detail-title">
<div>
<h1>{{.Task.Title}}</h1>
<p class="subtitle">SKU:{{.Task.SKU}} · 任务编号:{{.Task.ID}}</p>
</div>
<div class="title-actions">
<span class="status {{.Task.StatusClass}}" title="状态码 {{.Task.Status}}">
{{.Task.StatusLabel}}
</span>
{{if .Task.CanCancel}}
<button class="button danger-outline" type="button" data-open-cancel>取消任务</button>
{{end}}
</div>
</div>
{{if eq .Task.Status "SUCCEEDED"}}
<section class="success-banner" aria-labelledby="success-heading">
<div aria-hidden="true" class="success-mark">✓</div>
<div>
<h2 id="success-heading">验证完成,未提交订单</h2>
<p>采购人员已完成候选确认,系统只记录验证结果。</p>
</div>
</section>
{{end}}
<div class="detail-layout">
<section class="content-section" aria-labelledby="original-heading">
<h2 id="original-heading">原始采购要求</h2>
<p class="section-note">以下内容来自管理员输入,不会被 AI 派生结果覆盖。</p>
<div class="requirement-layout">
<figure class="reference-figure">
<img class="reference-image"
src="/api/v1/assets/{{pathPart .Task.ReferenceAssetID}}/content"
alt="商品参考图">
<figcaption>已规范化的参考图片</figcaption>
</figure>
<dl class="definition-list">
<dt>商品标题</dt><dd>{{.Task.Title}}</dd>
<dt>SKU</dt><dd>{{.Task.SKU}}</dd>
<dt>描述</dt><dd>{{if .Task.Description}}{{.Task.Description}}{{else}}未填写{{end}}</dd>
<dt>数量</dt><dd>{{.Task.Quantity}}</dd>
<dt>最高总预算</dt>
<dd>{{if .Task.MaxBudget}}{{.Task.MaxBudget}} 元{{else}}未设置{{end}}</dd>
</dl>
</div>
</section>
<aside class="content-section summary-section" aria-labelledby="summary-heading">
<h2 id="summary-heading">任务摘要</h2>
<dl class="definition-list">
<dt>状态</dt><dd>{{.Task.StatusLabel}}({{.Task.Status}})</dd>
<dt>创建时间</dt>
<dd><time datetime="{{machineTime .Task.CreatedAt}}">{{displayTime .Task.CreatedAt}}</time></dd>
<dt>最后更新</dt>
<dd><time datetime="{{machineTime .Task.UpdatedAt}}">{{displayTime .Task.UpdatedAt}}</time></dd>
</dl>
</aside>
</div>
{{if .Task.CanCancel}}
<noscript>
<section class="noscript-cancel" aria-labelledby="noscript-cancel-heading">
<h2 id="noscript-cancel-heading">确认取消任务</h2>
<p>取消后任务不会自动恢复。</p>
<form method="post" action="/tasks/{{pathPart .Task.ID}}/cancel">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="cancel_key" value="{{.CancelKey}}">
<button class="button danger" type="submit">确认取消</button>
</form>
</section>
</noscript>
<dialog class="confirm-dialog" data-cancel-dialog aria-labelledby="cancel-title">
<h2 id="cancel-title">确认取消任务?</h2>
<p>任务取消后不会自动恢复,采购执行员将不能再领取。</p>
<div class="dialog-actions">
<button class="button" type="button" data-keep-task>保留任务</button>
<form method="post" action="/tasks/{{pathPart .Task.ID}}/cancel" data-submit-form>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="cancel_key" value="{{.CancelKey}}">
<button class="button danger" type="submit" data-submit-button
data-loading-label="正在取消…">确认取消</button>
</form>
</div>
</dialog>
{{end}}
</main>
</body>
</html>
{{end}}
@@ -0,0 +1,126 @@
{{define "task-new"}}
<!doctype html>
<html lang="zh-CN">
<head>
<title>{{.Page.Title}} - 采购任务管理</title>
{{template "document-head" .}}
</head>
<body>
{{template "site-header" .}}
<main id="main-content" class="page form-page">
<div class="title-row">
<div>
<h1>新建采购任务</h1>
<p class="subtitle">填写商品要求和不可覆盖的采购约束</p>
</div>
</div>
{{if or .Notice .Errors.Form}}
<div class="notice notice-error" role="alert" tabindex="-1" data-error-summary>
{{if .Notice}}<p>{{.Notice}}</p>{{end}}
{{if .Errors.Form}}<p>{{.Errors.Form}}</p>{{end}}
</div>
{{end}}
<form class="task-form" method="post" action="/tasks" enctype="multipart/form-data"
novalidate data-dirty-form data-submit-form>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="upload_key" value="{{.UploadKey}}">
<input type="hidden" name="create_key" value="{{.CreateKey}}">
<input type="hidden" name="image_asset_id" value="{{.UploadedAsset.ID}}">
<section class="form-section" aria-labelledby="requirement-heading">
<h2 id="requirement-heading">商品要求</h2>
<div class="form-grid">
<div class="field span-2">
<label for="title">商品标题 <span class="required-text">必填</span></label>
<input id="title" name="title" value="{{.Form.Title}}" maxlength="120" required
autocomplete="off" aria-describedby="title-error"
{{if .Errors.Title}}aria-invalid="true" data-error-field{{end}}>
<p id="title-error" class="field-error">{{.Errors.Title}}</p>
</div>
<div class="field span-2">
<label for="sku">SKU <span class="required-text">必填</span></label>
<input id="sku" name="sku" value="{{.Form.SKU}}" maxlength="512" required
autocomplete="off" aria-describedby="sku-help sku-error"
{{if .Errors.SKU}}aria-invalid="true" data-error-field{{end}}>
<p id="sku-help" class="field-help">填写采购时用于核对的商品规格编号。</p>
<p id="sku-error" class="field-error">{{.Errors.SKU}}</p>
</div>
<div class="field span-2">
<label for="description">商品描述</label>
<textarea id="description" name="description" maxlength="8192"
aria-describedby="description-help description-error"
{{if .Errors.Description}}aria-invalid="true" data-error-field{{end}}>{{.Form.Description}}</textarea>
<p id="description-help" class="field-help">只填写采购判断所需信息,不填写订单、地址或个人信息。</p>
<p id="description-error" class="field-error">{{.Errors.Description}}</p>
</div>
<div class="field">
<label for="quantity">数量 <span class="required-text">必填</span></label>
<input id="quantity" name="quantity" type="number" value="{{.Form.Quantity}}"
min="1" step="1" inputmode="numeric" required aria-describedby="quantity-error"
{{if .Errors.Quantity}}aria-invalid="true" data-error-field{{end}}>
<p id="quantity-error" class="field-error">{{.Errors.Quantity}}</p>
</div>
<div class="field">
<label for="max-budget">最高总预算(元)</label>
<input id="max-budget" name="max_budget" type="text" value="{{.Form.MaxBudget}}"
inputmode="decimal" placeholder="不填写则不设上限"
aria-describedby="budget-help budget-error"
{{if .Errors.MaxBudget}}aria-invalid="true" data-error-field{{end}}>
<p id="budget-help" class="field-help">数量合计的最高预算,不是单件价格。</p>
<p id="budget-error" class="field-error">{{.Errors.MaxBudget}}</p>
</div>
</div>
</section>
<section class="form-section" aria-labelledby="image-heading">
<h2 id="image-heading">参考图片</h2>
<div class="upload-layout">
<div class="field">
<label for="image">商品参考图 <span class="required-text">必填</span></label>
{{if .UploadedAsset.ID}}
<p class="retained-file">已保留上传成功的参考图片</p>
<p class="field-help">再次提交会复用该图片,不会重复上传。</p>
{{else}}
<input id="image" name="image" type="file"
accept="image/png,image/jpeg,image/webp" required
aria-describedby="image-help image-error"
{{if .Errors.Image}}aria-invalid="true" data-error-field{{end}}>
<p id="image-help" class="field-help">支持 JPG、PNG、WebP;服务端会校验实际内容。</p>
{{end}}
<p id="image-error" class="field-error">{{.Errors.Image}}</p>
</div>
<figure class="image-preview">
<div class="image-placeholder" data-image-placeholder>尚未选择图片</div>
<img alt="待上传的商品参考图预览" data-image-preview hidden>
<figcaption data-image-name>
{{if .UploadedAsset.ID}}参考图片已保留{{else}}选择后仅在本地预览{{end}}
</figcaption>
</figure>
</div>
</section>
<div class="form-actions">
<a class="button" href="/tasks">取消</a>
<button class="button primary" type="submit" data-submit-button
data-loading-label="正在创建…">创建任务</button>
</div>
</form>
</main>
<dialog class="confirm-dialog" data-leave-dialog aria-labelledby="leave-title">
<h2 id="leave-title">放弃未提交内容?</h2>
<p>已填写的商品要求不会保存。</p>
<div class="dialog-actions">
<button class="button" type="button" data-stay>继续填写</button>
<a class="button danger" href="/tasks" data-discard>放弃并离开</a>
</div>
</dialog>
</body>
</html>
{{end}}
@@ -0,0 +1,93 @@
{{define "tasks"}}
<!doctype html>
<html lang="zh-CN">
<head>
<title>{{.Page.Title}} - 采购任务管理</title>
{{template "document-head" .}}
</head>
<body>
{{template "site-header" .}}
<main id="main-content" class="page">
<div class="title-row">
<div>
<h1>采购任务</h1>
<p class="subtitle">按最近创建时间查看采购任务状态</p>
</div>
<a class="button primary" href="/tasks/new">新建任务</a>
</div>
<form class="filters" method="get" action="/tasks" data-loading-form>
<div class="field compact">
<label for="task-search">搜索</label>
<input id="task-search" name="q" type="search" value="{{.Query}}"
placeholder="任务编号、标题或 SKU">
</div>
<div class="field compact">
<label for="task-status">状态</label>
<select id="task-status" name="status">
{{range .StatusOptions}}
<option value="{{.Value}}" {{if .Selected}}selected{{end}}>{{.Label}}</option>
{{end}}
</select>
</div>
<button class="button filter-button" type="submit" data-loading-label="正在加载…">应用筛选</button>
</form>
<section class="table-region" aria-labelledby="task-table-title" data-loading-region>
<h2 id="task-table-title" class="visually-hidden">任务列表结果</h2>
{{if .Items}}
<table>
<thead>
<tr>
<th scope="col">任务</th>
<th scope="col">状态</th>
<th scope="col">执行设备</th>
<th scope="col">更新时间</th>
<th scope="col"><span class="visually-hidden">操作</span></th>
</tr>
</thead>
<tbody>
{{range .Items}}
<tr>
<td data-label="任务">
<strong class="task-title">{{.Title}}</strong>
<span class="secondary">SKU:{{.SKU}}</span>
<span class="secondary">编号:{{.ID}}</span>
</td>
<td data-label="状态">
<span class="status {{.StatusClass}}" title="状态码 {{.Status}}">{{.StatusLabel}}</span>
</td>
<td data-label="执行设备">{{.DeviceName}}</td>
<td data-label="更新时间">
<time datetime="{{machineTime .UpdatedAt}}">{{displayTime .UpdatedAt}}</time>
</td>
<td data-label="操作">
<a class="detail-link" href="/tasks/{{pathPart .ID}}">查看详情</a>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="empty-state">
<h2>没有符合条件的任务</h2>
{{if or .Query .Status}}
<p>请调整搜索或状态筛选后重试。</p>
<a class="button" href="/tasks">清除筛选</a>
{{else}}
<p>创建第一条采购任务后,它会显示在这里。</p>
<a class="button primary" href="/tasks/new">新建任务</a>
{{end}}
</div>
{{end}}
</section>
{{if .NextCursor}}
<nav class="pagination" aria-label="任务分页">
<a class="button" href="/tasks?q={{.Query}}&amp;status={{.Status}}&amp;cursor={{.NextCursor}}">下一页</a>
</nav>
{{end}}
</main>
</body>
</html>
{{end}}