feat: 重构知识库文档导入任务化流程
- 新增上传建单、异步解析、分块处理与异步向量化闭环 - 收口分享页权限、完成态检索过滤与 SSE 局部状态刷新
This commit is contained in:
@@ -1,56 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import {
|
||||
ElAlert,
|
||||
ElCard,
|
||||
ElCol,
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElRow,
|
||||
ElSelect,
|
||||
ElSlider,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
interface StrategyConfig {
|
||||
chunkSize?: number;
|
||||
mdSplitterLevel?: number;
|
||||
overlapSize?: number;
|
||||
regex?: string;
|
||||
rowsPerChunk?: number;
|
||||
strategyCode?: string;
|
||||
import { api } from '#/api/request';
|
||||
import { buildKnowledgePath } from '#/views/ai/documentCollection/share-path';
|
||||
import SplitterDocPreview from '#/views/ai/documentCollection/SplitterDocPreview.vue';
|
||||
|
||||
interface SourceRange {
|
||||
end: number;
|
||||
start: number;
|
||||
}
|
||||
|
||||
interface StrategyCandidate {
|
||||
score?: number;
|
||||
strategyCode: string;
|
||||
strategyLabel: string;
|
||||
interface ChunkItem {
|
||||
answer?: string;
|
||||
charCount?: number;
|
||||
chunkId?: string;
|
||||
chunkType?: string;
|
||||
content?: string;
|
||||
headingPath?: string[];
|
||||
options?: Record<string, any>;
|
||||
partNo?: number;
|
||||
partTotal?: number;
|
||||
question?: string;
|
||||
sourceLabel?: string;
|
||||
sourceRanges?: SourceRange[];
|
||||
tokenEstimate?: number;
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
interface AnalysisResult {
|
||||
candidateStrategies?: StrategyCandidate[];
|
||||
confidence?: number;
|
||||
reasons?: string[];
|
||||
recommendedStrategyCode?: string;
|
||||
recommendedStrategyLabel?: string;
|
||||
recommendedStructureType?: string;
|
||||
}
|
||||
|
||||
interface AnalyzeItem {
|
||||
analysis?: AnalysisResult;
|
||||
interface PreviewItem {
|
||||
analysis?: {
|
||||
normalizedContent?: string;
|
||||
recommendedStrategyLabel?: string;
|
||||
};
|
||||
chunks?: ChunkItem[];
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
strategyConfig?: StrategyConfig;
|
||||
normalizedContent?: string;
|
||||
previewSessionId: string;
|
||||
strategyCode?: string;
|
||||
strategyLabel?: string;
|
||||
totalChunks?: number;
|
||||
totalWarnings?: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
analysisItems?: AnalyzeItem[];
|
||||
}>();
|
||||
const props = defineProps({
|
||||
documentId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
documentTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
endpointPrefix: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
knowledgeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
requestClient: {
|
||||
type: Object as any,
|
||||
default: () => api,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['cancel', 'started']);
|
||||
|
||||
const createDefaultFormState = () => ({
|
||||
chunkSize: 512,
|
||||
mdSplitterLevel: 2,
|
||||
overlapSize: 128,
|
||||
regex: '',
|
||||
rowsPerChunk: 1,
|
||||
strategyCode: 'AUTO',
|
||||
});
|
||||
|
||||
const formState = reactive(createDefaultFormState());
|
||||
const previewItems = ref<PreviewItem[]>([]);
|
||||
const currentPreviewSessionId = ref('');
|
||||
const activeDocumentId = ref(props.documentId || '');
|
||||
const previewError = ref('');
|
||||
const previewLoading = ref(false);
|
||||
const startLoading = ref(false);
|
||||
let previewDebounceTimer: null | ReturnType<typeof setTimeout> = null;
|
||||
let previewSequence = 0;
|
||||
|
||||
const strategyOptions = [
|
||||
{
|
||||
@@ -81,313 +128,379 @@ const strategyOptions = [
|
||||
|
||||
const mdLevels = [1, 2, 3, 4, 5, 6];
|
||||
|
||||
const formMap = reactive<Record<string, StrategyConfig>>({});
|
||||
const showLengthSettings = (strategyCode?: string) =>
|
||||
['AUTO', 'MARKDOWN_SECTION', 'OUTLINE_SECTION', 'PARAGRAPH_LENGTH'].includes(
|
||||
strategyCode || '',
|
||||
);
|
||||
|
||||
function createDefaultStrategyConfig(item?: AnalyzeItem): StrategyConfig {
|
||||
return {
|
||||
chunkSize: item?.strategyConfig?.chunkSize ?? 512,
|
||||
mdSplitterLevel: item?.strategyConfig?.mdSplitterLevel ?? 2,
|
||||
overlapSize: item?.strategyConfig?.overlapSize ?? 128,
|
||||
regex: item?.strategyConfig?.regex ?? '',
|
||||
rowsPerChunk: item?.strategyConfig?.rowsPerChunk ?? 1,
|
||||
strategyCode:
|
||||
item?.strategyConfig?.strategyCode ||
|
||||
item?.analysis?.recommendedStrategyCode ||
|
||||
'AUTO',
|
||||
};
|
||||
}
|
||||
const showOverlapSettings = (strategyCode?: string) =>
|
||||
['AUTO', 'PARAGRAPH_LENGTH'].includes(strategyCode || '');
|
||||
|
||||
function getStrategyForm(filePath: string, item?: AnalyzeItem): StrategyConfig {
|
||||
if (!formMap[filePath]) {
|
||||
formMap[filePath] = createDefaultStrategyConfig(item);
|
||||
const clearPreviewTimer = () => {
|
||||
if (!previewDebounceTimer) {
|
||||
return;
|
||||
}
|
||||
return formMap[filePath]!;
|
||||
}
|
||||
clearTimeout(previewDebounceTimer);
|
||||
previewDebounceTimer = null;
|
||||
};
|
||||
|
||||
const resetPreviewState = () => {
|
||||
previewItems.value = [];
|
||||
currentPreviewSessionId.value = '';
|
||||
previewError.value = '';
|
||||
};
|
||||
|
||||
const buildStrategyConfig = () => ({
|
||||
...formState,
|
||||
});
|
||||
|
||||
const normalizeSourceRanges = (ranges?: SourceRange[]) =>
|
||||
Array.isArray(ranges)
|
||||
? ranges.filter(
|
||||
(item) =>
|
||||
Number.isFinite(item?.start) &&
|
||||
Number.isFinite(item?.end) &&
|
||||
Number(item.end) > Number(item.start),
|
||||
)
|
||||
: [];
|
||||
|
||||
const normalizePreviewItems = (items: PreviewItem[]) =>
|
||||
(items || []).map((item) => ({
|
||||
...item,
|
||||
normalizedContent:
|
||||
item.normalizedContent || item.analysis?.normalizedContent || '',
|
||||
strategyLabel:
|
||||
item.strategyLabel || item.analysis?.recommendedStrategyLabel || '',
|
||||
totalChunks:
|
||||
Number(item.totalChunks || 0) > 0
|
||||
? item.totalChunks
|
||||
: (item.chunks || []).length,
|
||||
chunks: (item.chunks || []).map((chunk) => ({
|
||||
...chunk,
|
||||
sourceRanges: normalizeSourceRanges(
|
||||
chunk.sourceRanges ||
|
||||
(Array.isArray(chunk.options?.sourceRanges)
|
||||
? chunk.options?.sourceRanges
|
||||
: []),
|
||||
),
|
||||
})),
|
||||
}));
|
||||
|
||||
const generatePreview = async () => {
|
||||
if (!activeDocumentId.value) {
|
||||
return;
|
||||
}
|
||||
const requestSequence = ++previewSequence;
|
||||
previewLoading.value = true;
|
||||
previewError.value = '';
|
||||
try {
|
||||
const res = await props.requestClient.post(
|
||||
buildKnowledgePath(
|
||||
props.endpointPrefix,
|
||||
'/api/v1/document/import/task/preview',
|
||||
),
|
||||
{
|
||||
documentId: activeDocumentId.value,
|
||||
files: [
|
||||
{
|
||||
strategyConfig: buildStrategyConfig(),
|
||||
},
|
||||
],
|
||||
knowledgeId: props.knowledgeId,
|
||||
},
|
||||
);
|
||||
if (requestSequence !== previewSequence) {
|
||||
return;
|
||||
}
|
||||
const items = normalizePreviewItems(
|
||||
(res.data?.items || []) as PreviewItem[],
|
||||
);
|
||||
previewItems.value = items;
|
||||
currentPreviewSessionId.value = items[0]?.previewSessionId || '';
|
||||
previewError.value = '';
|
||||
} catch (error: any) {
|
||||
if (requestSequence !== previewSequence) {
|
||||
return;
|
||||
}
|
||||
const message =
|
||||
error?.message || $t('documentCollection.importDoc.previewRequestFailed');
|
||||
previewItems.value = [];
|
||||
currentPreviewSessionId.value = '';
|
||||
previewError.value = message;
|
||||
ElMessage.error(message);
|
||||
} finally {
|
||||
if (requestSequence === previewSequence) {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const schedulePreviewGeneration = () => {
|
||||
if (!activeDocumentId.value) {
|
||||
return;
|
||||
}
|
||||
clearPreviewTimer();
|
||||
resetPreviewState();
|
||||
previewDebounceTimer = setTimeout(() => {
|
||||
previewDebounceTimer = null;
|
||||
void generatePreview();
|
||||
}, 320);
|
||||
};
|
||||
|
||||
const handlePreviewSessionChange = (previewSessionId: string) => {
|
||||
currentPreviewSessionId.value = previewSessionId;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
previewSequence += 1;
|
||||
clearPreviewTimer();
|
||||
previewLoading.value = false;
|
||||
emits('cancel');
|
||||
};
|
||||
|
||||
const startIndex = async () => {
|
||||
if (!currentPreviewSessionId.value) {
|
||||
ElMessage.warning($t('documentCollection.importDoc.previewEmpty'));
|
||||
return;
|
||||
}
|
||||
startLoading.value = true;
|
||||
try {
|
||||
const res = await props.requestClient.post(
|
||||
buildKnowledgePath(
|
||||
props.endpointPrefix,
|
||||
'/api/v1/document/import/task/startIndex',
|
||||
),
|
||||
{
|
||||
documentId: activeDocumentId.value,
|
||||
knowledgeId: props.knowledgeId,
|
||||
previewSessionId: currentPreviewSessionId.value,
|
||||
},
|
||||
);
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('documentCollection.importDoc.indexQueued'));
|
||||
emits('started');
|
||||
}
|
||||
} finally {
|
||||
startLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.analysisItems,
|
||||
(items) => {
|
||||
for (const item of items || []) {
|
||||
formMap[item.filePath] = createDefaultStrategyConfig(item);
|
||||
() => props.documentId,
|
||||
(value) => {
|
||||
activeDocumentId.value = value || '';
|
||||
previewSequence += 1;
|
||||
clearPreviewTimer();
|
||||
Object.assign(formState, createDefaultFormState());
|
||||
resetPreviewState();
|
||||
if (activeDocumentId.value) {
|
||||
schedulePreviewGeneration();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const items = computed(() => props.analysisItems ?? []);
|
||||
|
||||
defineExpose({
|
||||
getPreviewRequestItems() {
|
||||
return items.value.map((item) => ({
|
||||
fileName: item.fileName,
|
||||
filePath: item.filePath,
|
||||
strategyConfig: {
|
||||
...getStrategyForm(item.filePath, item),
|
||||
},
|
||||
}));
|
||||
watch(
|
||||
formState,
|
||||
() => {
|
||||
if (!activeDocumentId.value) {
|
||||
return;
|
||||
}
|
||||
schedulePreviewGeneration();
|
||||
},
|
||||
});
|
||||
|
||||
function showLengthSettings(strategyCode?: string) {
|
||||
return [
|
||||
'AUTO',
|
||||
'MARKDOWN_SECTION',
|
||||
'OUTLINE_SECTION',
|
||||
'PARAGRAPH_LENGTH',
|
||||
].includes(strategyCode || '');
|
||||
}
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="strategy-container">
|
||||
<ElAlert
|
||||
:title="$t('documentCollection.importDoc.analysisTip')"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="strategy-tip"
|
||||
/>
|
||||
<div class="workbench">
|
||||
<ElForm :model="formState" label-position="top" class="workbench__form">
|
||||
<div class="workbench__form-grid">
|
||||
<ElFormItem
|
||||
:label="$t('documentCollection.importDoc.strategySelection')"
|
||||
class="workbench__form-full"
|
||||
>
|
||||
<ElSelect v-model="formState.strategyCode" class="w-full">
|
||||
<ElOption
|
||||
v-for="option in strategyOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<div class="strategy-list">
|
||||
<ElCard
|
||||
v-for="item in items"
|
||||
:key="item.filePath"
|
||||
class="strategy-card"
|
||||
shadow="never"
|
||||
<ElFormItem
|
||||
v-if="showLengthSettings(formState.strategyCode)"
|
||||
:label="$t('documentCollection.splitterDoc.chunkSize')"
|
||||
:class="
|
||||
showOverlapSettings(formState.strategyCode)
|
||||
? ''
|
||||
: 'workbench__form-full'
|
||||
"
|
||||
>
|
||||
<ElSlider
|
||||
v-model="formState.chunkSize"
|
||||
:max="2048"
|
||||
:min="128"
|
||||
show-input
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="showOverlapSettings(formState.strategyCode)"
|
||||
:label="$t('documentCollection.splitterDoc.overlapSize')"
|
||||
>
|
||||
<ElSlider
|
||||
v-model="formState.overlapSize"
|
||||
:max="512"
|
||||
:min="0"
|
||||
show-input
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="formState.strategyCode === 'MARKDOWN_SECTION'"
|
||||
:label="$t('documentCollection.splitterDoc.mdSplitterLevel')"
|
||||
class="workbench__form-full"
|
||||
>
|
||||
<ElSelect v-model="formState.mdSplitterLevel" class="w-full">
|
||||
<ElOption
|
||||
v-for="level in mdLevels"
|
||||
:key="level"
|
||||
:label="'#'.repeat(level)"
|
||||
:value="level"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="formState.strategyCode === 'CUSTOM_REGEX'"
|
||||
:label="$t('documentCollection.splitterDoc.regex')"
|
||||
class="workbench__form-full"
|
||||
>
|
||||
<ElInput v-model="formState.regex" />
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</ElForm>
|
||||
|
||||
<section class="workbench__content">
|
||||
<SplitterDocPreview
|
||||
:loading="previewLoading"
|
||||
:preview-items="previewItems"
|
||||
@preview-session-change="handlePreviewSessionChange"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div v-if="previewError" class="workbench__error">
|
||||
{{ previewError }}
|
||||
</div>
|
||||
|
||||
<div class="workbench__footer">
|
||||
<ElButton :disabled="startLoading" @click="handleCancel">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:disabled="previewLoading || !currentPreviewSessionId"
|
||||
:loading="startLoading"
|
||||
@click="startIndex"
|
||||
>
|
||||
<div class="strategy-card__header">
|
||||
<div>
|
||||
<div class="strategy-card__title">{{ item.fileName }}</div>
|
||||
<div class="strategy-card__meta">
|
||||
{{ item.analysis?.recommendedStructureType || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="strategy-card__badges">
|
||||
<ElTag type="success" effect="plain">
|
||||
{{
|
||||
item.analysis?.recommendedStrategyLabel ||
|
||||
$t('documentCollection.splitterDoc.autoStrategy')
|
||||
}}
|
||||
</ElTag>
|
||||
<ElTag effect="plain">
|
||||
{{ $t('documentCollection.importDoc.confidence') }}
|
||||
{{ item.analysis?.confidence ?? 0 }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElRow :gutter="16" class="strategy-card__content">
|
||||
<ElCol :span="12">
|
||||
<div class="strategy-block">
|
||||
<div class="strategy-block__label">
|
||||
{{ $t('documentCollection.importDoc.recommendReason') }}
|
||||
</div>
|
||||
<ul class="strategy-reason-list">
|
||||
<li
|
||||
v-for="reason in item.analysis?.reasons || []"
|
||||
:key="reason"
|
||||
class="strategy-reason-list__item"
|
||||
>
|
||||
{{ reason }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="strategy-block">
|
||||
<div class="strategy-block__label">
|
||||
{{ $t('documentCollection.importDoc.candidateStrategies') }}
|
||||
</div>
|
||||
<div class="strategy-candidate-list">
|
||||
<ElTag
|
||||
v-for="candidate in item.analysis?.candidateStrategies || []"
|
||||
:key="candidate.strategyCode"
|
||||
effect="plain"
|
||||
>
|
||||
{{ candidate.strategyLabel }} / {{ candidate.score }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :span="12">
|
||||
<ElForm
|
||||
:model="getStrategyForm(item.filePath, item)"
|
||||
label-position="top"
|
||||
class="strategy-form"
|
||||
>
|
||||
<ElFormItem
|
||||
:label="$t('documentCollection.importDoc.strategySelection')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="getStrategyForm(item.filePath, item).strategyCode"
|
||||
class="w-full"
|
||||
>
|
||||
<ElOption
|
||||
v-for="option in strategyOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="
|
||||
showLengthSettings(
|
||||
getStrategyForm(item.filePath, item).strategyCode,
|
||||
)
|
||||
"
|
||||
:label="$t('documentCollection.splitterDoc.chunkSize')"
|
||||
>
|
||||
<ElSlider
|
||||
v-model="getStrategyForm(item.filePath, item).chunkSize"
|
||||
:max="2048"
|
||||
:min="128"
|
||||
show-input
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="
|
||||
getStrategyForm(item.filePath, item).strategyCode ===
|
||||
'PARAGRAPH_LENGTH' ||
|
||||
getStrategyForm(item.filePath, item).strategyCode === 'AUTO'
|
||||
"
|
||||
:label="$t('documentCollection.splitterDoc.overlapSize')"
|
||||
>
|
||||
<ElSlider
|
||||
v-model="getStrategyForm(item.filePath, item).overlapSize"
|
||||
:max="512"
|
||||
:min="0"
|
||||
show-input
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="
|
||||
getStrategyForm(item.filePath, item).strategyCode ===
|
||||
'MARKDOWN_SECTION'
|
||||
"
|
||||
:label="$t('documentCollection.splitterDoc.mdSplitterLevel')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="getStrategyForm(item.filePath, item).mdSplitterLevel"
|
||||
class="w-full"
|
||||
>
|
||||
<ElOption
|
||||
v-for="level in mdLevels"
|
||||
:key="level"
|
||||
:label="'#'.repeat(level)"
|
||||
:value="level"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="
|
||||
getStrategyForm(item.filePath, item).strategyCode ===
|
||||
'CUSTOM_REGEX'
|
||||
"
|
||||
:label="$t('documentCollection.splitterDoc.regex')"
|
||||
>
|
||||
<ElInput v-model="getStrategyForm(item.filePath, item).regex" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElCard>
|
||||
{{ $t('button.startIndex') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.strategy-container {
|
||||
.workbench {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 20px;
|
||||
min-height: 100%;
|
||||
padding: 4px 4px 0;
|
||||
}
|
||||
|
||||
.strategy-tip {
|
||||
border-radius: 12px;
|
||||
.workbench__form {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.strategy-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.workbench__form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 6px 18px;
|
||||
}
|
||||
|
||||
.strategy-card {
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
.workbench__form-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.workbench__content {
|
||||
min-height: 620px;
|
||||
}
|
||||
|
||||
.workbench__error {
|
||||
padding: 14px 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: var(--el-color-danger-dark-2);
|
||||
background: color-mix(in srgb, var(--el-color-danger-light-9) 88%, white);
|
||||
border: 1px solid color-mix(in srgb, var(--el-color-danger) 14%, white);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.strategy-card__header {
|
||||
.workbench__footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 0 8px;
|
||||
margin-top: auto;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(246 248 251 / 0%) 0%,
|
||||
rgb(246 248 251 / 82%) 30%,
|
||||
rgb(246 248 251 / 100%) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.strategy-card__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.strategy-card__meta {
|
||||
margin-top: 4px;
|
||||
:deep(.el-form-item__label) {
|
||||
padding-bottom: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.strategy-card__badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
:deep(.workbench__form .el-input__wrapper),
|
||||
:deep(.workbench__form .el-select__wrapper) {
|
||||
box-shadow: 0 0 0 1px rgb(15 23 42 / 7%) inset;
|
||||
}
|
||||
|
||||
.strategy-card__content {
|
||||
margin-top: 16px;
|
||||
:deep(.workbench__form .el-slider__runway) {
|
||||
background: rgb(15 23 42 / 8%);
|
||||
}
|
||||
|
||||
.strategy-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
@media (max-width: 1180px) {
|
||||
.workbench__content {
|
||||
min-height: 520px;
|
||||
}
|
||||
}
|
||||
|
||||
.strategy-block + .strategy-block {
|
||||
margin-top: 16px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.workbench {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.strategy-block__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.workbench__header,
|
||||
.workbench__form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.strategy-reason-list {
|
||||
padding-left: 18px;
|
||||
margin: 0;
|
||||
line-height: 1.7;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.strategy-reason-list__item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.strategy-candidate-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.strategy-form {
|
||||
padding: 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 12px;
|
||||
.workbench__form,
|
||||
.workbench__content {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user