mirror of
https://github.com/crawlab-team/crawlab.git
synced 2026-01-21 17:21:09 +01:00
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:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -144,6 +144,7 @@ const common: LCommon = {
|
||||
indexes: 'Indexes',
|
||||
results: 'Results',
|
||||
output: 'Output',
|
||||
patterns: 'Patterns',
|
||||
},
|
||||
status: {
|
||||
unassigned: 'Unassigned',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -236,6 +236,7 @@ const layouts: LLayouts = {
|
||||
tabs: {
|
||||
overview: 'Overview',
|
||||
tasks: 'Task',
|
||||
patterns: 'Patterns',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ const autoprobe: LViewsAutoProbe = {
|
||||
url: 'URL',
|
||||
query: 'Query',
|
||||
status: 'Status',
|
||||
lastTask: 'Last Task',
|
||||
patterns: 'Patterns',
|
||||
},
|
||||
},
|
||||
navActions: {
|
||||
|
||||
@@ -144,6 +144,7 @@ const common: LCommon = {
|
||||
indexes: '索引',
|
||||
results: '结果',
|
||||
output: '输出',
|
||||
patterns: '模式',
|
||||
},
|
||||
status: {
|
||||
unassigned: '未指定',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -236,6 +236,7 @@ const layouts: LLayouts = {
|
||||
tabs: {
|
||||
overview: '概览',
|
||||
tasks: '任务',
|
||||
patterns: '模式',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,6 +5,8 @@ const autoprobe: LViewsAutoProbe = {
|
||||
url: 'URL',
|
||||
query: '查询',
|
||||
status: '状态',
|
||||
lastTask: '最近任务',
|
||||
patterns: '模式',
|
||||
},
|
||||
},
|
||||
navActions: {
|
||||
|
||||
@@ -157,6 +157,7 @@ export declare global {
|
||||
indexes: string;
|
||||
results: string;
|
||||
output: string;
|
||||
patterns: string;
|
||||
};
|
||||
status: {
|
||||
unassigned: string;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ export declare global {
|
||||
autoprobe: LListLayoutPage<{
|
||||
overview: string;
|
||||
tasks: string;
|
||||
patterns: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ interface LViewsAutoProbe {
|
||||
url: string;
|
||||
query: string;
|
||||
status: string;
|
||||
lastTask: string;
|
||||
patterns: string;
|
||||
};
|
||||
};
|
||||
navActions: LNavActions & {
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,10 +293,6 @@ defineOptions({ name: 'ClListLayout' });
|
||||
}
|
||||
}
|
||||
|
||||
&:deep(.tag) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
max-height: 52px;
|
||||
background-color: var(--cl-container-white-bg);
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user