feat: update AutoProbe components and enhance functionality

- Removed AutoProbeFieldRule component and integrated AutoProbeSelector for improved selector management.
- Updated AutoProbePagePatternDetail to utilize new table structure for better data representation.
- Enhanced internationalization support with new translations for selector types and field counts.
- Refactored AutoProbeDetailTabPatterns to streamline data handling and improve user experience.
This commit is contained in:
Marvin Zhang
2025-05-13 23:37:15 +08:00
parent 54ad06aff6
commit c654f03890
12 changed files with 311 additions and 300 deletions

View File

@@ -1,53 +0,0 @@
<script setup lang="ts">
const props = defineProps<{
fieldRule: FieldRule;
}>();
defineOptions({ name: 'ClAutoProbeFieldRule' });
</script>
<template>
<div class="field-rule">
<div class="name">
<span class="icon">
<cl-icon :icon="['fa', 'share-nodes']" />
</span>
<label>{{ fieldRule.name }}</label>
</div>
<div class="selector">
<span>{{ fieldRule.selector_type }}</span>
<span>{{ fieldRule.selector }}</span>
</div>
<div class="extraction">
<span>{{ fieldRule.extraction_type }}</span>
<span>{{ fieldRule.attribute_name }}</span>
</div>
</div>
</template>
<style scoped>
.field-rule {
display: flex;
align-items: center;
.name {
flex: 0 0 180px;
display: flex;
align-items: center;
.icon {
margin-right: 5px;
}
}
.selector {
display: flex;
align-items: center;
}
.extraction {
display: flex;
align-items: center;
}
}
</style>

View File

@@ -1,120 +1,125 @@
<script setup lang="ts">
<script setup lang="tsx">
import { computed } from 'vue';
import { useStore } from 'vuex';
import {
ClNavLink,
ClIcon,
ClAutoProbeSelector,
} from '@/components';
import { translate } from '@/utils';
// i18n
const t = translate;
// props
const props = defineProps<{
pagePattern: AutoProbeNavItem;
pageData?: any;
}>();
const t = translate;
// store
const store = useStore();
const { autoprobe: state } = store.state as RootStoreState;
const form = computed<AutoProbe>(() => state.form);
// Page pattern data
const pagePatternData = computed(() => form.value?.page_pattern);
// Stats
const fieldCount = computed(() => pagePatternData.value?.fields?.length || 0);
const listCount = computed(() => pagePatternData.value?.lists?.length || 0);
const hasPagination = computed(() => !!pagePatternData.value?.pagination);
// Check if we have top-level properties to display as tables
const topLevelArrays = computed(() => {
if (!props.pageData || typeof props.pageData !== 'object') return [];
const result = [];
for (const key in props.pageData) {
if (Array.isArray(props.pageData[key]) && props.pageData[key].length > 0) {
// Only include arrays that have objects inside (suitable for tables)
if (typeof props.pageData[key][0] === 'object' && props.pageData[key][0] !== null) {
result.push({
key,
data: props.pageData[key],
columns: getColumnsFromData(props.pageData[key])
});
}
}
}
return result;
});
// Helper function to extract columns from data
function getColumnsFromData(data: any[]): {prop: string; label: string}[] {
if (!data || !data.length) return [];
const firstItem = data[0];
if (typeof firstItem !== 'object' || firstItem === null) return [];
return Object.keys(firstItem).map(key => ({
prop: key,
label: key.charAt(0).toUpperCase() + key.slice(1) // Capitalize first letter
}));
}
// Format page data for display (for properties that aren't array tables)
const formattedPageData = computed(() => {
if (!props.pageData) return [];
// Convert the page data to a format suitable for display
const result = [];
const arrayKeys = topLevelArrays.value.map(item => item.key);
// If it's an object, display key-value pairs (excluding array tables)
if (typeof props.pageData === 'object' && !Array.isArray(props.pageData)) {
for (const key in props.pageData) {
// Skip keys that are already displayed as tables
if (arrayKeys.includes(key)) continue;
if (Object.prototype.hasOwnProperty.call(props.pageData, key)) {
const value = props.pageData[key];
let displayValue = value;
// Format based on value type
if (typeof value === 'object') {
try {
displayValue = JSON.stringify(value, null, 2);
} catch (e) {
displayValue = String(value);
}
const tableColumns = computed<TableColumns<AutoProbeNavItem>>(() => {
return [
{
key: 'name',
label: t('components.autoprobe.pagePattern.name'),
width: '200',
value: (row: AutoProbeNavItem) => {
let icon: Icon;
switch (row.type) {
case 'field':
icon = ['fa', 'tag'];
break;
case 'list':
icon = ['fa', 'list'];
break;
case 'pagination':
icon = ['fa', 'ellipsis-h'];
break;
default:
icon = ['fa', 'question'];
}
result.push({
key,
value: displayValue
});
}
}
}
// If it's an array but not suitable for tables
else if (Array.isArray(props.pageData)) {
try {
result.push({
key: 'array',
value: JSON.stringify(props.pageData, null, 2)
});
} catch (e) {
result.push({
key: 'array',
value: String(props.pageData)
});
}
return (
<ClNavLink onClick={() => {}}>
<span style={{ marginRight: '5px' }}>
<ClIcon icon={icon} />
</span>
{row.label}
</ClNavLink>
);
},
},
{
key: 'type',
label: t('components.autoprobe.pagePattern.type'),
width: '100',
value: (row: AutoProbeNavItem) => {
return t(`components.autoprobe.pagePattern.types.${row.type}`);
},
},
{
key: 'fields',
label: t('components.autoprobe.pagePattern.fieldCount'),
width: '80',
value: (row: AutoProbeNavItem) => {
if (row.type === 'list') {
return <ClNavLink label={row.fieldCount} onClick={() => {}} />;
}
},
},
{
key: 'selector',
label: t('components.autoprobe.pagePattern.selector'),
width: 'auto',
minWidth: '300',
value: (row: AutoProbeNavItem) => {
switch (row.type) {
case 'field':
return <ClAutoProbeSelector type="field" rule={row.field} />;
case 'pagination':
return <ClAutoProbeSelector type="pagination" rule={row.pagination} />;
}
},
},
] as TableColumns<AutoProbeNavItem>;
});
const tableData = computed<TableData<AutoProbeNavItem>>(() => {
const { pagePattern } = props;
if (!pagePattern?.children?.length) {
return [];
}
// For primitive types
else {
result.push({
key: 'value',
value: props.pageData
});
}
return result;
return pagePattern.children.map((item: AutoProbeNavItem) => {
switch (item.type) {
case 'list':
return {
name: item.name,
label: item.name,
type: item.type,
fieldCount: item.children?.length || 0,
};
case 'field':
return {
name: item.name,
label: item.name,
type: item.type,
field: item.field,
};
case 'pagination':
return {
name: item.name,
label: item.name,
type: item.type,
pagination: item.pagination,
};
default:
return {
name: item.name,
label: item.name,
type: item.type,
};
}
}) as TableData<AutoProbeNavItem>;
});
defineOptions({ name: 'ClAutoProbePagePatternDetail' });
@@ -122,186 +127,94 @@ defineOptions({ name: 'ClAutoProbePagePatternDetail' });
<template>
<div class="cl-autoprobe-page-pattern-detail">
<div class="header">
<h3>{{ t('components.autoprobe.pagePattern.title') }}</h3>
</div>
<div v-if="pagePatternData" class="content">
<el-descriptions :column="1" border>
<el-descriptions-item :label="t('components.autoprobe.pagePattern.name')">
{{ pagePatternData.name }}
</el-descriptions-item>
</el-descriptions>
<div class="stats-section">
<h4>{{ t('components.autoprobe.pagePattern.stats') }}</h4>
<el-row :gutter="20">
<el-col :span="8">
<div class="stat-card">
<div class="stat-value">{{ fieldCount }}</div>
<div class="stat-label">{{ t('components.autoprobe.pagePattern.fields') }}</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card">
<div class="stat-value">{{ listCount }}</div>
<div class="stat-label">{{ t('components.autoprobe.pagePattern.lists') }}</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card">
<div class="stat-value">{{ hasPagination ? '✓' : '✗' }}</div>
<div class="stat-label">{{ t('components.autoprobe.pagePattern.hasPagination') }}</div>
</div>
</el-col>
</el-row>
</div>
<!-- Array Tables Section - For data that can be displayed as tables -->
<template v-if="topLevelArrays.length">
<div v-for="(arrayInfo, index) in topLevelArrays" :key="index" class="page-data-section">
<h4>{{ arrayInfo.key }}</h4>
<div class="table">
<el-table
:data="arrayInfo.data"
border
style="width: 100%"
height="400"
highlight-current-row
>
<el-table-column
v-for="column in arrayInfo.columns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
sortable
>
<template #default="scope">
<template v-if="typeof scope.row[column.prop] !== 'object' || scope.row[column.prop] === null">
{{ scope.row[column.prop] }}
</template>
<pre v-else class="json-value">{{ JSON.stringify(scope.row[column.prop], null, 2) }}</pre>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<span class="total-items">
{{ `Total: ${arrayInfo.data.length} ${arrayInfo.data.length === 1 ? 'item' : 'items'}` }}
</span>
</div>
</div>
</div>
</template>
<!-- Other Page Data Section - For non-tabular data -->
<div v-if="formattedPageData.length" class="page-data-section">
<h4>{{ t('components.autoprobe.pagePattern.otherProperties') || 'Other Properties' }}</h4>
<el-table :data="formattedPageData" border style="width: 100%">
<el-table-column prop="key" :label="t('components.autoprobe.pageData.key') || 'Key'" width="200" />
<el-table-column :label="t('components.autoprobe.pageData.value') || 'Value'">
<template #default="scope">
<pre v-if="typeof scope.row.value === 'string' && (scope.row.value.startsWith('{') || scope.row.value.startsWith('['))" class="json-value">{{ scope.row.value }}</pre>
<span v-else>{{ scope.row.value }}</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div v-else class="not-found">
{{ t('components.autoprobe.pagePattern.notFound') || 'Page pattern details not found' }}
</div>
<cl-table :columns="tableColumns" :data="tableData" embedded hide-footer />
</div>
</template>
<style scoped>
.cl-autoprobe-page-pattern-detail {
padding: 16px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
.header {
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid var(--el-border-color-light);
h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
}
}
.content {
width: 100%;
.stats-section {
margin-top: 20px;
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 500;
}
.stat-card {
background-color: var(--el-fill-color-light);
border-radius: 4px;
padding: 16px;
text-align: center;
height: 100%;
.stat-value {
font-size: 24px;
font-weight: 500;
margin-bottom: 8px;
}
.stat-label {
color: var(--el-text-color-secondary);
font-size: 14px;
}
}
}
.page-data-section {
margin-top: 20px;
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 500;
}
}
.table {
width: 100%;
.el-table__inner-wrapper {
position: relative;
overflow: unset;
}
.el-table__header-wrapper {
position: sticky;
top: 0;
z-index: 1;
}
.table-footer {
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid var(--el-border-color);
.total-items {
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
}
.json-value {
margin: 0;
white-space: pre-wrap;
@@ -312,7 +225,7 @@ defineOptions({ name: 'ClAutoProbePagePatternDetail' });
overflow-y: auto;
}
}
.not-found {
color: var(--el-text-color-secondary);
font-style: italic;
@@ -320,4 +233,4 @@ defineOptions({ name: 'ClAutoProbePagePatternDetail' });
padding: 20px;
}
}
</style>
</style>

View File

@@ -121,7 +121,7 @@ defineOptions({ name: 'ClAutoProbePatternStats' });
<div v-else class="autoprobe-stats-empty">
<cl-tag
:icon="['fa', 'question-circle']"
label="No Pattern"
:label="t('common.status.unknown')"
type="info"
:clickable="clickable"
@click="emit('click')"
@@ -137,6 +137,5 @@ defineOptions({ name: 'ClAutoProbePatternStats' });
.autoprobe-stats-empty {
display: flex;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import { computed } from 'vue';
import { translate } from '@/utils';
const props = defineProps<{
type: 'field' | 'pagination';
rule: FieldRule | Pagination;
}>();
const t = translate;
const selectorIcon = computed<Icon>(() => {
const { rule } = props;
switch (rule.selector_type) {
case 'css':
return ['fab', 'css'];
case 'xpath':
return ['fa', 'code'];
case 'regex':
return ['fa', 'search'];
}
});
const selectorType = computed(() => {});
const extractionIcon = computed<Icon>(() => {
const { rule, type } = props;
if (type === 'field') {
switch ((rule as FieldRule).extraction_type) {
case 'attribute':
return ['fa', 'tag'];
case 'text':
return ['fa', 'font'];
case 'html':
return ['fa', 'code'];
default:
return ['fa', 'question'];
}
} else {
return ['fa', 'question'];
}
});
const extractionLabel = computed(() => {
const { rule, type } = props;
if (type === 'field') {
return (rule as FieldRule).extraction_type;
} else {
return '';
}
});
defineOptions({ name: 'ClAutoProbeSelector' });
</script>
<template>
<div class="selector">
<cl-tag
:icon="selectorIcon"
:label="rule.selector"
:tooltip="
t(
`components.autoprobe.pagePattern.selectorTypes.${rule.selector_type}`
)
"
/>
<template v-if="type === 'field'">
<span class="divider">
<cl-icon :icon="['fa', 'angle-right']" />
</span>
<cl-tag :icon="extractionIcon" :label="extractionLabel" />
</template>
</div>
</template>
<style scoped>
.selector {
display: flex;
align-items: center;
gap: 5px;
.divider {
color: var(--el-text-color-secondary);
font-size: 10px;
}
}
</style>

View File

@@ -20,12 +20,12 @@ import * as VariableNode from './ui/lexical/nodes/VariableNode';
import AssistantConsole from './core/ai/AssistantConsole.vue';
import AtomMaterialIcon from './ui/icon/AtomMaterialIcon.vue';
import AutoProbeFieldDetail from './core/autoprobe/AutoProbeFieldDetail.vue';
import AutoProbeFieldRule from './core/autoprobe/AutoProbeFieldRule.vue';
import AutoProbeForm from './core/autoprobe/AutoProbeForm.vue';
import AutoProbeListDetail from './core/autoprobe/AutoProbeListDetail.vue';
import AutoProbePagePatternDetail from './core/autoprobe/AutoProbePagePatternDetail.vue';
import AutoProbePaginationDetail from './core/autoprobe/AutoProbePaginationDetail.vue';
import AutoProbePatternStats from './core/autoprobe/AutoProbePatternStats.vue';
import AutoProbeSelector from './core/autoprobe/AutoProbeSelector.vue';
import AutoProbeTaskStatus from './core/autoprobe/AutoProbeTaskStatus.vue';
import BlockOptionsDropdownList from './ui/lexical/components/BlockOptionsDropdownList.vue';
import Box from './ui/box/Box.vue';
@@ -276,12 +276,12 @@ export {
AssistantConsole as ClAssistantConsole,
AtomMaterialIcon as ClAtomMaterialIcon,
AutoProbeFieldDetail as ClAutoProbeFieldDetail,
AutoProbeFieldRule as ClAutoProbeFieldRule,
AutoProbeForm as ClAutoProbeForm,
AutoProbeListDetail as ClAutoProbeListDetail,
AutoProbePagePatternDetail as ClAutoProbePagePatternDetail,
AutoProbePaginationDetail as ClAutoProbePaginationDetail,
AutoProbePatternStats as ClAutoProbePatternStats,
AutoProbeSelector as ClAutoProbeSelector,
AutoProbeTaskStatus as ClAutoProbeTaskStatus,
BlockOptionsDropdownList as ClBlockOptionsDropdownList,
Box as ClBox,

View File

@@ -74,12 +74,26 @@ const autoprobe: LComponentsAutoProbe = {
},
pagePattern: {
title: 'Page Pattern',
type: 'Type',
name: 'Name',
stats: 'Statistics',
fields: 'Fields',
lists: 'Lists',
hasPagination: 'Has Pagination',
notFound: 'Page pattern details not found',
fieldCount: 'Fields',
types: {
field: 'Field',
list: 'List',
pagination: 'Pagination',
},
selector: 'Selector',
selectorType: 'Selector Type',
selectorTypes: {
css: 'CSS',
xpath: 'XPath',
regex: 'Regex',
},
},
};

View File

@@ -74,12 +74,26 @@ const autoprobe: LComponentsAutoProbe = {
},
pagePattern: {
title: '页面模式',
type: '类型',
name: '名称',
stats: '统计',
fields: '字段',
lists: '列表',
hasPagination: '有分页',
notFound: '未找到页面模式详情',
fieldCount: '字段数',
types: {
field: '字段',
list: '列表',
pagination: '分页',
},
selector: '选择器',
selectorType: '选择器类型',
selectorTypes: {
css: 'CSS',
xpath: 'XPath',
regex: '正则表达式',
},
},
};

View File

@@ -74,11 +74,25 @@ interface LComponentsAutoProbe {
};
pagePattern: {
title: string;
type: string;
name: string;
stats: string;
fields: string;
lists: string;
hasPagination: string;
notFound: string;
fieldCount: string;
types: {
field: string;
list: string;
pagination: string;
};
selector: string;
selectorType: string;
selectorTypes: {
css: string;
xpath: string;
regex: string;
};
};
}

View File

@@ -19,12 +19,14 @@ export declare global {
type SelectorType = 'css' | 'xpath' | 'regex';
type ExtractType = 'text' | 'attribute' | 'html';
type PaginationType = 'next' | 'load' | 'scroll';
interface FieldRule {
interface BaseSelector {
name: string;
selector_type: SelectorType;
selector: string;
}
interface FieldRule extends BaseSelector {
extraction_type: ExtractType;
attribute_name?: string;
default_value?: string;
@@ -44,13 +46,7 @@ export declare global {
item_pattern: ItemPattern;
}
interface Pagination {
type: PaginationType;
selector_type?: SelectorType;
selector?: string;
max_pages?: number;
start_page?: number;
}
type Pagination = BaseSelector;
interface PagePattern {
name: string;
@@ -84,7 +80,18 @@ export declare global {
}
interface AutoProbeNavItem<T = any> extends NavItem<T> {
type?: 'page_pattern' | 'fields' | 'lists' | 'pagination' | 'list' | 'item' | 'field';
name?: string;
type?:
| 'page_pattern'
| 'fields'
| 'lists'
| 'pagination'
| 'list'
| 'item'
| 'field';
children?: AutoProbeNavItem[];
fieldCount?: number;
field?: FieldRule;
pagination?: Pagination;
}
}

View File

@@ -37,8 +37,10 @@ const processListItem = (list: ListRule): AutoProbeNavItem => {
children.push({
id: `${list.name}-${field.name}`,
label: field.name,
name: field.name,
icon: ['fa', 'tag'],
type: 'field',
field,
});
});
}
@@ -53,6 +55,7 @@ const processListItem = (list: ListRule): AutoProbeNavItem => {
return {
id: list.name,
label: `${list.name} (${children.length})`,
name: list.name,
type: 'list',
icon: ['fa', 'list'],
children,
@@ -69,8 +72,10 @@ const computedTreeItems = computed<AutoProbeNavItem[]>(() => {
children.push({
id: field.name,
label: field.name,
name: field.name,
icon: ['fa', 'tag'],
type: 'field',
field,
});
});
}
@@ -87,8 +92,10 @@ const computedTreeItems = computed<AutoProbeNavItem[]>(() => {
children.push({
id: 'pagination',
label: t('components.autoprobe.navItems.pagination'),
name: t('components.autoprobe.navItems.pagination'),
type: 'pagination',
icon: ['fa', 'pager'],
icon: ['fa', 'ellipsis-h'],
pagination: pagePagination.value,
});
}
@@ -96,6 +103,7 @@ const computedTreeItems = computed<AutoProbeNavItem[]>(() => {
{
id: 'page',
label: `${form.value.page_pattern.name} (${children.length})`,
name: form.value.page_pattern.name,
type: 'page_pattern',
icon: ['fa', 'network-wired'],
children,
@@ -120,10 +128,10 @@ const getFieldData = (fieldName: string) => {
// Function to get data for a specific navigation item
const getNavItemData = (item: AutoProbeNavItem) => {
if (!pageData.value) return undefined;
// Convert to Record<string, any> to handle dynamic property access
const data = pageData.value as Record<string, any>;
switch (item.type) {
case 'field':
// For fields, extract from page data by field name
@@ -330,28 +338,25 @@ defineOptions({ name: 'ClAutoProbeDetailTabPatterns' });
</div>
<div class="content">
<template v-if="activeNavItem?.type === 'field'">
<cl-auto-probe-field-detail
:field="activeNavItem"
:page-data="getNavItemData(activeNavItem)"
<cl-auto-probe-field-detail
:field="activeNavItem"
:page-data="getNavItemData(activeNavItem)"
/>
</template>
<template v-else-if="activeNavItem?.type === 'list'">
<cl-auto-probe-list-detail
:list="activeNavItem"
:page-data="getNavItemData(activeNavItem)"
<cl-auto-probe-list-detail
:list="activeNavItem"
:page-data="getNavItemData(activeNavItem)"
/>
</template>
<template v-else-if="activeNavItem?.type === 'pagination'">
<cl-auto-probe-pagination-detail
:pagination="activeNavItem"
:page-data="getNavItemData(activeNavItem)"
<cl-auto-probe-pagination-detail
:pagination="activeNavItem"
:page-data="getNavItemData(activeNavItem)"
/>
</template>
<template v-else-if="activeNavItem?.type === 'page_pattern'">
<cl-auto-probe-page-pattern-detail
:page-pattern="activeNavItem"
:page-data="getNavItemData(activeNavItem)"
/>
<cl-auto-probe-page-pattern-detail :page-pattern="activeNavItem" />
</template>
<div v-else class="placeholder">
{{
@@ -464,7 +469,6 @@ defineOptions({ name: 'ClAutoProbeDetailTabPatterns' });
.content {
flex: 1;
padding: 16px;
overflow: auto;
.detail-panel {

View File

@@ -11,7 +11,12 @@ import {
FILTER_OP_CONTAINS,
TABLE_COLUMN_NAME_ACTIONS,
} from '@/constants';
import { getIconByAction, onListFilterChangeByKey, translate } from '@/utils';
import {
getIconByAction,
onListFilterChangeByKey,
setupAutoUpdate,
translate,
} from '@/utils';
import {
ClNavLink,
ClAutoProbeTaskStatus,
@@ -30,7 +35,7 @@ const useAutoProbeList = () => {
const { commit } = store;
const { actionFunctions } = useList<AutoProbe>(ns, store);
const { deleteByIdConfirm } = actionFunctions;
const { getList, deleteByIdConfirm } = actionFunctions;
// nav actions
const navActions = computed<ListActionGroup[]>(() => [
@@ -185,6 +190,8 @@ const useAutoProbeList = () => {
] as TableColumns<AutoProbe>
);
setupAutoUpdate(getList);
return {
...useList<AutoProbe>(ns, store),
navActions,

View File

@@ -3,7 +3,12 @@ import { ref, computed, onBeforeMount } from 'vue';
import { ElSpace, ElMessage, ElMessageBox, ElCheckbox } from 'element-plus';
import { ClTag, ClNavLink, ClIcon } from '@/components';
import useRequest from '@/services/request';
import { getDefaultPagination, plainClone, translate } from '@/utils';
import {
EMPTY_OBJECT_ID,
getDefaultPagination,
plainClone,
translate,
} from '@/utils';
import {
ACTION_DELETE,
ACTION_EDIT,
@@ -37,7 +42,7 @@ const updateDefaultProviderId = async (id: string) => {
default_provider_id: id,
},
};
if (!settingAI.value) {
if (!settingAI.value?._id || settingAI.value._id === EMPTY_OBJECT_ID) {
await post('/settings/ai', { data });
} else {
await put('/settings/ai', { data });