完善各种需求
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import type { TraceabilityApi } from '#/api';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import {
|
||||
@@ -16,8 +17,8 @@ import {
|
||||
message,
|
||||
Modal,
|
||||
Row,
|
||||
Space,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Tabs,
|
||||
Tag,
|
||||
@@ -34,21 +35,21 @@ import {
|
||||
getTraceabilityOverview,
|
||||
getTraceabilityTemplate,
|
||||
getTraceabilityTemplates,
|
||||
uploadTraceabilityImage,
|
||||
updateTraceabilityNodeLibrary,
|
||||
updateTraceabilityTemplate,
|
||||
uploadTraceabilityImage,
|
||||
} from '#/api';
|
||||
import type { TraceabilityApi } from '#/api';
|
||||
|
||||
import CoordinateFieldEditor from './components/CoordinateFieldEditor.vue';
|
||||
import {
|
||||
buildOssStoredValue,
|
||||
createLocalUniqueId,
|
||||
cloneTemplateForSave,
|
||||
createEmptyField,
|
||||
createEmptyNode,
|
||||
createLocalUniqueId,
|
||||
fieldTypeOptions,
|
||||
getImagePreviewSrc,
|
||||
getFieldTypeLabel,
|
||||
getImagePreviewSrc,
|
||||
serializeImageValue,
|
||||
stripOssTempUrl,
|
||||
} from './shared';
|
||||
@@ -69,6 +70,8 @@ const selectedLibraryNodeId = ref('');
|
||||
const selectedTemplateFieldKey = ref('');
|
||||
const selectedLibraryFieldKey = ref('');
|
||||
const draggingTemplateNodeId = ref('');
|
||||
const draggingTemplateFieldKey = ref('');
|
||||
const draggingLibraryFieldKey = ref('');
|
||||
const overview = reactive<TraceabilityApi.Overview>({
|
||||
batchCount: 0,
|
||||
feedbackCount: 0,
|
||||
@@ -91,6 +94,10 @@ const editor = reactive<
|
||||
themeColor: '#1f4fd6',
|
||||
});
|
||||
const nodeIndex = ref(0);
|
||||
const templateSnapshot = ref('');
|
||||
const leaveDialogVisible = ref(false);
|
||||
const pendingTemplateSelectId = ref('');
|
||||
const bypassTemplateLeaveGuard = ref(false);
|
||||
|
||||
const createTemplateVisible = ref(false);
|
||||
const coverHistoryVisible = ref(false);
|
||||
@@ -104,11 +111,13 @@ const createTemplateForm = reactive<{
|
||||
industryName: string;
|
||||
name: string;
|
||||
productName: string;
|
||||
remark: string;
|
||||
themeColor: string;
|
||||
}>({
|
||||
coverImage: '',
|
||||
coverImagePreviewUrl: '',
|
||||
description: '',
|
||||
remark: '',
|
||||
industryName: '',
|
||||
name: '新建溯源模板',
|
||||
productName: '',
|
||||
@@ -133,6 +142,7 @@ const selectedTemplateSummary = computed(() =>
|
||||
const isTemplatePublished = computed(
|
||||
() => selectedTemplateSummary.value?.status === 'active',
|
||||
);
|
||||
const isTemplateDraft = computed(() => !isTemplatePublished.value);
|
||||
const currentLibraryNode = computed(() =>
|
||||
nodeLibrary.value.find((item) => item.libraryId === selectedLibraryNodeId.value),
|
||||
);
|
||||
@@ -162,8 +172,9 @@ function toEditableLibraryNode(item: TraceabilityApi.NodeLibraryItem): EditableT
|
||||
category: item.category,
|
||||
consumerVisible: item.consumerVisible,
|
||||
description: item.description,
|
||||
fields: item.fields.map((field) => ({
|
||||
fields: item.fields.map((field, fieldIndex) => ({
|
||||
...field,
|
||||
sort: field.sort ?? fieldIndex,
|
||||
defaultPreviewUrl: field.defaultPreviewUrl,
|
||||
options: [...(field.options ?? [])],
|
||||
fieldStyle: {
|
||||
@@ -183,7 +194,8 @@ function buildLibraryPayload(node: EditableTemplateNode) {
|
||||
category: node.category,
|
||||
consumerVisible: node.consumerVisible,
|
||||
description: node.description,
|
||||
fields: (node.fields ?? []).map((field) => ({
|
||||
fields: (node.fields ?? []).map((field, fieldIndex) => ({
|
||||
sort: field.sort ?? fieldIndex,
|
||||
defaultValue:
|
||||
field.type === 'image'
|
||||
? stripOssTempUrl(field.defaultValue ?? '')
|
||||
@@ -236,13 +248,53 @@ function applyEditor(detail: TraceabilityApi.TemplateDetail) {
|
||||
editor.nodes = (editor.nodes ?? []).map((node) => ({
|
||||
...node,
|
||||
id: node.id || createLocalUniqueId('node'),
|
||||
fields: (node.fields ?? []).map((field, fieldIndex) => ({
|
||||
...field,
|
||||
sort: field.sort ?? fieldIndex,
|
||||
})),
|
||||
locked: !!node.locked,
|
||||
}));
|
||||
nodeIndex.value = 0;
|
||||
syncSelectedTemplateField();
|
||||
templateSnapshot.value = JSON.stringify(cloneTemplateForSave(editor));
|
||||
}
|
||||
|
||||
const hasTemplateChanges = computed(() => {
|
||||
if (!selectedTemplateId.value) return false;
|
||||
return templateSnapshot.value !== JSON.stringify(cloneTemplateForSave(editor));
|
||||
});
|
||||
|
||||
function openTemplateLeaveDialog(nextId: string) {
|
||||
pendingTemplateSelectId.value = nextId;
|
||||
leaveDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function confirmTemplateLeave(action: 'cancel' | 'discard' | 'save') {
|
||||
leaveDialogVisible.value = false;
|
||||
const nextId = pendingTemplateSelectId.value;
|
||||
pendingTemplateSelectId.value = '';
|
||||
if (action === 'cancel' || !nextId) return;
|
||||
if (action === 'save') {
|
||||
await saveTemplate();
|
||||
}
|
||||
bypassTemplateLeaveGuard.value = true;
|
||||
try {
|
||||
await selectTemplate(nextId);
|
||||
} finally {
|
||||
bypassTemplateLeaveGuard.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function selectTemplate(id: string) {
|
||||
if (
|
||||
!bypassTemplateLeaveGuard.value &&
|
||||
selectedTemplateId.value &&
|
||||
id !== selectedTemplateId.value &&
|
||||
hasTemplateChanges.value
|
||||
) {
|
||||
openTemplateLeaveDialog(id);
|
||||
return;
|
||||
}
|
||||
selectedTemplateId.value = id;
|
||||
const detail = await getTraceabilityTemplate(id);
|
||||
applyEditor(detail);
|
||||
@@ -252,6 +304,7 @@ function openCreateTemplateModal() {
|
||||
createTemplateForm.coverImage = '';
|
||||
createTemplateForm.coverImagePreviewUrl = '';
|
||||
createTemplateForm.description = '';
|
||||
createTemplateForm.remark = '';
|
||||
createTemplateForm.industryName = '';
|
||||
createTemplateForm.name = '新建溯源模板';
|
||||
createTemplateForm.productName = '';
|
||||
@@ -337,7 +390,7 @@ async function handleTemplateCoverUpload(event: Event) {
|
||||
}
|
||||
|
||||
function triggerTemplateCoverSelect() {
|
||||
const input = document.getElementById('template-cover-upload');
|
||||
const input = document.querySelector('#template-cover-upload');
|
||||
input?.click();
|
||||
}
|
||||
|
||||
@@ -347,6 +400,7 @@ async function createTemplate() {
|
||||
const created = await createTraceabilityTemplate({
|
||||
coverImage: serializeImageValue(createTemplateForm.coverImage),
|
||||
description: createTemplateForm.description,
|
||||
remark: createTemplateForm.remark,
|
||||
industryName: createTemplateForm.industryName,
|
||||
name: createTemplateForm.name.trim() || '新建溯源模板',
|
||||
nodes: [createEmptyNode('public'), createEmptyNode('business')],
|
||||
@@ -436,8 +490,9 @@ function addNodeFromLibrary(category: 'business' | 'public', libraryId?: string)
|
||||
category: source.category,
|
||||
consumerVisible: source.consumerVisible,
|
||||
description: source.description,
|
||||
fields: source.fields.map((field) => ({
|
||||
fields: source.fields.map((field, fieldIndex) => ({
|
||||
...field,
|
||||
sort: field.sort ?? fieldIndex,
|
||||
options: [...(field.options ?? [])],
|
||||
})),
|
||||
libraryId: source.libraryId,
|
||||
@@ -489,14 +544,14 @@ function moveTemplateNode(
|
||||
const [movedNode] = movedNodes.splice(fromIndex, 1);
|
||||
movedNodes.splice(toIndex, 0, movedNode);
|
||||
|
||||
const reordered = nodes.slice();
|
||||
const reordered = [...nodes];
|
||||
let cursor = 0;
|
||||
editor.nodes = reordered.map((node) =>
|
||||
node.category === category ? movedNodes[cursor++] : node,
|
||||
);
|
||||
|
||||
const nextIndex = (editor.nodes ?? []).findIndex((node) => node.id === draggedId);
|
||||
if (nextIndex >= 0) {
|
||||
if (nextIndex !== -1) {
|
||||
nodeIndex.value = nextIndex;
|
||||
}
|
||||
}
|
||||
@@ -526,6 +581,7 @@ function clearTemplateNodeDragState() {
|
||||
function addField(target: EditableTemplateNode | null | undefined) {
|
||||
if (target?.locked) return;
|
||||
const field = createEmptyField();
|
||||
field.sort = target?.fields.length ?? 0;
|
||||
target?.fields.push(field);
|
||||
if (target === currentNode.value) {
|
||||
selectedTemplateFieldKey.value = field.key;
|
||||
@@ -539,6 +595,9 @@ function removeField(target: EditableTemplateNode | null | undefined, index: num
|
||||
if (target?.locked) return;
|
||||
const removed = target?.fields[index];
|
||||
target?.fields.splice(index, 1);
|
||||
target?.fields.forEach((field, fieldIndex) => {
|
||||
field.sort = fieldIndex;
|
||||
});
|
||||
if (target === currentNode.value && removed?.key === selectedTemplateFieldKey.value) {
|
||||
selectedTemplateFieldKey.value = target?.fields[0]?.key || '';
|
||||
}
|
||||
@@ -559,6 +618,54 @@ function confirmRemoveField(target: EditableTemplateNode | null | undefined, ind
|
||||
});
|
||||
}
|
||||
|
||||
function moveField(
|
||||
target: EditableTemplateNode | null | undefined,
|
||||
draggedKey: string,
|
||||
targetKey: string,
|
||||
) {
|
||||
const fields = target?.fields ?? [];
|
||||
if (!draggedKey || !targetKey || draggedKey === targetKey) return;
|
||||
const fromIndex = fields.findIndex((field) => field.key === draggedKey);
|
||||
const toIndex = fields.findIndex((field) => field.key === targetKey);
|
||||
if (fromIndex === -1 || toIndex === -1) return;
|
||||
const movedFields = [...fields];
|
||||
const [movedField] = movedFields.splice(fromIndex, 1);
|
||||
if (!movedField) return;
|
||||
movedFields.splice(toIndex, 0, movedField);
|
||||
target!.fields = movedFields.map((field, fieldIndex) => ({
|
||||
...field,
|
||||
sort: fieldIndex,
|
||||
}));
|
||||
}
|
||||
|
||||
function handleTemplateFieldDragStart(fieldKey?: string) {
|
||||
if (isTemplatePublished.value || currentNodeLocked.value) return;
|
||||
draggingTemplateFieldKey.value = fieldKey || '';
|
||||
}
|
||||
|
||||
function handleTemplateFieldDrop(targetFieldKey?: string) {
|
||||
if (isTemplatePublished.value || currentNodeLocked.value) return;
|
||||
moveField(currentNode.value, draggingTemplateFieldKey.value, targetFieldKey || '');
|
||||
draggingTemplateFieldKey.value = '';
|
||||
}
|
||||
|
||||
function clearTemplateFieldDragState() {
|
||||
draggingTemplateFieldKey.value = '';
|
||||
}
|
||||
|
||||
function handleLibraryFieldDragStart(fieldKey?: string) {
|
||||
draggingLibraryFieldKey.value = fieldKey || '';
|
||||
}
|
||||
|
||||
function handleLibraryFieldDrop(targetFieldKey?: string) {
|
||||
moveField(currentLibraryNode.value, draggingLibraryFieldKey.value, targetFieldKey || '');
|
||||
draggingLibraryFieldKey.value = '';
|
||||
}
|
||||
|
||||
function clearLibraryFieldDragState() {
|
||||
draggingLibraryFieldKey.value = '';
|
||||
}
|
||||
|
||||
function getFieldUploadKey(scope: string, field: TraceabilityApi.FieldDefinition) {
|
||||
return `${scope}:${field.key}`;
|
||||
}
|
||||
@@ -644,12 +751,12 @@ function getPresetValue(field: TraceabilityApi.FieldDefinition) {
|
||||
if (field.type === 'multi_select') {
|
||||
return Array.isArray(field.defaultValue)
|
||||
? field.defaultValue
|
||||
: field.defaultValue
|
||||
: (field.defaultValue
|
||||
? String(field.defaultValue)
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
: []);
|
||||
}
|
||||
return field.defaultValue;
|
||||
}
|
||||
@@ -726,11 +833,11 @@ async function removeLibraryNode() {
|
||||
title: '删除节点库节点',
|
||||
content: `确认删除节点库节点“${currentName}”吗?`,
|
||||
async onOk() {
|
||||
if (!currentLibraryNode.value?.persisted) {
|
||||
nodeLibrary.value = nodeLibrary.value.filter((item) => item.libraryId !== currentId);
|
||||
} else {
|
||||
if (currentLibraryNode.value?.persisted) {
|
||||
await deleteTraceabilityNodeLibrary(currentId);
|
||||
await loadNodeLibrary();
|
||||
} else {
|
||||
nodeLibrary.value = nodeLibrary.value.filter((item) => item.libraryId !== currentId);
|
||||
}
|
||||
selectedLibraryNodeId.value = nodeLibrary.value[0]?.libraryId || '';
|
||||
message.success('节点库节点已删除');
|
||||
@@ -758,11 +865,7 @@ async function saveLibraryNode() {
|
||||
message.success('节点库节点已保存');
|
||||
}
|
||||
|
||||
async function saveTemplate() {
|
||||
if (isTemplatePublished.value) {
|
||||
message.warning('模板已发布,不可修改');
|
||||
return;
|
||||
}
|
||||
async function persistTemplate(status?: 'active' | 'draft') {
|
||||
if (!editor.name?.trim()) {
|
||||
message.warning('请先填写模板名称');
|
||||
return;
|
||||
@@ -773,13 +876,16 @@ async function saveTemplate() {
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = cloneTemplateForSave(editor);
|
||||
const payload = {
|
||||
...cloneTemplateForSave(editor),
|
||||
status: status ?? editor.status ?? 'draft',
|
||||
};
|
||||
if (selectedTemplateId.value) {
|
||||
await updateTraceabilityTemplate(selectedTemplateId.value, payload);
|
||||
message.success('模板已更新');
|
||||
message.success(status === 'active' ? '模板已发布' : '模板已更新');
|
||||
} else {
|
||||
await createTraceabilityTemplate(payload);
|
||||
message.success('模板已创建');
|
||||
message.success(status === 'active' ? '模板已创建并发布' : '模板已创建');
|
||||
}
|
||||
await loadAll();
|
||||
} finally {
|
||||
@@ -787,22 +893,20 @@ async function saveTemplate() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTemplate() {
|
||||
if (isTemplatePublished.value) {
|
||||
message.warning('模板已发布,不可修改');
|
||||
return;
|
||||
}
|
||||
await persistTemplate('draft');
|
||||
}
|
||||
|
||||
async function publishTemplate() {
|
||||
if (!selectedTemplateId.value) {
|
||||
message.warning('请先创建并选择模板');
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await updateTraceabilityTemplate(selectedTemplateId.value, {
|
||||
...cloneTemplateForSave(editor),
|
||||
status: 'active',
|
||||
});
|
||||
message.success('模板已发布');
|
||||
await loadAll();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
await persistTemplate('active');
|
||||
}
|
||||
|
||||
function removeTemplate(id: string) {
|
||||
@@ -837,9 +941,9 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="trace-admin-page">
|
||||
<div class="trace-admin">
|
||||
<Tabs v-model:active-key="activeTab">
|
||||
<Tabs v-model:active-key="activeTab">
|
||||
<Tabs.TabPane key="templates" tab="模板中心">
|
||||
<Row :gutter="[16, 16]" align="stretch">
|
||||
<Col :lg="8" :xs="24">
|
||||
@@ -868,6 +972,9 @@ onMounted(async () => {
|
||||
<span>{{ item.nodeCount }} 节点</span>
|
||||
<span>{{ item.batchCount }} 批次</span>
|
||||
</div>
|
||||
<p class="template-card__remark">
|
||||
{{ item.remark || '暂无备注' }}
|
||||
</p>
|
||||
<p>{{ item.description || '暂无模板说明' }}</p>
|
||||
<div class="template-card__footer">
|
||||
<span>{{ item.productName || '未设置产品名称' }}</span>
|
||||
@@ -883,30 +990,59 @@ onMounted(async () => {
|
||||
|
||||
<Col :lg="16" :xs="24">
|
||||
<div class="template-main">
|
||||
<div class="template-summary" v-if="editor.name">
|
||||
<div class="summary-inline">
|
||||
<div class="summary-inline__item">
|
||||
<span>模板名称</span>
|
||||
<strong>{{ editor.name }}</strong>
|
||||
</div>
|
||||
<div class="summary-inline__item">
|
||||
<span>产品名称</span>
|
||||
<strong>{{ editor.productName || '未设置' }}</strong>
|
||||
</div>
|
||||
<div class="summary-inline__item">
|
||||
<span>行业名称</span>
|
||||
<strong>{{ editor.industryName || '未设置' }}</strong>
|
||||
</div>
|
||||
<div class="summary-inline__item">
|
||||
<span>模板状态</span>
|
||||
<strong>{{ selectedTemplateSummary?.status === 'active' ? '已发布' : '草稿' }}</strong>
|
||||
</div>
|
||||
<div class="summary-inline__item summary-inline__item--wide">
|
||||
<span>模板说明</span>
|
||||
<strong>{{ editor.description || '暂无模板说明' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Card
|
||||
v-if="selectedTemplateId"
|
||||
class="panel-card template-summary"
|
||||
:bordered="false"
|
||||
title="基础信息"
|
||||
>
|
||||
<template #extra>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
:disabled="!isTemplateDraft"
|
||||
:loading="saving"
|
||||
@click="saveTemplate"
|
||||
>
|
||||
保存草稿
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:disabled="!isTemplateDraft"
|
||||
:loading="saving"
|
||||
@click="publishTemplate"
|
||||
>
|
||||
发布模板
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
<Row :gutter="[12, 12]">
|
||||
<Col :md="12" :xs="24">
|
||||
<label class="field-label">模板名称</label>
|
||||
<Input v-model:value="editor.name" :disabled="!isTemplateDraft" />
|
||||
</Col>
|
||||
<Col :md="12" :xs="24">
|
||||
<label class="field-label">产品名称</label>
|
||||
<Input v-model:value="editor.productName" :disabled="!isTemplateDraft" />
|
||||
</Col>
|
||||
<Col :md="12" :xs="24">
|
||||
<label class="field-label">行业名称</label>
|
||||
<Input v-model:value="editor.industryName" :disabled="!isTemplateDraft" />
|
||||
</Col>
|
||||
<Col :md="12" :xs="24">
|
||||
<label class="field-label">模板备注</label>
|
||||
<Input v-model:value="editor.remark" :disabled="!isTemplateDraft" placeholder="仅内部可见" />
|
||||
</Col>
|
||||
<Col :span="24">
|
||||
<label class="field-label">模板说明</label>
|
||||
<Input.TextArea
|
||||
v-model:value="editor.description"
|
||||
:disabled="!isTemplateDraft"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
<Empty v-else description="点击左侧模板或新建模板开始配置" />
|
||||
|
||||
<Card class="panel-card editor-panel template-main__editor" title="节点编排">
|
||||
@@ -929,7 +1065,7 @@ onMounted(async () => {
|
||||
>
|
||||
{{ item.name }}
|
||||
</Menu.Item>
|
||||
<Menu.Item v-if="!businessLibraryNodes.length" key="business-empty" disabled>
|
||||
<Menu.Item v-if="businessLibraryNodes.length === 0" key="business-empty" disabled>
|
||||
暂无业务节点
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
@@ -953,7 +1089,7 @@ onMounted(async () => {
|
||||
>
|
||||
{{ item.name }}
|
||||
</Menu.Item>
|
||||
<Menu.Item v-if="!publicLibraryNodes.length" key="public-empty" disabled>
|
||||
<Menu.Item v-if="publicLibraryNodes.length === 0" key="public-empty" disabled>
|
||||
暂无公共资料节点
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
@@ -961,21 +1097,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
|
||||
<Space wrap class="editor-toolbar__right">
|
||||
<Button :loading="saving" :disabled="isTemplatePublished" @click="saveTemplate">
|
||||
保存草稿
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!isTemplatePublished"
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="publishTemplate"
|
||||
>
|
||||
发布模板
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="node-lane">
|
||||
@@ -1011,7 +1133,7 @@ onMounted(async () => {
|
||||
删除节点
|
||||
</Button>
|
||||
</button>
|
||||
<div v-if="!publicNodes.length" class="lane-empty">还没有公共资料节点</div>
|
||||
<div v-if="publicNodes.length === 0" class="lane-empty">还没有公共资料节点</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1048,7 +1170,7 @@ onMounted(async () => {
|
||||
删除节点
|
||||
</Button>
|
||||
</button>
|
||||
<div v-if="!businessNodes.length" class="lane-empty">还没有业务流程节点</div>
|
||||
<div v-if="businessNodes.length === 0" class="lane-empty">还没有业务流程节点</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1107,7 +1229,12 @@ onMounted(async () => {
|
||||
:key="`${field.key}-${fieldIndex}`"
|
||||
class="field-pill"
|
||||
:class="{ active: field.key === currentTemplateField?.key }"
|
||||
:draggable="!isTemplatePublished && !currentNodeLocked"
|
||||
type="button"
|
||||
@dragstart="handleTemplateFieldDragStart(field.key)"
|
||||
@dragover.prevent
|
||||
@drop.prevent="handleTemplateFieldDrop(field.key)"
|
||||
@dragend="clearTemplateFieldDragState"
|
||||
@click="selectedTemplateFieldKey = field.key"
|
||||
>
|
||||
<span>{{ getFieldTypeLabel(field.type) }}</span>
|
||||
@@ -1124,7 +1251,7 @@ onMounted(async () => {
|
||||
删除字段
|
||||
</Button>
|
||||
</button>
|
||||
<div v-if="!currentNode.fields.length" class="lane-empty">还没有字段,请先新增字段</div>
|
||||
<div v-if="currentNode.fields.length === 0" class="lane-empty">还没有字段,请先新增字段</div>
|
||||
</div>
|
||||
<div v-if="currentTemplateField" class="field-card">
|
||||
<div class="field-card__header">
|
||||
@@ -1305,6 +1432,12 @@ onMounted(async () => {
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
@update:value="(value) => updatePresetValue(currentTemplateField, value)"
|
||||
/>
|
||||
<CoordinateFieldEditor
|
||||
v-else-if="currentTemplateField.type === 'coordinate'"
|
||||
:disabled="isFieldPresetLocked(currentTemplateField)"
|
||||
:model-value="getPresetValue(currentTemplateField)"
|
||||
@update:model-value="(value) => updatePresetValue(currentTemplateField, value)"
|
||||
/>
|
||||
<Input.TextArea
|
||||
v-else-if="currentTemplateField.type === 'json'"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
@@ -1459,7 +1592,12 @@ onMounted(async () => {
|
||||
:key="`${field.key}-${fieldIndex}`"
|
||||
class="field-pill"
|
||||
:class="{ active: field.key === currentLibraryField?.key }"
|
||||
draggable="true"
|
||||
type="button"
|
||||
@dragstart="handleLibraryFieldDragStart(field.key)"
|
||||
@dragover.prevent
|
||||
@drop.prevent="handleLibraryFieldDrop(field.key)"
|
||||
@dragend="clearLibraryFieldDragState"
|
||||
@click="selectedLibraryFieldKey = field.key"
|
||||
>
|
||||
<span>{{ getFieldTypeLabel(field.type) }}</span>
|
||||
@@ -1469,7 +1607,7 @@ onMounted(async () => {
|
||||
删除字段
|
||||
</Button>
|
||||
</button>
|
||||
<div v-if="!currentLibraryNode.fields.length" class="lane-empty">还没有字段,请先新增字段</div>
|
||||
<div v-if="currentLibraryNode.fields.length === 0" class="lane-empty">还没有字段,请先新增字段</div>
|
||||
</div>
|
||||
<div v-if="currentLibraryField" class="field-card">
|
||||
<div class="field-card__header">
|
||||
@@ -1614,6 +1752,11 @@ onMounted(async () => {
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
@update:value="(value) => updatePresetValue(currentLibraryField, value)"
|
||||
/>
|
||||
<CoordinateFieldEditor
|
||||
v-else-if="currentLibraryField.type === 'coordinate'"
|
||||
:model-value="getPresetValue(currentLibraryField)"
|
||||
@update:model-value="(value) => updatePresetValue(currentLibraryField, value)"
|
||||
/>
|
||||
<Input.TextArea
|
||||
v-else-if="currentLibraryField.type === 'json'"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
@@ -1710,7 +1853,7 @@ onMounted(async () => {
|
||||
v-model="createTemplateForm.themeColor"
|
||||
class="color-input"
|
||||
type="color"
|
||||
>
|
||||
/>
|
||||
<span>{{ createTemplateForm.themeColor }}</span>
|
||||
</div>
|
||||
</Col>
|
||||
@@ -1724,7 +1867,7 @@ onMounted(async () => {
|
||||
<img
|
||||
:src="getImagePreviewSrc(createTemplateForm.coverImage, createTemplateForm.coverImagePreviewUrl)"
|
||||
alt="模板封面图"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
<div class="cover-selector__actions">
|
||||
<input
|
||||
@@ -1733,7 +1876,7 @@ onMounted(async () => {
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="handleTemplateCoverUpload"
|
||||
>
|
||||
/>
|
||||
<Button
|
||||
:loading="uploadingFieldKey === 'template-cover'"
|
||||
@click="triggerTemplateCoverSelect"
|
||||
@@ -1761,6 +1904,14 @@ onMounted(async () => {
|
||||
placeholder="说明此模板适用对象、展示重点和字段规范"
|
||||
/>
|
||||
</Col>
|
||||
<Col :span="24">
|
||||
<label class="field-label">备注</label>
|
||||
<Input.TextArea
|
||||
v-model:value="createTemplateForm.remark"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
placeholder="仅内部可见,用于记录模板背景、使用限制或补充说明"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal>
|
||||
|
||||
@@ -1769,13 +1920,13 @@ onMounted(async () => {
|
||||
title="选择历史封面图"
|
||||
:footer="null"
|
||||
>
|
||||
<div v-if="coverHistoryItems.length" class="cover-history-grid">
|
||||
<div v-if="coverHistoryItems.length > 0" class="cover-history-grid">
|
||||
<div
|
||||
v-for="item in coverHistoryItems"
|
||||
:key="item.id"
|
||||
class="cover-history-card"
|
||||
>
|
||||
<img :src="item.previewUrl" :alt="item.fileName">
|
||||
<img :src="item.previewUrl" :alt="item.fileName" />
|
||||
<div class="cover-history-card__meta">
|
||||
<strong>{{ item.fileName || item.objectName }}</strong>
|
||||
<span>{{ item.createdAt }}</span>
|
||||
@@ -1798,8 +1949,23 @@ onMounted(async () => {
|
||||
</div>
|
||||
<Empty v-else :description="coverHistoryLoading ? '正在加载历史封面图…' : '暂无历史封面图'" />
|
||||
</Modal>
|
||||
<Modal
|
||||
v-model:open="leaveDialogVisible"
|
||||
title="有未保存的改动"
|
||||
:mask-closable="false"
|
||||
:closable="true"
|
||||
:footer="null"
|
||||
@cancel="confirmTemplateLeave('cancel')"
|
||||
>
|
||||
<p>当前模板有未保存的修改,切换前要怎么处理?</p>
|
||||
<div class="leave-dialog__actions">
|
||||
<Button block @click="confirmTemplateLeave('save')">保存</Button>
|
||||
<Button block @click="confirmTemplateLeave('discard')">不保存</Button>
|
||||
<Button block @click="confirmTemplateLeave('cancel')">取消</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -2037,6 +2203,13 @@ onMounted(async () => {
|
||||
margin: 8px 0 6px;
|
||||
}
|
||||
|
||||
.template-card__remark {
|
||||
margin: 6px 0 0;
|
||||
color: #556070;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.template-card__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -2169,6 +2342,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.field-pill {
|
||||
cursor: grab;
|
||||
min-width: 200px;
|
||||
max-width: 220px;
|
||||
border: 1px solid #e5ebf5;
|
||||
@@ -2366,6 +2540,16 @@ onMounted(async () => {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.leave-dialog__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.leave-dialog__actions :deep(.ant-btn) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.field-toggle-row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user