<script lang="ts" setup>
|
import type { ActionItem, BasicColumn } from '@jnpf/ui/vxeTable';
|
|
import type { FieldDataType, ProviderProperty } from '#/api/x/eln/fields';
|
import type { SourceDataItem, SourceDataItemDraft, SourcePath, SourcePreviewSample } from '#/api/x/eln/sources';
|
|
import { computed, onActivated, onMounted, reactive, ref } from 'vue';
|
|
import { BasicDrawer, useDrawer } from '@jnpf/ui/drawer';
|
import { BasicForm, useForm } from '@jnpf/ui/form';
|
import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
|
|
import { EyeOutlined, SettingOutlined } from '@ant-design/icons-vue';
|
|
import { suggestFieldCode } from '#/api/x/eln/fields';
|
import {
|
createSourceDataItem,
|
getSourceDataItems,
|
getSourcePaths,
|
getSourcePreviewSamples,
|
previewSourceDataItem,
|
publishSourceDataItem,
|
saveSourceDataItem,
|
setSourcePropertyEnabled,
|
} from '#/api/x/eln/sources';
|
import { onlineUtils } from '#/utils/jnpf';
|
|
type StatusFilter = 'ALL' | 'DRAFT' | 'PUBLISHED';
|
interface DataItemRow extends SourceDataItem {
|
published?: SourceDataItem;
|
versions: SourceDataItem[];
|
}
|
|
const props = defineProps<{ fieldDataType?: FieldDataType }>();
|
const emit = defineEmits<{ select: [property: ProviderProperty] }>();
|
const saving = ref(false);
|
const pathsLoading = ref(false);
|
const samplesLoading = ref(false);
|
const samplesLoaded = ref(false);
|
const errorMessage = ref('');
|
const sampleLoadMessage = ref('');
|
const operationMessage = ref('');
|
const searchText = ref('');
|
const statusFilter = ref<StatusFilter>('ALL');
|
const items = ref<SourceDataItem[]>([]);
|
const paths = ref<SourcePath[]>([]);
|
const samples = ref<SourcePreviewSample[]>([]);
|
const editorOpen = ref(false);
|
const activeItem = ref<DataItemRow>();
|
const activeHistory = ref<SourceDataItem[]>([]);
|
const previewValue = ref<unknown>();
|
const previewReady = ref(false);
|
let suggestionSequence = 0;
|
let samplesRequest: Promise<void> | undefined;
|
let pathsRequest: Promise<void> | undefined;
|
|
const form = reactive({ displayName: '', id: '', outputKey: '', propertyCode: '', sampleId: '', sourcePathId: '' });
|
|
const TYPE_LABELS: Record<string, string> = { BOOLEAN: '布尔值', DATE: '日期', DATETIME: '日期时间', DECIMAL: '数值', TEXT: '文本' };
|
const statusOptions = [
|
{ label: '全部状态', value: 'ALL' },
|
{ label: '草稿', value: 'DRAFT' },
|
{ label: '已发布', value: 'PUBLISHED' },
|
];
|
|
const rows = computed<DataItemRow[]>(() => {
|
const grouped = new Map<string, SourceDataItem[]>();
|
items.value.forEach((item) => grouped.set(item.propertyCode, [...(grouped.get(item.propertyCode) || []), item]));
|
return [...grouped.values()].map((versions) => {
|
const ordered = [...versions].sort((left, right) => right.revision - left.revision);
|
const draft = ordered.find((item) => item.status === 'DRAFT');
|
const published = ordered.find((item) => item.status === 'PUBLISHED');
|
const current = draft || published || ordered[0];
|
if (!current) throw new Error('Data item group must not be empty');
|
return { ...current, published, versions: ordered };
|
});
|
});
|
const filteredRows = computed(() => {
|
const keyword = searchText.value.trim().toLowerCase();
|
return rows.value.filter(
|
(item) =>
|
(statusFilter.value === 'ALL' || item.status === statusFilter.value) &&
|
(!keyword || [item.displayName, item.propertyCode, item.sourcePath, item.outputName].some((value) => value?.toLowerCase().includes(keyword))),
|
);
|
});
|
const publishedPaths = computed(() => paths.value.filter((item) => item.status === 'PUBLISHED'));
|
const sourceOptions = computed(() =>
|
publishedPaths.value.flatMap((path) =>
|
path.outputs.map((output) => ({
|
label: output.path,
|
value: sourceValue(path.id, output.key),
|
})),
|
),
|
);
|
const selectedPath = computed(() => paths.value.find((item) => item.id === form.sourcePathId));
|
const selectedOutput = computed(() => selectedPath.value?.outputs.find((item) => item.key === form.outputKey));
|
const sampleOptions = computed(() => samples.value.map((item) => ({ label: sampleLabel(item), value: item.id })));
|
|
const columns: BasicColumn[] = [
|
{ dataIndex: 'displayName', minWidth: 170, title: '业务名称' },
|
{ dataIndex: 'sourcePath', minWidth: 300, title: '数据来源' },
|
{ dataIndex: 'outputName', minWidth: 140, title: '返回内容' },
|
{ dataIndex: 'scope', minWidth: 150, title: '适用于' },
|
{ dataIndex: 'dataType', slots: { default: 'dataType' }, title: '类型', width: 100 },
|
{ dataIndex: 'status', slots: { default: 'status' }, title: '状态', width: 100 },
|
];
|
const [registerSearchForm, { getFieldsValue, resetFields }] = useForm({
|
baseColProps: { lg: 8, md: 12, sm: 12, xl: 6, xs: 24 },
|
compact: true,
|
labelWidth: 76,
|
schemas: [
|
{
|
component: 'Input',
|
componentProps: { allowClear: true, placeholder: '业务名称或数据来源', submitOnPressEnter: true },
|
field: 'keyword',
|
label: '关键字',
|
},
|
{
|
component: 'Select',
|
componentProps: {
|
allowClear: false,
|
options: statusOptions.map((item) => ({ fullName: item.label, id: item.value })),
|
placeholder: '全部',
|
},
|
defaultValue: 'ALL',
|
field: 'status',
|
label: '状态',
|
},
|
],
|
showActionButtonGroup: true,
|
showAdvancedButton: false,
|
});
|
|
const [registerTable, { setLoading: setTableLoading }] = useVxeTable({
|
actionColumn: { dataIndex: 'action', fixed: 'right', title: '操作', width: props.fieldDataType ? 210 : 170 },
|
columns,
|
dataSource: filteredRows,
|
immediate: false,
|
pagination: false,
|
rowKey: 'propertyCode',
|
showIndexColumn: false,
|
showTableSetting: false,
|
});
|
const [registerEditor, { closeDrawer: closeEditorDrawer, openDrawer: openEditorDrawer }] = useDrawer();
|
const [registerHistory, { openDrawer: openHistoryDrawer }] = useDrawer();
|
|
onMounted(load);
|
onActivated(() => {
|
void refreshPaths().catch((error) => fail(sourceErrorMessage(error, '来源路径刷新失败,请重试')));
|
});
|
|
async function load() {
|
setTableLoading(true);
|
errorMessage.value = '';
|
try {
|
const [itemResponse] = await Promise.all([getSourceDataItems(), refreshPaths()]);
|
items.value = itemResponse.data || [];
|
} catch (error) {
|
errorMessage.value = sourceErrorMessage(error, 'LIMS 数据项加载失败,请重试');
|
} finally {
|
setTableLoading(false);
|
}
|
}
|
|
async function refreshPaths() {
|
if (pathsRequest) return pathsRequest;
|
pathsLoading.value = true;
|
pathsRequest = getSourcePaths()
|
.then((response) => {
|
paths.value = response.data || [];
|
})
|
.finally(() => {
|
pathsLoading.value = false;
|
pathsRequest = undefined;
|
});
|
return pathsRequest;
|
}
|
|
function handleSearch() {
|
const values = getFieldsValue();
|
searchText.value = typeof values.keyword === 'string' ? values.keyword.trim() : '';
|
statusFilter.value = statusOptions.some((item) => item.value === values.status) ? values.status : 'ALL';
|
}
|
|
async function handleReset() {
|
await resetFields();
|
searchText.value = '';
|
statusFilter.value = 'ALL';
|
}
|
|
async function ensurePreviewSamples() {
|
if (samplesLoaded.value) return;
|
if (samplesRequest) return samplesRequest;
|
samplesLoading.value = true;
|
sampleLoadMessage.value = '';
|
samplesRequest = getSourcePreviewSamples()
|
.then((response) => {
|
samples.value = response.data || [];
|
samplesLoaded.value = true;
|
})
|
.catch(() => {
|
samples.value = [];
|
sampleLoadMessage.value = '预览检验项暂不可用,请稍后重试';
|
})
|
.finally(() => {
|
samplesLoading.value = false;
|
samplesRequest = undefined;
|
});
|
return samplesRequest;
|
}
|
|
function openNewItem() {
|
activeItem.value = undefined;
|
previewReady.value = false;
|
previewValue.value = undefined;
|
operationMessage.value = '';
|
Object.assign(form, { displayName: '', id: '', outputKey: '', propertyCode: '', sampleId: '', sourcePathId: '' });
|
editorOpen.value = true;
|
openEditorDrawer(true);
|
void refreshPaths().catch((error) => fail(sourceErrorMessage(error, '来源路径刷新失败,请重试')));
|
void ensurePreviewSamples();
|
}
|
|
function openItem(item: DataItemRow) {
|
activeItem.value = item;
|
previewReady.value = false;
|
previewValue.value = undefined;
|
operationMessage.value = '';
|
Object.assign(form, {
|
displayName: item.displayName,
|
id: item.id,
|
outputKey: item.outputKey,
|
propertyCode: item.propertyCode,
|
sampleId: '',
|
sourcePathId: item.sourcePathId,
|
});
|
editorOpen.value = true;
|
openEditorDrawer(true);
|
void refreshPaths().catch((error) => fail(sourceErrorMessage(error, '来源路径刷新失败,请重试')));
|
void ensurePreviewSamples();
|
}
|
|
function closeEditor() {
|
editorOpen.value = false;
|
closeEditorDrawer();
|
}
|
|
async function suggestCodeFromDisplayName() {
|
const displayName = form.displayName.trim();
|
if (!displayName || activeItem.value) return;
|
const sequence = ++suggestionSequence;
|
try {
|
const response = await suggestFieldCode(displayName);
|
if (sequence === suggestionSequence) form.propertyCode = response.data.code;
|
} catch {
|
if (sequence === suggestionSequence) operationMessage.value = '数据标识生成失败,请重新输入业务名称';
|
}
|
}
|
|
function chooseSource(value: string) {
|
const split = value.indexOf(':');
|
form.sourcePathId = value.slice(0, split);
|
form.outputKey = value.slice(split + 1);
|
previewReady.value = false;
|
}
|
|
async function saveDraft(showMessage = true) {
|
if (!form.displayName.trim()) return fail('请输入业务名称');
|
if (!form.propertyCode) await suggestCodeFromDisplayName();
|
if (!form.propertyCode) return fail('未能生成数据标识,请重新输入业务名称');
|
if (!form.sourcePathId || !form.outputKey) return fail('请选择数据来源');
|
saving.value = true;
|
operationMessage.value = '';
|
const data: SourceDataItemDraft = {
|
displayName: form.displayName.trim(),
|
outputKey: form.outputKey,
|
propertyCode: form.propertyCode,
|
sourcePathId: form.sourcePathId,
|
...(activeItem.value ? { expectedEditVersion: activeItem.value.editVersion } : {}),
|
};
|
try {
|
const response = activeItem.value ? await saveSourceDataItem(activeItem.value.id, data) : await createSourceDataItem(data);
|
replaceItem(response.data);
|
activeItem.value = { ...response.data, versions: [response.data] };
|
form.id = response.data.id;
|
if (showMessage) operationMessage.value = '草稿已保存';
|
return response.data;
|
} catch (error) {
|
operationMessage.value = sourceErrorMessage(error, '数据项保存失败,请重试');
|
} finally {
|
saving.value = false;
|
}
|
}
|
|
async function preview() {
|
if (!form.sampleId) return fail('请选择一个有权限查看的检验项');
|
const draft = await saveDraft(false);
|
if (!draft) return;
|
saving.value = true;
|
try {
|
const response = await previewSourceDataItem(draft.id, form.sampleId);
|
previewValue.value = response.data.value;
|
previewReady.value = true;
|
operationMessage.value = '已使用所选检验项完成预览';
|
} catch (error) {
|
previewReady.value = false;
|
operationMessage.value = sourceErrorMessage(error, '数据项预览失败,请重试');
|
} finally {
|
saving.value = false;
|
}
|
}
|
|
async function publish() {
|
if (!form.sampleId) return fail('请选择一个有权限查看的检验项');
|
const draft = await saveDraft(false);
|
if (!draft) return;
|
saving.value = true;
|
try {
|
const response = await publishSourceDataItem(draft.id, { expectedEditVersion: draft.editVersion, sampleId: form.sampleId });
|
replaceItem(response.data);
|
operationMessage.value = '校验通过,数据项已发布';
|
await load();
|
closeEditor();
|
} catch (error) {
|
operationMessage.value = sourceErrorMessage(error, '数据项发布失败,请重试');
|
} finally {
|
saving.value = false;
|
}
|
}
|
|
async function toggleEnabled(item: DataItemRow) {
|
operationMessage.value = '';
|
try {
|
await setSourcePropertyEnabled(item.propertyCode, !item.enabled);
|
items.value = items.value.map((version) => (version.propertyCode === item.propertyCode ? { ...version, enabled: !item.enabled } : version));
|
} catch (error) {
|
operationMessage.value = sourceErrorMessage(error, '数据项启停失败,请重试');
|
}
|
}
|
|
function selectItem(item: DataItemRow) {
|
const published = item.published;
|
if (!published || !published.enabled || !canSelect(published)) return;
|
emit('select', {
|
dataType: published.dataType as FieldDataType,
|
enabled: true,
|
label: published.displayName,
|
property: published.propertyCode,
|
propertyRevision: published.revision,
|
readOnly: true,
|
source: 'LIMS_TASK',
|
});
|
}
|
|
function showHistory(item: DataItemRow) {
|
activeHistory.value = item.versions;
|
openHistoryDrawer(true);
|
}
|
function getTableActions(item: DataItemRow): ActionItem[] {
|
const actions: ActionItem[] = [
|
{ label: '编辑', onClick: () => openItem(item) },
|
{ label: '变更记录', onClick: () => showHistory(item) },
|
];
|
if (item.published && canSelect(item.published)) actions.unshift({ label: '选用', onClick: () => selectItem(item) });
|
if (item.published) {
|
actions.push({
|
color: item.enabled ? 'error' : 'success',
|
label: item.enabled ? '停用' : '启用',
|
onClick: () => toggleEnabled(item),
|
});
|
}
|
return actions;
|
}
|
function canSelect(item: SourceDataItem) {
|
if (!props.fieldDataType) return false;
|
if (item.dataType === 'BOOLEAN') return props.fieldDataType === 'CHECKBOX';
|
if (item.dataType === 'TEXT') return ['SELECT', 'TEXT', 'TEXTAREA'].includes(props.fieldDataType);
|
return item.dataType === props.fieldDataType;
|
}
|
function openPathManager() {
|
onlineUtils.route('/eln/source-paths', { title: '来源路径管理' });
|
}
|
|
function replaceItem(item: SourceDataItem) {
|
const index = items.value.findIndex((existing) => existing.id === item.id);
|
if (index === -1) items.value.push(item);
|
else items.value.splice(index, 1, item);
|
}
|
function fail(message: string) {
|
operationMessage.value = message;
|
return undefined;
|
}
|
function sourceValue(pathId: string, outputKey: string) {
|
return `${pathId}:${outputKey}`;
|
}
|
function selectedSourceValue() {
|
return form.sourcePathId && form.outputKey ? sourceValue(form.sourcePathId, form.outputKey) : undefined;
|
}
|
function sampleLabel(sample: SourcePreviewSample) {
|
return [sample.testNo, sample.sampleName, sample.batchNo, sample.testName].filter(Boolean).join(' · ') || sample.id;
|
}
|
function typeLabel(type: string) {
|
return TYPE_LABELS[type] || type;
|
}
|
function statusLabel(status: SourceDataItem['status']) {
|
return status === 'PUBLISHED' ? '已发布' : '草稿';
|
}
|
function sourceErrorMessage(error: unknown, fallback: string) {
|
const response = (error as { response?: { data?: { data?: { errorCode?: string }; msg?: string } } })?.response?.data;
|
const code = response?.data?.errorCode;
|
if (code === 'SOURCE_CONFLICT') return '配置已被其他人修改,请刷新后重试';
|
if (code === 'SOURCE_FORBIDDEN') return '没有配置或预览数据访问权限';
|
if (code === 'SOURCE_INVALID') return '来源配置校验失败,请检查数据来源和预览样例';
|
if (code === 'SOURCE_LIMIT') return '来源查询超过限制,请缩小范围后重试';
|
if (code === 'SOURCE_UNAVAILABLE') return 'LIMS 数据暂不可用,请稍后重试';
|
return response?.msg || (error instanceof Error && error.message) || fallback;
|
}
|
</script>
|
|
<template>
|
<div class="jnpf-content-wrapper-center source-workspace">
|
<div class="jnpf-content-wrapper-search-box">
|
<BasicForm @register="registerSearchForm" @reset="handleReset" @submit="handleSearch" />
|
</div>
|
<div class="jnpf-content-wrapper-content source-table-content">
|
<a-alert v-if="errorMessage" :message="errorMessage" show-icon type="error">
|
<template #action><a-button size="small" @click="load">重试</a-button></template>
|
</a-alert>
|
<div v-if="operationMessage && !editorOpen" class="operation-message" role="status">{{ operationMessage }}</div>
|
<BasicVxeTable @register="registerTable" :data-source="filteredRows">
|
<template #tableTitle>
|
<a-button data-test="new-source-property" type="primary" pre-icon="icon-ym icon-ym-btn-add" @click="openNewItem">新增数据项</a-button>
|
<a-button data-test="manage-source-paths" @click="openPathManager">
|
<template #icon><SettingOutlined /></template>
|
来源路径管理
|
</a-button>
|
</template>
|
<template #dataType="{ record }">{{ typeLabel(record.dataType) }}</template>
|
<template #status="{ record }">
|
<a-tag :color="record.status === 'PUBLISHED' ? 'success' : 'processing'">{{ statusLabel(record.status) }}</a-tag>
|
</template>
|
<template #action="{ record }">
|
<TableAction :actions="getTableActions(record)" />
|
</template>
|
</BasicVxeTable>
|
</div>
|
|
<BasicDrawer
|
@register="registerEditor"
|
@close="editorOpen = false"
|
show-footer
|
:show-ok-btn="false"
|
:title="activeItem ? '编辑 LIMS 数据项' : '新增 LIMS 数据项'"
|
width="640px">
|
<div class="editor-body">
|
<section class="form-section">
|
<h3>基本信息</h3>
|
<label
|
><span>业务名称</span
|
><a-input
|
v-model:value="form.displayName"
|
data-test="source-display-name"
|
:maxlength="200"
|
placeholder="例如:样品生产日期"
|
@blur="suggestCodeFromDisplayName"
|
/></label>
|
<label
|
><span>数据标识</span><a-input :value="form.propertyCode" data-test="source-property-code" disabled placeholder="根据业务名称自动生成"
|
/></label>
|
</section>
|
<section class="form-section">
|
<div class="section-heading">
|
<h3>数据来源</h3>
|
<a-button data-test="manage-source-paths" size="small" type="link" @click="openPathManager"><SettingOutlined />来源路径管理</a-button>
|
</div>
|
<label
|
><span>来源字段</span
|
><a-select
|
data-test="source-path-select"
|
show-search
|
:loading="pathsLoading"
|
:options="sourceOptions"
|
:value="selectedSourceValue()"
|
placeholder="请选择已登记的业务来源"
|
@change="chooseSource"
|
/></label>
|
<dl v-if="selectedOutput" class="source-summary">
|
<div>
|
<dt>已选路径</dt>
|
<dd>{{ selectedOutput.path }}</dd>
|
</div>
|
<div>
|
<dt>返回内容</dt>
|
<dd>{{ selectedOutput.returnContent }}</dd>
|
</div>
|
<div>
|
<dt>数据类型</dt>
|
<dd>{{ typeLabel(selectedOutput.dataType) }}</dd>
|
</div>
|
</dl>
|
</section>
|
<section class="form-section">
|
<h3>适用于</h3>
|
<div class="scope-value">{{ selectedPath?.scope || '选择数据来源后显示' }}</div>
|
</section>
|
<section class="form-section">
|
<h3>取值预览</h3>
|
<a-alert v-if="sampleLoadMessage" :message="sampleLoadMessage" show-icon type="warning" />
|
<div class="preview-controls">
|
<a-select
|
v-model:value="form.sampleId"
|
data-test="source-sample-select"
|
show-search
|
:loading="samplesLoading"
|
:options="sampleOptions"
|
placeholder="选择一个有权限查看的检验项" /><a-button :disabled="saving" @click="preview"><EyeOutlined />预览</a-button>
|
</div>
|
<div v-if="previewReady" class="preview-result">
|
<span>{{ selectedOutput?.displayName }}</span
|
><strong>{{ previewValue ?? '空值' }}</strong>
|
</div>
|
</section>
|
<div v-if="operationMessage" class="operation-message" role="status">{{ operationMessage }}</div>
|
</div>
|
<template #footer>
|
<div class="drawer-footer">
|
<a-button @click="closeEditor">取消</a-button><a-button :loading="saving" @click="saveDraft()">保存草稿</a-button
|
><a-button :loading="saving" type="primary" @click="publish">校验并发布</a-button>
|
</div>
|
</template>
|
</BasicDrawer>
|
|
<BasicDrawer @register="registerHistory" :show-ok-btn="false" title="变更记录" width="520px">
|
<ol class="history-list">
|
<li v-for="version in activeHistory" :key="version.id">
|
<div>
|
<strong>{{ statusLabel(version.status) }}</strong
|
><span>版本 {{ version.revision }}</span>
|
</div>
|
<p>{{ version.sourcePath }}</p>
|
<small
|
>{{ version.publishedAt || '尚未发布' }}<template v-if="version.publishedBy"> · {{ version.publishedBy }}</template></small
|
>
|
</li>
|
</ol>
|
</BasicDrawer>
|
</div>
|
</template>
|
|
<style lang="scss" scoped>
|
.source-workspace {
|
min-width: 0;
|
}
|
.source-table-content {
|
display: flex;
|
min-width: 0;
|
min-height: 0;
|
flex-direction: column;
|
}
|
:deep(.jnpf-vxe-table) {
|
min-width: 0;
|
}
|
.source-table-content > :deep(.jnpf-vxe-table) {
|
min-height: 0;
|
flex: 1;
|
}
|
.drawer-footer,
|
.section-heading,
|
.preview-controls {
|
display: flex;
|
align-items: center;
|
}
|
.operation-message {
|
padding: 9px 12px;
|
color: #7a5200;
|
background: #fff8db;
|
border: 1px solid #f3d675;
|
border-radius: 4px;
|
}
|
.editor-body {
|
padding: 0 20px 24px;
|
}
|
.form-section {
|
padding: 20px 0;
|
border-bottom: 1px solid #eef0f3;
|
}
|
.form-section:last-child {
|
border-bottom: 0;
|
}
|
.form-section h3 {
|
margin: 0 0 16px;
|
color: #1f2937;
|
font-size: 15px;
|
font-weight: 600;
|
letter-spacing: 0;
|
}
|
.form-section label {
|
display: grid;
|
grid-template-columns: 92px minmax(0, 1fr);
|
align-items: center;
|
gap: 12px;
|
margin-bottom: 14px;
|
}
|
.form-section label:last-child {
|
margin-bottom: 0;
|
}
|
.form-section label > span {
|
color: #4b5563;
|
}
|
.section-heading {
|
justify-content: space-between;
|
}
|
.section-heading h3 {
|
margin-bottom: 0;
|
}
|
.source-summary {
|
margin: 14px 0 0 104px;
|
padding: 12px 14px;
|
background: #f7f8fa;
|
border-radius: 4px;
|
}
|
.source-summary > div {
|
display: grid;
|
grid-template-columns: 76px minmax(0, 1fr);
|
gap: 8px;
|
padding: 3px 0;
|
}
|
.source-summary dt {
|
color: #6b7280;
|
}
|
.source-summary dd {
|
min-width: 0;
|
margin: 0;
|
overflow-wrap: anywhere;
|
color: #1f2937;
|
}
|
.scope-value {
|
padding: 10px 12px;
|
color: #374151;
|
background: #f7f8fa;
|
border-radius: 4px;
|
}
|
.preview-controls {
|
gap: 10px;
|
}
|
.preview-controls :deep(.ant-select) {
|
min-width: 0;
|
flex: 1;
|
}
|
.preview-result {
|
display: flex;
|
justify-content: space-between;
|
gap: 16px;
|
margin-top: 12px;
|
padding: 12px 14px;
|
background: #edf8f1;
|
border-left: 3px solid #3b9a62;
|
}
|
.preview-result strong {
|
min-width: 0;
|
overflow-wrap: anywhere;
|
color: #17663b;
|
}
|
.drawer-footer {
|
justify-content: flex-end;
|
gap: 10px;
|
}
|
.history-list {
|
margin: 0;
|
padding: 8px 20px 24px;
|
list-style: none;
|
}
|
.history-list li {
|
padding: 16px 0;
|
border-bottom: 1px solid #eef0f3;
|
}
|
.history-list li > div {
|
display: flex;
|
justify-content: space-between;
|
}
|
.history-list p {
|
margin: 8px 0;
|
color: #374151;
|
}
|
.history-list small {
|
color: #6b7280;
|
}
|
</style>
|