feat(t241): improve freight admin views
This commit is contained in:
@@ -48,24 +48,27 @@ type FreightSyncRun struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FreightOrder struct {
|
type FreightOrder struct {
|
||||||
ID string
|
ID string
|
||||||
CreatorSubject string
|
CreatorSubject string
|
||||||
SourceSystem string
|
SourceSystem string
|
||||||
ExternalStockID string
|
ExternalStockID string
|
||||||
SourceCode string
|
SourceCode string
|
||||||
PlatformOrderNo *string
|
PlatformOrderNo *string
|
||||||
ShopName *string
|
ShopName *string
|
||||||
SourceCreatedAt *time.Time
|
SourceCreatedAt *time.Time
|
||||||
OrderStatus *string
|
OrderStatus *string
|
||||||
PurchaseStatus *string
|
PurchaseStatus *string
|
||||||
IsCanceled *bool
|
IsCanceled *bool
|
||||||
CanonicalSHA256 string
|
CanonicalSHA256 string
|
||||||
Revision int
|
Revision int
|
||||||
FirstSyncRunID string
|
FirstSyncRunID string
|
||||||
LastSyncRunID string
|
LastSyncRunID string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
ItemCount int
|
ItemCount int
|
||||||
|
PreviewItemID string
|
||||||
|
PreviewTitle string
|
||||||
|
PreviewImageStatus FreightItemImageStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
type FreightOrderItem struct {
|
type FreightOrderItem struct {
|
||||||
|
|||||||
@@ -90,6 +90,12 @@ func TestFreightItemImagesAreCurrentRetryableAndRollbackGuarded(
|
|||||||
t.Fatalf("retry jobs = %+v, %v", jobs, err)
|
t.Fatalf("retry jobs = %+v, %v", jobs, err)
|
||||||
}
|
}
|
||||||
orders, _ := store.ListFreightOrders(ctx, "local-admin", 10)
|
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(
|
detail, err := store.GetFreightOrder(
|
||||||
ctx,
|
ctx,
|
||||||
"local-admin",
|
"local-admin",
|
||||||
|
|||||||
@@ -488,21 +488,80 @@ func (store *Store) ListFreightOrders(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, repositoryFailure(err)
|
return nil, repositoryFailure(err)
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
orders := make([]domain.FreightOrder, 0)
|
orders := make([]domain.FreightOrder, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
order, err := scanFreightOrder(rows)
|
order, err := scanFreightOrder(rows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
rows.Close()
|
||||||
return nil, repositoryFailure(err)
|
return nil, repositoryFailure(err)
|
||||||
}
|
}
|
||||||
orders = append(orders, order)
|
orders = append(orders, order)
|
||||||
}
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
|
rows.Close()
|
||||||
return nil, repositoryFailure(err)
|
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
|
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(
|
func (store *Store) GetFreightOrder(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
creatorSubject, orderID string,
|
creatorSubject, orderID string,
|
||||||
|
|||||||
@@ -451,6 +451,9 @@ func TestAdminFreightAPIImportsAllItemsWithoutPII(t *testing.T) {
|
|||||||
)
|
)
|
||||||
if list.Code != http.StatusOK ||
|
if list.Code != http.StatusOK ||
|
||||||
!strings.Contains(list.Body.String(), `"item_count":2`) ||
|
!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), "receiver") ||
|
||||||
responseContainsKey(mustDecodeAny(t, list), "receiverTel") ||
|
responseContainsKey(mustDecodeAny(t, list), "receiverTel") ||
|
||||||
responseContainsKey(mustDecodeAny(t, list), "receiverAddr") {
|
responseContainsKey(mustDecodeAny(t, list), "receiverAddr") {
|
||||||
|
|||||||
@@ -281,20 +281,29 @@ func nullableResponseString(value string) any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func freightOrderResponse(order domain.FreightOrder) gin.H {
|
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{
|
return gin.H{
|
||||||
"id": order.ID,
|
"id": order.ID,
|
||||||
"source_system": order.SourceSystem,
|
"source_system": order.SourceSystem,
|
||||||
"external_stock_id": order.ExternalStockID,
|
"external_stock_id": order.ExternalStockID,
|
||||||
"source_code": order.SourceCode,
|
"source_code": order.SourceCode,
|
||||||
"platform_order_no": order.PlatformOrderNo,
|
"platform_order_no": order.PlatformOrderNo,
|
||||||
"shop_name": order.ShopName,
|
"shop_name": order.ShopName,
|
||||||
"source_created_at": formatOptionalTime(order.SourceCreatedAt),
|
"source_created_at": formatOptionalTime(order.SourceCreatedAt),
|
||||||
"order_status": order.OrderStatus,
|
"order_status": order.OrderStatus,
|
||||||
"purchase_status": order.PurchaseStatus,
|
"purchase_status": order.PurchaseStatus,
|
||||||
"is_canceled": order.IsCanceled,
|
"is_canceled": order.IsCanceled,
|
||||||
"revision": order.Revision,
|
"revision": order.Revision,
|
||||||
"canonical_sha256": order.CanonicalSHA256,
|
"canonical_sha256": order.CanonicalSHA256,
|
||||||
"item_count": order.ItemCount,
|
"item_count": order.ItemCount,
|
||||||
"updated_at": formatTime(order.UpdatedAt),
|
"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{
|
service := &fakeFreightService{
|
||||||
fakeService: &fakeService{},
|
fakeService: &fakeService{},
|
||||||
orders: []FreightOrder{{
|
orders: []FreightOrder{{
|
||||||
ID: testTaskID,
|
ID: testTaskID,
|
||||||
ExternalStockID: "12",
|
ExternalStockID: "12",
|
||||||
SourceCode: `<script>private</script>`,
|
SourceCode: `<script>private</script>`,
|
||||||
ShopName: "测试店铺",
|
ShopName: "测试店铺",
|
||||||
ItemCount: 2,
|
ItemCount: 2,
|
||||||
Revision: 1,
|
Revision: 1,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
|
PreviewTitle: "脱敏商品标题",
|
||||||
|
PreviewImageStatus: "READY",
|
||||||
|
PreviewImageURL: "/api/v1/freight-items/preview/image",
|
||||||
}},
|
}},
|
||||||
createResult: FreightSync{
|
createResult: FreightSync{
|
||||||
ID: testTaskID,
|
ID: testTaskID,
|
||||||
@@ -947,7 +950,10 @@ func TestFreightPagesEscapeSourceDataAndCreateSynchronousOrderSync(t *testing.T)
|
|||||||
if list.Code != http.StatusOK ||
|
if list.Code != http.StatusOK ||
|
||||||
strings.Contains(list.Body.String(), `<script>private</script>`) ||
|
strings.Contains(list.Body.String(), `<script>private</script>`) ||
|
||||||
!strings.Contains(list.Body.String(), "<script>private") ||
|
!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)
|
t.Fatalf("freight list status/body = %d / %s", list.Code, list.Body)
|
||||||
}
|
}
|
||||||
assertSecurityHeaders(t, list)
|
assertSecurityHeaders(t, list)
|
||||||
@@ -1208,6 +1214,7 @@ func TestFreightImportLogsUnknownFailureWithoutRawError(t *testing.T) {
|
|||||||
|
|
||||||
func TestFreightDetailCreatesProcurementTaskWithCSRF(t *testing.T) {
|
func TestFreightDetailCreatesProcurementTaskWithCSRF(t *testing.T) {
|
||||||
const itemID = "00000000-0000-4000-8000-000000000002"
|
const itemID = "00000000-0000-4000-8000-000000000002"
|
||||||
|
price := int64(12950)
|
||||||
service := &fakeProcurementService{
|
service := &fakeProcurementService{
|
||||||
fakeFreightService: &fakeFreightService{
|
fakeFreightService: &fakeFreightService{
|
||||||
fakeService: &fakeService{},
|
fakeService: &fakeService{},
|
||||||
@@ -1216,14 +1223,20 @@ func TestFreightDetailCreatesProcurementTaskWithCSRF(t *testing.T) {
|
|||||||
ID: testTaskID,
|
ID: testTaskID,
|
||||||
ExternalStockID: "12",
|
ExternalStockID: "12",
|
||||||
SourceCode: "SOURCE-12",
|
SourceCode: "SOURCE-12",
|
||||||
|
ItemCount: 1,
|
||||||
},
|
},
|
||||||
Items: []FreightItemReview{{
|
Items: []FreightItemReview{{
|
||||||
Item: FreightOrderItem{
|
Item: FreightOrderItem{
|
||||||
ID: itemID,
|
ID: itemID,
|
||||||
ExternalItemID: "88",
|
ExternalItemID: "88",
|
||||||
Title: "商品",
|
Title: "商品",
|
||||||
SKU: "BLACK-L",
|
ProductSpec: "黑色,L",
|
||||||
Quantity: 2,
|
SKU: "黑色,L",
|
||||||
|
Quantity: 2,
|
||||||
|
OriginalUnitPriceMinor: &price,
|
||||||
|
OriginalCurrency: "TWD",
|
||||||
|
ImageStatus: "READY",
|
||||||
|
ImageURL: "/api/v1/freight-items/" + itemID + "/image",
|
||||||
},
|
},
|
||||||
Request: &ProcurementRequest{
|
Request: &ProcurementRequest{
|
||||||
ID: itemID,
|
ID: itemID,
|
||||||
@@ -1250,7 +1263,12 @@ func TestFreightDetailCreatesProcurementTaskWithCSRF(t *testing.T) {
|
|||||||
)
|
)
|
||||||
if detail.Code != http.StatusOK ||
|
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(), "可以生成任务") ||
|
||||||
|
!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)
|
t.Fatalf("detail status/body = %d / %s", detail.Code, detail.Body)
|
||||||
}
|
}
|
||||||
cookie := csrfCookie(t, detail)
|
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 {
|
type fakeService struct {
|
||||||
listInput ListTasksInput
|
listInput ListTasksInput
|
||||||
listResult TaskList
|
listResult TaskList
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package webui
|
|||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"path"
|
"path"
|
||||||
@@ -21,9 +22,11 @@ func NewRenderer() (*Renderer, error) {
|
|||||||
templates, err := template.New("admin").
|
templates, err := template.New("admin").
|
||||||
Option("missingkey=error").
|
Option("missingkey=error").
|
||||||
Funcs(template.FuncMap{
|
Funcs(template.FuncMap{
|
||||||
"displayTime": displayTime,
|
"displayTime": displayTime,
|
||||||
"machineTime": machineTime,
|
"machineTime": machineTime,
|
||||||
"pathPart": pathPart,
|
"pathPart": pathPart,
|
||||||
|
"formatMoney": formatMinorCurrency,
|
||||||
|
"imageStatusLabel": freightImageStatusLabel,
|
||||||
}).
|
}).
|
||||||
ParseFS(embeddedFiles, "templates/*.gohtml")
|
ParseFS(embeddedFiles, "templates/*.gohtml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -32,6 +35,34 @@ func NewRenderer() (*Renderer, error) {
|
|||||||
return &Renderer{templates: templates}, nil
|
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(
|
func (r *Renderer) Execute(
|
||||||
writer io.Writer,
|
writer io.Writer,
|
||||||
name string,
|
name string,
|
||||||
|
|||||||
@@ -590,6 +590,130 @@ tbody tr:last-child td {
|
|||||||
border-bottom: 0;
|
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,
|
.task-title,
|
||||||
.secondary {
|
.secondary {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -1262,6 +1386,12 @@ tbody tr:last-child td {
|
|||||||
.image-preview {
|
.image-preview {
|
||||||
max-width: 320px;
|
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) {
|
@media (max-width: 520px) {
|
||||||
@@ -1369,4 +1499,9 @@ tbody tr:last-child td {
|
|||||||
.definition-list dd {
|
.definition-list dd {
|
||||||
margin-bottom: 10px;
|
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>
|
<div><dt>采购状态</dt><dd>{{if .Detail.Order.PurchaseStatus}}{{.Detail.Order.PurchaseStatus}}{{else}}未提供{{end}}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
<section class="table-region" aria-labelledby="freight-items-title">
|
<section class="table-region freight-region" aria-labelledby="freight-items-title">
|
||||||
<h2 id="freight-items-title">商品明细</h2>
|
<div class="freight-table-heading">
|
||||||
|
<h2 id="freight-items-title">商品明细</h2>
|
||||||
|
<span class="secondary">共 {{.Detail.Order.ItemCount}} 项</span>
|
||||||
|
</div>
|
||||||
{{if .Detail.Items}}
|
{{if .Detail.Items}}
|
||||||
<table>
|
<table class="freight-table freight-items-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">商品</th>
|
<th scope="col">商品</th>
|
||||||
<th scope="col">规格 / SKU</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>
|
||||||
<th scope="col">采购处理</th>
|
<th scope="col">采购处理</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -43,17 +46,32 @@
|
|||||||
{{range .Detail.Items}}
|
{{range .Detail.Items}}
|
||||||
<tr>
|
<tr>
|
||||||
<td data-label="商品">
|
<td data-label="商品">
|
||||||
<strong>{{if .Item.Title}}{{.Item.Title}}{{else}}缺少标题{{end}}</strong>
|
<div class="freight-media freight-item-media">
|
||||||
<span class="secondary">明细 ID:{{.Item.ExternalItemID}}</span>
|
{{if .Item.ImageURL}}
|
||||||
{{if .Item.ProductThumbRef}}<span class="secondary">图片引用:{{.Item.ProductThumbRef}}</span>{{end}}
|
<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>
|
||||||
<td data-label="规格 / SKU">
|
<td data-label="规格 / SKU">
|
||||||
<span>{{if .Item.ProductSpec}}{{.Item.ProductSpec}}{{else}}未提供规格{{end}}</span>
|
<strong>{{if .Item.ProductSpec}}{{.Item.ProductSpec}}{{else}}未提供{{end}}</strong>
|
||||||
<span class="secondary">SKU:{{if .Item.SKU}}{{.Item.SKU}}{{else}}未提供{{end}}</span>
|
</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>
|
||||||
<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">
|
<td data-label="采购处理" class="procurement-actions">
|
||||||
{{if .Request}}
|
{{if .Request}}
|
||||||
<strong>{{.Request.StatusLabel}}</strong>
|
<strong>{{.Request.StatusLabel}}</strong>
|
||||||
|
|||||||
@@ -16,16 +16,16 @@
|
|||||||
<a class="button primary" href="/freight/import">导入货运单</a>
|
<a class="button primary" href="/freight/import">导入货运单</a>
|
||||||
</div>
|
</div>
|
||||||
{{if .Notice}}<div class="notice" role="status">{{.Notice}}</div>{{end}}
|
{{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>
|
<h2 id="freight-table-title" class="visually-hidden">货运单列表</h2>
|
||||||
{{if .Orders}}
|
{{if .Orders}}
|
||||||
<table>
|
<table class="freight-table freight-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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">商品</th>
|
|
||||||
<th scope="col">更新时间</th>
|
<th scope="col">更新时间</th>
|
||||||
<th scope="col"><span class="visually-hidden">操作</span></th>
|
<th scope="col"><span class="visually-hidden">操作</span></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -33,16 +33,33 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{{range .Orders}}
|
{{range .Orders}}
|
||||||
<tr>
|
<tr>
|
||||||
<td data-label="来源单号">
|
<td data-label="货运单">
|
||||||
<strong>{{if .SourceCode}}{{.SourceCode}}{{else}}{{.ExternalStockID}}{{end}}</strong>
|
<div class="freight-media">
|
||||||
<span class="secondary">ERP ID:{{.ExternalStockID}}</span>
|
{{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>
|
||||||
<td data-label="店铺">{{if .ShopName}}{{.ShopName}}{{else}}未提供{{end}}</td>
|
<td data-label="店铺">{{if .ShopName}}{{.ShopName}}{{else}}未提供{{end}}</td>
|
||||||
<td data-label="状态">
|
<td data-label="状态">
|
||||||
<span class="secondary">订单:{{if .OrderStatus}}{{.OrderStatus}}{{else}}未提供{{end}}</span>
|
<span class="secondary">订单:{{if .OrderStatus}}{{.OrderStatus}}{{else}}未提供{{end}}</span>
|
||||||
<span class="secondary">采购:{{if .PurchaseStatus}}{{.PurchaseStatus}}{{else}}未提供{{end}}</span>
|
<span class="secondary">采购:{{if .PurchaseStatus}}{{.PurchaseStatus}}{{else}}未提供{{end}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="商品">{{.ItemCount}} 项</td>
|
|
||||||
<td data-label="更新时间"><time datetime="{{machineTime .UpdatedAt}}">{{displayTime .UpdatedAt}}</time></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>
|
<td data-label="操作"><a class="detail-link" href="/freight/{{pathPart .ID}}">查看详情</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -136,17 +136,20 @@ type CreateFreightSyncInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FreightOrder struct {
|
type FreightOrder struct {
|
||||||
ID string
|
ID string
|
||||||
ExternalStockID string
|
ExternalStockID string
|
||||||
SourceCode string
|
SourceCode string
|
||||||
ShopName string
|
ShopName string
|
||||||
SourceCreatedAt time.Time
|
SourceCreatedAt time.Time
|
||||||
OrderStatus string
|
OrderStatus string
|
||||||
PurchaseStatus string
|
PurchaseStatus string
|
||||||
IsCanceled bool
|
IsCanceled bool
|
||||||
ItemCount int
|
ItemCount int
|
||||||
Revision int
|
Revision int
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
|
PreviewTitle string
|
||||||
|
PreviewImageStatus string
|
||||||
|
PreviewImageURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
type FreightOrderItem struct {
|
type FreightOrderItem struct {
|
||||||
|
|||||||
@@ -437,15 +437,21 @@ func (adapter *UsecaseAdapter) GetFreightWatermark(
|
|||||||
|
|
||||||
func freightOrderFrom(order domain.FreightOrder) FreightOrder {
|
func freightOrderFrom(order domain.FreightOrder) FreightOrder {
|
||||||
result := FreightOrder{
|
result := FreightOrder{
|
||||||
ID: order.ID,
|
ID: order.ID,
|
||||||
ExternalStockID: order.ExternalStockID,
|
ExternalStockID: order.ExternalStockID,
|
||||||
SourceCode: order.SourceCode,
|
SourceCode: order.SourceCode,
|
||||||
ShopName: stringValue(order.ShopName),
|
ShopName: stringValue(order.ShopName),
|
||||||
OrderStatus: stringValue(order.OrderStatus),
|
OrderStatus: stringValue(order.OrderStatus),
|
||||||
PurchaseStatus: stringValue(order.PurchaseStatus),
|
PurchaseStatus: stringValue(order.PurchaseStatus),
|
||||||
ItemCount: order.ItemCount,
|
ItemCount: order.ItemCount,
|
||||||
Revision: order.Revision,
|
Revision: order.Revision,
|
||||||
UpdatedAt: order.UpdatedAt,
|
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 {
|
if order.SourceCreatedAt != nil {
|
||||||
result.SourceCreatedAt = *order.SourceCreatedAt
|
result.SourceCreatedAt = *order.SourceCreatedAt
|
||||||
|
|||||||
+15
-6
@@ -5,7 +5,7 @@
|
|||||||
## 当前快照
|
## 当前快照
|
||||||
|
|
||||||
- 日期:2026-07-29
|
- 日期:2026-07-29
|
||||||
- 阶段:T-238 已完成顺运宝详情分钟精度时间兼容
|
- 阶段:T-241 已完成 ERP 货运列表与商品详情展示优化
|
||||||
- Git:当前分支为 `main`;T-001 至 T-004、T-101 至 T-104、T-201 至 T-219
|
- Git:当前分支为 `main`;T-001 至 T-004、T-101 至 T-104、T-201 至 T-219
|
||||||
均按文档提交、实现提交的顺序纳入历史
|
均按文档提交、实现提交的顺序纳入历史
|
||||||
- 生产代码:`android-buyer/` 已接入 Roubao Android 源码
|
- 生产代码:`android-buyer/` 已接入 Roubao Android 源码
|
||||||
@@ -19,7 +19,9 @@
|
|||||||
`/freight/import`、`/freight/{id}` 与对应 JSON API。T-237 已将完整单号改为 55 秒
|
`/freight/import`、`/freight/{id}` 与对应 JSON API。T-237 已将完整单号改为 55 秒
|
||||||
同步响应,成功后直接进入可见货运列表;超时/取消使用独立 cleanup context 写
|
同步响应,成功后直接进入可见货运列表;超时/取消使用独立 cleanup context 写
|
||||||
`FAILED`,日期范围仍后台异步。HTTP `WriteTimeout` 为 70 秒。T-238 已兼容真实详情
|
`FAILED`,日期范围仍后台异步。HTTP `WriteTimeout` 为 70 秒。T-238 已兼容真实详情
|
||||||
`created` 的分钟精度,仍按 Asia/Shanghai 严格解析并转 UTC。
|
`created` 的分钟精度,仍按 Asia/Shanghai 严格解析并转 UTC。T-239 至 T-241 已保存
|
||||||
|
`productSpec`、TWD 原始单价和数字图片引用,以有界并发缓存归一化 JPEG,并在鉴权
|
||||||
|
本地路由、货运列表和详情中显示图片、状态与采购字段。
|
||||||
- ERP Go 迁移:T-225 已用脱敏 fixture 固定 `internal/platform/shunyunbao` 的 header、
|
- ERP Go 迁移:T-225 已用脱敏 fixture 固定 `internal/platform/shunyunbao` 的 header、
|
||||||
单号/日期查询、分页、详情批量和字段 allowlist,并使货运用例依赖来源中立错误。T-226
|
单号/日期查询、分页、详情批量和字段 allowlist,并使货运用例依赖来源中立错误。T-226
|
||||||
已增加受锁保护的 Go 内存 Cookie jar、验证码 ticket、登录和用户校验,以及 ADMIN 的
|
已增加受锁保护的 Go 内存 Cookie jar、验证码 ticket、登录和用户校验,以及 ADMIN 的
|
||||||
@@ -41,7 +43,7 @@
|
|||||||
- Android Studio:未安装;`winget` 静默安装卡住后已终止,不阻塞命令行构建
|
- Android Studio:未安装;`winget` 静默安装卡住后已终止,不阻塞命令行构建
|
||||||
- 测试:T-219 Android Debug/Release 单元测试与构建和根 `init.ps1` 通过;
|
- 测试:T-219 Android Debug/Release 单元测试与构建和根 `init.ps1` 通过;
|
||||||
Debug APK `1.4.16 (21)` 已覆盖安装到 PKG110
|
Debug APK `1.4.16 (21)` 已覆盖安装到 PKG110
|
||||||
- 后端测试:T-226 至 T-238 已运行 `go test ./...`、`go test -race ./...`、`go vet ./...`
|
- 后端测试:T-226 至 T-241 已运行 `go test ./...`、`go test -race ./...`、`go vet ./...`
|
||||||
和三个 Go 入口构建;T-227 增加 Go source 的伪 ERP 会话预检、完整单号、日期分页去重、
|
和三个 Go 入口构建;T-227 增加 Go source 的伪 ERP 会话预检、完整单号、日期分页去重、
|
||||||
详情 allowlist 和稳定错误码覆盖;根 `init.ps1` 的 Android 测试/Debug APK 与 Go 标准
|
详情 allowlist 和稳定错误码覆盖;根 `init.ps1` 的 Android 测试/Debug APK 与 Go 标准
|
||||||
验证也通过,均未访问真实 ERP;
|
验证也通过,均未访问真实 ERP;
|
||||||
@@ -53,6 +55,10 @@
|
|||||||
- 管理 Web:真实 Gin/SQLite 流程已完成图片上传、任务创建、列表、详情参考图和
|
- 管理 Web:真实 Gin/SQLite 流程已完成图片上传、任务创建、列表、详情参考图和
|
||||||
非终态取消;执行中只显示“请求安全停止”,ADMIN 登录/退出和安全返回路径已接入,
|
非终态取消;执行中只显示“请求安全停止”,ADMIN 登录/退出和安全返回路径已接入,
|
||||||
货运列表/导入/详情在 1440×900、390×844、360×800 无横向溢出或页头重叠
|
货运列表/导入/详情在 1440×900、390×844、360×800 无横向溢出或页头重叠
|
||||||
|
- 货运管理展示:列表按外部商品 ID 确定性展示第一条当前商品的 56px 代表图、标题和
|
||||||
|
总项数;详情展示 84px/移动端 72px 本地图片、单一规格 SKU、数量、TWD 原始单价、
|
||||||
|
图片状态和采购操作。Playwright 已在 1440、768、390、375 四档宽度验证无横向溢出、
|
||||||
|
破图、遮挡和控制台错误。
|
||||||
- 鉴权:bcrypt 密码、8 小时管理 session、1 小时 App access token 和设备 secret
|
- 鉴权:bcrypt 密码、8 小时管理 session、1 小时 App access token 和设备 secret
|
||||||
均不明文落库;设备首次绑定原子化,禁用/过期/撤销每次请求重新检查;管理/App
|
均不明文落库;设备首次绑定原子化,禁用/过期/撤销每次请求重新检查;管理/App
|
||||||
登录各自按来源地址执行内存有界限流,账号和设备支持 `authctl` 启停
|
登录各自按来源地址执行内存有界限流,账号和设备支持 `authctl` 启停
|
||||||
@@ -203,6 +209,7 @@
|
|||||||
| `docs/tasks/T-238.md` | DONE | 兼容顺运宝详情分钟精度时间 |
|
| `docs/tasks/T-238.md` | DONE | 兼容顺运宝详情分钟精度时间 |
|
||||||
| `docs/tasks/T-239.md` | DONE | 冻结货运规格 SKU、TWD 原始单价和图片引用契约 |
|
| `docs/tasks/T-239.md` | DONE | 冻结货运规格 SKU、TWD 原始单价和图片引用契约 |
|
||||||
| `docs/tasks/T-240.md` | DONE | 后端受控缓存并鉴权提供货运商品图片 |
|
| `docs/tasks/T-240.md` | DONE | 后端受控缓存并鉴权提供货运商品图片 |
|
||||||
|
| `docs/tasks/T-241.md` | DONE | 优化货运列表代表图与商品采购信息展示 |
|
||||||
| `docs/design/` | 已确认 | T-202 原型索引、4 个管理页和 7 个 Android 页面 |
|
| `docs/design/` | 已确认 | T-202 原型索引、4 个管理页和 7 个 Android 页面 |
|
||||||
| `deepseek总结.txt` | 已有 | 历史讨论摘要,不是正式需求权威 |
|
| `deepseek总结.txt` | 已有 | 历史讨论摘要,不是正式需求权威 |
|
||||||
| `android-buyer/` | 已有 | Roubao `main` 固定 commit 的 Android 基线 |
|
| `android-buyer/` | 已有 | Roubao `main` 固定 commit 的 Android 基线 |
|
||||||
@@ -214,11 +221,13 @@
|
|||||||
## 任务摘要
|
## 任务摘要
|
||||||
|
|
||||||
- 已完成:T-001 至 T-004、T-101 至 T-104、T-201 至 T-219。
|
- 已完成:T-001 至 T-004、T-101 至 T-104、T-201 至 T-219。
|
||||||
- 已完成:另含 T-220 至 T-240 ERP 契约、货运存储、采购需求生成、日期增量同步、Go
|
- 已完成:另含 T-220 至 T-241 ERP 契约、货运存储、采购需求生成、日期增量同步、Go
|
||||||
直连协议、OCR 会话预检、稳定预检错误、安全诊断日志、直连 `FreightSource`、旧 Connector
|
直连协议、OCR 会话预检、稳定预检错误、安全诊断日志、直连 `FreightSource`、旧 Connector
|
||||||
清理、受控本地凭证加载、同步单号导入、分钟时间兼容、货运商品元数据契约和本地图片缓存。
|
清理、受控本地凭证加载、同步单号导入、分钟时间兼容、货运商品元数据契约、本地图片缓存
|
||||||
|
和货运管理展示优化。
|
||||||
- 进行中:无。
|
- 进行中:无。
|
||||||
- 下一步:在货运列表和详情中展示本地缩略图、原始单价和清晰的图片状态。
|
- 下一步:用真实 ERP 账号受控导入一个测试货运单,复核真实图片响应、缓存状态、规格、
|
||||||
|
TWD 原始单价和重新导入幂等性;不把真实数据或诊断响应提交到仓库。
|
||||||
|
|
||||||
## 当前可运行内容
|
## 当前可运行内容
|
||||||
|
|
||||||
|
|||||||
+17
-8
@@ -4,7 +4,7 @@ title: 优化 ERP 货运列表与商品详情展示
|
|||||||
phase: 2
|
phase: 2
|
||||||
deps:
|
deps:
|
||||||
- T-240
|
- T-240
|
||||||
status: TODO
|
status: DONE
|
||||||
created: 2026-07-29
|
created: 2026-07-29
|
||||||
context_ref: c7e152a
|
context_ref: c7e152a
|
||||||
work_branch: null
|
work_branch: null
|
||||||
@@ -50,13 +50,13 @@ ERP 地址、不改变采购操作的前提下完成界面收敛。
|
|||||||
|
|
||||||
## 验收要点
|
## 验收要点
|
||||||
|
|
||||||
- [ ] 货运列表显示确定性的第一商品代表图/占位、商品标题和总项数。
|
- [x] 货运列表显示确定性的第一商品代表图/占位、商品标题和总项数。
|
||||||
- [ ] 详情逐商品显示图片/占位、标题、规格/SKU、数量、TWD 原始单价和采购操作。
|
- [x] 详情逐商品显示图片/占位、标题、规格/SKU、数量、TWD 原始单价和采购操作。
|
||||||
- [ ] 页面不再显示 `productThumb` 原始引用,规格与 SKU 相同时不重复两行。
|
- [x] 页面不再显示 `productThumb` 原始引用,规格与 SKU 相同时不重复两行。
|
||||||
- [ ] 图片有固定尺寸、懒加载和 alt;非 READY 状态不发出图片请求、不显示破图。
|
- [x] 图片有固定尺寸、懒加载和 alt;非 READY 状态不发出图片请求、不显示破图。
|
||||||
- [ ] 既有 CSRF、采购需求、参考图上传和生成采购任务操作不回归。
|
- [x] 既有 CSRF、采购需求、参考图上传和生成采购任务操作不回归。
|
||||||
- [ ] 1440×900、768×1024、390×844 和 375×667 无横向溢出、遮挡或不可用操作。
|
- [x] 1440×900、768×1024、390×844 和 375×667 无横向溢出、遮挡或不可用操作。
|
||||||
- [ ] 标准 Go 测试、race、vet 和三个入口构建通过。
|
- [x] 标准 Go 测试、race、vet 和三个入口构建通过。
|
||||||
|
|
||||||
## 边界
|
## 边界
|
||||||
|
|
||||||
@@ -69,3 +69,12 @@ ERP 地址、不改变采购操作的前提下完成界面收敛。
|
|||||||
|
|
||||||
- 2026-07-29:创建任务。采用现有安静、工具型管理后台样式,新增货运专属高密度表格和固定
|
- 2026-07-29:创建任务。采用现有安静、工具型管理后台样式,新增货运专属高密度表格和固定
|
||||||
图片尺寸;保持既有语义色、系统字体、44px 操作目标和 SSR 交互。
|
图片尺寸;保持既有语义色、系统字体、44px 操作目标和 SSR 交互。
|
||||||
|
- 2026-07-29:列表查询在关闭主结果集后按订单读取第一条当前商品,避免 SQLite 单连接
|
||||||
|
嵌套查询阻塞;API/SSR 返回代表商品标题、数量、图片状态和仅限 READY 的本地图片 URL。
|
||||||
|
- 2026-07-29:详情收敛为图片、单一规格/SKU、数量、`TWD 0.00` 原始单价、状态和采购
|
||||||
|
操作;未提供、排队、缺失和失败图片使用同尺寸状态占位,不请求 ERP 图片地址。
|
||||||
|
- 2026-07-29:`go test ./...`、`go test -race ./...`、`go vet ./...` 和 API、authctl、
|
||||||
|
migrate 三个入口构建通过。Playwright 连接隔离 SQLite 和脱敏运行时图片,在
|
||||||
|
1440×900、768×1024、390×844、375×667 验证列表/详情:页面横向溢出和越界元素均为
|
||||||
|
0;缓存图自然尺寸 320×320,列表渲染 56×56,详情桌面 84×84、手机 72×72;图片请求
|
||||||
|
返回 200,控制台无警告或错误。
|
||||||
|
|||||||
Reference in New Issue
Block a user