增加字符串兼容性;去除国外开源地图软件代码;批次发布后仍可编辑;
This commit is contained in:
@@ -8,7 +8,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "com.bbitcn"
|
group = "com.bbitcn"
|
||||||
version = "0.0.4"
|
version = "0.0.5"
|
||||||
|
|
||||||
application {
|
application {
|
||||||
mainClass = "io.ktor.server.netty.EngineMain"
|
mainClass = "io.ktor.server.netty.EngineMain"
|
||||||
|
|||||||
@@ -110,7 +110,17 @@ class TraceabilityService(
|
|||||||
value.entries.joinToString(";") { "${it.key}: ${formatJsonValue(it.value)}" }
|
value.entries.joinToString(";") { "${it.key}: ${formatJsonValue(it.value)}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> value.toString().trim('"').ifBlank { "未填写" }
|
else -> normalizeDisplayText(value.toString().trim('"'))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizeDisplayText(text: String): String {
|
||||||
|
if (text.isBlank()) {
|
||||||
|
return "未填写"
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
.replace("\\r\\n", "\n")
|
||||||
|
.replace("\\n", "\n")
|
||||||
|
.replace("\\t", "\t")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatDateOnly(value: String): String {
|
private fun formatDateOnly(value: String): String {
|
||||||
@@ -126,7 +136,7 @@ class TraceabilityService(
|
|||||||
val lng = coordinate["lng"]?.jsonPrimitive?.doubleOrNull
|
val lng = coordinate["lng"]?.jsonPrimitive?.doubleOrNull
|
||||||
val lat = coordinate["lat"]?.jsonPrimitive?.doubleOrNull
|
val lat = coordinate["lat"]?.jsonPrimitive?.doubleOrNull
|
||||||
return when {
|
return when {
|
||||||
lng != null && lat != null -> "https://www.openstreetmap.org/?mlat=$lat&mlon=$lng#map=15/$lat/$lng"
|
lng != null && lat != null -> "https://uri.amap.com/marker?position=$lng,$lat&name=坐标位置&src=traceability"
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,7 +145,7 @@ class TraceabilityService(
|
|||||||
val coordinate = value as? JsonObject ?: return ""
|
val coordinate = value as? JsonObject ?: return ""
|
||||||
val lng = coordinate["lng"]?.jsonPrimitive?.doubleOrNull ?: return ""
|
val lng = coordinate["lng"]?.jsonPrimitive?.doubleOrNull ?: return ""
|
||||||
val lat = coordinate["lat"]?.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"
|
return "https://uri.amap.com/marker?position=$lng,$lat&name=坐标位置&src=traceability&callnative=0"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildPageViewModel(
|
private fun buildPageViewModel(
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ ktor:
|
|||||||
|
|
||||||
traceability:
|
traceability:
|
||||||
# 访问主服务的地址
|
# 访问主服务的地址
|
||||||
# core-base-url: "http://127.0.0.1:8089" # 开发
|
core-base-url: "http://127.0.0.1:8089" # 开发
|
||||||
core-base-url: "https://ai.ronsunny.cn:8090/api" # 生产
|
# core-base-url: "https://ai.ronsunny.cn:8090/api" # 生产
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>${page.batchName} - 溯源信息</title>
|
<title>${page.batchName} - 溯源信息</title>
|
||||||
<link rel="stylesheet" href="/static/traceability.css" />
|
<link rel="stylesheet" href="/static/traceability.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@10.6.1/ol.css" />
|
||||||
</head>
|
</head>
|
||||||
<body style="--traceability-primary:${page.themeColor};">
|
<body style="--traceability-primary:${page.themeColor};">
|
||||||
<div class="page-shell">
|
<div class="page-shell">
|
||||||
@@ -78,9 +79,11 @@
|
|||||||
<img class="kv-image" src="${entry.value}" alt="${entry.label}" />
|
<img class="kv-image" src="${entry.value}" alt="${entry.label}" />
|
||||||
</button>
|
</button>
|
||||||
<#elseif entry.type == "coordinate" && entry.value?has_content && entry.value != "未填写">
|
<#elseif entry.type == "coordinate" && entry.value?has_content && entry.value != "未填写">
|
||||||
<#if entry.mapEmbedUrl?has_content>
|
<div
|
||||||
<iframe class="kv-map" src="${entry.mapEmbedUrl}" loading="lazy" referrerpolicy="no-referrer-when-downgrade" title="${entry.label}"></iframe>
|
class="kv-map"
|
||||||
</#if>
|
data-coordinate="${entry.value?html}"
|
||||||
|
data-label="${entry.label?html}"
|
||||||
|
></div>
|
||||||
<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>
|
<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>
|
<#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>
|
<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>
|
||||||
@@ -115,9 +118,11 @@
|
|||||||
<img class="kv-image" src="${entry.value}" alt="${entry.label}" />
|
<img class="kv-image" src="${entry.value}" alt="${entry.label}" />
|
||||||
</button>
|
</button>
|
||||||
<#elseif entry.type == "coordinate" && entry.value?has_content && entry.value != "未填写">
|
<#elseif entry.type == "coordinate" && entry.value?has_content && entry.value != "未填写">
|
||||||
<#if entry.mapEmbedUrl?has_content>
|
<div
|
||||||
<iframe class="kv-map" src="${entry.mapEmbedUrl}" loading="lazy" referrerpolicy="no-referrer-when-downgrade" title="${entry.label}"></iframe>
|
class="kv-map"
|
||||||
</#if>
|
data-coordinate="${entry.value?html}"
|
||||||
|
data-label="${entry.label?html}"
|
||||||
|
></div>
|
||||||
<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>
|
<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>
|
<#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>
|
<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>
|
||||||
@@ -190,6 +195,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const openLayersScript = document.createElement('script');
|
||||||
|
openLayersScript.src = 'https://cdn.jsdelivr.net/npm/ol@10.6.1/dist/ol.js';
|
||||||
|
openLayersScript.defer = true;
|
||||||
|
openLayersScript.onload = initCoordinateMaps;
|
||||||
|
document.head.appendChild(openLayersScript);
|
||||||
|
|
||||||
const themeColor = '${page.themeColor}';
|
const themeColor = '${page.themeColor}';
|
||||||
function hexToRgb(hex) {
|
function hexToRgb(hex) {
|
||||||
const normalized = (hex || '').replace('#', '').trim();
|
const normalized = (hex || '').replace('#', '').trim();
|
||||||
@@ -287,6 +298,73 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function parseCoordinateValue(raw) {
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
const lng = Number(parsed.lng);
|
||||||
|
const lat = Number(parsed.lat);
|
||||||
|
if (Number.isFinite(lng) && Number.isFinite(lat)) {
|
||||||
|
return { lng, lat };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const parts = String(raw).split(',').map((item) => item.trim());
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const lng = Number(parts[0]);
|
||||||
|
const lat = Number(parts[1]);
|
||||||
|
if (Number.isFinite(lng) && Number.isFinite(lat)) {
|
||||||
|
return { lng, lat };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCoordinateMaps() {
|
||||||
|
if (!window.ol) return;
|
||||||
|
const mapNodes = document.querySelectorAll('.kv-map[data-coordinate]');
|
||||||
|
mapNodes.forEach((node) => {
|
||||||
|
if (node.dataset.initialized === 'true') return;
|
||||||
|
const parsed = parseCoordinateValue(node.dataset.coordinate);
|
||||||
|
if (!parsed) return;
|
||||||
|
node.dataset.initialized = 'true';
|
||||||
|
|
||||||
|
const map = new ol.Map({
|
||||||
|
target: node,
|
||||||
|
layers: [
|
||||||
|
new ol.layer.Tile({
|
||||||
|
source: new ol.source.XYZ({
|
||||||
|
url: 'https://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}',
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
view: new ol.View({
|
||||||
|
center: ol.proj.fromLonLat([parsed.lng, parsed.lat]),
|
||||||
|
zoom: 13,
|
||||||
|
}),
|
||||||
|
controls: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const marker = new ol.Feature({
|
||||||
|
geometry: new ol.geom.Point(ol.proj.fromLonLat([parsed.lng, parsed.lat])),
|
||||||
|
});
|
||||||
|
const vectorLayer = new ol.layer.Vector({
|
||||||
|
source: new ol.source.Vector({
|
||||||
|
features: [marker],
|
||||||
|
}),
|
||||||
|
style: new ol.style.Style({
|
||||||
|
image: new ol.style.Circle({
|
||||||
|
radius: 7,
|
||||||
|
fill: new ol.style.Fill({ color: primary }),
|
||||||
|
stroke: new ol.style.Stroke({ color: '#ffffff', width: 2 }),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
map.addLayer(vectorLayer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (window.location.search.includes('result=')) {
|
if (window.location.search.includes('result=')) {
|
||||||
const nextUrl = window.location.pathname + window.location.hash;
|
const nextUrl = window.location.pathname + window.location.hash;
|
||||||
window.history.replaceState({}, '', nextUrl);
|
window.history.replaceState({}, '', nextUrl);
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ object TraceabilityDao {
|
|||||||
|
|
||||||
fun listPreviewPages(): List<TracePreviewPageSummaryResponse> = transaction {
|
fun listPreviewPages(): List<TracePreviewPageSummaryResponse> = transaction {
|
||||||
TraceabilityPreviewPagesTable.selectAll()
|
TraceabilityPreviewPagesTable.selectAll()
|
||||||
.orderBy(TraceabilityPreviewPagesTable.updatedAt, SortOrder.DESC)
|
.orderBy(TraceabilityPreviewPagesTable.createdAt, SortOrder.DESC)
|
||||||
.map {
|
.map {
|
||||||
val code = it[TraceabilityPreviewPagesTable.previewCode]
|
val code = it[TraceabilityPreviewPagesTable.previewCode]
|
||||||
TracePreviewPageSummaryResponse(
|
TracePreviewPageSummaryResponse(
|
||||||
@@ -353,7 +353,7 @@ object TraceabilityDao {
|
|||||||
fun listNodeLibrary(): List<TraceNodeLibraryResponse> = transaction {
|
fun listNodeLibrary(): List<TraceNodeLibraryResponse> = transaction {
|
||||||
ensureDefaultNodeLibrarySeeded()
|
ensureDefaultNodeLibrarySeeded()
|
||||||
TraceabilityNodeLibraryTable.selectAll()
|
TraceabilityNodeLibraryTable.selectAll()
|
||||||
.orderBy(TraceabilityNodeLibraryTable.updatedAt, SortOrder.DESC)
|
.orderBy(TraceabilityNodeLibraryTable.createdAt, SortOrder.DESC)
|
||||||
.map {
|
.map {
|
||||||
TraceNodeLibraryResponse(
|
TraceNodeLibraryResponse(
|
||||||
id = it[TraceabilityNodeLibraryTable.id].value.toString(),
|
id = it[TraceabilityNodeLibraryTable.id].value.toString(),
|
||||||
@@ -482,7 +482,7 @@ object TraceabilityDao {
|
|||||||
.mapValues { (_, rows) -> rows.size }
|
.mapValues { (_, rows) -> rows.size }
|
||||||
|
|
||||||
TraceabilityTemplatesTable.selectAll()
|
TraceabilityTemplatesTable.selectAll()
|
||||||
.orderBy(TraceabilityTemplatesTable.updatedAt, SortOrder.DESC)
|
.orderBy(TraceabilityTemplatesTable.createdAt, SortOrder.DESC)
|
||||||
.map {
|
.map {
|
||||||
TraceTemplateSummaryResponse(
|
TraceTemplateSummaryResponse(
|
||||||
id = it[TraceabilityTemplatesTable.id].value.toString(),
|
id = it[TraceabilityTemplatesTable.id].value.toString(),
|
||||||
@@ -613,7 +613,7 @@ object TraceabilityDao {
|
|||||||
.associate { it[TraceabilityTemplatesTable.id].value to it[TraceabilityTemplatesTable.name] }
|
.associate { it[TraceabilityTemplatesTable.id].value to it[TraceabilityTemplatesTable.name] }
|
||||||
|
|
||||||
TraceabilityBatchesTable.selectAll()
|
TraceabilityBatchesTable.selectAll()
|
||||||
.orderBy(TraceabilityBatchesTable.updatedAt, SortOrder.DESC)
|
.orderBy(TraceabilityBatchesTable.createdAt, SortOrder.DESC)
|
||||||
.map {
|
.map {
|
||||||
val code = it[TraceabilityBatchesTable.batchCode]
|
val code = it[TraceabilityBatchesTable.batchCode]
|
||||||
TraceBatchSummaryResponse(
|
TraceBatchSummaryResponse(
|
||||||
|
|||||||
@@ -946,7 +946,7 @@ onMounted(async () => {
|
|||||||
<Tabs v-model:active-key="activeTab">
|
<Tabs v-model:active-key="activeTab">
|
||||||
<Tabs.TabPane key="templates" tab="模板中心">
|
<Tabs.TabPane key="templates" tab="模板中心">
|
||||||
<Row :gutter="[16, 16]" align="stretch">
|
<Row :gutter="[16, 16]" align="stretch">
|
||||||
<Col :lg="8" :xs="24">
|
<Col :lg="7" :xs="24">
|
||||||
<Card :loading="loading" class="panel-card editor-panel" title="模板列表">
|
<Card :loading="loading" class="panel-card editor-panel" title="模板列表">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<Button type="primary" @click="openCreateTemplateModal">
|
<Button type="primary" @click="openCreateTemplateModal">
|
||||||
@@ -988,12 +988,11 @@ onMounted(async () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col :lg="16" :xs="24">
|
<Col :lg="17" :xs="24">
|
||||||
<div class="template-main">
|
<div class="template-main">
|
||||||
<Card
|
<Card
|
||||||
v-if="selectedTemplateId"
|
v-if="selectedTemplateId"
|
||||||
class="panel-card template-summary"
|
class="panel-card template-summary"
|
||||||
:bordered="false"
|
|
||||||
title="基础信息"
|
title="基础信息"
|
||||||
>
|
>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
@@ -1445,6 +1444,14 @@ onMounted(async () => {
|
|||||||
:value="String(getPresetValue(currentTemplateField) || '')"
|
:value="String(getPresetValue(currentTemplateField) || '')"
|
||||||
@update:value="(value) => updatePresetValue(currentTemplateField, value)"
|
@update:value="(value) => updatePresetValue(currentTemplateField, value)"
|
||||||
/>
|
/>
|
||||||
|
<Input.TextArea
|
||||||
|
v-else-if="currentTemplateField.type === 'string'"
|
||||||
|
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||||
|
:disabled="isFieldPresetLocked(currentTemplateField)"
|
||||||
|
:placeholder="currentTemplateField.placeholder || '请输入字符串'"
|
||||||
|
:value="String(getPresetValue(currentTemplateField) ?? '')"
|
||||||
|
@update:value="(value) => updatePresetValue(currentTemplateField, value)"
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
v-else
|
v-else
|
||||||
:disabled="isFieldPresetLocked(currentTemplateField)"
|
:disabled="isFieldPresetLocked(currentTemplateField)"
|
||||||
@@ -1763,11 +1770,17 @@ onMounted(async () => {
|
|||||||
:value="String(getPresetValue(currentLibraryField) || '')"
|
:value="String(getPresetValue(currentLibraryField) || '')"
|
||||||
@update:value="(value) => updatePresetValue(currentLibraryField, value)"
|
@update:value="(value) => updatePresetValue(currentLibraryField, value)"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input.TextArea
|
||||||
v-else
|
v-else-if="currentLibraryField.type === 'string'"
|
||||||
:value="getPresetValue(currentLibraryField)"
|
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||||
@update:value="(value) => updatePresetValue(currentLibraryField, value)"
|
:value="String(getPresetValue(currentLibraryField) ?? '')"
|
||||||
/>
|
@update:value="(value) => updatePresetValue(currentLibraryField, value)"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-else
|
||||||
|
:value="getPresetValue(currentLibraryField)"
|
||||||
|
@update:value="(value) => updatePresetValue(currentLibraryField, value)"
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :md="10" :xs="24">
|
<Col :md="10" :xs="24">
|
||||||
<label class="field-label">文字颜色</label>
|
<label class="field-label">文字颜色</label>
|
||||||
@@ -2021,10 +2034,13 @@ onMounted(async () => {
|
|||||||
border: 1px solid #edf1f7;
|
border: 1px solid #edf1f7;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 14px 16px;
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.template-summary :deep(.ant-card-body) {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.summary-inline {
|
.summary-inline {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
@@ -2170,7 +2186,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.template-list {
|
.template-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-card {
|
.template-card {
|
||||||
@@ -2179,7 +2195,7 @@ onMounted(async () => {
|
|||||||
border: 1px solid #edf1f7;
|
border: 1px solid #edf1f7;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 14px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-card.active {
|
.template-card.active {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { TraceabilityApi } from '#/api';
|
import type { TraceabilityApi } from '#/api';
|
||||||
|
|
||||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { Button, Input, Modal, Space, message } from 'ant-design-vue';
|
import { Input } from 'ant-design-vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildCoordinateMapUrl,
|
|
||||||
formatCoordinateValue,
|
formatCoordinateValue,
|
||||||
normalizeCoordinateValue,
|
normalizeCoordinateValue,
|
||||||
} from '../shared';
|
} from '../shared';
|
||||||
@@ -25,12 +24,6 @@ const emit = defineEmits<{
|
|||||||
(event: 'update:modelValue', value: TraceabilityApi.CoordinateValue | null): void;
|
(event: 'update:modelValue', value: TraceabilityApi.CoordinateValue | null): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const pickerVisible = ref(false);
|
|
||||||
const mapLoading = ref(false);
|
|
||||||
const mapContainer = ref<HTMLDivElement | null>(null);
|
|
||||||
const leafletMap = ref<any>(null);
|
|
||||||
const leafletMarker = ref<any>(null);
|
|
||||||
|
|
||||||
const currentValue = computed(() => normalizeCoordinateValue(props.modelValue));
|
const currentValue = computed(() => normalizeCoordinateValue(props.modelValue));
|
||||||
const lngText = ref('');
|
const lngText = ref('');
|
||||||
const latText = ref('');
|
const latText = ref('');
|
||||||
@@ -70,130 +63,6 @@ function clearValue() {
|
|||||||
latText.value = '';
|
latText.value = '';
|
||||||
emit('update:modelValue', null);
|
emit('update:modelValue', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEmbedMapUrl() {
|
|
||||||
const normalized = currentValue.value;
|
|
||||||
if (normalized?.lng === null || normalized?.lng === undefined || normalized?.lat === null || normalized?.lat === undefined) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return `https://www.openstreetmap.org/export/embed.html?bbox=${normalized.lng - 0.01}%2C${normalized.lat - 0.01}%2C${normalized.lng + 0.01}%2C${normalized.lat + 0.01}&layer=mapnik&marker=${normalized.lat}%2C${normalized.lng}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openExternalMap() {
|
|
||||||
const url = buildCoordinateMapUrl(currentValue.value);
|
|
||||||
if (url) {
|
|
||||||
window.open(url, '_blank', 'noopener,noreferrer');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadValueFromMap(lng: number, lat: number, source: string = 'map') {
|
|
||||||
lngText.value = String(lng);
|
|
||||||
latText.value = String(lat);
|
|
||||||
emitCurrent(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadLeaflet() {
|
|
||||||
const windowAny = window as any;
|
|
||||||
if (windowAny.L) return windowAny.L;
|
|
||||||
|
|
||||||
if (!document.querySelector('link[data-traceability-leaflet]')) {
|
|
||||||
const link = document.createElement('link');
|
|
||||||
link.rel = 'stylesheet';
|
|
||||||
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
|
|
||||||
link.setAttribute('data-traceability-leaflet', '1');
|
|
||||||
document.head.appendChild(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
const existing = document.querySelector('script[data-traceability-leaflet]');
|
|
||||||
if (existing && windowAny.L) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (existing) {
|
|
||||||
existing.addEventListener('load', () => resolve(), { once: true });
|
|
||||||
existing.addEventListener('error', () => reject(new Error('leaflet load failed')), { once: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
|
|
||||||
script.async = true;
|
|
||||||
script.setAttribute('data-traceability-leaflet', '1');
|
|
||||||
script.onload = () => resolve();
|
|
||||||
script.onerror = () => reject(new Error('leaflet load failed'));
|
|
||||||
document.body.appendChild(script);
|
|
||||||
});
|
|
||||||
|
|
||||||
return windowAny.L;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMarkerPosition(lng: number, lat: number) {
|
|
||||||
if (!leafletMap.value) return;
|
|
||||||
const leaflet = (window as any).L;
|
|
||||||
if (!leafletMarker.value) {
|
|
||||||
leafletMarker.value = leaflet.marker([lat, lng], { draggable: !props.disabled }).addTo(leafletMap.value);
|
|
||||||
leafletMarker.value.on('dragend', (event: any) => {
|
|
||||||
const point = event.target.getLatLng();
|
|
||||||
loadValueFromMap(point.lng, point.lat);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
leafletMarker.value.setLatLng([lat, lng]);
|
|
||||||
}
|
|
||||||
leafletMap.value.setView([lat, lng], 15);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initMap() {
|
|
||||||
if (!pickerVisible.value || !mapContainer.value) return;
|
|
||||||
mapLoading.value = true;
|
|
||||||
try {
|
|
||||||
const leaflet = await loadLeaflet();
|
|
||||||
await nextTick();
|
|
||||||
if (!mapContainer.value) return;
|
|
||||||
|
|
||||||
if (!leafletMap.value) {
|
|
||||||
leafletMap.value = leaflet.map(mapContainer.value, {
|
|
||||||
zoomControl: true,
|
|
||||||
}).setView([39.9042, 116.4074], 5);
|
|
||||||
leaflet.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
||||||
attribution: '© OpenStreetMap contributors',
|
|
||||||
}).addTo(leafletMap.value);
|
|
||||||
leafletMap.value.on('click', (event: any) => {
|
|
||||||
if (props.disabled) return;
|
|
||||||
loadValueFromMap(event.latlng.lng, event.latlng.lat);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalized = currentValue.value;
|
|
||||||
if (normalized?.lng !== null && normalized?.lng !== undefined && normalized?.lat !== null && normalized?.lat !== undefined) {
|
|
||||||
updateMarkerPosition(normalized.lng, normalized.lat);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => leafletMap.value?.invalidateSize(), 120);
|
|
||||||
} catch {
|
|
||||||
message.error('地图控件加载失败,请先手动输入经纬度');
|
|
||||||
} finally {
|
|
||||||
mapLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openPicker() {
|
|
||||||
pickerVisible.value = true;
|
|
||||||
nextTick(() => {
|
|
||||||
void initMap();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(currentValue, (value) => {
|
|
||||||
if (value?.lng !== null && value?.lng !== undefined && value?.lat !== null && value?.lat !== undefined) {
|
|
||||||
updateMarkerPosition(value.lng, value.lat);
|
|
||||||
}
|
|
||||||
}, { deep: true });
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
leafletMap.value?.remove?.();
|
|
||||||
leafletMap.value = null;
|
|
||||||
leafletMarker.value = null;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -213,55 +82,9 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Space wrap class="coordinate-editor__actions">
|
|
||||||
<Button :disabled="disabled" size="small" @click="openPicker">
|
|
||||||
{{ disabled ? '查看地图' : '地图选点' }}
|
|
||||||
</Button>
|
|
||||||
<Button v-if="currentValue" size="small" @click="openExternalMap">
|
|
||||||
打开外部地图
|
|
||||||
</Button>
|
|
||||||
<Button v-if="currentValue && !disabled" size="small" @click="clearValue">
|
|
||||||
清空
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
|
|
||||||
<div v-if="currentValue" class="coordinate-editor__summary">
|
<div v-if="currentValue" class="coordinate-editor__summary">
|
||||||
{{ formatCoordinateValue(currentValue) }}
|
{{ formatCoordinateValue(currentValue) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<iframe
|
|
||||||
v-if="currentValue?.lng !== null && currentValue?.lng !== undefined && currentValue?.lat !== null && currentValue?.lat !== undefined"
|
|
||||||
:src="buildEmbedMapUrl()"
|
|
||||||
class="coordinate-editor__embed"
|
|
||||||
loading="lazy"
|
|
||||||
referrerpolicy="no-referrer-when-downgrade"
|
|
||||||
title="坐标预览地图"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
v-model:open="pickerVisible"
|
|
||||||
:footer="null"
|
|
||||||
:title="disabled ? '查看坐标' : '地图选点'"
|
|
||||||
width="920px"
|
|
||||||
@after-open-change="(open) => open && initMap()"
|
|
||||||
>
|
|
||||||
<div class="coordinate-picker">
|
|
||||||
<div class="coordinate-picker__sidebar">
|
|
||||||
<div class="coordinate-picker__tips">
|
|
||||||
<span>使用说明:</span>
|
|
||||||
<p>支持手动输入经纬度,或在地图上点击/拖拽标记点来确定位置。</p>
|
|
||||||
</div>
|
|
||||||
<div class="coordinate-picker__meta">
|
|
||||||
<span>经度:{{ lngText || '-' }}</span>
|
|
||||||
<span>纬度:{{ latText || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="coordinate-picker__map-wrap">
|
|
||||||
<div ref="mapContainer" class="coordinate-picker__map" />
|
|
||||||
<div v-if="mapLoading" class="coordinate-picker__mask">地图加载中...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -276,93 +99,10 @@ onBeforeUnmount(() => {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coordinate-editor__actions {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coordinate-editor__summary {
|
.coordinate-editor__summary {
|
||||||
color: #526277;
|
color: #526277;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coordinate-editor__embed {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 220px;
|
|
||||||
border: 1px solid #e7edf7;
|
|
||||||
border-radius: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coordinate-picker {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 280px minmax(0, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coordinate-picker__sidebar {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coordinate-picker__tips {
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid #e7edf7;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #fafcff;
|
|
||||||
color: #5f6b7c;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coordinate-picker__tips p {
|
|
||||||
margin: 6px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coordinate-picker__meta {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid #e7edf7;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #fff;
|
|
||||||
color: #4f6078;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coordinate-picker__map-wrap {
|
|
||||||
position: relative;
|
|
||||||
min-height: 520px;
|
|
||||||
border: 1px solid #e7edf7;
|
|
||||||
border-radius: 14px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coordinate-picker__map {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 520px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coordinate-picker__mask {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: rgba(255, 255, 255, 0.72);
|
|
||||||
color: #4f6078;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.coordinate-picker {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coordinate-picker__map-wrap,
|
|
||||||
.coordinate-picker__map {
|
|
||||||
min-height: 360px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Steps,
|
|
||||||
Tag,
|
Tag,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
@@ -28,6 +27,7 @@ import {
|
|||||||
getTraceabilityTemplates,
|
getTraceabilityTemplates,
|
||||||
publishTraceabilityBatch,
|
publishTraceabilityBatch,
|
||||||
resetTraceabilityBatchScanCount,
|
resetTraceabilityBatchScanCount,
|
||||||
|
updateTraceabilityBatchBase,
|
||||||
updateTraceabilityBatchStep,
|
updateTraceabilityBatchStep,
|
||||||
uploadTraceabilityImage,
|
uploadTraceabilityImage,
|
||||||
} from '#/api';
|
} from '#/api';
|
||||||
@@ -47,13 +47,11 @@ const selectedBatchId = ref('');
|
|||||||
const templates = ref<TraceabilityApi.TemplateSummary[]>([]);
|
const templates = ref<TraceabilityApi.TemplateSummary[]>([]);
|
||||||
const batches = ref<TraceabilityApi.BatchSummary[]>([]);
|
const batches = ref<TraceabilityApi.BatchSummary[]>([]);
|
||||||
const batchDetail = ref<null | TraceabilityApi.BatchDetail>(null);
|
const batchDetail = ref<null | TraceabilityApi.BatchDetail>(null);
|
||||||
const stepIndex = ref(0);
|
const selectedStepId = ref('');
|
||||||
const editableStartIndex = ref(0);
|
|
||||||
const createBatchVisible = ref(false);
|
const createBatchVisible = ref(false);
|
||||||
const formState = reactive({
|
const formState = reactive({
|
||||||
batchCode: '',
|
batchCode: '',
|
||||||
batchName: '',
|
batchName: '',
|
||||||
coverImage: '',
|
|
||||||
productName: '',
|
productName: '',
|
||||||
summary: '',
|
summary: '',
|
||||||
tagsText: '',
|
tagsText: '',
|
||||||
@@ -66,6 +64,7 @@ const batchSnapshot = ref('');
|
|||||||
const leaveDialogVisible = ref(false);
|
const leaveDialogVisible = ref(false);
|
||||||
const pendingBatchSelectId = ref('');
|
const pendingBatchSelectId = ref('');
|
||||||
const bypassBatchLeaveGuard = ref(false);
|
const bypassBatchLeaveGuard = ref(false);
|
||||||
|
const batchEditMode = ref(false);
|
||||||
const publishedTemplates = computed(() =>
|
const publishedTemplates = computed(() =>
|
||||||
templates.value.filter((item) => item.status === 'active'),
|
templates.value.filter((item) => item.status === 'active'),
|
||||||
);
|
);
|
||||||
@@ -88,7 +87,16 @@ const filteredBatches = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const currentStep = computed(
|
const currentStep = computed(
|
||||||
() => batchDetail.value?.steps?.[stepIndex.value] ?? null,
|
() =>
|
||||||
|
(batchDetail.value?.steps ?? []).find((item) => item.id === selectedStepId.value)
|
||||||
|
?? batchDetail.value?.steps?.[0]
|
||||||
|
?? null,
|
||||||
|
);
|
||||||
|
const publicSteps = computed(() =>
|
||||||
|
(batchDetail.value?.steps ?? []).filter((item) => item.category === 'public'),
|
||||||
|
);
|
||||||
|
const businessSteps = computed(() =>
|
||||||
|
(batchDetail.value?.steps ?? []).filter((item) => item.category !== 'public'),
|
||||||
);
|
);
|
||||||
const batchQrCode = useQRCode(
|
const batchQrCode = useQRCode(
|
||||||
computed(() => batchDetail.value?.publicUrl || ''),
|
computed(() => batchDetail.value?.publicUrl || ''),
|
||||||
@@ -96,28 +104,71 @@ const batchQrCode = useQRCode(
|
|||||||
);
|
);
|
||||||
const isPublished = computed(() => batchDetail.value?.status === 'published');
|
const isPublished = computed(() => batchDetail.value?.status === 'published');
|
||||||
const isLockedStep = computed(() => !!currentStep.value?.locked);
|
const isLockedStep = computed(() => !!currentStep.value?.locked);
|
||||||
const actualCurrentStepIndex = computed(() =>
|
|
||||||
isPublished.value
|
|
||||||
? (batchDetail.value?.currentStep ?? 0)
|
|
||||||
: editableStartIndex.value,
|
|
||||||
);
|
|
||||||
const isCurrentEditableStep = computed(
|
const isCurrentEditableStep = computed(
|
||||||
() => !!batchDetail.value && !isPublished.value,
|
() => !!currentStep.value && (!isPublished.value || batchEditMode.value),
|
||||||
);
|
);
|
||||||
const isLastStep = computed(() => {
|
|
||||||
if (!batchDetail.value?.steps?.length) return false;
|
|
||||||
return actualCurrentStepIndex.value >= batchDetail.value.steps.length - 1;
|
|
||||||
});
|
|
||||||
const allStepsCompleted = computed(() =>
|
const allStepsCompleted = computed(() =>
|
||||||
(batchDetail.value?.steps ?? []).every((item) => item.status === 'completed'),
|
(batchDetail.value?.steps ?? []).every((item) => item.status === 'completed'),
|
||||||
);
|
);
|
||||||
const stepActionText = computed(() =>
|
const canEditBaseInfo = computed(() => !isPublished.value || batchEditMode.value);
|
||||||
isLastStep.value ? '保存并发布' : '保存并切换至下一节点',
|
const canPublishBatch = computed(() => !isPublished.value && allStepsCompleted.value);
|
||||||
);
|
const publishButtonText = computed(() => (isPublished.value ? '更新批次' : '发布批次'));
|
||||||
const batchQrDownloadName = computed(
|
const batchQrDownloadName = computed(
|
||||||
() => `${batchDetail.value?.batchCode || 'batch'}.png`,
|
() => `${batchDetail.value?.batchCode || 'batch'}.png`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasBaseInfoChanges = computed(() => {
|
||||||
|
if (!selectedBatchId.value || !batchDetail.value) return false;
|
||||||
|
return JSON.stringify({
|
||||||
|
batchCode: formState.batchCode,
|
||||||
|
batchName: formState.batchName,
|
||||||
|
productName: formState.productName,
|
||||||
|
summary: formState.summary,
|
||||||
|
tagsText: formState.tagsText,
|
||||||
|
templateId: formState.templateId,
|
||||||
|
}) !== JSON.stringify({
|
||||||
|
batchCode: batchDetail.value.batchCode,
|
||||||
|
batchName: batchDetail.value.batchName,
|
||||||
|
productName: batchDetail.value.productName,
|
||||||
|
summary: batchDetail.value.summary,
|
||||||
|
tagsText: batchDetail.value.tags.join(','),
|
||||||
|
templateId: batchDetail.value.templateId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getNextPendingStep(category?: string) {
|
||||||
|
const steps = batchDetail.value?.steps ?? [];
|
||||||
|
if (category) {
|
||||||
|
const laneStep = steps.find(
|
||||||
|
(item) => item.category === category && item.status !== 'completed',
|
||||||
|
);
|
||||||
|
if (laneStep) return laneStep;
|
||||||
|
}
|
||||||
|
return steps.find((item) => item.status !== 'completed') ?? steps[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncSelectedStep(preferredCategory?: string) {
|
||||||
|
selectedStepId.value =
|
||||||
|
getNextPendingStep(preferredCategory)?.id
|
||||||
|
?? batchDetail.value?.steps?.[0]?.id
|
||||||
|
?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectStep(stepId: string) {
|
||||||
|
selectedStepId.value = stepId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCompletedAt(value?: string) {
|
||||||
|
const text = value?.trim();
|
||||||
|
if (!text) return new Date().toISOString();
|
||||||
|
if (text.includes('T')) {
|
||||||
|
const parsed = new Date(text);
|
||||||
|
return Number.isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString();
|
||||||
|
}
|
||||||
|
const parsed = new Date(text.replace(' ', 'T'));
|
||||||
|
return Number.isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
function downloadQrCode(dataUrl: string, fileName: string) {
|
function downloadQrCode(dataUrl: string, fileName: string) {
|
||||||
if (!dataUrl) return;
|
if (!dataUrl) return;
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
@@ -158,17 +209,15 @@ function applyBatch(detail: TraceabilityApi.BatchDetail) {
|
|||||||
batchDetail.value = structuredClone(detail);
|
batchDetail.value = structuredClone(detail);
|
||||||
formState.batchCode = detail.batchCode;
|
formState.batchCode = detail.batchCode;
|
||||||
formState.batchName = detail.batchName;
|
formState.batchName = detail.batchName;
|
||||||
formState.coverImage = detail.coverImage;
|
|
||||||
formState.productName = detail.productName;
|
formState.productName = detail.productName;
|
||||||
formState.summary = detail.summary;
|
formState.summary = detail.summary;
|
||||||
formState.tagsText = detail.tags.join(',');
|
formState.tagsText = detail.tags.join(',');
|
||||||
formState.templateId = detail.templateId;
|
formState.templateId = detail.templateId;
|
||||||
stepIndex.value = detail.currentStep ?? 0;
|
batchEditMode.value = false;
|
||||||
editableStartIndex.value = detail.currentStep ?? 0;
|
syncSelectedStep();
|
||||||
batchSnapshot.value = JSON.stringify({
|
batchSnapshot.value = JSON.stringify({
|
||||||
batchCode: formState.batchCode,
|
batchCode: formState.batchCode,
|
||||||
batchName: formState.batchName,
|
batchName: formState.batchName,
|
||||||
coverImage: formState.coverImage,
|
|
||||||
productName: formState.productName,
|
productName: formState.productName,
|
||||||
summary: formState.summary,
|
summary: formState.summary,
|
||||||
tagsText: formState.tagsText,
|
tagsText: formState.tagsText,
|
||||||
@@ -184,7 +233,6 @@ const hasBatchChanges = computed(() => {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
batchCode: formState.batchCode,
|
batchCode: formState.batchCode,
|
||||||
batchName: formState.batchName,
|
batchName: formState.batchName,
|
||||||
coverImage: formState.coverImage,
|
|
||||||
productName: formState.productName,
|
productName: formState.productName,
|
||||||
summary: formState.summary,
|
summary: formState.summary,
|
||||||
tagsText: formState.tagsText,
|
tagsText: formState.tagsText,
|
||||||
@@ -193,6 +241,9 @@ const hasBatchChanges = computed(() => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
const canUpdateBatch = computed(
|
||||||
|
() => isPublished.value && batchEditMode.value && hasBatchChanges.value,
|
||||||
|
);
|
||||||
|
|
||||||
function openBatchLeaveDialog(nextId: string) {
|
function openBatchLeaveDialog(nextId: string) {
|
||||||
pendingBatchSelectId.value = nextId;
|
pendingBatchSelectId.value = nextId;
|
||||||
@@ -205,7 +256,11 @@ async function confirmBatchLeave(action: 'cancel' | 'discard' | 'save') {
|
|||||||
pendingBatchSelectId.value = '';
|
pendingBatchSelectId.value = '';
|
||||||
if (action === 'cancel' || !nextId) return;
|
if (action === 'cancel' || !nextId) return;
|
||||||
if (action === 'save') {
|
if (action === 'save') {
|
||||||
await saveStep();
|
if (currentStep.value && isCurrentEditableStep.value) {
|
||||||
|
await saveStep();
|
||||||
|
} else {
|
||||||
|
await saveBatchBase();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bypassBatchLeaveGuard.value = true;
|
bypassBatchLeaveGuard.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -239,12 +294,10 @@ function openCreateBatchModal() {
|
|||||||
batchDetail.value = null;
|
batchDetail.value = null;
|
||||||
formState.batchCode = `TR-${new Date().getFullYear()}-${String(Date.now()).slice(-6)}`;
|
formState.batchCode = `TR-${new Date().getFullYear()}-${String(Date.now()).slice(-6)}`;
|
||||||
formState.batchName = '新建批次';
|
formState.batchName = '新建批次';
|
||||||
formState.coverImage = '';
|
|
||||||
formState.productName = '';
|
formState.productName = '';
|
||||||
formState.summary = '';
|
formState.summary = '';
|
||||||
formState.tagsText = '';
|
formState.tagsText = '';
|
||||||
formState.templateId = publishedTemplates.value[0]?.id ?? '';
|
formState.templateId = publishedTemplates.value[0]?.id ?? '';
|
||||||
stepIndex.value = 0;
|
|
||||||
createBatchVisible.value = true;
|
createBatchVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +311,6 @@ async function createBatch() {
|
|||||||
const detail = await createTraceabilityBatch({
|
const detail = await createTraceabilityBatch({
|
||||||
batchCode: formState.batchCode,
|
batchCode: formState.batchCode,
|
||||||
batchName: formState.batchName,
|
batchName: formState.batchName,
|
||||||
coverImage: formState.coverImage,
|
|
||||||
productName: formState.productName,
|
productName: formState.productName,
|
||||||
summary: formState.summary,
|
summary: formState.summary,
|
||||||
tags: formState.tagsText
|
tags: formState.tagsText
|
||||||
@@ -286,11 +338,8 @@ function getBatchStatusText(status: string) {
|
|||||||
return status || '进行中';
|
return status || '进行中';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStepStatusText(index: number) {
|
function getStepStatusText(step: TraceabilityApi.BatchStep) {
|
||||||
if (allStepsCompleted.value) {
|
return step.status === 'completed' ? '已完成' : '待填写';
|
||||||
return '已完成';
|
|
||||||
}
|
|
||||||
return index < actualCurrentStepIndex.value ? '已完成' : '进行中';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFieldValue(field: TraceabilityApi.FieldDefinition, value: any) {
|
function updateFieldValue(field: TraceabilityApi.FieldDefinition, value: any) {
|
||||||
@@ -310,9 +359,7 @@ function buildPersistedStepValues(step: TraceabilityApi.BatchStep) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isFieldValueLocked(field: TraceabilityApi.FieldDefinition) {
|
function isFieldValueLocked(field: TraceabilityApi.FieldDefinition) {
|
||||||
return (
|
return !isCurrentEditableStep.value || !!field.fixedPreset;
|
||||||
!isCurrentEditableStep.value || !!isPublished.value || !!field.fixedPreset
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeIntegerInput(value: string) {
|
function sanitizeIntegerInput(value: string) {
|
||||||
@@ -401,34 +448,125 @@ function removeBatch(id: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveBatchBase() {
|
||||||
|
if (!selectedBatchId.value || !batchDetail.value || !canEditBaseInfo.value) {
|
||||||
|
return batchDetail.value;
|
||||||
|
}
|
||||||
|
const detail = await updateTraceabilityBatchBase(selectedBatchId.value, {
|
||||||
|
batchCode: formState.batchCode,
|
||||||
|
batchName: formState.batchName,
|
||||||
|
productName: formState.productName,
|
||||||
|
summary: formState.summary,
|
||||||
|
tags: formState.tagsText
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
currentStep: batchDetail.value.currentStep,
|
||||||
|
});
|
||||||
|
batchDetail.value = structuredClone(detail);
|
||||||
|
batchSnapshot.value = JSON.stringify({
|
||||||
|
batchCode: detail.batchCode,
|
||||||
|
batchName: detail.batchName,
|
||||||
|
productName: detail.productName,
|
||||||
|
summary: detail.summary,
|
||||||
|
tagsText: detail.tags.join(','),
|
||||||
|
templateId: detail.templateId,
|
||||||
|
steps: detail.steps,
|
||||||
|
});
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveBatchInfo() {
|
||||||
|
if (!selectedBatchId.value || !hasBaseInfoChanges.value) return;
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await saveBatchBase();
|
||||||
|
message.success('基础信息已保存');
|
||||||
|
await loadLists();
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function publishOrUpdateBatch() {
|
||||||
|
if (!selectedBatchId.value) return;
|
||||||
|
if (!isPublished.value && !allStepsCompleted.value) {
|
||||||
|
message.warning('请先完成业务节点和公共资料节点的全部填报');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const actionLabel = isPublished.value ? '更新' : '发布';
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await saveBatchBase();
|
||||||
|
if (isPublished.value && batchEditMode.value && currentStep.value) {
|
||||||
|
const updatedDetail = await updateTraceabilityBatchStep(
|
||||||
|
selectedBatchId.value,
|
||||||
|
currentStep.value.id,
|
||||||
|
{
|
||||||
|
completedAt:
|
||||||
|
normalizeCompletedAt(currentStep.value.completedAt),
|
||||||
|
operatorName: currentStep.value.operatorName,
|
||||||
|
status: currentStep.value.status || 'completed',
|
||||||
|
values: buildPersistedStepValues(currentStep.value),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
batchDetail.value = structuredClone(updatedDetail);
|
||||||
|
}
|
||||||
|
const detail = await publishTraceabilityBatch(selectedBatchId.value);
|
||||||
|
applyBatch(detail);
|
||||||
|
message.success(`批次已${actionLabel}`);
|
||||||
|
await loadLists();
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startBatchEdit(id?: string) {
|
||||||
|
if (id && id !== selectedBatchId.value) {
|
||||||
|
await selectBatch(id);
|
||||||
|
}
|
||||||
|
if (!batchDetail.value) return;
|
||||||
|
batchEditMode.value = true;
|
||||||
|
selectedStepId.value = currentStep.value?.id || batchDetail.value.steps[0]?.id || '';
|
||||||
|
}
|
||||||
|
|
||||||
async function saveStep() {
|
async function saveStep() {
|
||||||
if (!selectedBatchId.value || !currentStep.value) return;
|
if (!selectedBatchId.value || !currentStep.value) return;
|
||||||
if (!isCurrentEditableStep.value || isPublished.value) {
|
if (!isCurrentEditableStep.value) {
|
||||||
message.warning('请先完成当前进行中的节点');
|
message.warning('请先进入可编辑节点后再保存');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
|
await saveBatchBase();
|
||||||
|
const targetStepId = currentStep.value.id;
|
||||||
|
const targetCategory = currentStep.value.category;
|
||||||
const detail = await updateTraceabilityBatchStep(
|
const detail = await updateTraceabilityBatchStep(
|
||||||
selectedBatchId.value,
|
selectedBatchId.value,
|
||||||
currentStep.value.id,
|
targetStepId,
|
||||||
{
|
{
|
||||||
completedAt: new Date().toISOString(),
|
completedAt:
|
||||||
|
currentStep.value.status === 'completed'
|
||||||
|
? normalizeCompletedAt(currentStep.value.completedAt)
|
||||||
|
: new Date().toISOString(),
|
||||||
operatorName: currentStep.value.operatorName,
|
operatorName: currentStep.value.operatorName,
|
||||||
status: 'completed',
|
status: isPublished.value ? (currentStep.value.status || 'completed') : 'completed',
|
||||||
values: buildPersistedStepValues(currentStep.value),
|
values: buildPersistedStepValues(currentStep.value),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
applyBatch(detail);
|
applyBatch(detail);
|
||||||
if (detail.steps.every((item) => item.status === 'completed')) {
|
if (isPublished.value) {
|
||||||
const published = await publishTraceabilityBatch(selectedBatchId.value);
|
batchEditMode.value = true;
|
||||||
applyBatch(published);
|
selectedStepId.value = targetStepId;
|
||||||
message.success('最后一个节点已保存并发布');
|
message.success('当前节点已保存,可继续修订后更新批次');
|
||||||
} else {
|
} else {
|
||||||
const nextIndex = detail.currentStep ?? 0;
|
selectedStepId.value =
|
||||||
editableStartIndex.value = nextIndex;
|
getNextPendingStep(targetCategory)?.id ?? detail.steps[0]?.id ?? '';
|
||||||
stepIndex.value = nextIndex;
|
message.success(
|
||||||
message.success('当前节点已保存,已切换至下一节点');
|
detail.steps.every((item) => item.status === 'completed')
|
||||||
|
? '当前节点已保存,全部节点已完成,可手动发布'
|
||||||
|
: '当前节点已保存,已切换到本条线的下一节点',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await loadLists();
|
await loadLists();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -468,7 +606,7 @@ onMounted(async () => {
|
|||||||
<div class="trace-operator-page">
|
<div class="trace-operator-page">
|
||||||
<div class="trace-operator">
|
<div class="trace-operator">
|
||||||
<Row :gutter="[16, 16]">
|
<Row :gutter="[16, 16]">
|
||||||
<Col :lg="7" :md="8" :sm="24" :xs="24">
|
<Col :lg="6" :md="7" :sm="24" :xs="24">
|
||||||
<Card
|
<Card
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
class="panel-card batch-panel-card"
|
class="panel-card batch-panel-card"
|
||||||
@@ -481,7 +619,7 @@ onMounted(async () => {
|
|||||||
allow-clear
|
allow-clear
|
||||||
placeholder="全部模板"
|
placeholder="全部模板"
|
||||||
class="batch-filter-select"
|
class="batch-filter-select"
|
||||||
style="width: 260px"
|
style="width: 180px"
|
||||||
/>
|
/>
|
||||||
<Button type="primary" @click="openCreateBatchModal">
|
<Button type="primary" @click="openCreateBatchModal">
|
||||||
新建批次
|
新建批次
|
||||||
@@ -525,10 +663,69 @@ onMounted(async () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col :lg="17" :md="16" :sm="24" :xs="24">
|
<Col :lg="18" :md="17" :sm="24" :xs="24">
|
||||||
<Space direction="vertical" size="middle" style="width: 100%">
|
<Space direction="vertical" size="middle" style="width: 100%">
|
||||||
|
<Card v-if="batchDetail" class="panel-card" title="基础信息设置">
|
||||||
|
<template #extra>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
v-if="isPublished"
|
||||||
|
@click="startBatchEdit()"
|
||||||
|
>
|
||||||
|
编辑批次
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="canEditBaseInfo"
|
||||||
|
:disabled="!hasBaseInfoChanges"
|
||||||
|
:loading="saving"
|
||||||
|
@click="saveBatchInfo"
|
||||||
|
>
|
||||||
|
保存基础信息
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
:disabled="isPublished ? !canUpdateBatch : !canPublishBatch"
|
||||||
|
:loading="saving"
|
||||||
|
@click="publishOrUpdateBatch"
|
||||||
|
>
|
||||||
|
{{ publishButtonText }}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</template>
|
||||||
|
<Row :gutter="[16, 16]">
|
||||||
|
<Col :md="12" :xs="24">
|
||||||
|
<label class="field-label">批次名称</label>
|
||||||
|
<Input v-model:value="formState.batchName" :disabled="!canEditBaseInfo" />
|
||||||
|
</Col>
|
||||||
|
<Col :md="12" :xs="24">
|
||||||
|
<label class="field-label">批次编码</label>
|
||||||
|
<Input v-model:value="formState.batchCode" :disabled="!canEditBaseInfo" />
|
||||||
|
</Col>
|
||||||
|
<Col :md="12" :xs="24">
|
||||||
|
<label class="field-label">产品名称</label>
|
||||||
|
<Input v-model:value="formState.productName" :disabled="!canEditBaseInfo" />
|
||||||
|
</Col>
|
||||||
|
<Col :md="12" :xs="24">
|
||||||
|
<label class="field-label">标签</label>
|
||||||
|
<Input
|
||||||
|
v-model:value="formState.tagsText"
|
||||||
|
:disabled="!canEditBaseInfo"
|
||||||
|
placeholder="用逗号分隔"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col :span="24">
|
||||||
|
<label class="field-label">批次概述</label>
|
||||||
|
<Input.TextArea
|
||||||
|
v-model:value="formState.summary"
|
||||||
|
:disabled="!canEditBaseInfo"
|
||||||
|
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
v-if="batchDetail?.publishedAt"
|
v-if="batchDetail?.publishedAt && !batchEditMode"
|
||||||
class="panel-card"
|
class="panel-card"
|
||||||
title="发布信息"
|
title="发布信息"
|
||||||
>
|
>
|
||||||
@@ -565,8 +762,8 @@ onMounted(async () => {
|
|||||||
<strong>{{ batchDetail.publishedAt || '未发布' }}</strong>
|
<strong>{{ batchDetail.publishedAt || '未发布' }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>当前状态</span>
|
<span>扫码次数</span>
|
||||||
<strong>{{ getBatchStatusText(batchDetail.status) }}</strong>
|
<strong>{{ batchDetail.scanCount }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="publish-qr">
|
<div class="publish-qr">
|
||||||
@@ -582,43 +779,60 @@ onMounted(async () => {
|
|||||||
<Card class="panel-card" title="节点填报">
|
<Card class="panel-card" title="节点填报">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<Button
|
<Button
|
||||||
v-if="!isPublished"
|
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="!currentStep || !isCurrentEditableStep"
|
:disabled="!currentStep || !isCurrentEditableStep"
|
||||||
:loading="saving"
|
:loading="saving"
|
||||||
@click="saveStep"
|
@click="saveStep"
|
||||||
>
|
>
|
||||||
{{ stepActionText }}
|
保存当前节点
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="batchDetail">
|
<template v-if="batchDetail">
|
||||||
<Steps
|
<div class="step-lanes">
|
||||||
class="step-strip"
|
<div class="node-lane">
|
||||||
:class="{ 'step-strip--published': isPublished }"
|
<div class="node-lane__title">公共资料</div>
|
||||||
:current="stepIndex"
|
<div class="node-strip">
|
||||||
size="small"
|
<button
|
||||||
@change="
|
v-for="item in publicSteps"
|
||||||
(value) => {
|
:key="item.id"
|
||||||
stepIndex = value;
|
class="node-pill"
|
||||||
if (!isPublished) {
|
:class="{ active: currentStep?.id === item.id, 'node-pill--completed': item.status === 'completed' }"
|
||||||
editableStartIndex = value;
|
type="button"
|
||||||
}
|
@click="selectStep(item.id)"
|
||||||
}
|
>
|
||||||
"
|
<span>公共资料</span>
|
||||||
>
|
<strong>{{ item.name }}</strong>
|
||||||
<Steps.Step
|
<small>{{ getStepStatusText(item) }}</small>
|
||||||
v-for="(item, index) in batchDetail.steps"
|
</button>
|
||||||
:key="item.id"
|
<div v-if="publicSteps.length === 0" class="lane-empty">还没有公共资料节点</div>
|
||||||
:title="item.name"
|
</div>
|
||||||
:description="getStepStatusText(index)"
|
</div>
|
||||||
/>
|
|
||||||
</Steps>
|
<div class="node-lane">
|
||||||
|
<div class="node-lane__title">业务流程</div>
|
||||||
|
<div class="node-strip">
|
||||||
|
<button
|
||||||
|
v-for="item in businessSteps"
|
||||||
|
:key="item.id"
|
||||||
|
class="node-pill"
|
||||||
|
:class="{ active: currentStep?.id === item.id, 'node-pill--completed': item.status === 'completed' }"
|
||||||
|
type="button"
|
||||||
|
@click="selectStep(item.id)"
|
||||||
|
>
|
||||||
|
<span>业务流程</span>
|
||||||
|
<strong>{{ item.name }}</strong>
|
||||||
|
<small>{{ getStepStatusText(item) }}</small>
|
||||||
|
</button>
|
||||||
|
<div v-if="businessSteps.length === 0" class="lane-empty">还没有业务流程节点</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="currentStep"
|
v-if="currentStep"
|
||||||
class="step-editor"
|
class="step-editor"
|
||||||
:class="{ 'step-editor--published': isPublished }"
|
:class="{ 'step-editor--published': isPublished && !batchEditMode }"
|
||||||
>
|
>
|
||||||
<div class="step-header">
|
<div class="step-header">
|
||||||
<div>
|
<div>
|
||||||
@@ -631,12 +845,14 @@ onMounted(async () => {
|
|||||||
<small class="step-hint">
|
<small class="step-hint">
|
||||||
{{
|
{{
|
||||||
isPublished
|
isPublished
|
||||||
? '当前批次已发布,溯源链已锁定为只读。'
|
? batchEditMode
|
||||||
|
? '当前批次处于更新模式,可逐个节点修订内容。'
|
||||||
|
: '当前批次已发布,点击“编辑批次”后才能修改。'
|
||||||
: isLockedStep
|
: isLockedStep
|
||||||
? '当前节点来自节点库,字段和值固定,不可修改。'
|
? '当前节点来自节点库,字段和值固定,不可修改。'
|
||||||
: isCurrentEditableStep
|
: isCurrentEditableStep
|
||||||
? '当前节点可填写并继续流转。'
|
? '当前节点可填写,保存后会推进当前分类的下一节点。'
|
||||||
: '当前查看的是非进行中节点,仅供浏览。'
|
: '当前批次已发布,点击“编辑批次”后才能修改。'
|
||||||
}}
|
}}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -660,7 +876,7 @@ onMounted(async () => {
|
|||||||
<Col :md="12" :xs="24">
|
<Col :md="12" :xs="24">
|
||||||
<label class="field-label">节点状态</label>
|
<label class="field-label">节点状态</label>
|
||||||
<div class="readonly-box">
|
<div class="readonly-box">
|
||||||
{{ getStepStatusText(stepIndex) }}
|
{{ getStepStatusText(currentStep) }}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -932,10 +1148,6 @@ onMounted(async () => {
|
|||||||
<label class="field-label">产品名称</label>
|
<label class="field-label">产品名称</label>
|
||||||
<Input v-model:value="formState.productName" />
|
<Input v-model:value="formState.productName" />
|
||||||
</Col>
|
</Col>
|
||||||
<Col :md="12" :xs="24">
|
|
||||||
<label class="field-label">封面图</label>
|
|
||||||
<Input v-model:value="formState.coverImage" />
|
|
||||||
</Col>
|
|
||||||
<Col :md="12" :xs="24">
|
<Col :md="12" :xs="24">
|
||||||
<label class="field-label">标签</label>
|
<label class="field-label">标签</label>
|
||||||
<Input v-model:value="formState.tagsText" placeholder="用逗号分隔" />
|
<Input v-model:value="formState.tagsText" placeholder="用逗号分隔" />
|
||||||
@@ -985,7 +1197,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.batch-list {
|
.batch-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.batch-toolbar {
|
.batch-toolbar {
|
||||||
@@ -1022,7 +1234,7 @@ onMounted(async () => {
|
|||||||
border: 1px solid #edf1f7;
|
border: 1px solid #edf1f7;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 16px;
|
padding: 14px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1069,24 +1281,67 @@ onMounted(async () => {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-strip {
|
.step-lanes {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
overflow: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-strip :deep(.ant-steps) {
|
.node-lane__title {
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-strip {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
row-gap: 12px;
|
gap: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-strip :deep(.ant-steps-item) {
|
.node-pill {
|
||||||
flex: 1 1 220px;
|
min-width: 180px;
|
||||||
|
border: 1px solid #edf1f7;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 14px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-pill.active {
|
||||||
|
border-color: #adc4ff;
|
||||||
|
background: #f5f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-pill--completed {
|
||||||
|
border-color: #d6e4d3;
|
||||||
|
background: #f6fbf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-pill span,
|
||||||
|
.node-pill small {
|
||||||
|
color: #1d4ed8;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-pill strong,
|
||||||
|
.node-pill small {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-pill small {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #8b96a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lane-empty {
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
}
|
border: 1px dashed #dbe3f0;
|
||||||
|
border-radius: 16px;
|
||||||
.step-strip :deep(.ant-steps-item-container) {
|
padding: 18px;
|
||||||
padding-right: 12px;
|
color: #8b96a8;
|
||||||
|
background: #fbfcff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-editor {
|
.step-editor {
|
||||||
@@ -1095,7 +1350,6 @@ onMounted(async () => {
|
|||||||
background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
|
background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-strip--published,
|
|
||||||
.step-editor--published {
|
.step-editor--published {
|
||||||
opacity: 0.58;
|
opacity: 0.58;
|
||||||
}
|
}
|
||||||
@@ -1249,10 +1503,6 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.step-strip :deep(.ant-steps-item) {
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-entry__head {
|
.field-entry__head {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -1265,6 +1515,10 @@ onMounted(async () => {
|
|||||||
.batch-toolbar :deep(.ant-select) {
|
.batch-toolbar :deep(.ant-select) {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-pill {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-trigger {
|
.upload-trigger {
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export function buildCoordinateMapUrl(value: any) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
if (normalized.lng !== null && normalized.lng !== undefined && normalized.lat !== null && normalized.lat !== undefined) {
|
if (normalized.lng !== null && normalized.lng !== undefined && normalized.lat !== null && normalized.lat !== undefined) {
|
||||||
return `https://www.openstreetmap.org/?mlat=${normalized.lat}&mlon=${normalized.lng}#map=15/${normalized.lat}/${normalized.lng}`;
|
return `https://uri.amap.com/marker?position=${normalized.lng},${normalized.lat}&name=坐标位置&src=traceability`;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -257,7 +257,7 @@ export function buildCoordinateEmbedUrl(value: any) {
|
|||||||
if (!normalized || normalized.lng === null || normalized.lng === undefined || normalized.lat === null || normalized.lat === undefined) {
|
if (!normalized || normalized.lng === null || normalized.lng === undefined || normalized.lat === null || normalized.lat === undefined) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return `https://www.openstreetmap.org/export/embed.html?bbox=${normalized.lng - 0.01}%2C${normalized.lat - 0.01}%2C${normalized.lng + 0.01}%2C${normalized.lat + 0.01}&layer=mapnik&marker=${normalized.lat}%2C${normalized.lng}`;
|
return `https://uri.amap.com/marker?position=${normalized.lng},${normalized.lat}&name=坐标位置&src=traceability&callnative=0`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEmptyField(): TraceabilityApi.FieldDefinition {
|
export function createEmptyField(): TraceabilityApi.FieldDefinition {
|
||||||
|
|||||||
Reference in New Issue
Block a user