init
This commit is contained in:
367
src/views/knowledge/ocr/KonvaPage.vue
Normal file
367
src/views/knowledge/ocr/KonvaPage.vue
Normal file
@@ -0,0 +1,367 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-container class="h-screen">
|
||||
<el-main class="p-0">
|
||||
<Splitpanes style="height: 100%">
|
||||
<Pane :size="70">
|
||||
<el-card class="h-full">
|
||||
<!-- Image Display -->
|
||||
<div class="relative w-full h-full">
|
||||
<div class="flex absolute top-0 left-0 z-20 items-center px-4 py-2 w-full bg-white bg-opacity-20">
|
||||
<el-button-group class="mr-4">
|
||||
<el-button @click="rotateImage(-90)" :icon="Refresh" circle></el-button>
|
||||
<el-button @click="zoomIn" :icon="ZoomIn" circle></el-button>
|
||||
<el-button @click="zoomOut" :icon="ZoomOut" circle></el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
<div class="mt-12 w-full h-[calc(100%-3rem)]">
|
||||
<div class="flex relative justify-center items-center w-full h-full" @wheel.prevent="handleWheel">
|
||||
<img
|
||||
v-if="imageUrl"
|
||||
:src="baseURL + imageUrl"
|
||||
class="max-h-[80vh] w-auto transition-transform duration-200 transform-gpu"
|
||||
:style="{
|
||||
transform: `rotate(${rotation}deg) scale(${scale})`,
|
||||
transformOrigin: 'center center',
|
||||
}"
|
||||
/>
|
||||
<div v-else class="flex flex-col justify-center items-center h-full">
|
||||
<upload-img v-model:imageUrl="imageUrl" :fileType="['image/png']">
|
||||
<template #empty>
|
||||
<el-icon><Picture /></el-icon>
|
||||
<span>请上传底图</span>
|
||||
</template>
|
||||
</upload-img>
|
||||
|
||||
<el-link
|
||||
v-if="isPreview"
|
||||
class="mt-4 w-full text-center"
|
||||
href="https://minio.cloud.vip/oss/202412/1735479611.zip"
|
||||
target="_blank"
|
||||
>点击下载示例图</el-link
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</Pane>
|
||||
<Pane :size="30">
|
||||
<Splitpanes horizontal>
|
||||
<Pane :size="20">
|
||||
<el-card class="h-full">
|
||||
<ControlPanel
|
||||
@clearAll="clearImage"
|
||||
@saveData="saveData"
|
||||
@parseData="parseData"
|
||||
:isPreview="isPreview"
|
||||
:isParsing="isParsing"
|
||||
:hasImage="!!imageUrl"
|
||||
/>
|
||||
</el-card>
|
||||
</Pane>
|
||||
<Pane :size="80">
|
||||
<el-card class="overflow-auto h-full">
|
||||
<DataDisplay
|
||||
:markerList="markerList"
|
||||
:isPreview="isPreview"
|
||||
@addMarker="handleAddMarker"
|
||||
@removeMarker="handleRemoveMarker"
|
||||
@editMarker="handleEditMarker"
|
||||
/>
|
||||
</el-card>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</el-main>
|
||||
|
||||
<!-- Loading Animation -->
|
||||
<div v-if="isParsing" class="flex absolute inset-0 z-50 justify-center items-center bg-black bg-opacity-50">
|
||||
<div class="flex justify-center items-center text-center">
|
||||
<span class="loading loading-ball loading-xs"></span>
|
||||
<span class="loading loading-ball loading-sm"></span>
|
||||
<span class="loading loading-ball loading-md"></span>
|
||||
<span class="loading loading-ball loading-lg"></span>
|
||||
</div>
|
||||
</div>
|
||||
</el-container>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { Splitpanes, Pane } from 'splitpanes';
|
||||
import { Refresh, ZoomIn, ZoomOut, Picture } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import 'splitpanes/dist/splitpanes.css';
|
||||
import ControlPanel from './components/ControlPanel.vue';
|
||||
import DataDisplay from './components/DataDisplay.vue';
|
||||
import { putObj, getObj, parseObj } from '/@/api/knowledge/ocr';
|
||||
import { Local } from '/@/utils/storage';
|
||||
import other from '/@/utils/other';
|
||||
|
||||
const route = useRoute();
|
||||
const id = ref(route.query.id);
|
||||
const isPreview = computed(() => route.query.preview === '1');
|
||||
|
||||
const imageUrl = ref<string | null>(null);
|
||||
const rotation = ref(0);
|
||||
const scale = ref(1);
|
||||
const isParsing = ref(false);
|
||||
const markerList = ref<any[]>([]);
|
||||
const markerTemplate = ref<any[]>([]); // 存储标记模板
|
||||
|
||||
const MIN_SCALE = 0.1;
|
||||
const MAX_SCALE = 10;
|
||||
const SCALE_FACTOR = 0.1;
|
||||
|
||||
// 添加模型选择的响应式变量
|
||||
const selectedVisionModel = ref<any>(null);
|
||||
const selectedChatModel = ref<any>(null);
|
||||
|
||||
function rotateImage(angle: number) {
|
||||
rotation.value = (rotation.value + angle + 360) % 360;
|
||||
}
|
||||
|
||||
function handleWheel(event: WheelEvent) {
|
||||
if (!imageUrl.value) return;
|
||||
|
||||
// deltaY 小于 0 表示向上滚动(放大),大于 0 表示向下滚动(缩小)
|
||||
const delta = -Math.sign(event.deltaY) * SCALE_FACTOR;
|
||||
const newScale = scale.value + delta;
|
||||
|
||||
// 限制缩放范围
|
||||
if (newScale >= MIN_SCALE && newScale <= MAX_SCALE) {
|
||||
scale.value = newScale;
|
||||
}
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
const newScale = scale.value * 1.1;
|
||||
if (newScale <= MAX_SCALE) {
|
||||
scale.value = newScale;
|
||||
}
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
const newScale = scale.value / 1.1;
|
||||
if (newScale >= MIN_SCALE) {
|
||||
scale.value = newScale;
|
||||
}
|
||||
}
|
||||
|
||||
function clearImage() {
|
||||
imageUrl.value = null;
|
||||
rotation.value = 0;
|
||||
scale.value = 1;
|
||||
}
|
||||
|
||||
function handleAddMarker(marker: any) {
|
||||
markerList.value.push(marker);
|
||||
}
|
||||
|
||||
function handleRemoveMarker(id: number) {
|
||||
const index = markerList.value.findIndex((marker) => marker.id === id);
|
||||
if (index !== -1) {
|
||||
markerList.value.splice(index, 1);
|
||||
|
||||
// 重新编号
|
||||
markerList.value = markerList.value.map((marker, idx) => ({
|
||||
...marker,
|
||||
label: (idx + 1).toString(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async function saveData() {
|
||||
// 检查底图是否存在
|
||||
if (!imageUrl.value) {
|
||||
ElMessage.warning('请先上传底图');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有标记点
|
||||
if (markerList.value.length === 0) {
|
||||
ElMessage.warning('请至少添加一个标记点');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查所有标记点是否都有名称和描述
|
||||
const incompleteMarker = markerList.value.find((marker) => !marker.name || !marker.description);
|
||||
if (incompleteMarker) {
|
||||
ElMessage.warning(`请完善标记点 ${incompleteMarker.label} 的信息`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await putObj({
|
||||
id: id.value,
|
||||
imageResource: imageUrl.value,
|
||||
ocrMarked: JSON.stringify({
|
||||
markerList: markerList.value,
|
||||
}),
|
||||
});
|
||||
ElMessage.success('保存成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('保存数据时出错');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
if (!id.value) {
|
||||
// 如果没有 id,清空所有数据
|
||||
clearAll();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getObj({ id: id.value });
|
||||
if (!response?.data?.[0]) {
|
||||
// 如果没有数据,清空所有数据
|
||||
clearAll();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = response.data[0];
|
||||
|
||||
// 预览模式下只加载标记模板,不加载图片
|
||||
if (isPreview.value) {
|
||||
if (data.ocrMarked) {
|
||||
const ocrMarked = JSON.parse(data.ocrMarked);
|
||||
if (ocrMarked.markerList) {
|
||||
markerTemplate.value = ocrMarked.markerList;
|
||||
// 清空标记值,只保留结构
|
||||
markerList.value = ocrMarked.markerList.map((marker: any) => ({
|
||||
...marker,
|
||||
markedValue: '', // 清空标记值
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 非预览模式下加载完整数据
|
||||
if (data.imageResource) {
|
||||
imageUrl.value = data.imageResource;
|
||||
}
|
||||
if (data.ocrMarked) {
|
||||
const ocrMarked = JSON.parse(data.ocrMarked);
|
||||
if (ocrMarked.markerList) {
|
||||
markerList.value = ocrMarked.markerList;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载数据失败');
|
||||
// 加载失败时也清空所有数据
|
||||
clearAll();
|
||||
}
|
||||
}
|
||||
|
||||
// 添加清空所有数据的函数
|
||||
function clearAll() {
|
||||
imageUrl.value = null;
|
||||
rotation.value = 0;
|
||||
scale.value = 1;
|
||||
markerList.value = [];
|
||||
markerTemplate.value = [];
|
||||
}
|
||||
|
||||
// 解析数据
|
||||
async function parseData() {
|
||||
if (!imageUrl.value) {
|
||||
ElMessage.warning('请先上传图片');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedChatModel.value = Local.get(`selectedAiModel:Chat`);
|
||||
|
||||
isParsing.value = true;
|
||||
try {
|
||||
// 从 imageUrl 中提取文件名
|
||||
const fileName = other.getQueryString(imageUrl.value, 'fileName');
|
||||
|
||||
const response = await parseObj({
|
||||
id: id.value,
|
||||
visionModelName: selectedVisionModel.value?.name,
|
||||
chatModelName: selectedChatModel.value?.name,
|
||||
imageResource: fileName,
|
||||
ocrMarked: JSON.stringify({
|
||||
markers: markerTemplate.value,
|
||||
markerList: markerList.value,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response?.data) {
|
||||
// 解析返回的 JSON 字符串
|
||||
const parsedData = JSON.parse(response.data);
|
||||
|
||||
if (!parsedData.isContain) {
|
||||
ElMessage.warning('上传底图和目标数据类型图片不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
// 将解析结果与标记模板匹配
|
||||
markerList.value = markerTemplate.value.map((template: any) => {
|
||||
const value = parsedData[template.name] || ''; // 使用模板的 name 属性作为键
|
||||
return {
|
||||
...template,
|
||||
markedValue: value,
|
||||
};
|
||||
});
|
||||
|
||||
ElMessage.success('解析完成');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Parse error:', error);
|
||||
ElMessage.error('解析失败');
|
||||
} finally {
|
||||
isParsing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理编辑标记
|
||||
function handleEditMarker(updatedMarker: any) {
|
||||
const index = markerList.value.findIndex((marker) => marker.id === updatedMarker.id);
|
||||
if (index !== -1) {
|
||||
// 保持原有的其他属性(如果有的话)
|
||||
markerList.value[index] = {
|
||||
...markerList.value[index],
|
||||
name: updatedMarker.name,
|
||||
description: updatedMarker.description,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 在组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-main {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.splitpanes__pane {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
margin: auto;
|
||||
will-change: transform;
|
||||
}
|
||||
</style>
|
||||
79
src/views/knowledge/ocr/components/ContextMenu.vue
Normal file
79
src/views/knowledge/ocr/components/ContextMenu.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<transition name="el-zoom-in-center">
|
||||
<div
|
||||
v-show="isShow"
|
||||
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
|
||||
:style="`top: ${y}px; left: ${x}px;`"
|
||||
>
|
||||
<ul class="el-dropdown-menu">
|
||||
<li
|
||||
v-for="item in menuItems"
|
||||
:key="item.text"
|
||||
class="el-dropdown-menu__item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
<span>{{ item.text }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="el-popper__arrow" :style="{ left: '10px' }"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
x: Number,
|
||||
y: Number,
|
||||
menuItems: Array,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['itemClick', 'close']);
|
||||
|
||||
const isShow = ref(false);
|
||||
|
||||
const handleItemClick = (item) => {
|
||||
emit('itemClick', item);
|
||||
isShow.value = false;
|
||||
};
|
||||
|
||||
const show = () => {
|
||||
isShow.value = true;
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
isShow.value = false;
|
||||
};
|
||||
|
||||
defineExpose({ show, hide });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.custom-contextmenu {
|
||||
position: fixed;
|
||||
z-index: 2190;
|
||||
min-width: 120px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item {
|
||||
font-size: 14px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
22
src/views/knowledge/ocr/components/ControlPanel.vue
Normal file
22
src/views/knowledge/ocr/components/ControlPanel.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-2 w-full">
|
||||
<el-button class="w-full" v-if="!isPreview" type="primary" @click="$emit('saveData')">
|
||||
<el-icon class="mr-1"><Document /></el-icon>保存数据
|
||||
</el-button>
|
||||
<el-button class="w-full" v-if="isPreview" type="success" @click="$emit('parseData')" :loading="isParsing" :disabled="isParsing || !hasImage">
|
||||
<el-icon class="mr-1"><Connection /></el-icon>{{ isParsing ? '解析中...' : '解析数据' }}
|
||||
</el-button>
|
||||
<el-button class="w-full" type="warning" @click="$emit('clearAll')" style="margin-left: 0">
|
||||
<el-icon class="mr-1"><Delete /></el-icon>清空图片
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue';
|
||||
defineProps({
|
||||
isPreview: Boolean,
|
||||
isParsing: Boolean,
|
||||
hasImage: Boolean,
|
||||
});
|
||||
</script>
|
||||
215
src/views/knowledge/ocr/components/DataDisplay.vue
Normal file
215
src/views/knowledge/ocr/components/DataDisplay.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex gap-2 items-center">
|
||||
<el-button v-if="!isPreview" type="primary" size="small" @click="handleAddMarker">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增标记
|
||||
</el-button>
|
||||
<ModelList v-if="isPreview" :modelType="['Chat']" :support-json="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="markerList"
|
||||
style="width: 100%"
|
||||
:border="true"
|
||||
size="large"
|
||||
row-class-name="hover:bg-gray-50"
|
||||
header-row-class-name="bg-gray-50"
|
||||
@row-dblclick="handleRowDblClick"
|
||||
>
|
||||
<el-table-column prop="label" label="#" width="60" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" effect="plain" round>{{ row.label }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="name" label="属性" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="font-medium">{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="isPreview ? '标记值' : '描述'" min-width="200" show-overflow-tooltip align="left">
|
||||
<template #default="scope">
|
||||
<el-tooltip
|
||||
:content="isPreview ? scope.row.markedValue : scope.row.description"
|
||||
placement="top"
|
||||
:effect="isPreview ? 'dark' : 'light'"
|
||||
:show-after="500"
|
||||
>
|
||||
<div class="px-2 py-1">
|
||||
<span class="text-gray-600">
|
||||
{{ isPreview ? scope.row.markedValue : scope.row.description }}
|
||||
</span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="!isPreview" label="操作" width="60" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button type="danger" :icon="Delete" circle size="small" @click="handleDelete(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 新增标记对话框 -->
|
||||
<el-dialog v-model="dialogVisible" title="新增标记" width="500px" :close-on-click-modal="false" destroy-on-close>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px" class="py-4">
|
||||
<el-form-item label="属性" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入属性名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="form.description" type="textarea" :rows="4" placeholder="请输入描述" show-word-limit maxlength="500" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确认</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑标记对话框 -->
|
||||
<el-dialog v-model="editDialogVisible" title="编辑标记" width="500px" :close-on-click-modal="false" destroy-on-close>
|
||||
<el-form :model="editForm" :rules="rules" ref="editFormRef" label-width="80px" class="py-4">
|
||||
<el-form-item label="属性" prop="name">
|
||||
<el-input v-model="editForm.name" placeholder="请输入属性名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="editForm.description" type="textarea" :rows="4" placeholder="请输入描述" show-word-limit maxlength="500" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitEditForm">确认</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElTable, ElTableColumn, ElButton, ElTooltip, ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus, Delete } from '@element-plus/icons-vue';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
import { rule } from '/@/utils/validate';
|
||||
|
||||
const ModelList = defineAsyncComponent(() => import('/@/views/knowledge/aiChat/components/widgets/modelList.vue'));
|
||||
|
||||
interface Props {
|
||||
markerList: Array<any>;
|
||||
isPreview: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'removeMarker', id: number): void;
|
||||
(e: 'editMarker', marker: any): void;
|
||||
(e: 'selectChatModel', model: any): void;
|
||||
(e: 'addMarker', marker: any): void;
|
||||
}>();
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const form = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入属性名', trigger: 'blur' },
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
{ validator: rule.validatorLowercase, trigger: 'blur' },
|
||||
],
|
||||
description: [
|
||||
{ required: true, message: '请输入描述', trigger: 'blur' },
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
function handleAddMarker() {
|
||||
dialogVisible.value = true;
|
||||
form.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
};
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
if (!formRef.value) return;
|
||||
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
const newMarker = {
|
||||
id: Date.now(),
|
||||
label: (props.markerList.length + 1).toString(),
|
||||
name: form.value.name,
|
||||
description: form.value.description,
|
||||
};
|
||||
|
||||
emit('addMarker', newMarker);
|
||||
dialogVisible.value = false;
|
||||
ElMessage.success('新增标记成功');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleDelete(row: any) {
|
||||
ElMessageBox.confirm('确认删除该标记吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
emit('removeMarker', row.id);
|
||||
ElMessage.success('删除成功');
|
||||
});
|
||||
}
|
||||
|
||||
// 添加编辑相关的响应式变量
|
||||
const editDialogVisible = ref(false);
|
||||
const editFormRef = ref<FormInstance>();
|
||||
const editForm = ref({
|
||||
id: '',
|
||||
label: '',
|
||||
name: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
// 处理双击行事件
|
||||
function handleRowDblClick(row: any) {
|
||||
if (props.isPreview) return; // 预览模式下不允许编辑
|
||||
|
||||
editForm.value = {
|
||||
id: row.id,
|
||||
label: row.label,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
};
|
||||
editDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 提交编辑表单
|
||||
function submitEditForm() {
|
||||
if (!editFormRef.value) return;
|
||||
|
||||
editFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
const updatedMarker = {
|
||||
...editForm.value,
|
||||
};
|
||||
|
||||
emit('editMarker', updatedMarker);
|
||||
editDialogVisible.value = false;
|
||||
ElMessage.success('编辑标记成功');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
84
src/views/knowledge/ocr/components/DescriptionForm.vue
Normal file
84
src/views/knowledge/ocr/components/DescriptionForm.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="标记信息"
|
||||
width="30%"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef">
|
||||
<el-form-item prop="name" label="字段名" :label-width="formLabelWidth">
|
||||
<el-input v-model="form.name" autocomplete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="description" label="字段描述" :label-width="formLabelWidth">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入描述"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {rule} from "/@/utils/validate";
|
||||
|
||||
const props = defineProps({
|
||||
initialName: String,
|
||||
initialDescription: String,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit', 'close']);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const formRef = ref(null);
|
||||
const form = reactive({
|
||||
name: props.initialName || '',
|
||||
description: props.initialDescription || ''
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{required: true, message: '请输入属性名', trigger: 'blur'},
|
||||
{validator: rule.overLength, trigger: 'blur'},
|
||||
{validator: rule.validatorLowercase, trigger: 'blur'}
|
||||
],
|
||||
description: [
|
||||
{required: true, message: '请输入描述', trigger: 'blur'},
|
||||
{validator: rule.overLength, trigger: 'blur'},
|
||||
]
|
||||
};
|
||||
|
||||
const formLabelWidth = '80px';
|
||||
|
||||
const submitForm = () => {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
emit('submit', {name: form.name, description: form.description});
|
||||
closeDialog();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const showDialog = (name, description) => {
|
||||
form.name = name || '';
|
||||
form.description = description || '';
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
defineExpose({showDialog});
|
||||
</script>
|
||||
155
src/views/knowledge/ocr/components/DrawingBoard.vue
Normal file
155
src/views/knowledge/ocr/components/DrawingBoard.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="w-full h-[80vh] overflow-y-auto relative">
|
||||
<div class="flex absolute top-0 left-0 z-20 items-center px-4 py-2 w-full bg-white bg-opacity-70">
|
||||
<el-button-group class="mr-4">
|
||||
<el-button @click="rotateImage(-90)" :icon="Refresh" circle></el-button>
|
||||
<el-button @click="zoomIn" :icon="ZoomIn" circle></el-button>
|
||||
<el-button @click="zoomOut" :icon="ZoomOut" circle></el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
<vk-stage
|
||||
v-if="imageUrl"
|
||||
ref="stageRef"
|
||||
class="mt-12 w-full cursor-pointer"
|
||||
:config="stageConfig"
|
||||
@dblclick="handleStageDoubleClick"
|
||||
@contextmenu.prevent="handleStageContextMenu"
|
||||
>
|
||||
<vk-layer>
|
||||
<vk-image :config="imageConfig" />
|
||||
</vk-layer>
|
||||
</vk-stage>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Refresh, ZoomIn, ZoomOut } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps(['config', 'imageUrl', 'imageElement', 'markers', 'isPreview']);
|
||||
const emit = defineEmits([
|
||||
'addMarker',
|
||||
'updateMarkerPosition',
|
||||
'updateMarkerSize',
|
||||
'removeMarker',
|
||||
'addDescription',
|
||||
'showContextMenu',
|
||||
'markerClick',
|
||||
'updateConfig',
|
||||
]);
|
||||
|
||||
const stageRef = ref(null);
|
||||
|
||||
const stageConfig = computed(() => {
|
||||
const containerWidth = document.querySelector('.konvajs-content')?.clientWidth || window.innerWidth;
|
||||
|
||||
return {
|
||||
width: containerWidth,
|
||||
height: props.config.height,
|
||||
scaleX: props.config.scale,
|
||||
scaleY: props.config.scale,
|
||||
style: { width: '100%' },
|
||||
};
|
||||
});
|
||||
|
||||
const imageConfig = computed(() => {
|
||||
const rotation = props.config.rotation || 0;
|
||||
const isRotated90or270 = rotation % 180 === 90;
|
||||
|
||||
return {
|
||||
x: props.config.width / 2,
|
||||
y: props.config.height / 2,
|
||||
image: props.imageElement,
|
||||
width: isRotated90or270 ? props.config.height : props.config.width,
|
||||
height: isRotated90or270 ? props.config.width : props.config.height,
|
||||
offsetX: (isRotated90or270 ? props.config.height : props.config.width) / 2,
|
||||
offsetY: (isRotated90or270 ? props.config.width : props.config.height) / 2,
|
||||
rotation: rotation,
|
||||
};
|
||||
});
|
||||
|
||||
const rotateImage = (angle) => {
|
||||
emit('updateConfig', {
|
||||
...props.config,
|
||||
rotation: (props.config.rotation || 0) + angle,
|
||||
});
|
||||
};
|
||||
|
||||
const zoomIn = () => {
|
||||
emit('updateConfig', {
|
||||
...props.config,
|
||||
scale: (props.config.scale || 1) * 1.1,
|
||||
});
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
emit('updateConfig', {
|
||||
...props.config,
|
||||
scale: (props.config.scale || 1) / 1.1,
|
||||
});
|
||||
};
|
||||
|
||||
const handleStageDoubleClick = (event) => {
|
||||
if (props.isPreview) return;
|
||||
const stage = event.target.getStage();
|
||||
const pos = stage.getPointerPosition();
|
||||
|
||||
// 获取旋转角度
|
||||
const rotation = props.config.rotation || 0;
|
||||
const isRotated90or270 = rotation % 180 === 90;
|
||||
|
||||
// 计算相对于图片中心的坐标
|
||||
const centerX = props.config.width / 2;
|
||||
const centerY = props.config.height / 2;
|
||||
|
||||
// 转换点击坐标到旋转后的坐标系
|
||||
let adjustedX = pos.x - centerX;
|
||||
let adjustedY = pos.y - centerY;
|
||||
|
||||
// 根据旋转角度调整坐标
|
||||
const rad = (rotation * Math.PI) / 180;
|
||||
const cos = Math.cos(rad);
|
||||
const sin = Math.sin(rad);
|
||||
|
||||
const rotatedX = adjustedX * cos + adjustedY * sin;
|
||||
const rotatedY = -adjustedX * sin + adjustedY * cos;
|
||||
|
||||
// 转换回绝对坐标
|
||||
const finalX = rotatedX + centerX;
|
||||
const finalY = rotatedY + centerY;
|
||||
|
||||
emit('addMarker', {
|
||||
evt: {
|
||||
offsetX: finalX,
|
||||
offsetY: finalY,
|
||||
rotation: rotation, // 传递当前旋转角度
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleStageContextMenu = (event) => {
|
||||
if (props.isPreview) return;
|
||||
const stage = event.target.getStage();
|
||||
const pos = stage.getPointerPosition();
|
||||
emit('showContextMenu', {
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
items: [{ text: '添加标记', action: () => handleStageDoubleClick(event) }],
|
||||
});
|
||||
};
|
||||
|
||||
const getStage = () => {
|
||||
return stageRef.value ? stageRef.value.getNode() : null;
|
||||
};
|
||||
|
||||
defineExpose({ getStage });
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.konvajs-content {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
110
src/views/knowledge/ocr/form.vue
Normal file
110
src/views/knowledge/ocr/form.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible"
|
||||
:close-on-click-modal="false" draggable>
|
||||
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
|
||||
<el-form-item label="标题" prop="ocrTitle">
|
||||
<el-input v-model="form.ocrTitle" placeholder="请输入标题"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="描述" prop="ocrPrompt">
|
||||
<el-input type="textarea" :rows="5" v-model="form.ocrPrompt" placeholder="请输入描述提示词"/>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" :disabled="loading">确 认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="AiOcrConfDialog">
|
||||
import {useDict} from '/@/hooks/dict';
|
||||
import {useMessage} from "/@/hooks/message";
|
||||
import {getObj, addObj, putObj, validateExist} from '/@/api/knowledge/ocr'
|
||||
import {rule} from '/@/utils/validate';
|
||||
|
||||
const emit = defineEmits(['refresh']);
|
||||
|
||||
// 定义变量内容
|
||||
const dataFormRef = ref();
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
// 定义字典
|
||||
|
||||
// 提交表单数据
|
||||
const form = reactive({
|
||||
id: '',
|
||||
ocrTitle: '',
|
||||
ocrPrompt: '',
|
||||
imageResource: '',
|
||||
ocrMarked: '',
|
||||
});
|
||||
|
||||
// 定义校验规则
|
||||
const dataRules = {
|
||||
ocrTitle: [
|
||||
{required: true, message: '请输入功能名称', trigger: 'blur'},
|
||||
{validator: rule.overLength, trigger: 'blur'}
|
||||
],
|
||||
ocrPrompt: [
|
||||
{required: true, message: '请输入提示词描述', trigger: 'blur'},
|
||||
{validator: rule.overLength, trigger: 'blur'},
|
||||
]
|
||||
};
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = (id: string) => {
|
||||
visible.value = true
|
||||
form.id = ''
|
||||
|
||||
// 重置表单数据
|
||||
nextTick(() => {
|
||||
dataFormRef.value?.resetFields();
|
||||
});
|
||||
|
||||
// 获取aiOcrConf信息
|
||||
if (id) {
|
||||
form.id = id
|
||||
getAiOcrConfData(id)
|
||||
}
|
||||
};
|
||||
|
||||
// 提交
|
||||
const onSubmit = async () => {
|
||||
const valid = await dataFormRef.value.validate().catch(() => {
|
||||
});
|
||||
if (!valid) return false;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
form.id ? await putObj(form) : await addObj(form);
|
||||
useMessage().success(form.id ? '修改成功' : '添加成功');
|
||||
visible.value = false;
|
||||
emit('refresh');
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 初始化表单数据
|
||||
const getAiOcrConfData = (id: string) => {
|
||||
// 获取数据
|
||||
loading.value = true
|
||||
getObj({id: id}).then((res: any) => {
|
||||
Object.assign(form, res.data[0])
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
};
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
openDialog
|
||||
});
|
||||
</script>
|
||||
235
src/views/knowledge/ocr/index.vue
Normal file
235
src/views/knowledge/ocr/index.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row v-show="showSearch">
|
||||
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
|
||||
<el-form-item label="标题" prop="ocrTitle">
|
||||
<el-input placeholder="请输入标题" v-model="state.queryForm.ocrTitle" />
|
||||
</el-form-item>
|
||||
<el-form-item label="提示词" prop="ocrPrompt">
|
||||
<el-input placeholder="请输入提示词" v-model="state.queryForm.ocrPrompt" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="getDataList"> 查询 </el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()" v-auth="'knowledge_ocr_add'">
|
||||
新 增
|
||||
</el-button>
|
||||
<el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'knowledge_ocr_del'" @click="handleDelete(selectObjs)">
|
||||
删 除
|
||||
</el-button>
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'knowledge_ocr_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
|
||||
<el-scrollbar class="h-[calc(100vh-280px)] mb-4">
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<div
|
||||
v-for="item in state.dataList"
|
||||
:key="item.id"
|
||||
class="group overflow-hidden bg-white rounded-lg shadow-sm border border-gray-100 transition-all duration-300 cursor-pointer dark:bg-gray-800 dark:border-gray-700 hover:shadow-lg hover:border-primary-100 hover:translate-y-[-2px]"
|
||||
>
|
||||
<div class="p-5">
|
||||
<div class="flex items-start" @click="go2marker(item, 1)">
|
||||
<div
|
||||
class="flex justify-center items-center text-lg font-medium text-white bg-indigo-600 rounded-lg transition-transform size-12 group-hover:scale-110"
|
||||
>
|
||||
{{ item.ocrTitle ? item.ocrTitle.substring(0, 1).toUpperCase() : '' }}
|
||||
</div>
|
||||
<div class="overflow-hidden flex-1 ml-3">
|
||||
<div class="text-base font-medium text-gray-900 truncate dark:text-white">
|
||||
{{ item.ocrTitle }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 描述区域 -->
|
||||
<div @click="go2marker(item, 1)" class="overflow-y-auto mt-4 h-16 text-sm text-gray-600 dark:text-gray-300 line-clamp-3">
|
||||
{{ item.ocrPrompt || '暂无描述' }}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-start items-center pt-3 mt-4 border-t border-gray-100 dark:border-gray-700">
|
||||
<el-button
|
||||
v-if="item.ocrMarked"
|
||||
class="!p-2 text-gray-600 rounded-full transition-colors dark:text-gray-300 hover:text-primary hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'knowledge_ocr_edit'"
|
||||
@click="go2marker(item, 1)"
|
||||
>
|
||||
<el-icon><Document /></el-icon>
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
class="!p-2 text-gray-600 rounded-full transition-colors dark:text-gray-300 hover:text-primary hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'knowledge_ocr_edit'"
|
||||
@click="go2marker(item)"
|
||||
>
|
||||
<el-icon><Picture /></el-icon>
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
class="!p-2 text-gray-600 rounded-full transition-colors dark:text-gray-300 hover:text-primary hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'knowledge_ocr_edit'"
|
||||
@click="formDialogRef.openDialog(item.id)"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
class="!p-2 text-gray-600 rounded-full transition-colors dark:text-gray-300 hover:text-primary hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'knowledge_ocr_del'"
|
||||
@click="handleDelete([item.id])"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
|
||||
<div class="flex-grow"></div>
|
||||
<div class="flex items-center text-xs text-gray-500 dark:text-gray-400">
|
||||
<el-icon class="mr-1"><Clock /></el-icon>
|
||||
{{ parseDate(item.createTime) }}
|
||||
</div>
|
||||
<el-checkbox
|
||||
class="ml-4"
|
||||
:value="selectObjs.includes(item.id)"
|
||||
@change="(val: boolean) => handleCardSelect(val, item.id)"
|
||||
></el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<!-- 无数据显示 -->
|
||||
<el-empty v-if="!state.dataList || state.dataList.length === 0" description="暂无数据"></el-empty>
|
||||
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
|
||||
<!-- 导入excel -->
|
||||
<upload-excel
|
||||
ref="excelUploadRef"
|
||||
title="导入"
|
||||
url="/knowledge/ocr/import"
|
||||
temp-url="/admin/sys-file/local/file/ocr.xlsx"
|
||||
@refreshDataList="getDataList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="systemAiOcrConf">
|
||||
import { BasicTableProps, useTable } from '/@/hooks/table';
|
||||
import { fetchList, delObjs } from '/@/api/knowledge/ocr';
|
||||
import { useMessage, useMessageBox } from '/@/hooks/message';
|
||||
import { Document, Picture, Edit, Delete, Clock } from '@element-plus/icons-vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { parseDate } from '/@/utils/formatTime';
|
||||
|
||||
// 引入组件
|
||||
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
|
||||
|
||||
// 定义变量内容
|
||||
const formDialogRef = ref();
|
||||
const excelUploadRef = ref();
|
||||
// 搜索变量
|
||||
const queryRef = ref();
|
||||
const showSearch = ref(true);
|
||||
// 多选变量
|
||||
const selectObjs = ref([]) as any;
|
||||
const multiple = ref(true);
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {},
|
||||
pageList: fetchList,
|
||||
dataList: [],
|
||||
});
|
||||
|
||||
// table hook
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile } = useTable(state);
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
// 清空搜索条件
|
||||
queryRef.value?.resetFields();
|
||||
// 清空多选
|
||||
selectObjs.value = [];
|
||||
getDataList();
|
||||
};
|
||||
|
||||
// 导出excel
|
||||
const exportExcel = () => {
|
||||
downBlobFile('/knowledge/ocr/export', Object.assign(state.queryForm, { ids: selectObjs }), 'ocr.xlsx');
|
||||
};
|
||||
|
||||
// 多选事件 - 为卡片视图添加的选择函数
|
||||
const handleCardSelect = (selected: boolean, id: string) => {
|
||||
if (selected) {
|
||||
selectObjs.value.push(id);
|
||||
} else {
|
||||
selectObjs.value = selectObjs.value.filter((item: string) => item !== id);
|
||||
}
|
||||
multiple.value = selectObjs.value.length === 0;
|
||||
};
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (ids: string[]) => {
|
||||
try {
|
||||
await useMessageBox().confirm('此操作将永久删除');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await delObjs(ids);
|
||||
getDataList();
|
||||
useMessage().success('删除成功');
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到底图标注页面
|
||||
const router = useRouter();
|
||||
const go2marker = (row: { id: string; ocrTitle: string }, preview?: number) => {
|
||||
router.push({ path: '/knowledge/ocr/KonvaPage', query: { id: row.id, preview, tagsViewName: row.ocrTitle } });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-scrollbar__wrap) {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.bg-primary-100 {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.text-primary-500 {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user