feat: add AutoProbe components for enhanced data management

- Introduced new components: AutoProbeFieldDetail, AutoProbeFieldRule, AutoProbeListDetail, AutoProbePagePatternDetail, AutoProbePaginationDetail, and AutoProbePatternStats for detailed data representation.
- Updated index.ts to export new components for easier access.
- Enhanced internationalization support by adding relevant translations for new components and features.
- Added a new tab for patterns in the AutoProbe detail view to improve user navigation and data organization.
This commit is contained in:
Marvin Zhang
2025-05-13 17:58:57 +08:00
parent c62d53576e
commit 54ad06aff6
28 changed files with 2049 additions and 9 deletions

View File

@@ -0,0 +1,270 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useStore } from 'vuex';
import { translate } from '@/utils';
// i18n
const t = translate;
// props
const props = defineProps<{
field: AutoProbeNavItem;
pageData?: any;
}>();
// store
const store = useStore();
const { autoprobe: state } = store.state as RootStoreState;
const form = computed<AutoProbe>(() => state.form);
// Find the actual field data from the form based on the nav item
const fieldData = computed<FieldRule | undefined>(() => {
if (!form.value?.page_pattern) return undefined;
// If it's a direct field of the page pattern
if (props.field.id.indexOf('-') === -1) {
return form.value.page_pattern.fields?.find(f => f.name === props.field.label);
}
// If it's a field within a list, parse the ID
const [listName, fieldName] = props.field.id.split('-');
// Find the list first
const findList = (lists: ListRule[] | undefined): ListRule | undefined => {
if (!lists) return undefined;
for (const list of lists) {
if (list.name === listName) return list;
const nestedResult = findList(list.item_pattern?.lists);
if (nestedResult) return nestedResult;
}
return undefined;
};
const list = findList(form.value.page_pattern.lists);
if (!list || !list.item_pattern?.fields) return undefined;
return list.item_pattern.fields.find(f => f.name === fieldName);
});
// Display value for selector - if empty, it points to itself (self)
const displaySelector = computed(() => {
if (!fieldData.value?.selector) return t('components.autoprobe.field.self') || 'self';
return fieldData.value.selector;
});
// Determine if we have tabular data (array of objects)
const isTabularData = computed(() => {
if (!props.pageData) return false;
// Check if it's an array with objects inside
if (Array.isArray(props.pageData) && props.pageData.length > 0 &&
typeof props.pageData[0] === 'object' && props.pageData[0] !== null) {
return true;
}
return false;
});
// Get table data for tabular display
const tableData = computed(() => {
if (!props.pageData || !isTabularData.value) return [];
return props.pageData;
});
// Get table columns
const tableColumns = computed(() => {
if (!tableData.value.length) return [];
// Get the first item to extract columns
const firstItem = tableData.value[0];
if (typeof firstItem !== 'object' || firstItem === null) return [];
// Extract columns from object keys
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 non-tabular data)
const formattedPageData = computed(() => {
if (!props.pageData || isTabularData.value) return null;
let displayValue = props.pageData;
// Format based on type
if (typeof displayValue === 'object') {
// For object or array, stringify with formatting
try {
displayValue = JSON.stringify(displayValue, null, 2);
} catch (e) {
displayValue = String(displayValue);
}
}
return displayValue;
});
defineOptions({ name: 'ClAutoProbeFieldDetail' });
</script>
<template>
<div class="cl-autoprobe-field-detail">
<div class="header">
<h3>{{ t('components.autoprobe.field.title') }}: {{ field.label }}</h3>
</div>
<div v-if="fieldData" class="content">
<el-descriptions :column="1" border>
<el-descriptions-item :label="t('components.autoprobe.field.name')">
{{ fieldData.name }}
</el-descriptions-item>
<el-descriptions-item :label="t('components.autoprobe.field.selector')">
{{ displaySelector }}
</el-descriptions-item>
<el-descriptions-item :label="t('components.autoprobe.field.type')">
{{ fieldData.selector_type }}
</el-descriptions-item>
<el-descriptions-item :label="t('components.autoprobe.field.extractionType')">
{{ fieldData.extraction_type }}
</el-descriptions-item>
<el-descriptions-item :label="t('components.autoprobe.field.attributeName')" v-if="fieldData.attribute_name">
{{ fieldData.attribute_name }}
</el-descriptions-item>
<el-descriptions-item :label="t('components.autoprobe.field.defaultValue')" v-if="fieldData.default_value">
{{ fieldData.default_value }}
</el-descriptions-item>
</el-descriptions>
<!-- Tabular Data Display -->
<div v-if="isTabularData" class="page-data-section">
<h4>{{ t('components.autoprobe.field.data') || 'Field Data' }}</h4>
<div class="table">
<el-table
:data="tableData"
border
style="width: 100%"
height="400"
highlight-current-row
>
<el-table-column
v-for="column in tableColumns"
: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: ${tableData.length} ${tableData.length === 1 ? 'item' : 'items'}` }}
</span>
</div>
</div>
</div>
<!-- Single Value or Non-tabular Data -->
<div v-else-if="pageData" class="page-data-section">
<h4>{{ t('components.autoprobe.field.pageData') || 'Page Data' }}</h4>
<el-card shadow="never" class="page-data-card">
<pre v-if="typeof formattedPageData === 'string' && (formattedPageData.startsWith('{') || formattedPageData.startsWith('['))" class="json-value">{{ formattedPageData }}</pre>
<span v-else>{{ formattedPageData }}</span>
</el-card>
</div>
</div>
<div v-else class="not-found">
{{ t('components.autoprobe.field.notFound') || 'Field details not found' }}
</div>
</div>
</template>
<style scoped>
.cl-autoprobe-field-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%;
.page-data-section {
margin-top: 20px;
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 500;
}
.page-data-card {
border: 1px solid var(--el-border-color-light);
}
}
.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;
word-break: break-word;
font-family: monospace;
font-size: 12px;
max-height: 150px;
overflow-y: auto;
}
}
.not-found {
color: var(--el-text-color-secondary);
font-style: italic;
text-align: center;
padding: 20px;
}
}
</style>

View File

@@ -0,0 +1,53 @@
<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

@@ -0,0 +1,359 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useStore } from 'vuex';
import { translate } from '@/utils';
// i18n
const t = translate;
// props
const props = defineProps<{
list: AutoProbeNavItem;
pageData?: any;
}>();
// store
const store = useStore();
const { autoprobe: state } = store.state as RootStoreState;
const form = computed<AutoProbe>(() => state.form);
// Find the actual list data from the form based on the nav item
const listData = computed<ListRule | undefined>(() => {
if (!form.value?.page_pattern) return undefined;
const listName = props.list.id;
// Find the list recursively
const findList = (lists: ListRule[] | undefined): ListRule | undefined => {
if (!lists) return undefined;
for (const list of lists) {
if (list.name === listName) return list;
const nestedResult = findList(list.item_pattern?.lists);
if (nestedResult) return nestedResult;
}
return undefined;
};
return findList(form.value.page_pattern.lists);
});
// Get raw array data for direct table display (when page data is an array)
const tableData = computed(() => {
if (!props.pageData) return [];
// If page data is an array, it's likely the list data itself
if (Array.isArray(props.pageData)) {
return props.pageData;
}
// If it's an object with a property matching the list id, try that
else if (typeof props.pageData === 'object' && props.pageData !== null &&
Array.isArray(props.pageData[props.list.id])) {
return props.pageData[props.list.id];
}
return [];
});
// Get table columns dynamically based on data
const tableColumns = computed(() => {
if (!tableData.value.length) return [];
// Get the first item to determine columns
const firstItem = tableData.value[0];
if (typeof firstItem !== 'object' || firstItem === null) {
// Simple value array
return [{ prop: 'value', label: 'Value' }];
}
// Extract columns from object properties
return Object.keys(firstItem).map(key => ({
prop: key,
label: key.charAt(0).toUpperCase() + key.slice(1) // Capitalize first letter
}));
});
// Determine if we should show the data table
const showDataTable = computed(() => {
return tableData.value.length > 0 && tableColumns.value.length > 0;
});
// Format page data for display (key-value format for non-array data)
const formattedPageData = computed(() => {
if (!props.pageData || showDataTable.value) return [];
// Convert the page data to a format suitable for display
const result = [];
// If it's an object, display key-value pairs
if (typeof props.pageData === 'object' && !Array.isArray(props.pageData)) {
for (const key in props.pageData) {
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);
}
}
result.push({
key,
value: displayValue
});
}
}
}
// If it's an array but not suitable for table display, process each item
else if (Array.isArray(props.pageData)) {
props.pageData.forEach((item, index) => {
let displayValue = item;
// Format based on type
if (typeof item === 'object') {
try {
displayValue = JSON.stringify(item, null, 2);
} catch (e) {
displayValue = String(item);
}
}
result.push({
key: `Item ${index + 1}`,
value: displayValue
});
});
}
// For primitive types
else {
result.push({
key: 'value',
value: props.pageData
});
}
return result;
});
// Compute fields and nested lists for display
const fields = computed(() => listData.value?.item_pattern?.fields || []);
const nestedLists = computed(() => listData.value?.item_pattern?.lists || []);
// Display value for list selector - if empty, it points to itself (self)
const displayListSelector = computed(() => {
if (!listData.value?.list_selector) return t('components.autoprobe.list.self') || 'self';
return listData.value.list_selector;
});
// Display value for item selector - if empty, it points to itself (self)
const displayItemSelector = computed(() => {
if (!listData.value?.item_selector) return t('components.autoprobe.list.self') || 'self';
return listData.value.item_selector;
});
// Process fields for display, marking empty selectors as "self"
const processedFields = computed(() => {
return fields.value.map(field => ({
...field,
displaySelector: field.selector || (t('components.autoprobe.field.self') || 'self')
}));
});
// Process nested lists for display, marking empty selectors as "self"
const processedNestedLists = computed(() => {
return nestedLists.value.map(nestedList => ({
...nestedList,
displayListSelector: nestedList.list_selector || (t('components.autoprobe.list.self') || 'self')
}));
});
defineOptions({ name: 'ClAutoProbeListDetail' });
</script>
<template>
<div class="cl-autoprobe-list-detail">
<div class="header">
<h3>{{ t('components.autoprobe.list.title') }}: {{ list.label }}</h3>
</div>
<div v-if="listData" class="content">
<el-descriptions :column="1" border>
<el-descriptions-item :label="t('components.autoprobe.list.name')">
{{ listData.name }}
</el-descriptions-item>
<el-descriptions-item :label="t('components.autoprobe.list.listSelector')">
{{ displayListSelector }}
</el-descriptions-item>
<el-descriptions-item :label="t('components.autoprobe.list.listSelectorType')">
{{ listData.list_selector_type }}
</el-descriptions-item>
<el-descriptions-item :label="t('components.autoprobe.list.itemSelector')">
{{ displayItemSelector }}
</el-descriptions-item>
<el-descriptions-item :label="t('components.autoprobe.list.itemSelectorType')">
{{ listData.item_selector_type }}
</el-descriptions-item>
</el-descriptions>
<div v-if="fields.length" class="fields-section">
<h4>{{ t('components.autoprobe.list.fields') }}</h4>
<el-table :data="processedFields" border style="width: 100%">
<el-table-column prop="name" :label="t('components.autoprobe.field.name')" width="200" />
<el-table-column :label="t('components.autoprobe.field.selector')">
<template #default="scope">
{{ scope.row.displaySelector }}
</template>
</el-table-column>
</el-table>
</div>
<div v-if="nestedLists.length" class="nested-lists-section">
<h4>{{ t('components.autoprobe.list.nestedLists') }}</h4>
<el-table :data="processedNestedLists" border style="width: 100%">
<el-table-column prop="name" :label="t('components.autoprobe.list.name')" width="200" />
<el-table-column :label="t('components.autoprobe.list.listSelector')">
<template #default="scope">
{{ scope.row.displayListSelector }}
</template>
</el-table-column>
</el-table>
</div>
<!-- Dynamic Data Table for List Items -->
<div v-if="showDataTable" class="page-data-section">
<h4>{{ t('components.autoprobe.list.items') || 'List Items' }}</h4>
<div class="table">
<el-table
:data="tableData"
border
style="width: 100%"
height="400"
highlight-current-row
>
<el-table-column
v-for="column in tableColumns"
: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: ${tableData.length} ${tableData.length === 1 ? 'item' : 'items'}` }}
</span>
</div>
</div>
</div>
<!-- Page Data Section (for non-tabular data) -->
<div v-else-if="formattedPageData.length" class="page-data-section">
<h4>{{ t('components.autoprobe.list.pageData') || 'Page Data' }}</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.list.notFound') || 'List details not found' }}
</div>
</div>
</template>
<style scoped>
.cl-autoprobe-list-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%;
.fields-section,
.nested-lists-section,
.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;
word-break: break-word;
font-family: monospace;
font-size: 12px;
max-height: 150px;
overflow-y: auto;
}
}
.not-found {
color: var(--el-text-color-secondary);
font-style: italic;
text-align: center;
padding: 20px;
}
}
</style>

View File

@@ -0,0 +1,323 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useStore } from 'vuex';
import { translate } from '@/utils';
// i18n
const t = translate;
// props
const props = defineProps<{
pagePattern: AutoProbeNavItem;
pageData?: any;
}>();
// 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);
}
}
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)
});
}
}
// For primitive types
else {
result.push({
key: 'value',
value: props.pageData
});
}
return result;
});
defineOptions({ name: 'ClAutoProbePagePatternDetail' });
</script>
<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>
</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;
word-break: break-word;
font-family: monospace;
font-size: 12px;
max-height: 150px;
overflow-y: auto;
}
}
.not-found {
color: var(--el-text-color-secondary);
font-style: italic;
text-align: center;
padding: 20px;
}
}
</style>

View File

@@ -0,0 +1,138 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useStore } from 'vuex';
import { translate } from '@/utils';
// i18n
const t = translate;
// props
const props = defineProps<{
pagination: AutoProbeNavItem;
pageData?: any;
}>();
// store
const store = useStore();
const { autoprobe: state } = store.state as RootStoreState;
const form = computed<AutoProbe>(() => state.form);
// Pagination data
const paginationData = computed(() => form.value?.page_pattern?.pagination);
// Format page data for display
const formattedPageData = computed(() => {
if (!props.pageData) return null;
let displayValue = props.pageData;
// Format based on type
if (typeof displayValue === 'object') {
// For object or array, stringify with formatting
try {
displayValue = JSON.stringify(displayValue, null, 2);
} catch (e) {
displayValue = String(displayValue);
}
}
return displayValue;
});
defineOptions({ name: 'ClAutoProbePaginationDetail' });
</script>
<template>
<div class="cl-autoprobe-pagination-detail">
<div class="header">
<h3>{{ t('components.autoprobe.pagination.title') }}</h3>
</div>
<div v-if="paginationData" class="content">
<el-descriptions :column="1" border>
<el-descriptions-item :label="t('components.autoprobe.pagination.type')">
{{ paginationData.type }}
</el-descriptions-item>
<el-descriptions-item :label="t('components.autoprobe.pagination.selectorType')" v-if="paginationData.selector_type">
{{ paginationData.selector_type }}
</el-descriptions-item>
<el-descriptions-item :label="t('components.autoprobe.pagination.selector')" v-if="paginationData.selector">
{{ paginationData.selector }}
</el-descriptions-item>
<el-descriptions-item :label="t('components.autoprobe.pagination.maxPages')" v-if="paginationData.max_pages">
{{ paginationData.max_pages }}
</el-descriptions-item>
<el-descriptions-item :label="t('components.autoprobe.pagination.startPage')" v-if="paginationData.start_page">
{{ paginationData.start_page }}
</el-descriptions-item>
</el-descriptions>
<!-- Page Data Section -->
<div v-if="pageData" class="page-data-section">
<h4>{{ t('components.autoprobe.pagination.pageData') || 'Page Data' }}</h4>
<el-card shadow="never" class="page-data-card">
<pre v-if="typeof formattedPageData === 'string' && formattedPageData.startsWith('{')" class="json-value">{{ formattedPageData }}</pre>
<pre v-else-if="typeof formattedPageData === 'string' && formattedPageData.startsWith('[')" class="json-value">{{ formattedPageData }}</pre>
<span v-else>{{ formattedPageData }}</span>
</el-card>
</div>
</div>
<div v-else class="not-found">
{{ t('components.autoprobe.pagination.notFound') || 'Pagination details not found' }}
</div>
</div>
</template>
<style scoped>
.cl-autoprobe-pagination-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%;
.page-data-section {
margin-top: 20px;
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 500;
}
.page-data-card {
border: 1px solid var(--el-border-color-light);
}
}
.json-value {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-family: monospace;
font-size: 12px;
}
}
.not-found {
color: var(--el-text-color-secondary);
font-style: italic;
text-align: center;
padding: 20px;
}
}
</style>

View File

@@ -0,0 +1,142 @@
<script setup lang="ts">
import { computed } from 'vue';
import { translate } from '@/utils';
const props = defineProps<{
autoprobe: AutoProbe;
clickable?: boolean;
}>();
const emit = defineEmits<{
(e: 'click'): void;
}>();
// i18n
const t = translate;
/**
* Recursively counts all lists in a given pattern (including nested lists)
*/
const countAllLists = (
pattern: ItemPattern | PagePattern | undefined
): number => {
if (!pattern || (!pattern.lists && !pattern.fields)) return 0;
// Count top-level lists
let count = pattern.lists?.length || 0;
// Count nested lists in each list's item pattern
if (pattern.lists) {
for (const list of pattern.lists) {
count += countAllLists(list.item_pattern);
}
}
return count;
};
/**
* Recursively counts all fields in a given pattern (including fields in nested lists)
*/
const countAllFields = (
pattern: ItemPattern | PagePattern | undefined
): number => {
if (!pattern || (!pattern.lists && !pattern.fields)) return 0;
// Count top-level fields
let count = pattern.fields?.length || 0;
// Count fields in nested lists
if (pattern.lists) {
for (const list of pattern.lists) {
count += countAllFields(list.item_pattern);
}
}
return count;
};
// Computed statistics
const hasPattern = computed(() => {
return !!props.autoprobe?.page_pattern;
});
const totalFields = computed(() => {
return countAllFields(props.autoprobe?.page_pattern);
});
const totalLists = computed(() => {
return countAllLists(props.autoprobe?.page_pattern);
});
const hasPagination = computed(() => {
return !!props.autoprobe?.page_pattern?.pagination;
});
const paginationType = computed(() => {
if (!hasPagination.value) return null;
return props.autoprobe?.page_pattern?.pagination?.type;
});
defineOptions({ name: 'ClAutoProbePatternStats' });
</script>
<template>
<div v-if="hasPattern" class="autoprobe-stats">
<cl-tag
:icon="['fa', 'list']"
:label="totalFields.toString()"
:tooltip="t('components.autoprobe.stats.totalFields')"
type="primary"
:clickable="clickable"
@click="emit('click')"
/>
<cl-tag
:icon="['fa', 'table']"
:label="totalLists.toString()"
:tooltip="t('components.autoprobe.stats.totalLists')"
type="success"
:clickable="clickable"
@click="emit('click')"
/>
<cl-tag
v-if="hasPagination"
:icon="['fa', 'copy']"
:label="paginationType"
:tooltip="t('components.autoprobe.stats.paginationType')"
type="warning"
:clickable="clickable"
@click="emit('click')"
/>
<cl-tag
v-else
:icon="['fa', 'copy']"
label="none"
:tooltip="t('components.autoprobe.stats.noPagination')"
type="info"
:clickable="clickable"
@click="emit('click')"
/>
</div>
<div v-else class="autoprobe-stats-empty">
<cl-tag
:icon="['fa', 'question-circle']"
label="No Pattern"
type="info"
:clickable="clickable"
@click="emit('click')"
/>
</div>
</template>
<style scoped>
.autoprobe-stats {
display: flex;
gap: 10px;
}
.autoprobe-stats-empty {
display: flex;
justify-content: center;
}
</style>

View File

@@ -19,7 +19,13 @@ import * as theme from './ui/lexical/utils/theme';
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 AutoProbeTaskStatus from './core/autoprobe/AutoProbeTaskStatus.vue';
import BlockOptionsDropdownList from './ui/lexical/components/BlockOptionsDropdownList.vue';
import Box from './ui/box/Box.vue';
@@ -269,7 +275,13 @@ export {
VariableNode as VariableNode,
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,
AutoProbeTaskStatus as ClAutoProbeTaskStatus,
BlockOptionsDropdownList as ClBlockOptionsDropdownList,
Box as ClBox,

View File

@@ -25,3 +25,4 @@ export const TAB_NAME_USERS = 'users';
export const TAB_NAME_ROLES = 'roles';
export const TAB_NAME_PERMISSIONS = 'permissions';
export const TAB_NAME_PAGES = 'pages';
export const TAB_NAME_PATTERNS = 'patterns';

View File

@@ -144,6 +144,7 @@ const common: LCommon = {
indexes: 'Indexes',
results: 'Results',
output: 'Output',
patterns: 'Patterns',
},
status: {
unassigned: 'Unassigned',

View File

@@ -24,6 +24,63 @@ const autoprobe: LComponentsAutoProbe = {
},
},
},
stats: {
totalFields: 'Total Fields',
totalLists: 'Total Lists',
paginationType: 'Pagination Type',
noPagination: 'No Pagination',
},
navItems: {
lists: 'Lists',
fields: 'Fields',
pagination: 'Pagination',
list: 'List',
field: 'Field',
},
patterns: {
selectItem: 'Select an item to view details',
},
field: {
title: 'Field',
name: 'Name',
selector: 'Selector',
type: 'Selector Type',
extractionType: 'Extraction Type',
attributeName: 'Attribute Name',
defaultValue: 'Default Value',
notFound: 'Field details not found',
self: 'self (points to itself)',
},
list: {
title: 'List',
name: 'Name',
listSelector: 'List Selector',
listSelectorType: 'List Selector Type',
itemSelector: 'Item Selector',
itemSelectorType: 'Item Selector Type',
fields: 'Fields',
nestedLists: 'Nested Lists',
notFound: 'List details not found',
self: 'self (points to itself)',
},
pagination: {
title: 'Pagination',
type: 'Type',
selectorType: 'Selector Type',
selector: 'Selector',
maxPages: 'Max Pages',
startPage: 'Start Page',
notFound: 'Pagination details not found',
},
pagePattern: {
title: 'Page Pattern',
name: 'Name',
stats: 'Statistics',
fields: 'Fields',
lists: 'Lists',
hasPagination: 'Has Pagination',
notFound: 'Page pattern details not found',
},
};
export default autoprobe;

View File

@@ -236,6 +236,7 @@ const layouts: LLayouts = {
tabs: {
overview: 'Overview',
tasks: 'Task',
patterns: 'Patterns',
},
},
}

View File

@@ -5,6 +5,8 @@ const autoprobe: LViewsAutoProbe = {
url: 'URL',
query: 'Query',
status: 'Status',
lastTask: 'Last Task',
patterns: 'Patterns',
},
},
navActions: {

View File

@@ -144,6 +144,7 @@ const common: LCommon = {
indexes: '索引',
results: '结果',
output: '输出',
patterns: '模式',
},
status: {
unassigned: '未指定',

View File

@@ -24,6 +24,63 @@ const autoprobe: LComponentsAutoProbe = {
},
},
},
stats: {
totalFields: '字段总数',
totalLists: '列表总数',
paginationType: '分页类型',
noPagination: '无分页',
},
navItems: {
lists: '列表',
fields: '字段',
pagination: '分页',
list: '列表',
field: '字段',
},
patterns: {
selectItem: '选择一个项目查看详情',
},
field: {
title: '字段',
name: '名称',
selector: '选择器',
type: '选择器类型',
extractionType: '提取类型',
attributeName: '属性名称',
defaultValue: '默认值',
notFound: '未找到字段详情',
self: '自身指向自己',
},
list: {
title: '列表',
name: '名称',
listSelector: '列表选择器',
listSelectorType: '列表选择器类型',
itemSelector: '项目选择器',
itemSelectorType: '项目选择器类型',
fields: '字段',
nestedLists: '嵌套列表',
notFound: '未找到列表详情',
self: '自身指向自己',
},
pagination: {
title: '分页',
type: '类型',
selectorType: '选择器类型',
selector: '选择器',
maxPages: '最大页数',
startPage: '起始页',
notFound: '未找到分页详情',
},
pagePattern: {
title: '页面模式',
name: '名称',
stats: '统计',
fields: '字段',
lists: '列表',
hasPagination: '有分页',
notFound: '未找到页面模式详情',
},
};
export default autoprobe;

View File

@@ -236,6 +236,7 @@ const layouts: LLayouts = {
tabs: {
overview: '概览',
tasks: '任务',
patterns: '模式',
},
},
},

View File

@@ -5,6 +5,8 @@ const autoprobe: LViewsAutoProbe = {
url: 'URL',
query: '查询',
status: '状态',
lastTask: '最近任务',
patterns: '模式',
},
},
navActions: {

View File

@@ -157,6 +157,7 @@ export declare global {
indexes: string;
results: string;
output: string;
patterns: string;
};
status: {
unassigned: string;

View File

@@ -24,4 +24,61 @@ interface LComponentsAutoProbe {
};
};
};
stats: {
totalFields: string;
totalLists: string;
paginationType: string;
noPagination: string;
};
navItems: {
lists: string;
fields: string;
pagination: string;
list: string;
field: string;
};
patterns: {
selectItem: string;
};
field: {
title: string;
name: string;
selector: string;
type: string;
extractionType: string;
attributeName: string;
defaultValue: string;
notFound: string;
self: string;
};
list: {
title: string;
name: string;
listSelector: string;
listSelectorType: string;
itemSelector: string;
itemSelectorType: string;
fields: string;
nestedLists: string;
notFound: string;
self: string;
};
pagination: {
title: string;
type: string;
selectorType: string;
selector: string;
maxPages: string;
startPage: string;
notFound: string;
};
pagePattern: {
title: string;
name: string;
stats: string;
fields: string;
lists: string;
hasPagination: string;
notFound: string;
};
}

View File

@@ -125,6 +125,7 @@ export declare global {
autoprobe: LListLayoutPage<{
overview: string;
tasks: string;
patterns: string;
}>;
};
}

View File

@@ -5,6 +5,8 @@ interface LViewsAutoProbe {
url: string;
query: string;
status: string;
lastTask: string;
patterns: string;
};
};
navActions: LNavActions & {

View File

@@ -3,9 +3,19 @@ export declare global {
name?: string;
url?: string;
query?: string;
last_task_id?: string;
last_task?: AutoProbeTask;
default_task_id?: string;
page_pattern?: PagePattern;
page_data?: PageData;
}
type AutoProbeTaskStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
type AutoProbeTaskStatus =
| 'pending'
| 'running'
| 'completed'
| 'failed'
| 'cancelled';
type SelectorType = 'css' | 'xpath' | 'regex';
type ExtractType = 'text' | 'attribute' | 'html';
@@ -72,4 +82,9 @@ export declare global {
url: string;
html?: string;
}
interface AutoProbeNavItem<T = any> extends NavItem<T> {
type?: 'page_pattern' | 'fields' | 'lists' | 'pagination' | 'list' | 'item' | 'field';
children?: AutoProbeNavItem[];
}
}

View File

@@ -293,10 +293,6 @@ defineOptions({ name: 'ClListLayout' });
}
}
&:deep(.tag) {
margin-right: 10px;
}
.nav-actions {
max-height: 52px;
background-color: var(--cl-container-white-bg);

View File

@@ -1,9 +1,14 @@
import { TAB_NAME_OVERVIEW, TAB_NAME_TASKS } from '@/constants/tab';
import {
TAB_NAME_OVERVIEW,
TAB_NAME_PATTERNS,
TAB_NAME_TASKS,
} from '@/constants/tab';
import {
ClAutoProbeList,
ClAutoProbeDetail,
ClAutoProbeDetailTabOverview,
ClAutoProbeDetailTabTasks,
ClAutoProbeDetailTabPatterns,
} from '@/views';
import { getIconByTabName, translate } from '@/utils';
import { RouteLocation } from 'vue-router';
@@ -42,6 +47,12 @@ export default [
icon: getIconByTabName(TAB_NAME_TASKS),
component: async () => ClAutoProbeDetailTabTasks,
},
{
path: TAB_NAME_PATTERNS,
title: t('layouts.routes.autoprobe.detail.tabs.patterns'),
icon: getIconByTabName(TAB_NAME_PATTERNS),
component: async () => ClAutoProbeDetailTabPatterns,
},
],
},
] as Array<ExtendedRouterRecord>;

View File

@@ -4,7 +4,11 @@ import {
getDefaultStoreMutations,
getDefaultStoreState,
} from '@/utils/store';
import { TAB_NAME_OVERVIEW, TAB_NAME_TASKS } from '@/constants/tab';
import {
TAB_NAME_OVERVIEW,
TAB_NAME_PATTERNS,
TAB_NAME_TASKS,
} from '@/constants/tab';
import { translate } from '@/utils/i18n';
import useRequest from '@/services/request';
@@ -18,6 +22,7 @@ const state = {
tabs: [
{ id: TAB_NAME_OVERVIEW, title: t('common.tabs.overview') },
{ id: TAB_NAME_TASKS, title: t('common.tabs.tasks') },
{ id: TAB_NAME_PATTERNS, title: t('common.tabs.patterns') },
],
} as AutoProbeStoreState;

View File

@@ -58,6 +58,7 @@ import {
TAB_NAME_OUTPUT,
TAB_NAME_OVERVIEW,
TAB_NAME_PAGES,
TAB_NAME_PATTERNS,
TAB_NAME_PERMISSIONS,
TAB_NAME_RESULTS,
TAB_NAME_ROLES,
@@ -127,6 +128,8 @@ export const getIconByTabName = (tabName: string): Icon => {
return ['fa', 'user-check'];
case TAB_NAME_PAGES:
return ['fa', 'file-alt'];
case TAB_NAME_PATTERNS:
return ['fa', 'network-wired'];
default:
return ['fa', 'circle'];
}

View File

@@ -0,0 +1,486 @@
<script setup lang="ts">
import { computed, ref, watch, onMounted } from 'vue';
import { useStore } from 'vuex';
import { plainClone, translate, selectElement } from '@/utils';
import { ElTree, ElInput, ElScrollbar } from 'element-plus';
import type { FilterNodeMethodFunction } from 'element-plus/es/components/tree/src/tree.type';
import { debounce } from 'lodash';
// i18n
const t = translate;
// store
const store = useStore();
const { autoprobe: state } = store.state as RootStoreState;
// form data
const form = computed<AutoProbe>(() => state.form);
const pageFields = computed(() => form.value?.page_pattern?.fields);
const pageLists = computed(() => form.value?.page_pattern?.lists);
const pagePagination = computed(() => form.value?.page_pattern?.pagination);
const pageData = computed(() => form.value?.page_data || {});
const treeRef = ref<InstanceType<typeof ElTree>>();
const searchKeyword = ref('');
const showSearch = ref(false);
const activeNavItem = ref<AutoProbeNavItem>();
const defaultExpandedKeys = ref<string[]>([]);
// Helper function to recursively process list items
const processListItem = (list: ListRule): AutoProbeNavItem => {
const children: AutoProbeNavItem[] = [];
// Add fields directly if they exist
if (list.item_pattern?.fields && list.item_pattern.fields.length > 0) {
list.item_pattern.fields.forEach((field: FieldRule) => {
children.push({
id: `${list.name}-${field.name}`,
label: field.name,
icon: ['fa', 'tag'],
type: 'field',
});
});
}
// Recursively process nested lists if they exist
if (list.item_pattern?.lists && list.item_pattern.lists.length > 0) {
list.item_pattern.lists.forEach((nestedList: ListRule) => {
children.push(processListItem(nestedList));
});
}
return {
id: list.name,
label: `${list.name} (${children.length})`,
type: 'list',
icon: ['fa', 'list'],
children,
} as AutoProbeNavItem;
};
const computedTreeItems = computed<AutoProbeNavItem[]>(() => {
if (!form.value?.page_pattern) return [];
const children: AutoProbeNavItem[] = [];
// Add fields directly if they exist
if (pageFields.value) {
pageFields.value.forEach(field => {
children.push({
id: field.name,
label: field.name,
icon: ['fa', 'tag'],
type: 'field',
});
});
}
// Add lists directly if they exist
if (pageLists.value) {
pageLists.value.forEach(list => {
children.push(processListItem(list));
});
}
// Add pagination if it exists
if (pagePagination.value) {
children.push({
id: 'pagination',
label: t('components.autoprobe.navItems.pagination'),
type: 'pagination',
icon: ['fa', 'pager'],
});
}
return [
{
id: 'page',
label: `${form.value.page_pattern.name} (${children.length})`,
type: 'page_pattern',
icon: ['fa', 'network-wired'],
children,
},
];
});
const treeItems = ref<AutoProbeNavItem[]>([]);
watch(
() => state.form,
() => {
treeItems.value = plainClone(computedTreeItems.value);
}
);
// Function to get field data from page data
const getFieldData = (fieldName: string) => {
if (!pageData.value) return undefined;
// Use type assertion to treat pageData as a record with string keys
return (pageData.value as Record<string, any>)[fieldName];
};
// 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
return getFieldData(item.id);
case 'list':
// For lists, get the corresponding array in page data
return data[item.id];
case 'pagination':
// For pagination, extract pagination-related data
return data.pagination;
case 'page_pattern':
// Return all page data for page pattern
return pageData.value;
default:
return undefined;
}
};
// Context menu functionality
const activeContextMenuNavItem = ref<AutoProbeNavItem>();
const contextMenuVisibleMap = ref<Record<string, boolean>>({});
const isContextMenuVisible = (id: string) => {
if (!contextMenuItems.value?.length) return false;
if (!activeContextMenuNavItem.value) return false;
if (activeContextMenuNavItem.value?.id !== id) return false;
return contextMenuVisibleMap.value[id] || false;
};
const onActionsClick = (item: AutoProbeNavItem) => {
activeContextMenuNavItem.value = item;
contextMenuVisibleMap.value[item.id] = true;
};
const onContextMenuHide = (id: string) => {
activeContextMenuNavItem.value = undefined;
contextMenuVisibleMap.value[id] = false;
};
const contextMenuItems = computed(() => {
if (!activeContextMenuNavItem.value) return [];
const { id, type } = activeContextMenuNavItem.value;
if (!contextMenuVisibleMap.value[id]) return [];
switch (type) {
case 'field':
return [
{
title: t('common.actions.view'),
icon: ['fa', 'eye'],
action: () =>
selectNode(activeContextMenuNavItem.value as AutoProbeNavItem),
},
];
case 'list':
return [
{
title: t('common.actions.view'),
icon: ['fa', 'eye'],
action: () =>
selectNode(activeContextMenuNavItem.value as AutoProbeNavItem),
},
];
default:
return [];
}
});
// Search functionality
const onSearchFilter: FilterNodeMethodFunction = (value, data) => {
if (!value) return true;
return data.label.toLowerCase().includes(value.toLowerCase());
};
const debouncedFilter = debounce(() => {
treeRef.value?.filter(searchKeyword.value);
}, 300);
watch(searchKeyword, debouncedFilter);
// Node selection and expansion tracking
const onNodeClick = async (data: AutoProbeNavItem) => {
await selectNode(data);
};
const selectNode = async (data: AutoProbeNavItem) => {
const { id } = data;
activeNavItem.value = data;
// Display content in the right panel based on selected node
// (Implementation would depend on what content you want to show)
// Highlight current node
setTimeout(() => {
treeRef.value?.setCurrentKey(id);
}, 0);
};
const onNodeExpand = (data: AutoProbeNavItem) => {
defaultExpandedKeys.value.push(data.id);
};
const onNodeCollapse = (data: AutoProbeNavItem) => {
const idx = defaultExpandedKeys.value.findIndex(id => id === data.id);
defaultExpandedKeys.value.splice(idx, 1);
};
const onContextMenuClick = (event: MouseEvent, data: AutoProbeNavItem) => {
event.stopPropagation();
activeContextMenuNavItem.value = data;
contextMenuVisibleMap.value[data.id] = true;
};
const onSearchClick = () => {
showSearch.value = !showSearch.value;
};
const onRefresh = () => {
// Refresh data if needed
treeItems.value = plainClone(computedTreeItems.value);
};
// Sidebar resizing
const widthKey = ref('autoprobe.sidebar.width');
const sidebarRef = ref<HTMLElement | null>(null);
onMounted(() => {
treeItems.value = plainClone(computedTreeItems.value);
});
defineOptions({ name: 'ClAutoProbeDetailTabPatterns' });
</script>
<template>
<div class="autoprobe-detail-tab-patterns">
<div ref="sidebarRef" class="sidebar">
<cl-resize-handle :target-ref="sidebarRef" :size-key="widthKey" />
<div class="sidebar-actions">
<cl-icon :icon="['fa', 'refresh']" @click="onRefresh" />
<cl-icon
:class="showSearch ? 'selected' : ''"
:icon="['fa', 'search']"
@click="onSearchClick"
/>
</div>
<div v-if="showSearch" class="sidebar-search">
<el-input
v-model="searchKeyword"
:placeholder="t('common.search.placeholder')"
clearable
@clear="
() => {
searchKeyword = '';
showSearch = false;
}
"
/>
</div>
<el-scrollbar>
<el-tree
ref="treeRef"
node-key="id"
:data="treeItems"
:filter-node-method="onSearchFilter"
:expand-on-click-node="false"
:default-expanded-keys="defaultExpandedKeys"
highlight-current
@node-click="onNodeClick"
@node-contextmenu="onContextMenuClick"
@node-expand="onNodeExpand"
@node-collapse="onNodeCollapse"
>
<template #default="{ data }">
<cl-context-menu
:visible="isContextMenuVisible(data.id)"
:style="{ flex: 1, paddingRight: '5px' }"
>
<template #reference>
<div class="node-wrapper" :title="data.label">
<span class="icon-wrapper">
<cl-icon
v-if="data.loading"
:icon="['fa', 'spinner']"
spinning
/>
<cl-icon v-else :icon="data.icon || ['fa', 'folder']" />
</span>
<span class="label">
{{ data.label }}
</span>
</div>
<div class="actions">
<cl-icon
class="more"
:icon="['fa', 'ellipsis']"
@click.stop="onActionsClick(data)"
/>
</div>
</template>
<cl-context-menu-list
v-if="isContextMenuVisible(data.id)"
:items="contextMenuItems"
@hide="onContextMenuHide(data.id)"
/>
</cl-context-menu>
</template>
</el-tree>
</el-scrollbar>
</div>
<div class="content">
<template v-if="activeNavItem?.type === 'field'">
<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)"
/>
</template>
<template v-else-if="activeNavItem?.type === 'pagination'">
<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)"
/>
</template>
<div v-else class="placeholder">
{{
t('components.autoprobe.patterns.selectItem') ||
'Select an item to view details'
}}
</div>
</div>
</div>
</template>
<style scoped>
.autoprobe-detail-tab-patterns {
height: 100%;
display: flex;
.sidebar {
flex: 0 0 240px;
height: 100%;
overflow: hidden;
border-right: 1px solid var(--el-border-color);
display: flex;
flex-direction: column;
position: relative;
.sidebar-actions {
height: 41px;
flex: 0 0 41px;
padding: 5px;
display: flex;
align-items: center;
gap: 5px;
color: var(--cl-primary-color);
border-bottom: 1px solid var(--el-border-color);
& > * {
display: flex;
align-items: center;
}
&:deep(.icon) {
cursor: pointer;
padding: 6px;
font-size: 14px;
width: 14px;
height: 14px;
border-radius: 50%;
}
&:deep(.icon.selected),
&:deep(.icon:hover) {
background-color: var(--cl-primary-plain-color);
}
}
.sidebar-search {
height: 38px;
flex: 0 0 38px;
border-bottom: 1px solid var(--el-border-color);
&:deep(.el-input .el-input__wrapper) {
box-shadow: none;
border: none;
}
}
.el-tree {
min-width: fit-content;
&:deep(.el-tree-node__content:hover .actions .icon) {
display: flex !important;
}
&:deep(.el-tree-node__content) {
width: 100%;
position: relative;
.node-wrapper {
display: flex;
align-items: center;
position: relative;
width: 100%;
.icon-wrapper {
width: 20px;
display: flex;
}
.label {
flex: 0 0 auto;
}
}
.actions {
display: flex;
gap: 5px;
position: absolute;
top: 0;
right: 5px;
height: 100%;
align-items: center;
&:deep(.icon.more) {
display: none;
}
}
}
}
}
.content {
flex: 1;
padding: 16px;
overflow: auto;
.detail-panel {
border: 1px solid var(--el-border-color);
border-radius: 4px;
padding: 16px;
}
.placeholder {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: var(--el-text-color-secondary);
font-style: italic;
}
}
}
</style>

View File

@@ -8,12 +8,15 @@ import {
ACTION_FILTER_SEARCH,
ACTION_RUN,
ACTION_VIEW,
ACTION_VIEW_SPIDERS,
FILTER_OP_CONTAINS,
TABLE_COLUMN_NAME_ACTIONS,
} from '@/constants';
import { getIconByAction, onListFilterChangeByKey, translate } from '@/utils';
import { ClNavLink } from '@/components';
import {
ClNavLink,
ClAutoProbeTaskStatus,
ClAutoProbePatternStats,
} from '@/components';
import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
@@ -92,12 +95,50 @@ const useAutoProbeList = () => {
label: t('views.autoprobe.table.columns.url'),
icon: ['fa', 'at'],
width: 'auto',
minWidth: '200',
value: (row: AutoProbe) => (
<ClNavLink path={row.url} label={row.url} external />
),
hasFilter: true,
allowFilterSearch: true,
},
{
key: 'last_task',
label: t('views.autoprobe.table.columns.lastTask'),
icon: ['fa', 'heartbeat'],
width: '120',
value: (row: AutoProbe) => {
const { status, error } = row.last_task || {};
if (!status) return;
return (
<ClAutoProbeTaskStatus
status={status}
error={error}
clickable
onClick={() => router.push(`/autoprobes/${row._id}/tasks`)}
/>
);
},
hasFilter: true,
allowFilterSearch: true,
},
{
key: 'patterns',
label: t('views.autoprobe.table.columns.patterns'),
icon: ['fa', 'network-wired'],
width: '200',
value: (row: AutoProbe) => {
return (
<ClAutoProbePatternStats
autoprobe={row}
clickable
onClick={() => router.push(`/autoprobes/${row._id}/patterns`)}
/>
);
},
hasFilter: true,
allowFilterSearch: true,
},
{
key: TABLE_COLUMN_NAME_ACTIONS,
label: t('components.table.columns.actions'),

View File

@@ -1,5 +1,6 @@
import AutoProbeDetail from './autoprobe/detail/AutoProbeDetail.vue';
import AutoProbeDetailTabOverview from './autoprobe/detail/tabs/AutoProbeDetailTabOverview.vue';
import AutoProbeDetailTabPatterns from './autoprobe/detail/tabs/AutoProbeDetailTabPatterns.vue';
import AutoProbeDetailTabTasks from './autoprobe/detail/tabs/AutoProbeDetailTabTasks.vue';
import AutoProbeList from './autoprobe/list/AutoProbeList.vue';
import DatabaseDetail from './database/detail/DatabaseDetail.vue';
@@ -125,6 +126,7 @@ import useUserList from './user/list/useUserList';
export {
AutoProbeDetail as ClAutoProbeDetail,
AutoProbeDetailTabOverview as ClAutoProbeDetailTabOverview,
AutoProbeDetailTabPatterns as ClAutoProbeDetailTabPatterns,
AutoProbeDetailTabTasks as ClAutoProbeDetailTabTasks,
AutoProbeList as ClAutoProbeList,
DatabaseDetail as ClDatabaseDetail,