feat(t241): improve freight admin views
This commit is contained in:
@@ -48,24 +48,27 @@ type FreightSyncRun struct {
|
||||
}
|
||||
|
||||
type FreightOrder struct {
|
||||
ID string
|
||||
CreatorSubject string
|
||||
SourceSystem string
|
||||
ExternalStockID string
|
||||
SourceCode string
|
||||
PlatformOrderNo *string
|
||||
ShopName *string
|
||||
SourceCreatedAt *time.Time
|
||||
OrderStatus *string
|
||||
PurchaseStatus *string
|
||||
IsCanceled *bool
|
||||
CanonicalSHA256 string
|
||||
Revision int
|
||||
FirstSyncRunID string
|
||||
LastSyncRunID string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ItemCount int
|
||||
ID string
|
||||
CreatorSubject string
|
||||
SourceSystem string
|
||||
ExternalStockID string
|
||||
SourceCode string
|
||||
PlatformOrderNo *string
|
||||
ShopName *string
|
||||
SourceCreatedAt *time.Time
|
||||
OrderStatus *string
|
||||
PurchaseStatus *string
|
||||
IsCanceled *bool
|
||||
CanonicalSHA256 string
|
||||
Revision int
|
||||
FirstSyncRunID string
|
||||
LastSyncRunID string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ItemCount int
|
||||
PreviewItemID string
|
||||
PreviewTitle string
|
||||
PreviewImageStatus FreightItemImageStatus
|
||||
}
|
||||
|
||||
type FreightOrderItem struct {
|
||||
|
||||
@@ -90,6 +90,12 @@ func TestFreightItemImagesAreCurrentRetryableAndRollbackGuarded(
|
||||
t.Fatalf("retry jobs = %+v, %v", jobs, err)
|
||||
}
|
||||
orders, _ := store.ListFreightOrders(ctx, "local-admin", 10)
|
||||
if len(orders) != 1 ||
|
||||
orders[0].PreviewItemID != ready.FreightOrderItemID ||
|
||||
orders[0].PreviewTitle != "商品一" ||
|
||||
orders[0].PreviewImageStatus != domain.FreightItemImageReady {
|
||||
t.Fatalf("freight order preview = %+v", orders)
|
||||
}
|
||||
detail, err := store.GetFreightOrder(
|
||||
ctx,
|
||||
"local-admin",
|
||||
|
||||
@@ -488,21 +488,80 @@ func (store *Store) ListFreightOrders(
|
||||
if err != nil {
|
||||
return nil, repositoryFailure(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
orders := make([]domain.FreightOrder, 0)
|
||||
for rows.Next() {
|
||||
order, err := scanFreightOrder(rows)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return nil, repositoryFailure(err)
|
||||
}
|
||||
orders = append(orders, order)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return nil, repositoryFailure(err)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, repositoryFailure(err)
|
||||
}
|
||||
for index := range orders {
|
||||
if err := store.loadFreightOrderPreview(
|
||||
ctx,
|
||||
&orders[index],
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return orders, nil
|
||||
}
|
||||
|
||||
func (store *Store) loadFreightOrderPreview(
|
||||
ctx context.Context,
|
||||
order *domain.FreightOrder,
|
||||
) error {
|
||||
var itemID, title string
|
||||
var itemThumb, imageThumb, imageStatus sql.NullString
|
||||
err := store.db.QueryRowContext(
|
||||
ctx,
|
||||
`SELECT item.id, item.title, item.product_thumb_ref,
|
||||
image.product_thumb_ref, image.status
|
||||
FROM freight_order_items AS item
|
||||
LEFT JOIN freight_item_images AS image
|
||||
ON image.freight_order_item_id = item.id
|
||||
WHERE item.freight_order_id = ? AND item.is_present = 1
|
||||
ORDER BY CAST(item.external_item_id AS INTEGER),
|
||||
item.external_item_id
|
||||
LIMIT 1`,
|
||||
order.ID,
|
||||
).Scan(
|
||||
&itemID,
|
||||
&title,
|
||||
&itemThumb,
|
||||
&imageThumb,
|
||||
&imageStatus,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return repositoryFailure(err)
|
||||
}
|
||||
order.PreviewItemID = itemID
|
||||
order.PreviewTitle = title
|
||||
switch {
|
||||
case !itemThumb.Valid:
|
||||
order.PreviewImageStatus = domain.FreightItemImageNone
|
||||
case imageThumb.Valid && imageStatus.Valid &&
|
||||
imageThumb.String == itemThumb.String:
|
||||
order.PreviewImageStatus = domain.FreightItemImageStatus(
|
||||
imageStatus.String,
|
||||
)
|
||||
default:
|
||||
order.PreviewImageStatus = domain.FreightItemImagePending
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) GetFreightOrder(
|
||||
ctx context.Context,
|
||||
creatorSubject, orderID string,
|
||||
|
||||
@@ -451,6 +451,9 @@ func TestAdminFreightAPIImportsAllItemsWithoutPII(t *testing.T) {
|
||||
)
|
||||
if list.Code != http.StatusOK ||
|
||||
!strings.Contains(list.Body.String(), `"item_count":2`) ||
|
||||
!strings.Contains(list.Body.String(), `"preview_title":"商品一"`) ||
|
||||
!strings.Contains(list.Body.String(), `"preview_image_status":"READY"`) ||
|
||||
!strings.Contains(list.Body.String(), `"/api/v1/freight-items/`) ||
|
||||
responseContainsKey(mustDecodeAny(t, list), "receiver") ||
|
||||
responseContainsKey(mustDecodeAny(t, list), "receiverTel") ||
|
||||
responseContainsKey(mustDecodeAny(t, list), "receiverAddr") {
|
||||
|
||||
@@ -281,20 +281,29 @@ func nullableResponseString(value string) any {
|
||||
}
|
||||
|
||||
func freightOrderResponse(order domain.FreightOrder) gin.H {
|
||||
var previewImageURL any
|
||||
if order.PreviewImageStatus == domain.FreightItemImageReady {
|
||||
previewImageURL = "/api/v1/freight-items/" +
|
||||
order.PreviewItemID + "/image"
|
||||
}
|
||||
return gin.H{
|
||||
"id": order.ID,
|
||||
"source_system": order.SourceSystem,
|
||||
"external_stock_id": order.ExternalStockID,
|
||||
"source_code": order.SourceCode,
|
||||
"platform_order_no": order.PlatformOrderNo,
|
||||
"shop_name": order.ShopName,
|
||||
"source_created_at": formatOptionalTime(order.SourceCreatedAt),
|
||||
"order_status": order.OrderStatus,
|
||||
"purchase_status": order.PurchaseStatus,
|
||||
"is_canceled": order.IsCanceled,
|
||||
"revision": order.Revision,
|
||||
"canonical_sha256": order.CanonicalSHA256,
|
||||
"item_count": order.ItemCount,
|
||||
"updated_at": formatTime(order.UpdatedAt),
|
||||
"id": order.ID,
|
||||
"source_system": order.SourceSystem,
|
||||
"external_stock_id": order.ExternalStockID,
|
||||
"source_code": order.SourceCode,
|
||||
"platform_order_no": order.PlatformOrderNo,
|
||||
"shop_name": order.ShopName,
|
||||
"source_created_at": formatOptionalTime(order.SourceCreatedAt),
|
||||
"order_status": order.OrderStatus,
|
||||
"purchase_status": order.PurchaseStatus,
|
||||
"is_canceled": order.IsCanceled,
|
||||
"revision": order.Revision,
|
||||
"canonical_sha256": order.CanonicalSHA256,
|
||||
"item_count": order.ItemCount,
|
||||
"preview_item_id": nullableResponseString(order.PreviewItemID),
|
||||
"preview_title": nullableResponseString(order.PreviewTitle),
|
||||
"preview_image_status": order.PreviewImageStatus,
|
||||
"preview_image_url": previewImageURL,
|
||||
"updated_at": formatTime(order.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -928,13 +928,16 @@ func TestFreightPagesEscapeSourceDataAndCreateSynchronousOrderSync(t *testing.T)
|
||||
service := &fakeFreightService{
|
||||
fakeService: &fakeService{},
|
||||
orders: []FreightOrder{{
|
||||
ID: testTaskID,
|
||||
ExternalStockID: "12",
|
||||
SourceCode: `<script>private</script>`,
|
||||
ShopName: "测试店铺",
|
||||
ItemCount: 2,
|
||||
Revision: 1,
|
||||
UpdatedAt: now,
|
||||
ID: testTaskID,
|
||||
ExternalStockID: "12",
|
||||
SourceCode: `<script>private</script>`,
|
||||
ShopName: "测试店铺",
|
||||
ItemCount: 2,
|
||||
Revision: 1,
|
||||
UpdatedAt: now,
|
||||
PreviewTitle: "脱敏商品标题",
|
||||
PreviewImageStatus: "READY",
|
||||
PreviewImageURL: "/api/v1/freight-items/preview/image",
|
||||
}},
|
||||
createResult: FreightSync{
|
||||
ID: testTaskID,
|
||||
@@ -947,7 +950,10 @@ func TestFreightPagesEscapeSourceDataAndCreateSynchronousOrderSync(t *testing.T)
|
||||
if list.Code != http.StatusOK ||
|
||||
strings.Contains(list.Body.String(), `<script>private</script>`) ||
|
||||
!strings.Contains(list.Body.String(), "<script>private") ||
|
||||
!strings.Contains(list.Body.String(), "2 项") {
|
||||
!strings.Contains(list.Body.String(), "共 2 项") ||
|
||||
!strings.Contains(list.Body.String(), `width="56" height="56"`) ||
|
||||
!strings.Contains(list.Body.String(), `loading="lazy"`) ||
|
||||
!strings.Contains(list.Body.String(), "脱敏商品标题") {
|
||||
t.Fatalf("freight list status/body = %d / %s", list.Code, list.Body)
|
||||
}
|
||||
assertSecurityHeaders(t, list)
|
||||
@@ -1208,6 +1214,7 @@ func TestFreightImportLogsUnknownFailureWithoutRawError(t *testing.T) {
|
||||
|
||||
func TestFreightDetailCreatesProcurementTaskWithCSRF(t *testing.T) {
|
||||
const itemID = "00000000-0000-4000-8000-000000000002"
|
||||
price := int64(12950)
|
||||
service := &fakeProcurementService{
|
||||
fakeFreightService: &fakeFreightService{
|
||||
fakeService: &fakeService{},
|
||||
@@ -1216,14 +1223,20 @@ func TestFreightDetailCreatesProcurementTaskWithCSRF(t *testing.T) {
|
||||
ID: testTaskID,
|
||||
ExternalStockID: "12",
|
||||
SourceCode: "SOURCE-12",
|
||||
ItemCount: 1,
|
||||
},
|
||||
Items: []FreightItemReview{{
|
||||
Item: FreightOrderItem{
|
||||
ID: itemID,
|
||||
ExternalItemID: "88",
|
||||
Title: "商品",
|
||||
SKU: "BLACK-L",
|
||||
Quantity: 2,
|
||||
ID: itemID,
|
||||
ExternalItemID: "88",
|
||||
Title: "商品",
|
||||
ProductSpec: "黑色,L",
|
||||
SKU: "黑色,L",
|
||||
Quantity: 2,
|
||||
OriginalUnitPriceMinor: &price,
|
||||
OriginalCurrency: "TWD",
|
||||
ImageStatus: "READY",
|
||||
ImageURL: "/api/v1/freight-items/" + itemID + "/image",
|
||||
},
|
||||
Request: &ProcurementRequest{
|
||||
ID: itemID,
|
||||
@@ -1250,7 +1263,12 @@ func TestFreightDetailCreatesProcurementTaskWithCSRF(t *testing.T) {
|
||||
)
|
||||
if detail.Code != http.StatusOK ||
|
||||
!strings.Contains(detail.Body.String(), "生成采购任务") ||
|
||||
!strings.Contains(detail.Body.String(), "可以生成任务") {
|
||||
!strings.Contains(detail.Body.String(), "可以生成任务") ||
|
||||
!strings.Contains(detail.Body.String(), "TWD 129.50") ||
|
||||
!strings.Contains(detail.Body.String(), `width="84" height="84"`) ||
|
||||
!strings.Contains(detail.Body.String(), `loading="lazy"`) ||
|
||||
strings.Contains(detail.Body.String(), "图片引用") ||
|
||||
strings.Count(detail.Body.String(), "黑色,L") != 1 {
|
||||
t.Fatalf("detail status/body = %d / %s", detail.Code, detail.Body)
|
||||
}
|
||||
cookie := csrfCookie(t, detail)
|
||||
@@ -1283,6 +1301,26 @@ func TestFreightDetailCreatesProcurementTaskWithCSRF(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFreightPresentationFormatsMoneyAndImageStates(t *testing.T) {
|
||||
value := int64(12905)
|
||||
if got := formatMinorCurrency(&value, "TWD"); got != "TWD 129.05" {
|
||||
t.Fatalf("formatMinorCurrency() = %q", got)
|
||||
}
|
||||
if got := formatMinorCurrency(nil, "TWD"); got != "未提供" {
|
||||
t.Fatalf("nil formatMinorCurrency() = %q", got)
|
||||
}
|
||||
for status, want := range map[string]string{
|
||||
"": "未提供图片",
|
||||
"PENDING": "图片待获取",
|
||||
"MISSING": "ERP 无图片",
|
||||
"FAILED": "图片获取失败",
|
||||
} {
|
||||
if got := freightImageStatusLabel(status); got != want {
|
||||
t.Fatalf("freightImageStatusLabel(%q) = %q", status, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type fakeService struct {
|
||||
listInput ListTasksInput
|
||||
listResult TaskList
|
||||
|
||||
@@ -3,6 +3,7 @@ package webui
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"path"
|
||||
@@ -21,9 +22,11 @@ func NewRenderer() (*Renderer, error) {
|
||||
templates, err := template.New("admin").
|
||||
Option("missingkey=error").
|
||||
Funcs(template.FuncMap{
|
||||
"displayTime": displayTime,
|
||||
"machineTime": machineTime,
|
||||
"pathPart": pathPart,
|
||||
"displayTime": displayTime,
|
||||
"machineTime": machineTime,
|
||||
"pathPart": pathPart,
|
||||
"formatMoney": formatMinorCurrency,
|
||||
"imageStatusLabel": freightImageStatusLabel,
|
||||
}).
|
||||
ParseFS(embeddedFiles, "templates/*.gohtml")
|
||||
if err != nil {
|
||||
@@ -32,6 +35,34 @@ func NewRenderer() (*Renderer, error) {
|
||||
return &Renderer{templates: templates}, nil
|
||||
}
|
||||
|
||||
func formatMinorCurrency(value *int64, currency string) string {
|
||||
if value == nil {
|
||||
return "未提供"
|
||||
}
|
||||
if currency == "" {
|
||||
currency = "TWD"
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"%s %d.%02d",
|
||||
currency,
|
||||
*value/100,
|
||||
*value%100,
|
||||
)
|
||||
}
|
||||
|
||||
func freightImageStatusLabel(status string) string {
|
||||
switch status {
|
||||
case "PENDING":
|
||||
return "图片待获取"
|
||||
case "MISSING":
|
||||
return "ERP 无图片"
|
||||
case "FAILED":
|
||||
return "图片获取失败"
|
||||
default:
|
||||
return "未提供图片"
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) Execute(
|
||||
writer io.Writer,
|
||||
name string,
|
||||
|
||||
@@ -590,6 +590,130 @@ tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.freight-region {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.freight-table-heading {
|
||||
min-height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.freight-table-heading h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.freight-list-table th:first-child {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.freight-list-table th:nth-child(2) {
|
||||
width: 22%;
|
||||
}
|
||||
|
||||
.freight-list-table th:nth-child(3) {
|
||||
width: 14%;
|
||||
}
|
||||
|
||||
.freight-list-table th:nth-child(4) {
|
||||
width: 13%;
|
||||
}
|
||||
|
||||
.freight-list-table th:nth-child(5) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.freight-list-table th:last-child {
|
||||
width: 11%;
|
||||
}
|
||||
|
||||
.freight-items-table th:first-child {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.freight-items-table th:nth-child(2) {
|
||||
width: 14%;
|
||||
}
|
||||
|
||||
.freight-items-table th:nth-child(3) {
|
||||
width: 7%;
|
||||
}
|
||||
|
||||
.freight-items-table th:nth-child(4) {
|
||||
width: 13%;
|
||||
}
|
||||
|
||||
.freight-items-table th:nth-child(5) {
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
.freight-items-table th:last-child {
|
||||
width: 24%;
|
||||
}
|
||||
|
||||
.freight-media {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
}
|
||||
|
||||
.freight-item-media {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.freight-media-copy {
|
||||
min-width: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.freight-product-title {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.freight-thumb {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
object-fit: contain;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 5px;
|
||||
background: var(--surface-soft);
|
||||
}
|
||||
|
||||
.freight-thumb-list {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.freight-thumb-detail {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
}
|
||||
|
||||
.freight-thumb-placeholder {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 5px;
|
||||
border-style: dashed;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.freight-money,
|
||||
.freight-number {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-title,
|
||||
.secondary {
|
||||
display: block;
|
||||
@@ -1262,6 +1386,12 @@ tbody tr:last-child td {
|
||||
.image-preview {
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.freight-list-table td:first-child,
|
||||
.freight-items-table td:first-child,
|
||||
.freight-items-table td:last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
@@ -1369,4 +1499,9 @@ tbody tr:last-child td {
|
||||
.definition-list dd {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.freight-thumb-detail {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,17 +25,20 @@
|
||||
<div><dt>采购状态</dt><dd>{{if .Detail.Order.PurchaseStatus}}{{.Detail.Order.PurchaseStatus}}{{else}}未提供{{end}}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
<section class="table-region" aria-labelledby="freight-items-title">
|
||||
<h2 id="freight-items-title">商品明细</h2>
|
||||
<section class="table-region freight-region" aria-labelledby="freight-items-title">
|
||||
<div class="freight-table-heading">
|
||||
<h2 id="freight-items-title">商品明细</h2>
|
||||
<span class="secondary">共 {{.Detail.Order.ItemCount}} 项</span>
|
||||
</div>
|
||||
{{if .Detail.Items}}
|
||||
<table>
|
||||
<table class="freight-table freight-items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">商品</th>
|
||||
<th scope="col">规格 / SKU</th>
|
||||
<th scope="col">数量</th>
|
||||
<th scope="col">采购状态</th>
|
||||
<th scope="col">来源版本</th>
|
||||
<th scope="col">ERP 原始单价</th>
|
||||
<th scope="col">货运状态</th>
|
||||
<th scope="col">采购处理</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -43,17 +46,32 @@
|
||||
{{range .Detail.Items}}
|
||||
<tr>
|
||||
<td data-label="商品">
|
||||
<strong>{{if .Item.Title}}{{.Item.Title}}{{else}}缺少标题{{end}}</strong>
|
||||
<span class="secondary">明细 ID:{{.Item.ExternalItemID}}</span>
|
||||
{{if .Item.ProductThumbRef}}<span class="secondary">图片引用:{{.Item.ProductThumbRef}}</span>{{end}}
|
||||
<div class="freight-media freight-item-media">
|
||||
{{if .Item.ImageURL}}
|
||||
<img class="freight-thumb freight-thumb-detail" src="{{.Item.ImageURL}}"
|
||||
width="84" height="84" loading="lazy" decoding="async"
|
||||
alt="{{if .Item.Title}}{{.Item.Title}}{{else}}货运商品{{end}}图片">
|
||||
{{else}}
|
||||
<span class="freight-thumb freight-thumb-detail freight-thumb-placeholder"
|
||||
aria-label="{{imageStatusLabel .Item.ImageStatus}}">
|
||||
{{imageStatusLabel .Item.ImageStatus}}
|
||||
</span>
|
||||
{{end}}
|
||||
<span class="freight-media-copy">
|
||||
<strong class="freight-product-title">{{if .Item.Title}}{{.Item.Title}}{{else}}缺少标题{{end}}</strong>
|
||||
<span class="secondary">明细 ID:{{.Item.ExternalItemID}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="规格 / SKU">
|
||||
<span>{{if .Item.ProductSpec}}{{.Item.ProductSpec}}{{else}}未提供规格{{end}}</span>
|
||||
<span class="secondary">SKU:{{if .Item.SKU}}{{.Item.SKU}}{{else}}未提供{{end}}</span>
|
||||
<strong>{{if .Item.ProductSpec}}{{.Item.ProductSpec}}{{else}}未提供{{end}}</strong>
|
||||
</td>
|
||||
<td data-label="数量"><span class="freight-number">{{if .Item.Quantity}}{{.Item.Quantity}}{{else}}未提供{{end}}</span></td>
|
||||
<td data-label="ERP 原始单价"><span class="freight-money">{{formatMoney .Item.OriginalUnitPriceMinor .Item.OriginalCurrency}}</span></td>
|
||||
<td data-label="货运状态">
|
||||
<span>{{if .Item.PurchaseStatus}}{{.Item.PurchaseStatus}}{{else}}未提供{{end}}</span>
|
||||
<span class="secondary">来源版本 {{.Item.Revision}}</span>
|
||||
</td>
|
||||
<td data-label="数量">{{if .Item.Quantity}}{{.Item.Quantity}}{{else}}未提供{{end}}</td>
|
||||
<td data-label="采购状态">{{if .Item.PurchaseStatus}}{{.Item.PurchaseStatus}}{{else}}未提供{{end}}</td>
|
||||
<td data-label="来源版本">{{.Item.Revision}}</td>
|
||||
<td data-label="采购处理" class="procurement-actions">
|
||||
{{if .Request}}
|
||||
<strong>{{.Request.StatusLabel}}</strong>
|
||||
|
||||
@@ -16,16 +16,16 @@
|
||||
<a class="button primary" href="/freight/import">导入货运单</a>
|
||||
</div>
|
||||
{{if .Notice}}<div class="notice" role="status">{{.Notice}}</div>{{end}}
|
||||
<section class="table-region" aria-labelledby="freight-table-title">
|
||||
<section class="table-region freight-region" aria-labelledby="freight-table-title">
|
||||
<h2 id="freight-table-title" class="visually-hidden">货运单列表</h2>
|
||||
{{if .Orders}}
|
||||
<table>
|
||||
<table class="freight-table freight-list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<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"><span class="visually-hidden">操作</span></th>
|
||||
</tr>
|
||||
@@ -33,16 +33,33 @@
|
||||
<tbody>
|
||||
{{range .Orders}}
|
||||
<tr>
|
||||
<td data-label="来源单号">
|
||||
<strong>{{if .SourceCode}}{{.SourceCode}}{{else}}{{.ExternalStockID}}{{end}}</strong>
|
||||
<span class="secondary">ERP ID:{{.ExternalStockID}}</span>
|
||||
<td data-label="货运单">
|
||||
<div class="freight-media">
|
||||
{{if .PreviewImageURL}}
|
||||
<img class="freight-thumb freight-thumb-list" src="{{.PreviewImageURL}}"
|
||||
width="56" height="56" loading="lazy" decoding="async"
|
||||
alt="{{if .PreviewTitle}}{{.PreviewTitle}}{{else}}货运商品{{end}}图片">
|
||||
{{else}}
|
||||
<span class="freight-thumb freight-thumb-list freight-thumb-placeholder"
|
||||
aria-label="{{imageStatusLabel .PreviewImageStatus}}">
|
||||
{{imageStatusLabel .PreviewImageStatus}}
|
||||
</span>
|
||||
{{end}}
|
||||
<span class="freight-media-copy">
|
||||
<strong>{{if .SourceCode}}{{.SourceCode}}{{else}}{{.ExternalStockID}}{{end}}</strong>
|
||||
<span class="secondary">ERP ID:{{.ExternalStockID}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="商品摘要">
|
||||
<strong class="freight-product-title">{{if .PreviewTitle}}{{.PreviewTitle}}{{else}}未提供商品标题{{end}}</strong>
|
||||
<span class="secondary">共 {{.ItemCount}} 项</span>
|
||||
</td>
|
||||
<td data-label="店铺">{{if .ShopName}}{{.ShopName}}{{else}}未提供{{end}}</td>
|
||||
<td data-label="状态">
|
||||
<span class="secondary">订单:{{if .OrderStatus}}{{.OrderStatus}}{{else}}未提供{{end}}</span>
|
||||
<span class="secondary">采购:{{if .PurchaseStatus}}{{.PurchaseStatus}}{{else}}未提供{{end}}</span>
|
||||
</td>
|
||||
<td data-label="商品">{{.ItemCount}} 项</td>
|
||||
<td data-label="更新时间"><time datetime="{{machineTime .UpdatedAt}}">{{displayTime .UpdatedAt}}</time></td>
|
||||
<td data-label="操作"><a class="detail-link" href="/freight/{{pathPart .ID}}">查看详情</a></td>
|
||||
</tr>
|
||||
|
||||
@@ -136,17 +136,20 @@ type CreateFreightSyncInput struct {
|
||||
}
|
||||
|
||||
type FreightOrder struct {
|
||||
ID string
|
||||
ExternalStockID string
|
||||
SourceCode string
|
||||
ShopName string
|
||||
SourceCreatedAt time.Time
|
||||
OrderStatus string
|
||||
PurchaseStatus string
|
||||
IsCanceled bool
|
||||
ItemCount int
|
||||
Revision int
|
||||
UpdatedAt time.Time
|
||||
ID string
|
||||
ExternalStockID string
|
||||
SourceCode string
|
||||
ShopName string
|
||||
SourceCreatedAt time.Time
|
||||
OrderStatus string
|
||||
PurchaseStatus string
|
||||
IsCanceled bool
|
||||
ItemCount int
|
||||
Revision int
|
||||
UpdatedAt time.Time
|
||||
PreviewTitle string
|
||||
PreviewImageStatus string
|
||||
PreviewImageURL string
|
||||
}
|
||||
|
||||
type FreightOrderItem struct {
|
||||
|
||||
@@ -437,15 +437,21 @@ func (adapter *UsecaseAdapter) GetFreightWatermark(
|
||||
|
||||
func freightOrderFrom(order domain.FreightOrder) FreightOrder {
|
||||
result := FreightOrder{
|
||||
ID: order.ID,
|
||||
ExternalStockID: order.ExternalStockID,
|
||||
SourceCode: order.SourceCode,
|
||||
ShopName: stringValue(order.ShopName),
|
||||
OrderStatus: stringValue(order.OrderStatus),
|
||||
PurchaseStatus: stringValue(order.PurchaseStatus),
|
||||
ItemCount: order.ItemCount,
|
||||
Revision: order.Revision,
|
||||
UpdatedAt: order.UpdatedAt,
|
||||
ID: order.ID,
|
||||
ExternalStockID: order.ExternalStockID,
|
||||
SourceCode: order.SourceCode,
|
||||
ShopName: stringValue(order.ShopName),
|
||||
OrderStatus: stringValue(order.OrderStatus),
|
||||
PurchaseStatus: stringValue(order.PurchaseStatus),
|
||||
ItemCount: order.ItemCount,
|
||||
Revision: order.Revision,
|
||||
UpdatedAt: order.UpdatedAt,
|
||||
PreviewTitle: order.PreviewTitle,
|
||||
PreviewImageStatus: string(order.PreviewImageStatus),
|
||||
}
|
||||
if order.PreviewImageStatus == domain.FreightItemImageReady {
|
||||
result.PreviewImageURL = "/api/v1/freight-items/" +
|
||||
order.PreviewItemID + "/image"
|
||||
}
|
||||
if order.SourceCreatedAt != nil {
|
||||
result.SourceCreatedAt = *order.SourceCreatedAt
|
||||
|
||||
Reference in New Issue
Block a user