启动训练修改

盘点修改
测试页面
master
LAPTOP-S9HJSOEB\昊天 4 months ago
parent 55e69d0f2d
commit a3068a3ba3

@ -17,13 +17,19 @@
min-height: 100vh;
}
.container {
max-width: 600px;
max-width: 900px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
h1 {
text-align: center;
margin-bottom: 25px;
@ -237,14 +243,26 @@
<div class="container">
<h1>盘点/随行工具</h1>
<div class="form-group">
<label for="countInput">个数</label>
<input type="number" id="countInput" value="21" min="1">
</div>
<div class="form-group">
<label for="specInput">品规</label>
<input type="text" id="specInput" value="0143">
<div class="form-row">
<div class="form-group">
<label for="countInput">个数</label>
<input type="number" id="countInput" value="21" min="1">
</div>
<div class="form-group">
<label for="specInput">品规</label>
<input type="text" id="specInput" value="0143">
</div>
<div class="form-group">
<label for="trayCodeInput">托盘码</label>
<input type="text" id="trayCodeInput" value="UR202606">
</div>
<div class="form-group">
<label for="scanCodeInput">相机扫码</label>
<input type="text" id="scanCodeInput" value="UR202606">
</div>
</div>
<div class="switch-container">
@ -275,6 +293,8 @@
const loopToggle = document.getElementById('loopToggle');
const countInput = document.getElementById('countInput');
const specInput = document.getElementById('specInput');
const trayCodeInput = document.getElementById('trayCodeInput');
const scanCodeInput = document.getElementById('scanCodeInput');
const status = document.getElementById('status');
const progressBar = document.getElementById('progressBar');
const logContainer = document.getElementById('logContainer');
@ -282,6 +302,7 @@
let isExecuting = false;
let shouldStop = false;
let currentTaskId = ''; // 当前任务ID,同一轮内保持一致
let currentBatchNumber = ''; // 当前批次号,循环模式下同一轮内保持不变
// 生成新的 taskId(基于时间戳)
function generateTaskId() {
@ -298,6 +319,20 @@
return `${year}${month}${day}-${hours}${minutes}${seconds}-${ms}`;
}
// 生成批次号(基于时间戳)
function generateBatchNumber() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const ms = String(now.getMilliseconds()).padStart(3, '0');
return `BN${year}${month}${day}${hours}${minutes}${seconds}${ms}`;
}
// 写死的接口列表
const checkList = [
{
@ -334,15 +369,15 @@
shelfCode: '',
pltCode: '',
countNumber: '', // 个数
wmsCode: 'UR202606',
wmsTrayCode: 'UR202606',
wmsCode: '{code}',
wmsTrayCode: '{trayCode}',
wmsCategory: '',
wmsCount: '',
wmsItemCode: '{spec}',
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '',
batchNumber: '{batchNumber}',
extendInfo: null
},
timeout: 0
@ -389,7 +424,7 @@
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '',
batchNumber: '{batchNumber}',
extendInfo: null
},
timeout: 0
@ -404,8 +439,8 @@
const followList = [
{
name: '货物放回',
url: 'http://192.168.100.99:8083/retract',
name: '货物归位',
url: 'http://192.168.100.99:8083/extend',
method: 'GET',
timeout: 0
},
@ -445,7 +480,7 @@
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '',
batchNumber: '{batchNumber}',
extendInfo: null
},
timeout: 0
@ -486,14 +521,14 @@
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '',
batchNumber: '{batchNumber}',
extendInfo: null
},
timeout: 0
},
{
name: '货物拉出',
url: 'http://192.168.100.99:8083/extend',
url: 'http://192.168.100.99:8083/retract',
method: 'GET',
timeout: 0
},
@ -533,7 +568,7 @@
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '',
batchNumber: '{batchNumber}',
extendInfo: null
},
timeout: 0
@ -574,14 +609,14 @@
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '',
batchNumber: '{batchNumber}',
extendInfo: null
},
timeout: 0
},
{
name: '货物放回',
url: 'http://192.168.100.99:8083/retract',
url: 'http://192.168.100.99:8083/extend',
method: 'GET',
timeout: 0
}
@ -622,7 +657,7 @@
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '',
batchNumber: '{batchNumber}',
extendInfo: null
},
timeout: 0
@ -663,11 +698,17 @@
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '',
batchNumber: '{batchNumber}',
extendInfo: null
},
timeout: 0
},
{
name: '货物放回',
url: 'http://192.168.100.99:8083/retract',
method: 'GET',
timeout: 0
}
];
// 格式化时间
@ -690,7 +731,7 @@
}
// 替换参数中的占位符
function replaceParams(params, count, spec, taskId) {
function replaceParams(params, count, spec, code, trayCode, taskId, batchNumber) {
if (!params) return {};
const result = {};
for (const key in params) {
@ -699,7 +740,10 @@
result[key] = value
.replace('{count}', count)
.replace('{spec}', spec)
.replace('{taskId}', taskId);
.replace('{code}', code)
.replace('{trayCode}', trayCode)
.replace('{taskId}', taskId)
.replace('{batchNumber}', batchNumber);
} else {
result[key] = value;
}
@ -708,17 +752,19 @@
}
// 执行单个请求
async function executeRequest(item, taskId) {
async function executeRequest(item, taskId, batchNumber) {
let url = item.url;
const name = item.name;
const count = parseInt(countInput.value) || 21;
const spec = specInput.value || '0143';
const code = scanCodeInput.value || 'UR202606';
const trayCode = trayCodeInput.value || 'UR202606';
const method = item.method || 'POST';
const baseParams = item.params || {};
const timeout = item.timeout || 0; // 0 表示不超时
// 替换参数中的占位符(包括 taskId)
const params = replaceParams(baseParams, count, spec, taskId);
// 替换参数中的占位符(包括 taskId 和 batchNumber)
const params = replaceParams(baseParams, count, spec, code, trayCode, taskId, batchNumber);
const startTime = new Date();
addLog(`<span class="log-time">[${formatTime(startTime)}]</span> <span class="log-process">${name}</span> <span class="log-start">开始</span> [${method}]`);
@ -786,14 +832,14 @@
}
// 执行列表
async function executeList(list, type, taskId) {
async function executeList(list, type, taskId, batchNumber) {
const totalSteps = list.length;
for (let i = 0; i < totalSteps; i++) {
status.textContent = `${type === 'check' ? '盘点' : '随行'}中: ${i + 1}/${totalSteps} - ${list[i].name}`;
progressBar.style.width = `${((i + 1) / totalSteps) * 100}%`;
await executeRequest(list[i], taskId);
await executeRequest(list[i], taskId, batchNumber);
// 每个请求完成后延时 0.5 秒
await new Promise(resolve => setTimeout(resolve, 500));
@ -814,15 +860,19 @@
activeBtn.textContent = '执行中...';
let loopCount = 0;
// 循环模式下生成批次号且保持不变,非循环模式每次重新生成
if (!isLoop || !currentBatchNumber) {
currentBatchNumber = generateBatchNumber();
}
try {
do {
loopCount++;
// 每一轮生成新的 taskId
currentTaskId = generateTaskId();
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ========== 第 ${loopCount} ${type === 'check' ? '盘点' : '随行'} ========== (taskId: ${currentTaskId})`);
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ========== 第 ${loopCount} ${type === 'check' ? '盘点' : '随行'} ========== (taskId: ${currentTaskId}, batch: ${currentBatchNumber})`);
await executeList(list, type, currentTaskId);
await executeList(list, type, currentTaskId, currentBatchNumber);
if (shouldStop) {
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ========== 用户停止 ==========`);

@ -85,4 +85,14 @@ export const DatasApi = {
exportModel: async (id: number) => {
return await request.download({ url: `/annotation/datas/export-model?id=` + id })
},
// 启动训练
startTrain: async (id: number) => {
return await request.post({ url: `/annotation/datas/start-train?id=` + id })
},
// 获取训练日志
getTrainLog: async (id: number) => {
return await request.get({ url: `/annotation/datas/train-log?id=` + id })
},
}

@ -98,6 +98,10 @@ export const MarkApi = {
getProjectStatusList: async (data: { status: number }) => {
return await request.post({ url: `/annotation/datas/listStatus`,data })
},
// 更新项目状态
updateProjectStatus: async (data: { id: number; status: number }) => {
return await request.put({ url: `/annotation/datas/update`, data })
},
// 创建标注类型
createType: async (data: Partial<AnnotationTypeVO>) => {
return await request.post({ url: `/annotation/types/create`, data })

@ -149,24 +149,31 @@
增强
</el-button>
<router-link :to="'/annotation/mark?id=' + scope.row.id ">
<el-button
link
type="primary"
>
标注
</el-button>
<!-- 标注按钮:状态为2时不显示 -->
<router-link v-if="scope.row.status !== '2'" :to="'/annotation/mark?id=' + scope.row.id ">
<el-button link type="primary">标注</el-button>
</router-link>
<router-link :to="'/annotation/train?name=' + encodeURIComponent(scope.row.name)">
<el-button
link
type="primary"
:disabled="scope.row.status < 2"
>
训练
</el-button>
</router-link>
<!-- 训练按钮:当状态为2时显示"启动训练",否则禁用 -->
<el-button
v-if="scope.row.status >= '2'"
link
type="primary"
@click="handleStartTrain(scope.row)"
>
训练
</el-button>
<!-- 查看日志按钮:仅在训练相关状态时显示 -->
<el-button
v-if="scope.row.status >= '2'"
link
type="info"
@click="handleViewLog(scope.row)"
>
日志
</el-button>
<router-link
v-if="scope.row.status === '4'"
@ -230,6 +237,36 @@
<el-button type="primary" @click="executeEnhancement">确定</el-button>
</template>
</el-dialog>
<!-- 训练日志对话框 -->
<el-dialog
v-model="logDialogVisible"
:title="logDialogTitle"
width="90%"
:close-on-click-modal="false"
:fullscreen="logFullscreen"
>
<template #header>
<div class="log-dialog-header">
<span>训练日志</span>
<el-button
link
type="primary"
@click="logFullscreen = !logFullscreen"
>
<Icon :icon="logFullscreen ? 'ep:scale-to-original' : 'ep:full-screen'" />
</el-button>
</div>
</template>
<div class="log-container">
<pre v-if="trainLog">{{ trainLog }}</pre>
<div v-else class="loading-log">加载中...</div>
</div>
<template #footer>
<el-button @click="logDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="handleRefreshLog">刷新</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
@ -267,6 +304,13 @@ const enhanceDialogVisible = ref(false)
const selectedEnhancements = ref<string[]>([])
const currentEnhanceRow = ref<DatasVO | null>(null)
// 日志对话框相关状态
const logDialogVisible = ref(false)
const logDialogTitle = ref('训练日志')
const logFullscreen = ref(false)
const trainLog = ref('')
const currentLogRow = ref<DatasVO | null>(null)
// 增强选项配置
const enhancementOptions = [
{ label: '亮度调整(Brightness):随机调整图像的亮度', value: 'brightness' },
@ -360,6 +404,46 @@ const handleExportModel = async (id: number) => {
message.error('导出失败: ' + (error as Error).message)
}
}
// 启动训练
const handleStartTrain = async (row: DatasVO) => {
try {
await message.confirm('确定要启动训练吗?', '启动训练')
await DatasApi.startTrain(row.id!)
message.success('训练已启动')
// 刷新列表
await getList()
} catch (error) {
if ((error as any) !== 'cancel') {
message.error('启动训练失败: ' + (error as Error).message)
}
}
}
// 查看日志
const handleViewLog = async (row: DatasVO) => {
currentLogRow.value = row
logDialogVisible.value = true
trainLog.value = ''
await fetchTrainLog(row.id!)
}
// 获取训练日志
const fetchTrainLog = async (id: number) => {
try {
const data: any = await DatasApi.getTrainLog(id)
trainLog.value = typeof data === 'string' ? data : JSON.stringify(data, null, 2)
} catch (error) {
trainLog.value = '获取日志失败: ' + (error as Error).message
}
}
// 刷新日志
const handleRefreshLog = async () => {
if (currentLogRow.value?.id) {
await fetchTrainLog(currentLogRow.value.id)
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
@ -428,4 +512,35 @@ onMounted(() => {
background-color: #f5f7fa;
}
}
.log-container {
max-height: 500px;
overflow-y: auto;
background-color: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
line-height: 1.5;
}
.log-container pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.loading-log {
color: #d4d4d4;
text-align: center;
padding: 20px;
}
.log-dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
</style>

@ -95,6 +95,8 @@
v-for="(annotation, index) in annotations"
:key="annotation.id"
class="annotation-item"
:class="{ 'selected': selectedAnnotationId === annotation.id }"
@click="selectAnnotationById(annotation, index)"
>
<div class="annotation-header">
<el-select
@ -102,6 +104,7 @@
placeholder="选择类型"
size="small"
@change="(value) => setAnnotationType(index, value)"
@click.stop
class="type-select"
>
<el-option
@ -115,7 +118,7 @@
link
type="danger"
size="small"
@click="deleteAnnotation(annotation, index)"
@click.stop="deleteAnnotation(annotation, index)"
class="delete-annotation"
>
<Icon icon="ep:close" />
@ -236,30 +239,83 @@
<el-aside class="right-container" width="320px">
<el-collapse v-model="activeRightPanels" accordion class="custom-collapse">
<!-- 图片选择面板 -->
<el-collapse-item title="图片选择" name="images">
<el-collapse-item name="images">
<template #title>
<span class="panel-title">图片选择</span>
<el-progress
type="circle"
:percentage="annotationProgress"
:width="36"
:stroke-width="3"
:show-text="false"
class="title-progress"
/>
<span class="progress-text-small">{{ annotationProgress }}%</span>
</template>
<el-scrollbar class="image-scrollbar">
<div class="image-grid">
<div
v-for="image in imageList"
:key="image.id"
class="image-item"
:class="{ active: currentImage && currentImage.id === image.id }"
@click="selectImage(image)"
>
<div class="image-wrapper">
<img
:src="image.path"
:alt="image.name"
class="thumbnail"
@error="handleImageError"
/>
<div v-if="image.status === 1" class="status-indicator completed">
<Icon icon="ep:check" />
<!-- 待标注图片 -->
<div v-if="unannotatedImages.length > 0" class="image-section">
<div class="section-label" @click="unannotatedCollapsed = !unannotatedCollapsed">
<span class="label-text">待标注 ({{ unannotatedImages.length }})</span>
<Icon :icon="unannotatedCollapsed ? 'ep:caret-right' : 'ep:caret-bottom'" class="collapse-icon" />
</div>
<div v-show="!unannotatedCollapsed" class="image-grid">
<div
v-for="image in unannotatedImages"
:key="image.id"
class="image-item unannotated"
:class="{ active: currentImage && currentImage.id === image.id }"
@click="selectImage(image)"
>
<div class="image-wrapper">
<img
:src="image.path"
:alt="image.name"
class="thumbnail"
@error="handleImageError"
/>
<div class="status-indicator pending">
<Icon icon="ep:more" />
</div>
</div>
<div class="image-name">{{ image.name }}</div>
</div>
<div class="image-name">{{ image.name }}</div>
</div>
</div>
<!-- 已标注图片 -->
<div v-if="annotatedImages.length > 0" class="image-section">
<div class="section-label" @click="annotatedCollapsed = !annotatedCollapsed">
<span class="label-text">已标注 ({{ annotatedImages.length }})</span>
<Icon :icon="annotatedCollapsed ? 'ep:caret-right' : 'ep:caret-bottom'" class="collapse-icon" />
</div>
<div v-show="!annotatedCollapsed" class="image-grid">
<div
v-for="image in annotatedImages"
:key="image.id"
class="image-item annotated"
:class="{ active: currentImage && currentImage.id === image.id }"
@click="selectImage(image)"
>
<div class="image-wrapper">
<img
:src="image.path"
:alt="image.name"
class="thumbnail"
@error="handleImageError"
/>
<div class="status-indicator completed">
<Icon icon="ep:check" />
</div>
</div>
<div class="image-name">{{ image.name }}</div>
</div>
</div>
</div>
<div v-if="imageList.length === 0" class="empty-images">
<el-empty description="暂无图片" :image-size="60" />
</div>
</el-scrollbar>
</el-collapse-item>
</el-collapse>
@ -450,18 +506,32 @@ const initAnnotorious = async () => {
// 删除标注
const deleteAnnotation = async (annotation: any, index: number) => {
try {
// 清除选中状态
selectedAnnotationId.value = null;
selectedAnnoId.value = null;
// 从 Annotorious 中删除标注
if (annoInstance && annotation.annotationData) {
annoInstance.removeAnnotation(annotation.annotationData);
if (annoInstance) {
// 优先使用 annotationData.id
const annoId = annotation.annotationData?.id || annotation.id;
if (annoId) {
try {
annoInstance.removeAnnotation(annoId);
} catch (e) {
console.warn('从 Annotorious 删除标注失败:', e);
}
}
}
// 从本地标注列表中删除
annotations.value.splice(index, 1);
// 如果需要,可以调用后端API删除标注
// await MarkApi.deleteMark(annotation.id);
// 从本地标注列表中删除(支持通过 id 或 annotationData.id 匹配)
const idx = annotations.value.findIndex(a =>
a.id === annotation.id || (annotation.annotationData && a.annotationData?.id === annotation.annotationData.id)
);
if (idx !== -1) {
annotations.value.splice(idx, 1);
}
console.log('删除成功');
console.log('删除成功, 剩余标注:', annotations.value.length);
} catch (error) {
console.error('删除标注失败:', error);
alert('删除失败');
@ -612,11 +682,30 @@ const imageList = ref<Image[]>([])
const annotationTypes = ref<AnnotationType[]>([])
const annotations = ref<Annotation[]>([])
// 计算属性:待标注和已标注的图片
const unannotatedImages = computed(() => imageList.value.filter(img => img.status !== 1))
const annotatedImages = computed(() => imageList.value.filter(img => img.status === 1))
// 图片选择区域折叠状态
const unannotatedCollapsed = ref(false)
const annotatedCollapsed = ref(false)
// 标注进度
const annotationProgress = computed(() => {
if (imageList.value.length === 0) return 0
return Math.round((imageList.value.filter(img => img.status === 1).length / imageList.value.length) * 100)
})
// 分类模式相关数据
const selectedClassificationType = ref<number | null>(null)
const classificationAnnotations = ref<{ [key: number]: number }>({}) // 图片ID -> 类型ID的映射
const globalClassificationType = ref<number | null>(null) // 全局分类类型,切换图片时保持
// 标注选中状态
const selectedAnnotationId = ref<number | null>(null)
const selectedAnnoId = ref<string | null>(null) // 当前选中的 Annotorious 标注ID
// 添加类型对话框相关
const addTypeDialogVisible = ref(false)
@ -627,6 +716,7 @@ const newTypeForm = reactive({
})
const typeFormRef = ref()
// 表单验证规则
const typeFormRules = {
name: [
@ -720,6 +810,21 @@ const updateImageStatus = async (imageId: number, status: number) => {
console.error('更新图片状态失败:', error)
}
}
// 检查并更新项目状态
const checkAndUpdateProjectStatus = async () => {
// 检查是否所有图片都已标注完成
const allAnnotated = imageList.value.every(img => img.status === 1)
if (allAnnotated && currentProject.value) {
try {
await MarkApi.updateProjectStatus({ id: currentProject.value.id, status: 2 })
console.log('项目已全部标注完成,状态已更新为2')
} catch (error) {
console.error('更新项目状态失败:', error)
}
}
}
// 获取图片列表
const getImageList = async (projectId: number) => {
try {
@ -798,6 +903,9 @@ const handleClassificationChange = async (typeId: number) => {
// 调用API更新图片状态
await MarkApi.updateImageStatus({ id: currentImage.value.id, status: 1 });
// 检查并更新项目状态
await checkAndUpdateProjectStatus();
console.log('分类标注保存成功');
} catch (error) {
console.error('保存分类标注失败:', error);
@ -875,6 +983,8 @@ const selectImage = async (image: Image) => {
// 调用API更新图片状态
try {
await MarkApi.updateImageStatus({ id: currentImage.value.id, status: 1 });
// 检查并更新项目状态
await checkAndUpdateProjectStatus();
} catch (updateError) {
console.error('更新图片状态失败:', updateError);
}
@ -916,6 +1026,8 @@ const selectImage = async (image: Image) => {
imageInList.status = 1;
}
await MarkApi.updateImageStatus({ id: currentImage.value.id, status: 1 });
// 检查并更新项目状态
await checkAndUpdateProjectStatus();
}
} catch (error) {
console.error('保存分类标注失败:', error);
@ -941,6 +1053,8 @@ const selectImage = async (image: Image) => {
if (currentProject.value.type === '1') {
// 检测模式:初始化 annotorious 并加载标注
annotations.value = [];
selectedAnnotationId.value = null; // 清除选中状态
selectedAnnoId.value = null; // 清除选中状态
await initAnnotorious();
} else if (currentProject.value.type === '2') {
// 分类模式:加载已有分类标注
@ -955,6 +1069,10 @@ const selectImage = async (image: Image) => {
// 修改 handleAnnotationCreated 函数
const handleAnnotationCreated = (annotation: any) => {
console.log('创建标注:', annotation);
// 创建新标注后,设置新的选中状态
selectedAnnoId.value = annotation.id;
selectedAnnotationId.value = Date.now();
// 解析标注位置信息
const positionInfo = parseAnnotationPosition(annotation);
console.log(positionInfo);
@ -1024,11 +1142,13 @@ const handleAnnotationCreated = (annotation: any) => {
}
const colorValue = typeof color === 'string' ? color : color.toString();
const isSelected = selectedAnnoId.value === annotationItem.id;
return {
fill: colorValue,
fillOpacity: 0.25,
stroke: colorValue,
strokeWidth: 2,
fillOpacity: isSelected ? 0.4 : 0.25,
stroke: isSelected ? '#409EFF' : colorValue,
strokeWidth: isSelected ? 3 : 2,
};
} else {
return null;
@ -1074,7 +1194,114 @@ const handleAnnotationDeleted = (annotation: any) => {
// 处理点击标注事件
const handleClickAnnotation = (annotation: any, shape: any, sel: any) => {
console.log('点击标注:', annotation);
// 可以在这里实现标注选中后的操作,如高亮显示等
// 设置选中状态,高亮左侧对应项
if (annotation && annotation.id) {
// 记录当前选中的 Annotorious 标注 ID
selectedAnnoId.value = annotation.id;
// 通过 annotationData.id 查找左侧对应的标注
const matchedAnnotation = annotations.value.find((item: Annotation) =>
item.annotationData && item.annotationData.id === annotation.id
);
if (matchedAnnotation) {
selectedAnnotationId.value = matchedAnnotation.id;
} else {
// 如果没找到匹配的,使用 annotation.id 作为后备
selectedAnnotationId.value = annotation.id;
}
// 滚动到对应的左侧标注项
nextTick(() => {
const selectedItem = document.querySelector('.annotation-item.selected');
if (selectedItem) {
selectedItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
});
// 手动刷新样式以显示选中效果
if (annoInstance) {
annoInstance.setStyle((annotationItem: any, state: any) => {
if (annotationItem.bodies && annotationItem.bodies.length > 0) {
let color = annotationItem.bodies[0];
// 修复可能损坏的颜色数据结构
if (typeof color === 'object' && color !== null) {
if (!Array.isArray(color)) {
const keys = Object.keys(color).filter(k => k !== 'annotation').sort((a, b) => parseInt(a) - parseInt(b));
color = keys.map(k => color[k]).join('');
}
}
const colorValue = typeof color === 'string' ? color : color.toString();
const isSelected = selectedAnnoId.value === annotationItem.id;
return {
fill: colorValue,
fillOpacity: isSelected ? 0.4 : 0.25,
stroke: isSelected ? '#409EFF' : colorValue,
strokeWidth: isSelected ? 3 : 2,
};
}
return null;
});
}
}
};
// 点击左侧标注列表选中对应的标注
const selectAnnotationById = (annotation: Annotation, index: number) => {
selectedAnnotationId.value = annotation.id;
// 获取 Annotorious 标注 ID
const annoId = annotation.annotationData?.id || null;
selectedAnnoId.value = annoId;
// 通过刷新样式来高亮对应的标注
if (annoInstance) {
try {
annoInstance.setStyle((annotationItem: any, state: any) => {
if (annotationItem.bodies && annotationItem.bodies.length > 0) {
let color = annotationItem.bodies[0];
// 修复可能损坏的颜色数据结构
if (typeof color === 'object' && color !== null) {
if (!Array.isArray(color)) {
const keys = Object.keys(color).filter(k => k !== 'annotation').sort((a, b) => parseInt(a) - parseInt(b));
color = keys.map(k => color[k]).join('');
}
}
const colorValue = typeof color === 'string' ? color : color.toString();
const isSelected = selectedAnnoId.value === annotationItem.id;
return {
fill: colorValue,
fillOpacity: isSelected ? 0.4 : 0.25,
stroke: isSelected ? '#409EFF' : colorValue,
strokeWidth: isSelected ? 3 : 2,
};
}
return null;
});
console.log('高亮标注:', annoId);
} catch (error) {
console.error('高亮标注失败:', error);
}
}
console.log('选中左侧标注:', annotation);
};
// 触发标注选中效果(备用方法)
const triggerAnnotationSelection = (annotationData: any) => {
if (!annoInstance || !annotationData) return;
// 手动触发点击事件
const selector = document.querySelector(`[data-id="${annotationData.id}"]`);
if (selector) {
selector.dispatchEvent(new MouseEvent('click', { bubbles: true }));
}
};
// 处理选择变化事件
@ -1212,11 +1439,13 @@ const setAnnotationType = (annotationIndex: number, typeId: number) => {
const finalColor = typeof color === 'string' ? color : color.toString();
annoData.bodies[0] = finalColor;
const isSelected = selectedAnnoId.value === annoData.id;
return {
fill: finalColor,
fillOpacity: 0.25,
stroke: finalColor,
strokeWidth: 2,
fillOpacity: isSelected ? 0.4 : 0.25,
stroke: isSelected ? '#409EFF' : finalColor,
strokeWidth: isSelected ? 3 : 2,
};
}
@ -1238,15 +1467,38 @@ const setAnnotationType = (annotationIndex: number, typeId: number) => {
// 替换 selectAnnotationOnImage 函数
const selectAnnotationOnImage = (annotation: any) => {
if (annoInstance && annotation.annotationData) {
// 使用 Annotorious 的正确 API 来选中标注
// 更新选中状态
selectedAnnoId.value = annotation.annotationData.id;
selectedAnnotationId.value = annotation.id;
// 刷新样式以显示高亮
try {
// 先清除当前选择
annoInstance.clearSelection();
// 选中指定的标注
annoInstance.selectAnnotation(annotation.annotationData.id);
console.log('选中标注:', annotation);
annoInstance.setStyle((annotationItem: any, state: any) => {
if (annotationItem.bodies && annotationItem.bodies.length > 0) {
let color = annotationItem.bodies[0];
if (typeof color === 'object' && color !== null) {
if (!Array.isArray(color)) {
const keys = Object.keys(color).filter(k => k !== 'annotation').sort((a, b) => parseInt(a) - parseInt(b));
color = keys.map(k => color[k]).join('');
}
}
const colorValue = typeof color === 'string' ? color : color.toString();
const isSelected = selectedAnnoId.value === annotationItem.id;
return {
fill: colorValue,
fillOpacity: isSelected ? 0.4 : 0.25,
stroke: isSelected ? '#409EFF' : colorValue,
strokeWidth: isSelected ? 3 : 2,
};
}
return null;
});
console.log('高亮标注:', annotation);
} catch (error) {
console.error('选中标注失败:', error);
console.error('高亮标注失败:', error);
}
}
};
@ -1404,11 +1656,13 @@ const loadExistingAnnotations = () => {
}
const colorValue = typeof color === 'string' ? color : color.toString();
const isSelected = selectedAnnoId.value === annotationItem.id;
return {
fill: colorValue,
fillOpacity: 0.25,
stroke: colorValue,
strokeWidth: 2,
fillOpacity: isSelected ? 0.4 : 0.25,
stroke: isSelected ? '#409EFF' : colorValue,
strokeWidth: isSelected ? 3 : 2,
};
} else {
return null;
@ -1758,33 +2012,41 @@ onUnmounted(() => {
.annotation-list {
display: flex;
flex-direction: column;
gap: 12px;
gap: 8px;
}
.annotation-item {
background: #f8f9fa;
border-radius: 8px;
border-radius: 6px;
border: 1px solid #e9ecef;
padding: 12px;
padding: 8px 10px;
transition: all 0.3s ease;
width:90%;
&:hover {
background: #e9ecef;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.annotation-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
gap: 6px;
margin-bottom: 6px;
.type-select {
flex: 1;
:deep(.el-input__wrapper) {
padding: 2px 8px;
}
:deep(.el-input__inner) {
font-size: 12px;
}
}
.delete-annotation {
color: #f56c6c;
padding: 2px;
&:hover {
background: rgba(245, 108, 108, 0.1);
@ -1795,20 +2057,32 @@ onUnmounted(() => {
.annotation-coords {
display: flex;
flex-direction: column;
gap: 4px;
gap: 2px;
.coord-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
gap: 4px;
font-size: 11px;
color: #606266;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.el-icon {
color: #909399;
flex-shrink: 0;
}
}
}
// 选中状态样式
&.selected {
background: #ecf5ff;
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
transform: translateX(4px);
}
}
// 进度信息
@ -1824,17 +2098,56 @@ onUnmounted(() => {
}
}
// 图片分组样式
.image-section {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
.section-label {
margin-bottom: 10px;
padding-bottom: 6px;
border-bottom: 1px solid #ebeef5;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
user-select: none;
.label-text {
font-size: 12px;
font-weight: 600;
color: #606266;
}
.collapse-icon {
color: #909399;
transition: transform 0.3s;
}
&:hover .collapse-icon {
color: #409eff;
}
}
}
.empty-images {
padding: 20px 0;
}
// 图片网格
.image-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
gap: 10px;
}
.image-item {
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 8px;
padding: 6px;
cursor: pointer;
transition: all 0.3s ease;
background: white;
@ -1842,19 +2155,32 @@ onUnmounted(() => {
&:hover {
border-color: #409eff;
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(64, 158, 255, 0.2);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}
&.active {
border-color: #409eff;
background: linear-gradient(135deg, rgba(64, 158, 255, 0.1) 0%, rgba(64, 158, 255, 0.05) 100%);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
&.annotated,
&.unannotated {
border-left-color: #409eff;
}
}
&.annotated:not(.active) {
border-left: 3px solid #67c23a;
}
&.unannotated:not(.active) {
border-left: 3px solid #e6a23c;
}
.image-wrapper {
position: relative;
width: 100%;
height: 100px;
height: 80px;
overflow: hidden;
border-radius: 6px;
background: #f5f7fa;
@ -1872,28 +2198,32 @@ onUnmounted(() => {
.status-indicator {
position: absolute;
bottom: 6px;
right: 6px;
width: 24px;
height: 24px;
bottom: 4px;
right: 4px;
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
font-size: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
&.completed {
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
}
&.pending {
background: linear-gradient(135deg, #909399 0%, #b1b3b8 100%);
}
}
}
.image-name {
text-align: center;
margin-top: 8px;
font-size: 12px;
margin-top: 6px;
font-size: 11px;
font-weight: 500;
color: #303133;
overflow: hidden;
@ -1904,6 +2234,13 @@ onUnmounted(() => {
// 图片面板包装器
.image-panel-wrapper {
display: flex;
gap: 8px;
height: 100%;
}
// 滚动条样式
.left-scrollbar {
height: 60vh;
@ -1924,21 +2261,52 @@ onUnmounted(() => {
}
}
// 面板标题样式
.panel-title {
font-weight: 600;
color: #303133;
}
.title-progress {
margin-left: 8px;
:deep(.el-progress-circle__track) {
stroke: #e4e7ed;
}
:deep(.el-progress-circle__path) {
stroke: #67c23a;
}
}
.progress-text-small {
font-size: 11px;
color: #909399;
margin-left: 4px;
}
.image-scrollbar {
height: calc(100vh - 200px);
height: calc(100vh - 250px);
flex: 1;
min-width: 0;
overflow-y: auto;
:deep(.el-scrollbar__bar) {
&.is-vertical {
width: 6px;
/* 自定义滚动条样式 */
&::-webkit-scrollbar {
width: 8px;
}
.el-scrollbar__thumb {
background: rgba(144, 147, 153, 0.3);
border-radius: 3px;
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
&:hover {
background: rgba(144, 147, 153, 0.5);
}
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
&:hover {
background: #a1a1a1;
}
}
}

@ -166,7 +166,7 @@
<el-button
link
:type='scope.row.status==0?"warning":(scope.row["wms"+capitalize(column.label)] != scope.row[(column.label)]?"danger":"success")'>
:type='scope.row[(column.label)]===""?"warning":(scope.row["wms"+capitalize(column.label)] != scope.row[(column.label)]?"danger":"success")'>
{{ scope.row["wms"+capitalize(column.label)] }}
</el-button>
</template>

Loading…
Cancel
Save