完善各种需求
This commit is contained in:
@@ -14,7 +14,6 @@ fun Application.module() {
|
||||
monitor.subscribe(ApplicationStopped) {
|
||||
traceabilityClient.close()
|
||||
}
|
||||
|
||||
attributes.put(TraceabilityAttributes.ServiceKey, traceabilityService)
|
||||
|
||||
configureHTTP()
|
||||
|
||||
@@ -59,6 +59,7 @@ data class TraceBatchDetailResponse(
|
||||
val summary: String,
|
||||
val coverImage: String,
|
||||
val coverImagePreviewUrl: String = "",
|
||||
val themeColor: String = "#1f4fd6",
|
||||
val tags: List<String>,
|
||||
val status: String,
|
||||
val currentStep: Int,
|
||||
@@ -108,6 +109,8 @@ data class DisplayEntry(
|
||||
val type: String = "string",
|
||||
val bold: Boolean = false,
|
||||
val color: String = "",
|
||||
val mapUrl: String = "",
|
||||
val mapEmbedUrl: String = "",
|
||||
)
|
||||
|
||||
data class PublicSectionView(
|
||||
@@ -133,6 +136,7 @@ data class PageViewModel(
|
||||
val templateName: String,
|
||||
val summary: String,
|
||||
val coverImage: String,
|
||||
val themeColor: String,
|
||||
val scanCount: Int,
|
||||
val publishedAt: String,
|
||||
val tagsText: String,
|
||||
|
||||
@@ -4,6 +4,9 @@ import io.ktor.server.config.ApplicationConfig
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.doubleOrNull
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
data class TraceabilityPublicConfig(
|
||||
val coreBaseUrl: String,
|
||||
@@ -15,38 +18,21 @@ class TraceabilityService(
|
||||
) {
|
||||
suspend fun loadPage(code: String): PageViewModel? {
|
||||
val detail = client.fetchPublicDetail(code, increaseScan = true) ?: return null
|
||||
val batch = detail.batch
|
||||
|
||||
return PageViewModel(
|
||||
code = batch.batchCode,
|
||||
batchName = batch.batchName,
|
||||
productName = batch.productName.ifBlank { batch.templateName },
|
||||
templateName = batch.templateName,
|
||||
summary = batch.summary.ifBlank { "该批次已完成关键环节留痕,可查看公开资料与业务流程。" },
|
||||
coverImage = batch.coverImagePreviewUrl.ifBlank { batch.coverImage },
|
||||
scanCount = batch.scanCount,
|
||||
publishedAt = formatDateOnly(batch.publishedAt),
|
||||
tagsText = batch.tags.joinToString("、").ifBlank { "暂无标签" },
|
||||
publicSections = detail.publicSections.map(::toPublicSectionView),
|
||||
businessSections = detail.businessSections.map(::toTimelineSectionView),
|
||||
return buildPageViewModel(
|
||||
detail = detail,
|
||||
summary = detail.batch.summary.ifBlank { "暂无说明" },
|
||||
templateName = detail.batch.templateName,
|
||||
publishedAt = detail.batch.publishedAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadPreviewPage(code: String): PageViewModel? {
|
||||
val detail = client.fetchPreviewDetail(code) ?: return null
|
||||
val batch = detail.batch
|
||||
return PageViewModel(
|
||||
code = batch.batchCode,
|
||||
batchName = batch.batchName,
|
||||
productName = batch.productName.ifBlank { batch.templateName },
|
||||
return buildPageViewModel(
|
||||
detail = detail,
|
||||
summary = detail.batch.summary.ifBlank { "暂无说明" },
|
||||
templateName = "预演页",
|
||||
summary = batch.summary.ifBlank { "当前为预演页内容,可持续调整字段和值供客户确认。" },
|
||||
coverImage = batch.coverImagePreviewUrl.ifBlank { batch.coverImage },
|
||||
scanCount = batch.scanCount,
|
||||
publishedAt = formatDateOnly(batch.updatedAt),
|
||||
tagsText = batch.tags.joinToString("、").ifBlank { "暂无标签" },
|
||||
publicSections = detail.publicSections.map(::toPublicSectionView),
|
||||
businessSections = detail.businessSections.map(::toTimelineSectionView),
|
||||
publishedAt = detail.batch.updatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -97,19 +83,33 @@ class TraceabilityService(
|
||||
return step.values.entries.map { (key, value) ->
|
||||
val field = step.fields.find { it.key == key }
|
||||
val imageUrl = step.valuePreviewUrls[key].orEmpty()
|
||||
val mapUrl = if ((field?.type ?: "string") == "coordinate") buildCoordinateMapUrl(value) else ""
|
||||
val mapEmbedUrl = if ((field?.type ?: "string") == "coordinate") buildCoordinateEmbedUrl(value) else ""
|
||||
DisplayEntry(
|
||||
label = field?.label ?: key,
|
||||
value = if ((field?.type ?: "string") == "image" && imageUrl.isNotBlank()) imageUrl else formatJsonValue(value),
|
||||
type = field?.type ?: "string",
|
||||
bold = field?.fieldStyle?.bold ?: false,
|
||||
color = field?.fieldStyle?.color.orEmpty(),
|
||||
mapUrl = mapUrl,
|
||||
mapEmbedUrl = mapEmbedUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatJsonValue(value: JsonElement): String = when (value) {
|
||||
is JsonArray -> value.joinToString("、") { formatJsonValue(it) }
|
||||
is JsonObject -> value.entries.joinToString(";") { "${it.key}: ${formatJsonValue(it.value)}" }
|
||||
is JsonObject -> {
|
||||
val lng = value["lng"]?.jsonPrimitive?.doubleOrNull
|
||||
val lat = value["lat"]?.jsonPrimitive?.doubleOrNull
|
||||
if (lng != null || lat != null) {
|
||||
listOfNotNull(
|
||||
if (lng != null && lat != null) "$lng, $lat" else null,
|
||||
).joinToString(" | ").ifBlank { "未填写" }
|
||||
} else {
|
||||
value.entries.joinToString(";") { "${it.key}: ${formatJsonValue(it.value)}" }
|
||||
}
|
||||
}
|
||||
else -> value.toString().trim('"').ifBlank { "未填写" }
|
||||
}
|
||||
|
||||
@@ -120,6 +120,51 @@ class TraceabilityService(
|
||||
}
|
||||
return text.substringBefore(" ").substringBefore("T")
|
||||
}
|
||||
|
||||
private fun buildCoordinateMapUrl(value: JsonElement): String {
|
||||
val coordinate = value as? JsonObject ?: return ""
|
||||
val lng = coordinate["lng"]?.jsonPrimitive?.doubleOrNull
|
||||
val lat = coordinate["lat"]?.jsonPrimitive?.doubleOrNull
|
||||
return when {
|
||||
lng != null && lat != null -> "https://www.openstreetmap.org/?mlat=$lat&mlon=$lng#map=15/$lat/$lng"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildCoordinateEmbedUrl(value: JsonElement): String {
|
||||
val coordinate = value as? JsonObject ?: return ""
|
||||
val lng = coordinate["lng"]?.jsonPrimitive?.doubleOrNull ?: return ""
|
||||
val lat = coordinate["lat"]?.jsonPrimitive?.doubleOrNull ?: return ""
|
||||
return "https://www.openstreetmap.org/export/embed.html?bbox=${lng - 0.01}%2C${lat - 0.01}%2C${lng + 0.01}%2C${lat + 0.01}&layer=mapnik&marker=$lat%2C$lng"
|
||||
}
|
||||
|
||||
private fun buildPageViewModel(
|
||||
detail: TraceabilityPublicDetailResponse,
|
||||
summary: String,
|
||||
templateName: String,
|
||||
publishedAt: String,
|
||||
): PageViewModel {
|
||||
val batch = detail.batch
|
||||
return PageViewModel(
|
||||
code = batch.batchCode,
|
||||
batchName = batch.batchName,
|
||||
productName = batch.productName.ifBlank { batch.templateName },
|
||||
templateName = templateName,
|
||||
summary = summary,
|
||||
coverImage = batch.coverImagePreviewUrl.ifBlank { batch.coverImage },
|
||||
themeColor = normalizeThemeColor(batch.themeColor),
|
||||
scanCount = batch.scanCount,
|
||||
publishedAt = formatDateOnly(publishedAt),
|
||||
tagsText = batch.tags.joinToString("、").ifBlank { "暂无标签" },
|
||||
publicSections = detail.publicSections.map(::toPublicSectionView),
|
||||
businessSections = detail.businessSections.map(::toTimelineSectionView),
|
||||
)
|
||||
}
|
||||
|
||||
private fun normalizeThemeColor(value: String): String {
|
||||
val text = value.trim()
|
||||
return if (text.matches(Regex("^#([0-9a-fA-F]{6})$"))) text else "#1f4fd6"
|
||||
}
|
||||
}
|
||||
|
||||
fun ApplicationConfig.toTraceabilityPublicConfig(): TraceabilityPublicConfig {
|
||||
|
||||
@@ -7,5 +7,5 @@ ktor:
|
||||
|
||||
traceability:
|
||||
# 访问主服务的地址
|
||||
# core-base-url: "http://127.0.0.1:8089" # 开发
|
||||
core-base-url: "https://ai.ronsunny.cn:8090/api" # 生产
|
||||
core-base-url: "http://127.0.0.1:8089" # 开发
|
||||
# core-base-url: "https://ai.ronsunny.cn:8090/api" # 生产
|
||||
|
||||
@@ -7,19 +7,27 @@ body {
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
color: #182235;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(23, 92, 230, 0.12), transparent 32%),
|
||||
radial-gradient(circle at top left, var(--traceability-primary-glow), transparent 32%),
|
||||
linear-gradient(180deg, #f9fbff 0%, #f3f6fb 100%);
|
||||
--traceability-primary: #1d4ed8;
|
||||
--traceability-primary-strong: #163fae;
|
||||
--traceability-primary-soft: rgba(29, 78, 216, 0.12);
|
||||
--traceability-primary-border: rgba(29, 78, 216, 0.18);
|
||||
--traceability-primary-glow: rgba(29, 78, 216, 0.08);
|
||||
--traceability-primary-tint: #edf3ff;
|
||||
--traceability-primary-tint-strong: #e5eefc;
|
||||
--traceability-primary-tint-deep: #d7e6fa;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #1d4ed8;
|
||||
color: var(--traceability-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.page-shell {
|
||||
max-width: 1440px;
|
||||
max-width: 1520px;
|
||||
margin: 0 auto;
|
||||
padding: 36px 40px 56px;
|
||||
padding: 20px 14px 44px;
|
||||
}
|
||||
|
||||
.hero,
|
||||
@@ -51,9 +59,9 @@ a {
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 18px;
|
||||
padding: 26px;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(280px, 0.6fr);
|
||||
gap: 16px;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.hero h1,
|
||||
@@ -64,8 +72,8 @@ a {
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
margin-top: 16px;
|
||||
font-size: 34px;
|
||||
margin-top: 8px;
|
||||
font-size: 30px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@@ -78,14 +86,14 @@ a {
|
||||
}
|
||||
|
||||
.hero p {
|
||||
margin: 14px 0 0;
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
|
||||
.hero__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.stat-card,
|
||||
@@ -107,8 +115,8 @@ a {
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
min-height: 104px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||
min-height: 88px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, var(--traceability-primary-tint) 100%);
|
||||
}
|
||||
|
||||
.stat-card span,
|
||||
@@ -131,29 +139,29 @@ a {
|
||||
|
||||
.hero__aside {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-top: 18px;
|
||||
padding: 24px;
|
||||
margin-top: 14px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.tabs-panel {
|
||||
padding-top: 18px;
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.tabs-nav {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
padding: 6px;
|
||||
border: 1px solid #e8eef7;
|
||||
border-radius: 999px;
|
||||
background: #f7faff;
|
||||
margin-bottom: 18px;
|
||||
background: var(--traceability-primary-tint);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
@@ -176,9 +184,9 @@ a {
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: linear-gradient(135deg, #2b63e3, #1f4fd6);
|
||||
background: linear-gradient(135deg, var(--traceability-primary), var(--traceability-primary-strong));
|
||||
color: #fff;
|
||||
box-shadow: 0 10px 24px rgba(29, 78, 216, 0.2);
|
||||
box-shadow: 0 10px 24px var(--traceability-primary-soft);
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
@@ -194,29 +202,29 @@ a {
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
border: 1px dashed #d7e1f0;
|
||||
border: 1px dashed var(--traceability-primary-border);
|
||||
border-radius: 18px;
|
||||
background: #fafcff;
|
||||
color: #7d8899;
|
||||
background: linear-gradient(180deg, #fafcff 0%, var(--traceability-primary-tint) 100%);
|
||||
color: #60708a;
|
||||
padding: 24px 18px;
|
||||
}
|
||||
|
||||
.notice {
|
||||
margin-top: 18px;
|
||||
border-radius: 18px;
|
||||
background: #ecfdf3;
|
||||
border: 1px solid #ccebd9;
|
||||
color: #0b7a4b;
|
||||
background: linear-gradient(180deg, var(--traceability-primary-tint-strong) 0%, var(--traceability-primary-tint) 100%);
|
||||
border: 1px solid var(--traceability-primary-border);
|
||||
color: var(--traceability-primary-strong);
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.public-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
padding: 18px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.info-card__desc {
|
||||
@@ -226,24 +234,24 @@ a {
|
||||
.kv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.kv-card {
|
||||
padding: 12px 14px;
|
||||
background: #fafcff;
|
||||
padding: 11px 12px;
|
||||
background: linear-gradient(180deg, #fafcff 0%, var(--traceability-primary-tint) 100%);
|
||||
}
|
||||
|
||||
.timeline {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: grid;
|
||||
grid-template-columns: 30px minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
grid-template-columns: 24px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.timeline-item__rail {
|
||||
@@ -256,16 +264,16 @@ a {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: #1d4ed8;
|
||||
box-shadow: 0 0 0 5px rgba(29, 78, 216, 0.12);
|
||||
background: var(--traceability-primary);
|
||||
box-shadow: 0 0 0 5px var(--traceability-primary-soft);
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 2px;
|
||||
flex: 1;
|
||||
min-height: 70px;
|
||||
min-height: 52px;
|
||||
margin-top: 8px;
|
||||
background: linear-gradient(180deg, rgba(29, 78, 216, 0.32), rgba(29, 78, 216, 0.04));
|
||||
background: linear-gradient(180deg, var(--traceability-primary-border), rgba(29, 78, 216, 0.04));
|
||||
}
|
||||
|
||||
.timeline-item:last-child .line {
|
||||
@@ -273,8 +281,8 @@ a {
|
||||
}
|
||||
|
||||
.timeline-item__body {
|
||||
padding: 18px;
|
||||
background: linear-gradient(180deg, #fff 0%, #fbfcff 100%);
|
||||
padding: 14px;
|
||||
background: linear-gradient(180deg, #fff 0%, var(--traceability-primary-tint) 100%);
|
||||
}
|
||||
|
||||
.timeline-item__head {
|
||||
@@ -287,14 +295,52 @@ a {
|
||||
.kv-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 280px;
|
||||
max-height: 240px;
|
||||
object-fit: cover;
|
||||
border-radius: 14px;
|
||||
margin-top: 10px;
|
||||
border: 1px solid #e6edf8;
|
||||
border: 1px solid var(--traceability-primary-border);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.kv-image-button {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.kv-image-button span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--traceability-primary-soft);
|
||||
color: var(--traceability-primary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.kv-map {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 220px;
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--traceability-primary-border);
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.kv-link {
|
||||
display: inline-flex;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.feedback-form {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
@@ -338,7 +384,7 @@ a {
|
||||
padding: 0 18px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #2b63e3, #1f4fd6);
|
||||
background: linear-gradient(135deg, var(--traceability-primary), var(--traceability-primary-strong));
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
@@ -354,13 +400,66 @@ a {
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
padding: 24px 4px 8px;
|
||||
padding: 18px 4px 8px;
|
||||
text-align: center;
|
||||
color: #7d8899;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.image-viewer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.image-viewer.is-open {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.image-viewer__mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.78);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.image-viewer__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(92vw, 1080px);
|
||||
max-height: 90vh;
|
||||
margin: 0;
|
||||
padding: 18px;
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
.image-viewer__content img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 84vh;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.image-viewer__close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 12px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--traceability-primary-tint);
|
||||
color: var(--traceability-primary-strong);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.hero,
|
||||
.form-grid,
|
||||
@@ -380,7 +479,7 @@ a {
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-shell {
|
||||
padding: 24px 18px 44px;
|
||||
padding: 16px 12px 32px;
|
||||
}
|
||||
|
||||
.tabs-nav {
|
||||
@@ -393,4 +492,8 @@ a {
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>${page.batchName} - 溯源信息</title>
|
||||
<link rel="stylesheet" href="/static/traceability.css" />
|
||||
</head>
|
||||
<body>
|
||||
<body style="--traceability-primary:${page.themeColor};">
|
||||
<div class="page-shell">
|
||||
<#if page.coverImage?has_content>
|
||||
<section class="cover-panel">
|
||||
@@ -37,10 +37,6 @@
|
||||
</div>
|
||||
|
||||
<div class="hero__aside">
|
||||
<div class="summary-card">
|
||||
<span>发布时间</span>
|
||||
<strong>${page.publishedAt}</strong>
|
||||
</div>
|
||||
<#if page.tagsText != "暂无标签">
|
||||
<div class="summary-card">
|
||||
<span>标签</span>
|
||||
@@ -78,7 +74,14 @@
|
||||
<div class="kv-card">
|
||||
<span>${entry.label}</span>
|
||||
<#if entry.type == "image" && entry.value?has_content && entry.value != "未填写">
|
||||
<img class="kv-image" src="${entry.value}" alt="${entry.label}" />
|
||||
<button class="kv-image-button" type="button" data-image-src="${entry.value}" data-image-alt="${entry.label}">
|
||||
<img class="kv-image" src="${entry.value}" alt="${entry.label}" />
|
||||
</button>
|
||||
<#elseif entry.type == "coordinate" && entry.value?has_content && entry.value != "未填写">
|
||||
<#if entry.mapEmbedUrl?has_content>
|
||||
<iframe class="kv-map" src="${entry.mapEmbedUrl}" loading="lazy" referrerpolicy="no-referrer-when-downgrade" title="${entry.label}"></iframe>
|
||||
</#if>
|
||||
<strong<#if entry.bold || entry.color?has_content> style="<#if entry.bold>font-weight:700;</#if><#if entry.color?has_content>color:${entry.color};</#if>"</#if>>${entry.value}</strong>
|
||||
<#else>
|
||||
<strong<#if entry.bold || entry.color?has_content> style="<#if entry.bold>font-weight:700;</#if><#if entry.color?has_content>color:${entry.color};</#if>"</#if>>${entry.value}</strong>
|
||||
</#if>
|
||||
@@ -108,7 +111,14 @@
|
||||
<div class="kv-card">
|
||||
<span>${entry.label}</span>
|
||||
<#if entry.type == "image" && entry.value?has_content && entry.value != "未填写">
|
||||
<img class="kv-image" src="${entry.value}" alt="${entry.label}" />
|
||||
<button class="kv-image-button" type="button" data-image-src="${entry.value}" data-image-alt="${entry.label}">
|
||||
<img class="kv-image" src="${entry.value}" alt="${entry.label}" />
|
||||
</button>
|
||||
<#elseif entry.type == "coordinate" && entry.value?has_content && entry.value != "未填写">
|
||||
<#if entry.mapEmbedUrl?has_content>
|
||||
<iframe class="kv-map" src="${entry.mapEmbedUrl}" loading="lazy" referrerpolicy="no-referrer-when-downgrade" title="${entry.label}"></iframe>
|
||||
</#if>
|
||||
<strong<#if entry.bold || entry.color?has_content> style="<#if entry.bold>font-weight:700;</#if><#if entry.color?has_content>color:${entry.color};</#if>"</#if>>${entry.value}</strong>
|
||||
<#else>
|
||||
<strong<#if entry.bold || entry.color?has_content> style="<#if entry.bold>font-weight:700;</#if><#if entry.color?has_content>color:${entry.color};</#if>"</#if>>${entry.value}</strong>
|
||||
</#if>
|
||||
@@ -180,8 +190,64 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const themeColor = '${page.themeColor}';
|
||||
function hexToRgb(hex) {
|
||||
const normalized = (hex || '').replace('#', '').trim();
|
||||
if (!/^[0-9a-fA-F]{6}$/.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
r: parseInt(normalized.slice(0, 2), 16),
|
||||
g: parseInt(normalized.slice(2, 4), 16),
|
||||
b: parseInt(normalized.slice(4, 6), 16),
|
||||
};
|
||||
}
|
||||
|
||||
function rgbToHex(r, g, b) {
|
||||
return '#' + [r, g, b]
|
||||
.map((value) => Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
function mixColor(baseHex, targetHex, ratio) {
|
||||
const base = hexToRgb(baseHex);
|
||||
const target = hexToRgb(targetHex);
|
||||
if (!base || !target) {
|
||||
return baseHex;
|
||||
}
|
||||
const mix = (from, to) => from + (to - from) * ratio;
|
||||
return rgbToHex(mix(base.r, target.r), mix(base.g, target.g), mix(base.b, target.b));
|
||||
}
|
||||
|
||||
function hexToRgba(hex, alpha) {
|
||||
const rgb = hexToRgb(hex);
|
||||
if (!rgb) {
|
||||
return 'rgba(29, 78, 216, ' + alpha + ')';
|
||||
}
|
||||
return 'rgba(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ', ' + alpha + ')';
|
||||
}
|
||||
|
||||
const primary = hexToRgb(themeColor) ? themeColor : '#1f4fd6';
|
||||
const strong = mixColor(primary, '#000000', 0.18);
|
||||
const soft = hexToRgba(primary, 0.12);
|
||||
const border = hexToRgba(primary, 0.18);
|
||||
const glow = hexToRgba(primary, 0.08);
|
||||
const tint = mixColor(primary, '#ffffff', 0.92);
|
||||
const tintStrong = mixColor(primary, '#ffffff', 0.86);
|
||||
const tintDeep = mixColor(primary, '#ffffff', 0.78);
|
||||
const body = document.body;
|
||||
body.style.setProperty('--traceability-primary', primary);
|
||||
body.style.setProperty('--traceability-primary-strong', strong);
|
||||
body.style.setProperty('--traceability-primary-soft', soft);
|
||||
body.style.setProperty('--traceability-primary-border', border);
|
||||
body.style.setProperty('--traceability-primary-glow', glow);
|
||||
body.style.setProperty('--traceability-primary-tint', tint);
|
||||
body.style.setProperty('--traceability-primary-tint-strong', tintStrong);
|
||||
body.style.setProperty('--traceability-primary-tint-deep', tintDeep);
|
||||
|
||||
const tabButtons = document.querySelectorAll('.tab-btn');
|
||||
const tabPanels = document.querySelectorAll('.tab-panel');
|
||||
const imageButtons = document.querySelectorAll('.kv-image-button');
|
||||
|
||||
tabButtons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
@@ -193,6 +259,34 @@
|
||||
});
|
||||
});
|
||||
|
||||
const viewer = document.createElement('div');
|
||||
viewer.className = 'image-viewer';
|
||||
viewer.innerHTML = '<div class="image-viewer__mask"></div><figure class="image-viewer__content"><button class="image-viewer__close" type="button" aria-label="关闭放大图">×</button><img alt=""></figure>';
|
||||
document.body.appendChild(viewer);
|
||||
|
||||
const viewerImage = viewer.querySelector('.image-viewer__content img');
|
||||
const closeViewer = () => viewer.classList.remove('is-open');
|
||||
|
||||
viewer.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (target === viewer || target.classList.contains('image-viewer__mask') || target.classList.contains('image-viewer__close')) {
|
||||
closeViewer();
|
||||
}
|
||||
});
|
||||
|
||||
imageButtons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const src = button.dataset.imageSrc || '';
|
||||
const alt = button.dataset.imageAlt || '';
|
||||
if (!src) {
|
||||
return;
|
||||
}
|
||||
viewerImage.src = src;
|
||||
viewerImage.alt = alt;
|
||||
viewer.classList.add('is-open');
|
||||
});
|
||||
});
|
||||
|
||||
if (window.location.search.includes('result=')) {
|
||||
const nextUrl = window.location.pathname + window.location.hash;
|
||||
window.history.replaceState({}, '', nextUrl);
|
||||
@@ -200,3 +294,4 @@
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user