修复溯源模块大量问题
This commit is contained in:
@@ -38,7 +38,7 @@ fun Application.configureRouting() {
|
||||
call.respond(mapOf("status" to "ok"))
|
||||
}
|
||||
|
||||
get("/p/{code}") {
|
||||
get("/f10/{code}") {
|
||||
val code = call.parameters["code"]?.trim().orEmpty()
|
||||
if (code.isBlank()) {
|
||||
call.respondText("批次编码不能为空", status = HttpStatusCode.BadRequest)
|
||||
@@ -75,6 +75,36 @@ fun Application.configureRouting() {
|
||||
)
|
||||
}
|
||||
|
||||
get("/preview/{code}") {
|
||||
val code = call.parameters["code"]?.trim().orEmpty()
|
||||
if (code.isBlank()) {
|
||||
call.respondText("预演页编码不能为空", status = HttpStatusCode.BadRequest)
|
||||
return@get
|
||||
}
|
||||
|
||||
val page = call.traceabilityService().loadPreviewPage(code)
|
||||
if (page == null) {
|
||||
call.respond(
|
||||
HttpStatusCode.NotFound,
|
||||
FreeMarkerContent(
|
||||
"error.ftl",
|
||||
mapOf("message" to "未找到对应的预演页,请确认链接是否正确。"),
|
||||
),
|
||||
)
|
||||
return@get
|
||||
}
|
||||
|
||||
call.respond(
|
||||
FreeMarkerContent(
|
||||
"traceability.ftl",
|
||||
mapOf(
|
||||
"page" to page,
|
||||
"feedbackMessage" to "",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
post("/feedback") {
|
||||
val params = call.receiveParameters()
|
||||
val code = params["batchCode"]?.trim().orEmpty()
|
||||
@@ -92,7 +122,7 @@ fun Application.configureRouting() {
|
||||
rating = params["rating"]?.toIntOrNull() ?: 5,
|
||||
)
|
||||
val result = if (response.status) "success" else "failed"
|
||||
call.respondRedirect("/p/$code?result=$result")
|
||||
call.respondRedirect("/f10/$code?result=$result")
|
||||
}
|
||||
|
||||
staticResources("/static", "static")
|
||||
|
||||
@@ -52,6 +52,18 @@ class TraceabilityClient(
|
||||
return payload.data
|
||||
}
|
||||
|
||||
suspend fun fetchPreviewDetail(code: String): TraceabilityPublicDetailResponse? {
|
||||
val response = client.get {
|
||||
url("$coreBaseUrl/traceability/public/preview-page/by-code/$code")
|
||||
accept(ContentType.Application.Json)
|
||||
}
|
||||
if (!response.status.isSuccess()) {
|
||||
return null
|
||||
}
|
||||
val payload = response.body<ApiResponse<TraceabilityPublicDetailResponse>>()
|
||||
return payload.data
|
||||
}
|
||||
|
||||
suspend fun submitFeedback(request: SubmitTraceabilityFeedbackRequest): ApiResponse<TraceabilityFeedbackResponse> {
|
||||
val response = client.post {
|
||||
url("$coreBaseUrl/traceability/public/feedback")
|
||||
|
||||
@@ -11,6 +11,12 @@ data class ApiResponse<T>(
|
||||
val data: T? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TraceFieldStyleResponse(
|
||||
val bold: Boolean = false,
|
||||
val color: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TraceFieldDefinitionResponse(
|
||||
val key: String,
|
||||
@@ -20,7 +26,9 @@ data class TraceFieldDefinitionResponse(
|
||||
val visible: Boolean = true,
|
||||
val placeholder: String = "",
|
||||
val defaultValue: JsonElement? = null,
|
||||
val defaultPreviewUrl: String? = null,
|
||||
val options: List<String> = emptyList(),
|
||||
val fieldStyle: TraceFieldStyleResponse = TraceFieldStyleResponse(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -35,6 +43,7 @@ data class TraceBatchStepResponse(
|
||||
val status: String,
|
||||
val operatorName: String,
|
||||
val values: JsonObject,
|
||||
val valuePreviewUrls: Map<String, String> = emptyMap(),
|
||||
val completedAt: String = "",
|
||||
val fields: List<TraceFieldDefinitionResponse> = emptyList(),
|
||||
)
|
||||
@@ -49,6 +58,7 @@ data class TraceBatchDetailResponse(
|
||||
val productName: String,
|
||||
val summary: String,
|
||||
val coverImage: String,
|
||||
val coverImagePreviewUrl: String = "",
|
||||
val tags: List<String>,
|
||||
val status: String,
|
||||
val currentStep: Int,
|
||||
@@ -96,6 +106,8 @@ data class DisplayEntry(
|
||||
val label: String,
|
||||
val value: String,
|
||||
val type: String = "string",
|
||||
val bold: Boolean = false,
|
||||
val color: String = "",
|
||||
)
|
||||
|
||||
data class PublicSectionView(
|
||||
@@ -116,7 +128,6 @@ data class TimelineSectionView(
|
||||
|
||||
data class PageViewModel(
|
||||
val code: String,
|
||||
val pageUrl: String,
|
||||
val batchName: String,
|
||||
val productName: String,
|
||||
val templateName: String,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.bbitcn
|
||||
package com.bbitcn
|
||||
|
||||
import io.ktor.server.config.ApplicationConfig
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
@@ -7,7 +7,6 @@ import kotlinx.serialization.json.JsonObject
|
||||
|
||||
data class TraceabilityPublicConfig(
|
||||
val coreBaseUrl: String,
|
||||
val publicBaseUrl: String,
|
||||
)
|
||||
|
||||
class TraceabilityService(
|
||||
@@ -20,12 +19,11 @@ class TraceabilityService(
|
||||
|
||||
return PageViewModel(
|
||||
code = batch.batchCode,
|
||||
pageUrl = "${config.publicBaseUrl.trimEnd('/')}/p/${batch.batchCode}",
|
||||
batchName = batch.batchName,
|
||||
productName = batch.productName.ifBlank { batch.templateName },
|
||||
templateName = batch.templateName,
|
||||
summary = batch.summary.ifBlank { "该批次已完成关键环节留痕,可查看公开资料与业务流程。" },
|
||||
coverImage = batch.coverImage,
|
||||
coverImage = batch.coverImagePreviewUrl.ifBlank { batch.coverImage },
|
||||
scanCount = batch.scanCount,
|
||||
publishedAt = formatDateOnly(batch.publishedAt),
|
||||
tagsText = batch.tags.joinToString("、").ifBlank { "暂无标签" },
|
||||
@@ -34,6 +32,24 @@ class TraceabilityService(
|
||||
)
|
||||
}
|
||||
|
||||
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 },
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun submitFeedback(
|
||||
code: String,
|
||||
type: String,
|
||||
@@ -80,10 +96,13 @@ class TraceabilityService(
|
||||
private fun toDisplayEntries(step: TraceBatchStepResponse): List<DisplayEntry> {
|
||||
return step.values.entries.map { (key, value) ->
|
||||
val field = step.fields.find { it.key == key }
|
||||
val imageUrl = step.valuePreviewUrls[key].orEmpty()
|
||||
DisplayEntry(
|
||||
label = field?.label ?: key,
|
||||
value = formatJsonValue(value),
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -106,6 +125,5 @@ class TraceabilityService(
|
||||
fun ApplicationConfig.toTraceabilityPublicConfig(): TraceabilityPublicConfig {
|
||||
return TraceabilityPublicConfig(
|
||||
coreBaseUrl = property("traceability.core-base-url").getString().trimEnd('/'),
|
||||
publicBaseUrl = property("traceability.public-base-url").getString().trimEnd('/'),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@ ktor:
|
||||
port: 8081
|
||||
|
||||
traceability:
|
||||
core-base-url: "http://127.0.0.1:8089"
|
||||
public-base-url: "http://127.0.0.1:8081"
|
||||
# 访问主服务的地址
|
||||
# core-base-url: "http://127.0.0.1:8089" # 开发
|
||||
core-base-url: "https://ai.ronsunny.cn:8090/api" # 生产
|
||||
|
||||
@@ -17,9 +17,9 @@ a {
|
||||
}
|
||||
|
||||
.page-shell {
|
||||
max-width: 1240px;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 16px 48px;
|
||||
padding: 36px 40px 56px;
|
||||
}
|
||||
|
||||
.hero,
|
||||
@@ -30,6 +30,25 @@ a {
|
||||
box-shadow: 0 16px 48px rgba(16, 24, 40, 0.08);
|
||||
}
|
||||
|
||||
.cover-panel {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.cover-card {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(228, 234, 245, 0.9);
|
||||
border-radius: 28px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 16px 48px rgba(16, 24, 40, 0.08);
|
||||
}
|
||||
|
||||
.cover-card img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
@@ -37,11 +56,6 @@ a {
|
||||
padding: 26px;
|
||||
}
|
||||
|
||||
.hero--with-cover {
|
||||
grid-template-columns: minmax(0, 1.2fr) 320px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.hero h1,
|
||||
.panel h2,
|
||||
.info-card h3,
|
||||
@@ -69,26 +83,11 @@ a {
|
||||
|
||||
.hero__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.hero__cover {
|
||||
overflow: hidden;
|
||||
border: 1px solid #e8eef7;
|
||||
border-radius: 22px;
|
||||
background: #fff;
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
.hero__cover img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.stat-card,
|
||||
.summary-card,
|
||||
.kv-card,
|
||||
@@ -134,6 +133,7 @@ a {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.panel {
|
||||
@@ -146,9 +146,9 @@ a {
|
||||
}
|
||||
|
||||
.tabs-nav {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
border: 1px solid #e8eef7;
|
||||
border-radius: 999px;
|
||||
@@ -157,7 +157,10 @@ a {
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
min-width: 112px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
min-height: 42px;
|
||||
padding: 0 18px;
|
||||
border: none;
|
||||
@@ -168,6 +171,8 @@ a {
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
@@ -348,6 +353,14 @@ a {
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
padding: 24px 4px 8px;
|
||||
text-align: center;
|
||||
color: #7d8899;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.hero,
|
||||
.form-grid,
|
||||
@@ -363,14 +376,21 @@ a {
|
||||
.timeline-item__head {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-shell {
|
||||
padding: 24px 18px 44px;
|
||||
}
|
||||
|
||||
.tabs-nav {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
border-radius: 20px;
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,15 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-shell">
|
||||
<section class="hero<#if page.coverImage?has_content> hero--with-cover</#if>">
|
||||
<#if page.coverImage?has_content>
|
||||
<section class="cover-panel">
|
||||
<div class="cover-card">
|
||||
<img src="${page.coverImage}" alt="${page.batchName}" />
|
||||
</div>
|
||||
</section>
|
||||
</#if>
|
||||
|
||||
<section class="hero">
|
||||
<div class="hero__content">
|
||||
<h1>${page.batchName}</h1>
|
||||
<p>${page.summary}</p>
|
||||
@@ -21,51 +29,35 @@
|
||||
<span>产品名称</span>
|
||||
<strong>${page.productName}</strong>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span>所属模板</span>
|
||||
<strong>${page.templateName}</strong>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span>累计访问</span>
|
||||
<strong>${page.scanCount}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<#if page.coverImage?has_content>
|
||||
<div class="hero__cover">
|
||||
<img src="${page.coverImage}" alt="${page.productName}" />
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<div class="hero__aside">
|
||||
<div class="summary-card">
|
||||
<span>发布时间</span>
|
||||
<strong>${page.publishedAt}</strong>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span>标签</span>
|
||||
<strong>${page.tagsText}</strong>
|
||||
</div>
|
||||
<#if page.tagsText != "暂无标签">
|
||||
<div class="summary-card">
|
||||
<span>标签</span>
|
||||
<strong>${page.tagsText}</strong>
|
||||
</div>
|
||||
</#if>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<#if feedbackMessage?has_content>
|
||||
<section class="notice">${feedbackMessage}</section>
|
||||
</#if>
|
||||
|
||||
<section class="panel tabs-panel">
|
||||
<div class="tabs-nav" role="tablist" aria-label="溯源页面内容切换">
|
||||
<button class="tab-btn active" data-tab-target="timeline-panel" type="button">溯源链</button>
|
||||
<button class="tab-btn" data-tab-target="public-panel" type="button">公开资料</button>
|
||||
<button class="tab-btn" data-tab-target="feedback-panel" type="button">反馈与投诉</button>
|
||||
<button class="tab-btn" data-tab-target="feedback-panel" type="button">反馈投诉</button>
|
||||
</div>
|
||||
|
||||
<div id="timeline-panel" class="tab-panel active">
|
||||
<div class="panel__head">
|
||||
<div>
|
||||
<h2>溯源链</h2>
|
||||
<p>按业务流程顺序查看本批次的处理过程与留痕信息。</p>
|
||||
</div>
|
||||
</div>
|
||||
<#if page.businessSections?size gt 0>
|
||||
<div class="timeline">
|
||||
<#list page.businessSections as section>
|
||||
@@ -85,10 +77,10 @@
|
||||
<#list section.entries as entry>
|
||||
<div class="kv-card">
|
||||
<span>${entry.label}</span>
|
||||
<#if entry.type == "image" && entry.value != "未填写">
|
||||
<#if entry.type == "image" && entry.value?has_content && entry.value != "未填写">
|
||||
<img class="kv-image" src="${entry.value}" alt="${entry.label}" />
|
||||
<#else>
|
||||
<strong>${entry.value}</strong>
|
||||
<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>
|
||||
</div>
|
||||
</#list>
|
||||
@@ -103,12 +95,6 @@
|
||||
</div>
|
||||
|
||||
<div id="public-panel" class="tab-panel">
|
||||
<div class="panel__head">
|
||||
<div>
|
||||
<h2>公开资料</h2>
|
||||
<p>面向消费者展示的企业资料、资质证明及其他公开信息。</p>
|
||||
</div>
|
||||
</div>
|
||||
<#if page.publicSections?size gt 0>
|
||||
<div class="public-grid">
|
||||
<#list page.publicSections as section>
|
||||
@@ -121,10 +107,10 @@
|
||||
<#list section.entries as entry>
|
||||
<div class="kv-card">
|
||||
<span>${entry.label}</span>
|
||||
<#if entry.type == "image" && entry.value != "未填写">
|
||||
<#if entry.type == "image" && entry.value?has_content && entry.value != "未填写">
|
||||
<img class="kv-image" src="${entry.value}" alt="${entry.label}" />
|
||||
<#else>
|
||||
<strong>${entry.value}</strong>
|
||||
<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>
|
||||
</div>
|
||||
</#list>
|
||||
@@ -140,10 +126,15 @@
|
||||
<div id="feedback-panel" class="tab-panel">
|
||||
<div class="panel__head">
|
||||
<div>
|
||||
<h2>反馈与投诉</h2>
|
||||
<h2>反馈投诉</h2>
|
||||
<p>如发现信息异常、商品质量问题,或有建议,可直接提交。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<#if feedbackMessage?has_content>
|
||||
<div class="notice">${feedbackMessage}</div>
|
||||
</#if>
|
||||
|
||||
<form class="feedback-form" method="post" action="/feedback">
|
||||
<input type="hidden" name="batchCode" value="${page.code}" />
|
||||
<div class="form-grid">
|
||||
@@ -155,6 +146,7 @@
|
||||
<option value="consult">咨询</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="form-item">
|
||||
<span>满意度</span>
|
||||
<select name="rating">
|
||||
@@ -165,19 +157,26 @@
|
||||
<option value="1">1 分</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="form-item">
|
||||
<span>联系方式</span>
|
||||
<input name="contact" placeholder="手机号 / 邮箱 / 微信" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="form-item form-item--full">
|
||||
<span>反馈内容</span>
|
||||
<textarea name="content" placeholder="请填写你要反馈的问题或建议" required></textarea>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="submit-btn">提交反馈</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="page-footer">
|
||||
技术支持:四川主干信息技术有限公司 BBITCN Co.,Ltd
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user