调整数据库表名,增加催青阶段分析演示模块

This commit is contained in:
BBIT-Kai
2026-01-04 16:06:13 +08:00
parent 209d087fa5
commit e2982b141d
16 changed files with 589 additions and 34 deletions
+1
View File
@@ -1,6 +1,7 @@
from .aimessage import *
from .aiprofile import *
from .aisession import *
from .image_ysa import *
from .iot import *
from .knowledge import *
from .license import *
+1 -1
View File
@@ -41,7 +41,7 @@ def get_all_ai_bot(user_id: str, module: str) -> List[Dict]:
with pg_pool.getConn() as conn:
with conn.cursor() as cur:
# 查询用户角色
cur.execute("SELECT roles FROM users WHERE id = %s", (user_id,))
cur.execute("SELECT roles FROM sys_users WHERE id = %s", (user_id,))
role_row = cur.fetchone()
if not role_row:
return [] # 用户不存在
+122
View File
@@ -0,0 +1,122 @@
import json
from config.minIO import get_temp_url
from config.pgDb import pg_pool
from utils import MyUtils
def insert_ysa_image(
file_name,
resolution,
size,
cocoon_count,
max_confidence,
min_confidence,
average_confidence,
other_info,
preprocess_time_ms,
inference_time_ms,
postprocess_time_ms,
name,
image_pre,
image_after,
created_by,
):
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
other_info = json.dumps(other_info)
cursor.execute(
"""
INSERT INTO image_sca (
upload_datetime, file_name, resolution, size, cocoon_count, max_confidence, min_confidence,
average_confidence, other_info, preprocess_time_ms, inference_time_ms, postprocess_time_ms, name, image_pre, image_after, created_by
)
VALUES (NOW(), %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s )
RETURNING id
""",
(
file_name,
resolution,
size,
cocoon_count,
max_confidence,
min_confidence,
average_confidence,
other_info,
preprocess_time_ms,
inference_time_ms,
postprocess_time_ms,
name,
image_pre,
image_after,
created_by,
),
)
new_id = cursor.fetchone()[0]
conn.commit()
return new_id
def get_ysa_image_list(user_id, name, page=1, page_size=10):
"""
获取用户已分析图片列表,带分页
"""
offset = (page - 1) * page_size
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
# 1️⃣ 查询总条数
# ✅ 改进版:支持 name 为空时统计全部,不为空时模糊统计
cursor.execute(
"""
SELECT COUNT(*)
FROM image_ysa
WHERE created_by = %s
AND (%s = '' OR name LIKE '%%' || %s || '%%')
""",
(user_id, name, name),
)
total = cursor.fetchone()[0]
# 2️⃣ 查询当前页数据
# ✅ 改进版
cursor.execute(
"""
SELECT id, name, upload_datetime, file_name, image_pre, image_after, resolution,
size, silkworm_count, max_confidence, min_confidence, average_confidence, other_info, preprocess_time_ms, inference_time_ms, postprocess_time_ms
FROM image_ysa
WHERE created_by = %s
AND (%s = '' OR name LIKE '%%' || %s || '%%')
ORDER BY upload_datetime DESC
LIMIT %s OFFSET %s
""",
(user_id, name, name, page_size, offset),
)
rows = cursor.fetchall()
result = []
for row in rows:
result.append(
{
"id": row[0],
"name": row[1],
"upload_datetime": MyUtils.format_datetime(row[2]),
"file_name": row[3],
"image_pre": get_temp_url("image-ysa", "raw/" + row[4]),
"image_after": get_temp_url("image-ysa", "ai/" + row[5]),
"resolution": row[6],
"size": MyUtils.safe_round(row[7] / 1024, 2),
"silkworm_count": row[8],
"max_confidence": row[9],
"min_confidence": row[10],
"average_confidence": row[11],
"other_info": row[12],
"preprocess_time_ms": MyUtils.safe_round(row[13], 4),
"inference_time_ms": MyUtils.safe_round(row[14], 4),
"postprocess_time_ms": MyUtils.safe_round(row[15], 4),
}
)
return total, result
+3 -3
View File
@@ -10,7 +10,7 @@ def insert_license_image(
with conn.cursor() as cursor:
cursor.execute(
"""
INSERT INTO license_images (
INSERT INTO image_license (
created_by, created_at, file_name, resolution, size, name, oss,
type, content
)
@@ -36,7 +36,7 @@ def get_license_image_list(user_id, page=1, page_size=10):
cursor.execute(
"""
SELECT COUNT(*)
FROM license_images
FROM image_license
WHERE created_by = %s
""",
(user_id,),
@@ -47,7 +47,7 @@ def get_license_image_list(user_id, page=1, page_size=10):
cursor.execute(
"""
SELECT created_at, file_name, resolution, size, name, oss, id, type, content
FROM license_images
FROM image_license
WHERE created_by = %s
ORDER BY created_at DESC
LIMIT %s OFFSET %s
+3 -3
View File
@@ -27,7 +27,7 @@ def insert_sca_image(
other_info = json.dumps(other_info)
cursor.execute(
"""
INSERT INTO sca_images (
INSERT INTO image_sca (
upload_datetime, file_name, resolution, size, cocoon_count, max_confidence, min_confidence,
average_confidence, other_info, preprocess_time_ms, inference_time_ms, postprocess_time_ms, name, image_pre, image_after, created_by
)
@@ -70,7 +70,7 @@ def get_sca_image_list(user_id, name, page=1, page_size=10):
cursor.execute(
"""
SELECT COUNT(*)
FROM sca_images
FROM image_sca
WHERE created_by = %s
AND (%s = '' OR name LIKE '%%' || %s || '%%')
""",
@@ -85,7 +85,7 @@ def get_sca_image_list(user_id, name, page=1, page_size=10):
"""
SELECT id, name, upload_datetime, file_name, image_pre, image_after, resolution,
size, cocoon_count, max_confidence, min_confidence, average_confidence, other_info, preprocess_time_ms, inference_time_ms, postprocess_time_ms
FROM sca_images
FROM image_sca
WHERE created_by = %s
AND (%s = '' OR name LIKE '%%' || %s || '%%')
ORDER BY upload_datetime DESC
+4 -4
View File
@@ -17,7 +17,7 @@ def get_sca_video_list(name, page=1, page_size=10):
cursor.execute(
"""
SELECT COUNT(*)
FROM sca_videos
FROM video_sca
WHERE (%s = '' OR name LIKE '%%' || %s || '%%')
""",
(name, name),
@@ -30,7 +30,7 @@ def get_sca_video_list(name, page=1, page_size=10):
SELECT id, name, raw_object_name, ai_object_name, duration, size, video_codec, audio_codec,
overall_bit_rate, resolution, sc_analysis_time, sc_analysis_total_count, sc_analysis_max_count,
sc_analysis_primary_type, sc_analysis_secondary_type, other_info, created_at
FROM sca_videos
FROM video_sca
WHERE (%s = '' OR name LIKE '%%' || %s || '%%')
ORDER BY created_at DESC
LIMIT %s OFFSET %s
@@ -67,7 +67,7 @@ def get_sca_video_list(name, page=1, page_size=10):
return total, result
def get_sca_video_details(v_id):
def get_video_sca_details(v_id):
"""
获取指定视频的分析明细列表
"""
@@ -76,7 +76,7 @@ def get_sca_video_details(v_id):
cursor.execute(
"""
SELECT id, v_id, time_stamp, other_info
FROM sca_video_details
FROM video_sca_details
WHERE v_id = %s
ORDER BY time_stamp ASC
""",
+11 -9
View File
@@ -446,7 +446,7 @@ def get_user_list_db_page(
with conn.cursor() as cursor:
# ---- 统计总数 ----
count_sql = f"SELECT COUNT(*) FROM users u {where_clause};"
count_sql = f"SELECT COUNT(*) FROM sys_users u {where_clause};"
cursor.execute(count_sql, params)
total = cursor.fetchone()[0]
@@ -461,7 +461,7 @@ def get_user_list_db_page(
u.dept_id,
d.name AS dept_name,
u.created_at
FROM users u
FROM sys_users u
LEFT JOIN sys_dept d ON u.dept_id = d.id
{where_clause}
ORDER BY u.created_at DESC
@@ -511,7 +511,9 @@ def get_user_list_db_page(
def db_user_name_exists(username: str) -> bool:
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT COUNT(*) FROM users WHERE email = %s;", (username,))
cursor.execute(
"SELECT COUNT(*) FROM sys_users WHERE email = %s;", (username,)
)
return cursor.fetchone()[0] > 0
@@ -521,7 +523,7 @@ def insert_user(data: dict) -> str:
with conn.cursor() as cursor:
cursor.execute(
"""
INSERT INTO users (username, email, phone, is_active, dept_id, password_hash)
INSERT INTO sys_users (username, email, phone, is_active, dept_id, password_hash)
VALUES (%s, %s, %s, %s, %s, %s) RETURNING id;
""",
(
@@ -578,7 +580,7 @@ def patch_user_db(id: str, data: dict) -> int:
# 如果有需要更新的字段
if fields:
sql = f"UPDATE users SET {', '.join(fields)} WHERE id = %s"
sql = f"UPDATE sys_users SET {', '.join(fields)} WHERE id = %s"
params.append(id)
cursor.execute(sql, tuple(params))
@@ -603,7 +605,7 @@ def delete_user_db(id: str) -> int:
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute("DELETE FROM sys_user_role WHERE user_id=%s;", (id,))
cursor.execute("DELETE FROM users WHERE id=%s;", (id,))
cursor.execute("DELETE FROM sys_users WHERE id=%s;", (id,))
conn.commit()
return cursor.rowcount
@@ -882,7 +884,7 @@ def get_dept_ids_by_user_id(user_id: UUID) -> list:
# 第一步:通过 user_id 查找其所属的 dept_id
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT dept_id FROM users WHERE id = %s", (user_id,))
cursor.execute("SELECT dept_id FROM sys_users WHERE id = %s", (user_id,))
dept_id = cursor.fetchone()
if not dept_id:
@@ -914,7 +916,7 @@ def get_dept_ids_by_user_id(user_id: UUID) -> list:
# 第一步:通过 user_id 查找其所属的 dept_id
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT dept_id FROM users WHERE id = %s", (user_id,))
cursor.execute("SELECT dept_id FROM sys_users WHERE id = %s", (user_id,))
dept_id = cursor.fetchone()
if not dept_id:
@@ -946,7 +948,7 @@ def get_dept_id_by_user_id(user_id: str) -> str:
# 通过 user_id 查找其所属的 dept_id
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT dept_id FROM users WHERE id = %s", (user_id,))
cursor.execute("SELECT dept_id FROM sys_users WHERE id = %s", (user_id,))
dept_id = cursor.fetchone()
dept_id = dept_id[0]
return str(dept_id)
+2 -2
View File
@@ -12,7 +12,7 @@ def get_ticket_image_list(user_id):
moisture_content, cocoon_weight, defective_pupa_count,
fresh_shell_weight, sample_count, barcode, oss,
net_weight_total, evaluator, reviewer,id ,dead_pupa_count
FROM ticket_images
FROM image_ticket
WHERE created_by = %s
""",
(user_id,),
@@ -66,7 +66,7 @@ def insert_ticket_image(
with conn.cursor() as cursor:
cursor.execute(
"""
INSERT INTO ticket_images (
INSERT INTO image_ticket (
created_by, file_name, resolution, size, name,
moisture_content, cocoon_weight, defective_pupa_count, dead_pupa_count,
fresh_shell_weight, sample_count, barcode, oss,
+6 -6
View File
@@ -1,9 +1,8 @@
from sqlalchemy import text
from sqlalchemy import text
from sqlalchemy import text
from config.pgDb import pg_pool
from config.ssDb import mssql_pool
from sqlalchemy import text
def executeSQL(sql: str):
"""
@@ -14,11 +13,14 @@ def executeSQL(sql: str):
# SQLAlchemy 2.x 返回 Row 对象,转成 dict
return [dict(row._mapping) for row in result]
def get_company_list(user_id: str):
# 1️⃣ 从 PostgreSQL 获取 tenant_id
with pg_pool.getConn() as pg_conn:
with pg_conn.cursor() as cur:
cur.execute("SELECT bbit_tenant_id FROM users WHERE id = %s", (user_id,))
cur.execute(
"SELECT bbit_tenant_id FROM sys_users WHERE id = %s", (user_id,)
)
row = cur.fetchone()
tenant_id = row[0] if row else None
@@ -33,5 +35,3 @@ def get_company_list(user_id: str):
with mssql_pool.getConn() as mssql_conn:
result = mssql_conn.execute(query, params)
return [{"id": str(row[0]), "name": row[1]} for row in result.fetchall()]
+20 -1
View File
@@ -153,6 +153,25 @@ def getSilkwormCocoonAnalysisTasks(
)
# ————————————————————————————————催青阶段分析————————————————————————————————————————————————
@visionRouter.get("/getPromoteStageAnalysisTasks")
def getPromoteStageAnalysisTasks(
user_id: UUID = Depends(get_user_id_from_token),
name: str = "",
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
):
if not user_id:
return {"error": "userId is required"}
total, items = pg.get_ysa_image_list(user_id, name, page=page, page_size=page_size)
return BaseResponse(
data={
"total": total,
"items": items,
}
)
# ————————————————————————————————蚕茧视频识别任务————————————————————————————————————————————————
@@ -190,4 +209,4 @@ def getAnalyticsDetailBySCVideoId(
):
if not user_id:
return {"error": "userId is required"}
return BaseResponse(data=pg.get_sca_video_details(vId))
return BaseResponse(data=pg.get_video_sca_details(vId))
@@ -5,7 +5,7 @@ import org.jetbrains.exposed.v1.core.dao.id.UUIDTable
import org.jetbrains.exposed.v1.datetime.timestamp
import org.jetbrains.exposed.v1.json.json
object UsersTable : UUIDTable("users") {
object UsersTable : UUIDTable("sys_users") {
// 用户名
val username = varchar("username", length = 50).nullable()
@@ -4,7 +4,7 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
import org.jetbrains.exposed.v1.core.dao.id.UUIDTable
object VideoAnalyticsDetailsTable : UUIDTable("video_analytics_details") {
object VideoAnalyticsDetailsTable : UUIDTable("video_act_details") {
val vId = uuid("v_id").references(VideosTable.id)
val aTimeStamp = integer("time_stamp")
val aTotalPeople = integer("total_people")
@@ -5,7 +5,7 @@ import org.jetbrains.exposed.v1.core.dao.id.UUIDTable
import org.jetbrains.exposed.v1.datetime.datetime
object VideosTable : UUIDTable("videos") {
object VideosTable : UUIDTable("video_act") {
val vName = varchar("name", 255)
val vObjectName = varchar("object_name", 255)
val vFileName = varchar("file_name", 255)
+1
View File
@@ -4,3 +4,4 @@ export * from './license';
export * from './sca';
export * from './sca2';
export * from './ticket';
export * from './ysa';
+25
View File
@@ -0,0 +1,25 @@
import { pyRequestClient } from '#/api/request';
/**
* 获取已分析的图片列表
*/
export async function getSilkwormCocoonAnalysisTasks(
name = '',
page = 1,
pageSize = 9,
) {
return pyRequestClient.get('/cv/getPromoteStageAnalysisTasks', {
params: { name, page, page_size: pageSize },
});
}
/**
* 上传图片分析任务
*/
export async function createSilkwormCocoonAnalysisTask(formData: FormData) {
return pyRequestClient.post('/cv/createPromoteStageAnalysisTask', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
+387 -2
View File
@@ -1,5 +1,390 @@
<script setup lang="ts">
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
computed,
onActivated,
onDeactivated,
onMounted,
ref,
watch,
} from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { Button, Form, Input, message } from 'ant-design-vue';
import * as api from '#/api';
import { createSilkwormCocoonAnalysisTask } from '#/api';
const list = ref<any[]>([]);
const error = ref<null | string>(null);
const filterKeyword = ref('');
const selectedItem = ref<any>(null);
async function loadList() {
error.value = null;
const res = await api.getSilkwormCocoonAnalysisTasks(
filterKeyword.value,
page.value,
pageSize.value,
);
list.value = res.items || [];
total.value = res.total;
}
function refreshList() {
filterKeyword.value = '';
loadList();
message.success('列表加载完成');
}
function createTask() {
modalApi.open();
}
async function selectItem(item: any) {
selectedItem.value = item;
refreshLineChart();
}
// 监听关键词变化,调用防抖接口
watch(filterKeyword, () => {
loadList();
});
watch(selectedItem, () => {});
onMounted(() => {
loadList();
});
const showInfoStr = ref<Record<string, number | string>>({});
const chartRef1 = ref<EchartsUIType>();
const { renderEcharts: renderEcharts1 } = useEcharts(chartRef1);
const chartRef2 = ref<EchartsUIType>();
const { renderEcharts: renderEcharts2 } = useEcharts(chartRef2);
function refreshLineChart() {
const data = selectedItem.value;
showInfoStr.value = {
项目名: data.name,
项目上传时间: data.upload_datetime,
文件名: data.file_name,
文件大小: `${data.size} MB`,
分辨率: data.resolution,
最高置信度: data.max_confidence,
最低置信度: data.min_confidence,
平均置信度: data.average_confidence,
预处理时间: `${data.preprocess_time_ms} ms`,
推理时间: `${data.inference_time_ms} ms`,
后处理时间: `${data.postprocess_time_ms} ms`,
};
renderEcharts1({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: [
{
type: 'category',
data: Object.keys(data.other_info),
axisTick: {
alignWithLabel: true,
},
},
],
yAxis: [
{
type: 'value',
},
],
series: [
{
name: '数量',
type: 'bar',
barWidth: '30%',
data: Object.values(data.other_info).map(Number),
},
],
});
renderEcharts2({
legend: { top: '5%', left: 'center' },
series: [
{
animationDelay() {
return Math.random() * 100;
},
animationEasing: 'exponentialInOut',
animationType: 'scale',
avoidLabelOverlap: false,
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
data: Object.entries(data.other_info).map(([key, val]) => ({
name: key,
value: Number(val),
})),
itemStyle: {
borderRadius: 10,
borderWidth: 3,
},
label: {
position: 'center',
show: false,
},
labelLine: {
show: false,
},
padAngle: 5,
name: '蚕茧分类',
radius: ['10%', '70%'],
type: 'pie',
},
],
tooltip: {
trigger: 'item',
},
});
}
onDeactivated(() => {
// 离开路由时清理状态
selectedItem.value = null;
showInfoStr.value = {};
});
onActivated(() => {
// 回来的时候重新刷新一次列表
loadList();
});
const projectName = ref('');
const fileName = ref('');
const selectedFile = ref<File | null>(null);
const fileInputRef = ref<HTMLInputElement | null>(null);
const [Modal, modalApi] = useVbenModal({
title: '新建幼蚕分析任务',
class: 'w-[600px]',
onCancel() {
modalApi.close();
},
onConfirm() {
if (!selectedFile.value) {
message.warning('请选择幼蚕图片');
return;
}
uploadFile();
},
});
async function uploadFile() {
// 先关闭弹窗
modalApi.close();
const formData = new FormData();
formData.append('file', selectedFile.value!);
formData.append('projectName', projectName.value);
await createSilkwormCocoonAnalysisTask(formData).then(() => {
// 清空表单
projectName.value = '';
fileName.value = '';
selectedFile.value = null;
});
// 接口完成后再触发事件
message.success('分析完成');
loadList();
}
function selectFile() {
fileInputRef.value?.click();
}
function handleFileChange(event: Event) {
const files = (event.target as HTMLInputElement).files;
if (files && files.length > 0) {
selectedFile.value = files[0];
fileName.value = files[0].name;
}
}
// 分页参数
const page = ref(1);
const pageSize = ref(9);
const total = ref(0); // 总条数
// 计算总页数
const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
function changePage(newPage) {
page.value = newPage;
loadList();
}
</script>
<template>
<div>
<h1>正在开发中敬请期待</h1>
<div class="flex h-[90dvh] w-full flex-col">
<Modal>
<Form layout="vertical">
<Form.Item label="任务名称">
<Input v-model:value="projectName" />
</Form.Item>
<Form.Item label="上传幼蚕图片*" required>
<div
@click="selectFile"
style="
padding: 16px;
text-align: center;
cursor: pointer;
border: 1px dashed #d9d9d9;
"
>
{{ fileName || '点击选择文件' }}
<input
type="file"
accept="image/*"
ref="fileInputRef"
@change="handleFileChange"
style="display: none"
/>
</div>
</Form.Item>
</Form>
</Modal>
<div class="flex h-full w-full bg-gray-50">
<!-- 左侧筛选 + 列表 -->
<div class="flex w-64 flex-col border-r bg-white p-4">
<!-- 按钮组 -->
<div class="mb-4 flex justify-between space-x-2">
<Button
type="primary"
@click="createTask"
class="flex-1"
>
新建任务
</Button>
<Button @click="refreshList" class="flex-1"> 刷新列表 </Button>
</div>
<!-- 筛选框 -->
<input
v-model="filterKeyword"
placeholder="筛选分析任务"
class="mb-4 rounded-md border px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary"
/>
<!-- 列表 -->
<div class="flex-1 space-y-2 overflow-auto">
<div
v-for="item in list"
:key="item.v_id"
@click="selectItem(item)"
class="cursor-pointer rounded border p-3 hover:bg-gray-100"
:class="{ 'bg-gray-100': item.id === selectedItem?.id }"
>
<div class="text-base font-medium">{{ item.name }}</div>
<div class="text-sm text-gray-400">{{ item.upload_datetime }}</div>
</div>
</div>
<!-- 分页 -->
<div class="mt-2 flex justify-center space-x-2">
<button
:disabled="page === 1"
@click="changePage(page - 1)"
class="rounded border px-1"
>
上一页
</button>
<span> {{ page }} / {{ totalPages }} </span>
<button
:disabled="page === totalPages"
@click="changePage(page + 1)"
class="rounded border px-1"
>
下一页
</button>
</div>
</div>
<!-- 右侧Tab 内容区 -->
<div class="flex flex-1 flex-col overflow-hidden p-6">
<div
v-if="!selectedItem"
class="flex h-full items-center justify-center text-gray-400"
>
请先选择左侧列表中的分析任务
</div>
<template v-else>
<div class="flex h-full flex-col gap-4">
<!-- 主内容区域左右结构 -->
<div class="flex flex-1 gap-4">
<!-- 左侧 -->
<div class="flex w-72 flex-col gap-4">
<!-- 视频基础信息展示 -->
<div
class="w-full rounded border bg-white p-4"
id="video_base_info"
>
<div
v-for="(value, key) in showInfoStr"
:key="key"
class="mb-2 flex text-sm text-gray-700"
>
<div class="w-32 font-medium text-gray-900">
{{ key }}
</div>
<div class="flex-1 break-all text-gray-600">
{{ value || '—' }}
</div>
</div>
</div>
<!-- 空白卡片 -->
<div class="flex-1 rounded border bg-white p-4">
<EchartsUI ref="chartRef2" />
</div>
</div>
<!-- 右侧 -->
<div class="flex flex-1 flex-col gap-4">
<!-- 左右两个图片显示 -->
<div class="flex flex-1 gap-4">
<!-- 左图 -->
<div
class="flex flex-1 items-center justify-center rounded border bg-white p-4"
>
<img
:src="selectedItem?.image_pre"
alt=""
class="h-[30dvh] w-full object-contain"
/>
</div>
<!-- 右图 -->
<div
class="flex flex-1 items-center justify-center rounded border bg-white p-4"
>
<img
:src="selectedItem?.image_after"
alt=""
class="h-[30dvh] w-full object-contain"
/>
</div>
</div>
<!-- 柱状图区域 -->
<div class="flex flex-1 flex-col rounded border bg-white p-4">
<EchartsUI ref="chartRef1" class="flex-1" />
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</template>