2026-08-04 14:57:40 +08:00
{{define "tasks.html"}}
<!doctype html>
< html lang = "zh-CN" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > 采购任务 · 采购服务</ title >
< style >
2026-08-04 19:42:35 +08:00
:root{--bg:#f4f7fb;--surface:#fff;--text:#172033;--muted:#526079;--border:#cfd8e6;--primary:#155eef;--danger:#b42318;--success:#067647;--focus:#ffbf47;font-family:"Segoe UI","Microsoft YaHei UI",system-ui,sans-serif}*{box-sizing:border-box}html{min-width:320px;background:var(--bg)}body{min-height:100dvh;margin:0;color:var(--text);background:var(--bg);font-size:16px;line-height:1.55}button,input,select{font:inherit}:focus-visible{outline:3px solid var(--focus);outline-offset:3px}.skip{position:fixed;z-index:100;top:8px;left:8px;padding:10px;color:#fff;background:#172033;transform:translateY(-160%)}.skip:focus{transform:translateY(0)}header{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:64px;padding:10px clamp(16px,4vw,40px);border-bottom:1px solid var(--border);background:var(--surface)}.brand{font-weight:700}.brand b{display:inline-grid;place-items:center;width:32px;height:32px;margin-right:8px;border-radius:8px;background:var(--primary);color:#fff;font-size:.82rem}.logout,.button{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:9px 14px;border:1px solid var(--border);border-radius:8px;color:var(--text);background:#fff;font-weight:700;text-decoration:none;cursor:pointer}.button.primary{border-color:var(--primary);background:var(--primary);color:#fff}.button:disabled,.filters input:disabled,.filters select:disabled{opacity:.5;cursor:not-allowed}main{width:min(100% - 32px,1200px);margin:32px auto}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:16px}.toolbar-actions,.filters,.actions,.batch-actions{display:flex;flex-wrap:wrap;gap:10px}.muted,.placeholder,.not-selectable{color:var(--muted)}.filters{align-items:end;margin:0 0 16px}.filter-field{display:grid;gap:4px}.filter-field label{font-weight:700}.filters input,.filters select{min-height:44px;min-width:180px;padding:8px 10px;border:1px solid var(--border);border-radius:8px;background:#fff}.filters [aria-invalid=true]{border-color:var(--danger)}.batch-bar{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:0 0 16px;padding:14px 16px;border:1px solid var(--border);border-radius:12px;background:var(--surface)}.batch-bar p{margin:2px 0}.batch-summary{font-weight:700}.batch-message{min-height:1.55em;color:var(--muted)}.table-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:12px;background:var(--surface)}table{width:100%;min-width:880px;border-collapse:collapse}th,td{padding:12px 14px;border-bottom:1px solid var(--border);text-align:left;vertical-align:top}th{background:#f8fafc;font-size:.88rem}td a{color:#124cc5;font-weight:700;text-underline-offset:3px}tr[data-task-row]{cursor:pointer}tr[data-task-row]:hover,tr[data-task-row]:focus{background:#f4f7ff}tr[data-task-row]:focus{outline:3px solid var(--focus);outline-offset:-3px}.detail-link-button{display:block;min-height:44px;margin-top:5px;padding:5px 0;border:0;color:#124cc5;background:transparent;font-weight:700;text-decoration:underline;text-underline-offset:3px;cursor:pointer}.select-cell{width:64px;text-align:center}.checkbox-target{display:inline-grid;place-items:center;min-width:44px;min-height:44px;margin:-10px;cursor:pointer}.checkbox-target input{width:18px;height:18px}.status{display:inline-block;padding:3px 8px;border-radius:999px;background:#eaf1ff;color:#173d8f;font-size:.85rem;font-weight:700}.empty,.success{padding:20px;border:1px solid var(--border);border-radius:12px;background:var(--surface)}.success{margin:0 0 16px;border-color:#9dd9b8;background:#ecfdf3;color:var(--success)}.modal-scrim{position:fixed;z-index:20;inset:0;background:rgba(23,32,51,.52)}dialog.create-dialog[open]{position:fixed;z-index:30;top:50%;left:50%;width:min(calc(100% - 24px),640px);max-height:calc(100dvh - 24px);margin:0;padding:28px;overflow-y:auto;border:1px solid var(--border);border-radius:14px;box-shadow:0 18px 48px rgba(23,32,51,.24);transform:translate(-50%,-50%);background:var(--surface)}.detail-drawer{width:min(760px,calc(100% - 24px));max-width:none;height:100dvh;
2026-08-04 14:57:40 +08:00
</ head >
< body >
2026-08-04 16:33:22 +08:00
< a class = "skip" href = "#main" > 跳到主要内容</ a >
< header >< div class = "brand" >< b aria-hidden = "true" > 采</ b > 采购服务</ div >< form method = "post" action = "/logout" >< input type = "hidden" name = "csrf_token" value = "{{.CSRFToken}}" >< button class = "logout" type = "submit" > 退出登录</ button ></ form ></ header >
2026-08-04 18:24:39 +08:00
{{if .FullPage}}< main class = "form-page" id = "main" > {{template "form" .}}</ main > {{else}}< main id = "main" >
< div class = "toolbar" >< div >< h1 > 采购任务</ h1 >< p class = "muted" > 查询任务并统一授权待开始任务。</ p ></ div >< div class = "toolbar-actions" >< button class = "button" type = "button" disabled > 导入</ button >< a class = "button primary" href = "/tasks?create=1" > 创建任务</ a ></ div ></ div >
{{if .FilterErrors}}< div class = "summary" role = "alert" aria-live = "assertive" >< p > 请修正筛选条件后重新查询。</ p >< ul > {{with index .FilterErrors "status"}}< li >< a href = "#filter-status" > 状态:{{.}}</ a ></ li > {{end}}{{with index .FilterErrors "created_from"}}< li >< a href = "#filter-created-from" > 开始日期:{{.}}</ a ></ li > {{end}}{{with index .FilterErrors "created_to"}}< li >< a href = "#filter-created-to" > 结束日期:{{.}}</ a ></ li > {{end}}</ ul ></ div > {{end}}
< form class = "filters" method = "get" action = "/tasks" aria-label = "任务筛选" >
< div class = "filter-field" >< label for = "filter-keyword" > 关键词</ label >< input id = "filter-keyword" name = "keyword" type = "search" value = "{{.Filter.Keyword}}" placeholder = "标题或商品编号" ></ div >
< div class = "filter-field" >< label for = "filter-status" > 状态</ label >< select id = "filter-status" name = "status" aria-invalid = "{{if index .FilterErrors " status "}} true {{ else }} false {{ end }}"{{ with index . FilterErrors " status "}} aria-describedby = "filter-status-error" {{ end }} > {{if index .FilterErrors "status"}}< option value = "{{.Filter.Status}}" selected > 无效状态:{{.Filter.Status}}</ option > {{end}}< option value = "" {{ if eq . Filter . Status ""}} selected {{ end }} > 全部状态</ option >< option value = "DRAFT" {{ if eq . Filter . Status " DRAFT "}} selected {{ end }} > 待开始</ option >< option value = "PENDING" {{ if eq . Filter . Status " PENDING "}} selected {{ end }} > 已授权待领取</ option >< option value = "CLAIMED" {{ if eq . Filter . Status " CLAIMED "}} selected {{ end }} > 已领取</ option >< option value = "ORDERING" {{ if eq . Filter . Status " ORDERING "}} selected {{ end }} > 执行中</ option >< option value = "NEEDS_MANUAL" {{ if eq . Filter . Status " NEEDS_MANUAL "}} selected {{ end }} > 待人工处理</ option >< option value = "WAITING_PAYMENT" {{ if eq . Filter . Status " WAITING_PAYMENT "}} selected {{ end }} > 待付款</ option >< option value = "RECONCILIATION_REQUIRED" {{ if eq . Filter . Status " RECONCILIATION_REQUIRED "}} selected {{ end }} > 围栏后待调和</ option >< option value = "SUCCEEDED" {{ if eq . Filter . Status " SUCCEEDED "}} selected {{ end }} > 已完成</ option >< option value = "FAILED" {{ if eq . Filter . Status " FAILED "}} selected {{ end }} > 失败</ option >< option value = "CANCELED" {{ if eq . Filter . Status " CANCELED "}} selected {{ end }} > 已取消</ option ></ select > {{with index .FilterErrors "status"}}< p class = "error" id = "filter-status-error" > {{.}}</ p > {{end}}</ div >
< div class = "filter-field" >< label for = "filter-created-from" > 开始日期</ label >< input id = "filter-created-from" name = "created_from" type = "date" value = "{{.Filter.CreatedFrom}}" aria-invalid = "{{if index .FilterErrors " created_from "}} true {{ else }} false {{ end }}"{{ with index . FilterErrors " created_from "}} aria-describedby = "filter-created-from-error" {{ end }} > {{with index .FilterErrors "created_from"}}< p class = "error" id = "filter-created-from-error" > {{.}}</ p > {{end}}</ div >
< div class = "filter-field" >< label for = "filter-created-to" > 结束日期</ label >< input id = "filter-created-to" name = "created_to" type = "date" value = "{{.Filter.CreatedTo}}" aria-invalid = "{{if index .FilterErrors " created_to "}} true {{ else }} false {{ end }}"{{ with index . FilterErrors " created_to "}} aria-describedby = "filter-created-to-error" {{ end }} > {{with index .FilterErrors "created_to"}}< p class = "error" id = "filter-created-to-error" > {{.}}</ p > {{end}}</ div >
< div class = "actions" >< button class = "button primary" type = "submit" > 筛选</ button >< a class = "button" href = "/tasks" > 清除筛选</ a ></ div >
</ form >
{{if .Success}}< p class = "success" role = "status" > 任务已创建,已显示在列表首行。</ p > {{end}}
< form data-start-purchases data-start-key = "{{.StartKey}}" data-csrf = "{{.CSRFToken}}" >
< section class = "batch-bar" aria-label = "批量开始采购" >< div >< p class = "batch-summary" data-selection-summary aria-live = "polite" > 已选 0 条,最高总额 ¥0.00</ p >< p class = "muted" id = "payment-note" > 采购工具会逐条创建待付款订单,系统不会付款。</ p >< p class = "batch-message" id = "start-feedback" data-start-feedback role = "status" aria-live = "polite" ></ p ></ div >< div class = "batch-actions" >< button class = "button primary" type = "submit" data-start-button aria-describedby = "payment-note start-feedback" disabled > 开始采购(只创建待付款订单)</ button ></ div ></ section >
2026-08-04 19:42:35 +08:00
< div class = "table-wrap" >< table >< thead >< tr >< th class = "select-cell" scope = "col" >< label class = "checkbox-target" >< span class = "sr-only" > 选择全部当前筛选结果中的待开始任务</ span >< input type = "checkbox" data-select-all aria-label = "选择全部任务" {{ if not . Tasks }} disabled {{ end }} ></ label ></ th >< th scope = "col" > 标题</ th >< th scope = "col" > 颜色分类</ th >< th scope = "col" > 尺码</ th >< th scope = "col" > 价格上限</ th >< th scope = "col" > 数量</ th >< th scope = "col" > 采购结果</ th >< th scope = "col" > 状态</ th >< th scope = "col" > 创建时间(上海)</ th ></ tr ></ thead >< tbody > {{if .Tasks}}{{range .Tasks}}< tr data-task-row data-detail-url = "/tasks/{{.ID}}" tabindex = "0" aria-label = "查看任务详情:{{.Title}}" >< td class = "select-cell" > {{if eq .Status "DRAFT"}}< label class = "checkbox-target" >< span class = "sr-only" > 选择任务 {{.Title}}</ span >< input type = "checkbox" name = "task_ids" value = "{{.ID}}" data-task-id = "{{.ID}}" data-task-version = "{{.Version}}" data-price = "{{.MaxTotalPrice}}" aria-label = "选择任务 {{.Title}}" ></ label > {{else}}< span class = "not-selectable" > —< span class = "sr-only" > {{statusLabel .Status}}任务不可选择</ span ></ span > {{end}}</ td >< td >< a href = "https://mobile.yangkeduo.com/goods.html?goods_id={{.GoodsID}}" target = "_blank" rel = "noopener noreferrer" > {{.Title}}</ a >< button class = "detail-link-button" type = "button" data-open-detail > 查看详情</ button ></ td >< td > {{.SKUColor}}</ td >< td > {{.SKUSize}}</ td >< td > ¥{{.MaxTotalPrice}}</ td >< td > {{.Quantity}}</ td >< td > —</ td >< td >< span class = "status" > {{statusLabel .Status}}</ span ></ td >< td >< time datetime = "{{shanghaiDateTime .CreatedAt}}" > {{shanghaiTime .CreatedAt}}</ time ></ td ></ tr > {{end}}{{else}}< tr >< td colspan = "9" > {{if .FilterErrors}}< section class = "empty" >< h2 > 筛选条件有误</ h2 >< p > 请修正上方标出的字段后重新查询。</ p ></ section > {{else if .HasFilter}}< section class = "empty" >< h2 > 没有符合筛选条件的任务</ h2 >< p >< a class = "button" href = "/tasks" > 清除筛选</ a ></ p ></ section > {{else}}< section class = "empty" >< h2 > 还没有采购任务</ h2 >< p > 创建一条手工任务后会显示在这里。</ p ></ section > {{end}}</ td ></ tr > {{end}}</ tbody ></ table ></ div >
2026-08-04 18:24:39 +08:00
</ form >
2026-08-04 19:42:35 +08:00
</ main > {{if .OpenForm}}< div class = "modal-scrim" aria-hidden = "true" ></ div >< dialog open class = "create-dialog" aria-modal = "true" aria-labelledby = "form-title" > {{template "form" .}}</ dialog > {{end}}< dialog class = "detail-drawer" data-detail-drawer aria-modal = "true" aria-labelledby = "detail-drawer-title" >< div class = "detail-drawer-head" >< h2 id = "detail-drawer-title" > 任务详情</ h2 >< button class = "button" type = "button" data-close-detail > 关闭</ button ></ div >< div class = "detail-drawer-body" data-detail-body aria-live = "polite" ></ div ></ dialog >< script src = "/static/tasks.js" defer ></ script > {{end}}
2026-08-04 14:57:40 +08:00
</ body >
</ html >
{{end}}
2026-08-04 16:33:22 +08:00
{{define "form"}}< h1 id = "form-title" > 创建采购任务</ h1 >< p class = "muted" > 保存后仅生成待开始任务,不会执行其他动作。</ p > {{if .Errors}}< div class = "summary" role = "alert" aria-live = "assertive" >< p > 请修正下列字段后再保存。</ p >< ul > {{with index .Errors "title"}}< li >< a href = "#title" > 任务名称:{{.}}</ a ></ li > {{end}}{{with index .Errors "product_url"}}< li >< a href = "#product_url" > 商品链接:{{.}}</ a ></ li > {{end}}{{with index .Errors "sku_color"}}< li >< a href = "#sku_color" > 颜色分类:{{.}}</ a ></ li > {{end}}{{with index .Errors "sku_size"}}< li >< a href = "#sku_size" > 尺码:{{.}}</ a ></ li > {{end}}{{with index .Errors "quantity"}}< li >< a href = "#quantity" > 数量:{{.}}</ a ></ li > {{end}}{{with index .Errors "max_total_price"}}< li >< a href = "#max_total_price" > 价格上限:{{.}}</ a ></ li > {{end}}{{with index .Errors "create_key"}}< li > {{.}}</ li > {{end}}</ ul ></ div > {{end}}< form method = "post" action = "/tasks" class = "form-grid" >< input type = "hidden" name = "csrf_token" value = "{{.CSRFToken}}" >< input type = "hidden" name = "create_key" value = "{{.Form.CreateKey}}" >< input type = "hidden" name = "form_mode" value = "{{if .FullPage}}full{{else}}dialog{{end}}" > {{template "field" (list "title" "任务名称" .Form.Title .Errors .FocusField)}}{{template "field" (list "product_url" "商品链接" .Form.ProductURL .Errors .FocusField)}}{{template "field" (list "sku_color" "颜色分类" .Form.SKUColor .Errors .FocusField)}}{{template "field" (list "sku_size" "尺码" .Form.SKUSize .Errors .FocusField)}}{{template "field" (list "quantity" "数量" .Form.Quantity .Errors .FocusField)}}{{template "field" (list "max_total_price" "价格上限" .Form.MaxTotalPrice .Errors .FocusField)}}< div class = "actions" >< button class = "button primary" type = "submit" > 保存任务</ button >< a class = "button" href = "/tasks" > 取消</ a ></ div ></ form > {{end}}
{{define "field"}}{{$name:=index . 0}}{{$label:=index . 1}}{{$value:=index . 2}}{{$errors:=index . 3}}{{$focus:=index . 4}}< div class = "field" >< label for = "{{$name}}" > {{$label}} < span class = "required" aria-hidden = "true" > *</ span >< span class = "sr-only" > (必填)</ span ></ label >< input id = "{{$name}}" name = "{{$name}}" value = "{{$value}}" required {{ if eq $ focus $ name }} autofocus {{ end }} aria-invalid = "{{if index $errors $name}}true{{else}}false{{end}}" {{ with index $ errors $ name }} aria-describedby = "{{$name}}-error" {{ end }} {{ if eq $ name " product_url "}} type = "url" inputmode = "url" maxlength = "2048" {{ else if eq $ name " quantity "}} type = "number" inputmode = "numeric" min = "1" step = "1" {{ else if eq $ name " max_total_price "}} type = "text" inputmode = "decimal" pattern = "[0-9]+(\.[0-9]{1,2})?" maxlength = "64" {{ else if eq $ name " title "}} type = "text" maxlength = "120" {{ else }} type = "text" maxlength = "80" {{ end }} > {{with index $errors $name}}< p class = "error" id = "{{$name}}-error" > {{.}}</ p > {{end}}</ div > {{end}}