新增宣传页 Mock 模块并接入首页路由。

从 xh_test 同步 ScenicPromotion 演示页,补齐 MVVM 结构、本地资源、单元测试与模块文档,注册 promotion_page 首页入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-02 15:29:25 +08:00
parent a528d9b0d7
commit 1b1e93b9ca
30 changed files with 2661 additions and 4 deletions

View File

@ -0,0 +1,360 @@
//
// ScenicPromotionMockStore.swift
// suixinkan
//
// Created by Codex on 2026/7/2.
//
import Foundation
/// Mock
enum ScenicPromotionMockStore {
/// Mock
static let mockBoard = ScenicPromotionBoard(
scenicName: "那拉提景区",
subtitle: "实时查看打卡点排队信息",
qrCodeTitle: "扫码查看排队导览",
spots: [
ScenicPromotionSpot(
id: "bridge",
name: "网红桥",
iconName: "bridge.2",
queue: ScenicPromotionQueueInfo(
waitingPeople: 54,
estimatedMinutes: 18,
averageMinutes: 16,
nextNumber: "B055",
currentNumber: "B054",
shootingDuration: "10分钟30秒"
),
videos: [
ScenicPromotionMedia(
id: "bridge_video_1",
mode: .video,
title: "网红桥",
subtitle: "雪山下的蓝色木桥,旅拍打卡热门点",
duration: "00:32",
palette: [0x1D8BFF, 0x8FD7FF, 0x2F8F45],
assetName: "ScenicPromotionThumbPortrait",
heroAssetName: "ScenicPromotionHero",
previewWidth: 72
),
ScenicPromotionMedia(
id: "bridge_video_2",
mode: .video,
title: "汉地拔冲",
subtitle: "雪山草甸与观景石同框",
duration: "00:28",
palette: [0x2A7BEF, 0xB7E5FF, 0x74B84A],
assetName: "ScenicPromotionThumbMarker",
heroAssetName: nil,
previewWidth: 144
),
ScenicPromotionMedia(
id: "bridge_video_3",
mode: .video,
title: "草甸旅拍",
subtitle: "绿色草坡上的打卡瞬间",
duration: "00:22",
palette: [0x2A7BEF, 0xB7E5FF, 0x74B84A],
assetName: "ScenicPromotionThumbGrass",
heroAssetName: nil,
previewWidth: 72
),
ScenicPromotionMedia(
id: "bridge_video_4",
mode: .video,
title: "骑马路线",
subtitle: "游客骑行与雪山远景",
duration: "00:24",
palette: [0x246BDF, 0x9BD9FF, 0x7EAC3D],
assetName: "ScenicPromotionThumbHorse",
heroAssetName: nil,
previewWidth: 72
)
],
photos: [
ScenicPromotionMedia(
id: "bridge_photo_1",
mode: .photo,
title: "桥边人像",
subtitle: "白裙、花束、雪山背景",
duration: nil,
palette: [0x1B7DFF, 0xA9E8FF, 0xF5D36C],
assetName: "ScenicPromotionThumbPortrait",
heroAssetName: nil,
previewWidth: 72
),
ScenicPromotionMedia(
id: "bridge_photo_2",
mode: .photo,
title: "全景草甸",
subtitle: "高山草甸与云层层次",
duration: nil,
palette: [0x3CA7FF, 0xC8F1FF, 0x4AAE57],
assetName: "ScenicPromotionThumbMarker",
heroAssetName: nil,
previewWidth: 144
),
ScenicPromotionMedia(
id: "bridge_photo_3",
mode: .photo,
title: "骑马路线",
subtitle: "游客骑行与雪山远景",
duration: nil,
palette: [0x246BDF, 0x9BD9FF, 0x7EAC3D],
assetName: "ScenicPromotionThumbHorse",
heroAssetName: nil,
previewWidth: 72
),
ScenicPromotionMedia(
id: "bridge_photo_4",
mode: .photo,
title: "草甸旅拍",
subtitle: "绿色草坡上的打卡瞬间",
duration: nil,
palette: [0x246BDF, 0x9BD9FF, 0x7EAC3D],
assetName: "ScenicPromotionThumbGrass",
heroAssetName: nil,
previewWidth: 72
)
]
),
ScenicPromotionSpot(
id: "hendi",
name: "汉地拔冲",
iconName: "mountain.2.fill",
queue: ScenicPromotionQueueInfo(
waitingPeople: 32,
estimatedMinutes: 12,
averageMinutes: 14,
nextNumber: "H036",
currentNumber: "H035",
shootingDuration: "8分钟20秒"
),
videos: [
ScenicPromotionMedia(
id: "hendi_video_1",
mode: .video,
title: "汉地拔冲",
subtitle: "雪山下的绿色画卷,旅拍打卡圣地",
duration: "00:32",
palette: [0x0B78F6, 0xB4E8FF, 0x4E9D3B],
assetName: "ScenicPromotionThumbMarker",
heroAssetName: "ScenicPromotionHero",
previewWidth: 144
)
],
photos: [
ScenicPromotionMedia(
id: "hendi_photo_1",
mode: .photo,
title: "雪山草原",
subtitle: "开阔草原与雪山远景",
duration: nil,
palette: [0x167CFF, 0xBDEBFF, 0x58B15D],
assetName: "ScenicPromotionThumbGrass",
heroAssetName: nil,
previewWidth: 72
),
ScenicPromotionMedia(
id: "hendi_photo_2",
mode: .photo,
title: "观景石",
subtitle: "点位标识与远山构图",
duration: nil,
palette: [0x1D6BD8, 0xA7DBFF, 0x769A5A],
assetName: "ScenicPromotionThumbMarker",
heroAssetName: nil,
previewWidth: 144
)
]
),
ScenicPromotionSpot(
id: "grassland",
name: "空中草原",
iconName: "leaf.fill",
queue: ScenicPromotionQueueInfo(
waitingPeople: 21,
estimatedMinutes: 9,
averageMinutes: 13,
nextNumber: "G022",
currentNumber: "G021",
shootingDuration: "7分钟40秒"
),
videos: [
ScenicPromotionMedia(
id: "grassland_video_1",
mode: .video,
title: "空中草原",
subtitle: "草甸花海与远山同框",
duration: "00:26",
palette: [0x2A7BEF, 0xB7E5FF, 0x74B84A],
assetName: "ScenicPromotionThumbGrass",
heroAssetName: nil,
previewWidth: 96
)
],
photos: [
ScenicPromotionMedia(
id: "grassland_photo_1",
mode: .photo,
title: "草原花海",
subtitle: "绿色草坡上的花海样片",
duration: nil,
palette: [0x3CA7FF, 0xC8F1FF, 0x4AAE57],
assetName: "ScenicPromotionThumbGrass",
heroAssetName: nil,
previewWidth: 96
)
]
),
ScenicPromotionSpot(
id: "valley",
name: "河谷花海",
iconName: "camera.macro",
queue: ScenicPromotionQueueInfo(
waitingPeople: 18,
estimatedMinutes: 8,
averageMinutes: 12,
nextNumber: "V019",
currentNumber: "V018",
shootingDuration: "6分钟50秒"
),
videos: [
ScenicPromotionMedia(
id: "valley_video_1",
mode: .video,
title: "河谷花海",
subtitle: "花海步道与雪山远景",
duration: "00:24",
palette: [0x1B7DFF, 0xA9E8FF, 0xF5D36C],
assetName: "ScenicPromotionThumbPortrait",
heroAssetName: nil,
previewWidth: 96
)
],
photos: [
ScenicPromotionMedia(
id: "valley_photo_1",
mode: .photo,
title: "花海人像",
subtitle: "花海前景与游客人像",
duration: nil,
palette: [0x1B7DFF, 0xA9E8FF, 0xF5D36C],
assetName: "ScenicPromotionThumbPortrait",
heroAssetName: nil,
previewWidth: 96
)
]
),
ScenicPromotionSpot(
id: "snowtrail",
name: "雪山栈道",
iconName: "snowflake",
queue: ScenicPromotionQueueInfo(
waitingPeople: 27,
estimatedMinutes: 11,
averageMinutes: 15,
nextNumber: "S028",
currentNumber: "S027",
shootingDuration: "9分钟10秒"
),
videos: [
ScenicPromotionMedia(
id: "snowtrail_video_1",
mode: .video,
title: "雪山栈道",
subtitle: "木栈道与雪山背景",
duration: "00:29",
palette: [0x246BDF, 0x9BD9FF, 0x7EAC3D],
assetName: "ScenicPromotionThumbHorse",
heroAssetName: nil,
previewWidth: 96
)
],
photos: [
ScenicPromotionMedia(
id: "snowtrail_photo_1",
mode: .photo,
title: "栈道远眺",
subtitle: "雪山、栈道、游客同框",
duration: nil,
palette: [0x246BDF, 0x9BD9FF, 0x7EAC3D],
assetName: "ScenicPromotionThumbHorse",
heroAssetName: nil,
previewWidth: 96
)
]
)
]
)
/// Mock
static let mockQueuePoints: [ScenicPromotionQueuePoint] = [
ScenicPromotionQueuePoint(id: "queue_bridge", name: "网红桥排队点", areaName: "那拉提景区"),
ScenicPromotionQueuePoint(id: "queue_hendi", name: "汉地拔冲排队点", areaName: "那拉提景区"),
ScenicPromotionQueuePoint(id: "queue_grass", name: "草甸旅拍排队点", areaName: "那拉提景区")
]
/// Mock
static let mockMaterialPoints: [ScenicPromotionMaterialPoint] = [
ScenicPromotionMaterialPoint(id: "material_bridge", name: "网红桥", areaName: "宣传素材点"),
ScenicPromotionMaterialPoint(id: "material_hendi", name: "汉地拔冲", areaName: "宣传素材点"),
ScenicPromotionMaterialPoint(id: "material_grass", name: "空中草原", areaName: "宣传素材点")
]
/// Mock
static let mockMaterialsByPointID: [String: [ScenicPromotionManagedMaterial]] = [
"material_bridge": [
ScenicPromotionManagedMaterial(
id: "material_bridge_video_1",
pointID: "material_bridge",
kind: .video,
title: "网红桥视频1",
subtitle: "雪山草甸旅拍视频",
assetName: "ScenicPromotionHero",
duration: "00:32",
createdAt: "2026-06-25 18:20"
),
ScenicPromotionManagedMaterial(
id: "material_bridge_image_1",
pointID: "material_bridge",
kind: .image,
title: "网红桥图片1",
subtitle: "游客桥边人像样片",
assetName: "ScenicPromotionThumbPortrait",
duration: nil,
createdAt: "2026-06-25 18:24"
)
],
"material_hendi": [
ScenicPromotionManagedMaterial(
id: "material_hendi_image_1",
pointID: "material_hendi",
kind: .image,
title: "汉地拔冲图片1",
subtitle: "观景石与雪山远景",
assetName: "ScenicPromotionThumbMarker",
duration: nil,
createdAt: "2026-06-25 18:30"
)
]
]
/// Mock
static func assetName(for kind: ScenicPromotionUploadKind, index: Int) -> String {
switch kind {
case .video:
return index.isMultiple(of: 2) ? "ScenicPromotionThumbMarker" : "ScenicPromotionHero"
case .image:
let assets = [
"ScenicPromotionThumbPortrait",
"ScenicPromotionThumbGrass",
"ScenicPromotionThumbHorse",
"ScenicPromotionThumbMarker"
]
return assets[(index - 1) % assets.count]
}
}
}