修复切换<视频智能分析>与<蚕茧仪评分析>Tab页后其他页面无法正常加载的问题

This commit is contained in:
BBIT-Kai
2025-08-22 09:57:27 +08:00
parent cdce90bb27
commit 10ca219970
7 changed files with 335 additions and 229 deletions
+22
View File
@@ -8,6 +8,12 @@ export namespace AuthApi {
account?: string; account?: string;
password?: string; password?: string;
} }
/** 注册 */
export interface RegisterParams {
account?: string;
code?: string;
password?: string;
}
export interface Token { export interface Token {
token: string; token: string;
@@ -39,6 +45,22 @@ export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/user/login', data); return requestClient.post<AuthApi.LoginResult>('/user/login', data);
} }
/**
* 发送验证码
*/
export async function sendCodeApi(str: string) {
return requestClient.post<any>('/user/sendCode', { str });
}
/**
* 注册
*/
export async function registerApi(data: AuthApi.RegisterParams) {
// 密码加密
data.password = sha256(data.password?.toString() || '');
return requestClient.post<any>('/user/register', data);
}
/** /**
* 刷新accessToken * 刷新accessToken
*/ */
+1
View File
@@ -46,5 +46,6 @@ export const overridesPreferences = defineOverridesPreferences({
}, },
tabbar: { tabbar: {
middleClickToClose: true, middleClickToClose: true,
keepAlive: false,
}, },
}); });
@@ -22,7 +22,7 @@ const routes: RouteRecordRaw[] = [
authority: ['iva'], authority: ['iva'],
icon: 'mdi:video', icon: 'mdi:video',
title: $t('ai.intelligence_video_analysis'), title: $t('ai.intelligence_video_analysis'),
keepAlive: false, keepAlive: true,
}, },
component: () => import('#/views/ai/iva/index.vue'), component: () => import('#/views/ai/iva/index.vue'),
}, },
@@ -33,6 +33,7 @@ const routes: RouteRecordRaw[] = [
authority: ['sca'], authority: ['sca'],
icon: 'mdi:ice-cream', icon: 'mdi:ice-cream',
title: $t('ai.silkworm_cocoon_analysis'), title: $t('ai.silkworm_cocoon_analysis'),
keepAlive: false,
}, },
component: () => import('#/views/ai/sca/index.vue'), component: () => import('#/views/ai/sca/index.vue'),
}, },
@@ -43,6 +44,7 @@ const routes: RouteRecordRaw[] = [
authority: ['ysa'], authority: ['ysa'],
icon: 'mdi:account-key-outline', icon: 'mdi:account-key-outline',
title: $t('ai.young_silkworm_analysis'), title: $t('ai.young_silkworm_analysis'),
keepAlive: false,
}, },
component: () => import('#/views/ai/ysa/index.vue'), component: () => import('#/views/ai/ysa/index.vue'),
}, },
@@ -53,7 +55,7 @@ const routes: RouteRecordRaw[] = [
meta: { meta: {
icon: 'mdi:wall-fire', icon: 'mdi:wall-fire',
iframeSrc: 'http://171.212.101.199:13010/', iframeSrc: 'http://171.212.101.199:13010/',
keepAlive: true, keepAlive: false,
title: '检索增强生成', title: '检索增强生成',
}, },
}, },
@@ -2,25 +2,72 @@
import type { VbenFormSchema } from '@vben/common-ui'; import type { VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types'; import type { Recordable } from '@vben/types';
import { computed, h, ref } from 'vue'; import { computed, h, ref, useTemplateRef } from 'vue';
import { AuthenticationRegister, z } from '@vben/common-ui'; import { AuthenticationRegister, z } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { registerApi, sendCodeApi } from '#/api';
import { useAuthStore } from '#/store';
defineOptions({ name: 'Register' }); defineOptions({ name: 'Register' });
const loading = ref(false); const loading = ref(false);
const CODE_LENGTH = 4;
const registerRef =
useTemplateRef<InstanceType<typeof AuthenticationRegister>>('registerRef');
const sending = ref(false);
const formSchema = computed((): VbenFormSchema[] => { const formSchema = computed((): VbenFormSchema[] => {
return [ return [
{ {
component: 'VbenInput', component: 'VbenInput',
componentProps: { componentProps: {
placeholder: $t('authentication.usernameTip'), placeholder: $t('authentication.emailTip'),
}, },
fieldName: 'username', fieldName: 'account',
label: $t('authentication.username'), label: $t('authentication.email'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }), rules: z
.string()
.min(1, { message: $t('authentication.emailTip') })
.refine((v) => /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/.test(v), {
message: $t('邮箱格式错误'),
}),
},
{
component: 'VbenPinInput',
componentProps: {
codeLength: CODE_LENGTH,
createText(countdown: number) {
if (sending.value) {
return $t('正在发送中');
}
return countdown > 0
? $t('authentication.sendText', [countdown])
: $t('authentication.sendCode');
},
placeholder: $t('authentication.code'),
async handleSendCode() {
sending.value = true;
try {
const formApi = registerRef.value?.getFormApi();
if (!formApi) throw new Error('formApi is not ready');
await formApi.validateField('account');
const isPhoneReady = await formApi.isFieldValid('account');
if (!isPhoneReady) throw new Error('邮箱不可为空');
const { account } = await formApi.getValues();
await sendCodeApi(account);
} finally {
sending.value = false;
}
},
},
fieldName: 'code',
label: $t('authentication.code'),
rules: z.string().length(CODE_LENGTH, {
message: $t('authentication.codeTip', [CODE_LENGTH]),
}),
}, },
{ {
component: 'VbenInputPassword', component: 'VbenInputPassword',
@@ -80,15 +127,32 @@ const formSchema = computed((): VbenFormSchema[] => {
}, },
]; ];
}); });
async function handleSubmit(value: Recordable<any>) {
loading.value = true;
try {
const params = {
account: value.account,
code: value.code,
password: value.password,
};
const { accessToken } = await registerApi(params);
function handleSubmit(value: Recordable<any>) { if (accessToken) {
// eslint-disable-next-line no-console // 注册成功后,把 token 交给 authStore 走统一流程
console.log('register submit:', value); const authStore = useAuthStore();
authStore.authLogin({ account: value.account, password: value.password });
}
} catch (error) {
console.error('注册失败', error);
} finally {
loading.value = false;
}
} }
</script> </script>
<template> <template>
<AuthenticationRegister <AuthenticationRegister
ref="registerRef"
:form-schema="formSchema" :form-schema="formSchema"
:loading="loading" :loading="loading"
@submit="handleSubmit" @submit="handleSubmit"
+128 -126
View File
@@ -345,152 +345,154 @@ function onListItemClick(video: any) {
</script> </script>
<template> <template>
<BaseModal /> <div class="flex h-full w-full flex-col">
<CreateVideoTaskModal /> <BaseModal />
<div class="flex h-full w-full bg-gray-50"> <CreateVideoTaskModal />
<!-- 左侧筛选 + 列表 --> <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="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"> <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> </Button>
</div> <Button @click="refreshList" class="flex-1"> 刷新列表 </Button>
<!-- 筛选框 -->
<input
v-model="filterKeyword"
placeholder="筛选分析任务"
class="focus:ring-primary mb-4 rounded-md border px-3 py-2 focus:outline-none focus:ring-2"
/>
<!-- 列表 -->
<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.v_id === selectedItem?.v_id }"
>
<div class="text-base font-medium">{{ item.v_name }}</div>
<div class="text-sm text-gray-400">{{ item.v_a_time }}</div>
</div> </div>
</div> <!-- 筛选框 -->
</div> <input
v-model="filterKeyword"
<!-- 右侧Tab 内容区 --> placeholder="筛选分析任务"
<div class="flex flex-1 flex-col overflow-hidden p-6"> class="focus:ring-primary mb-4 rounded-md border px-3 py-2 focus:outline-none focus:ring-2"
<!-- Tab 标题 --> />
<div class="mb-4 flex shrink-0 space-x-4 border-b"> <!-- 列表 -->
<button <div class="flex-1 space-y-2 overflow-auto">
v-for="tab in tabs"
:key="tab.key"
@click="activeTab = tab.key as 'detail' | 'video'"
class="px-4 py-2"
:class="[
activeTab === tab.key
? 'border-primary text-primary border-b-2'
: 'hover:text-primary text-gray-500',
]"
>
{{ tab.label }}
</button>
</div>
<!-- Tab 内容滚动区域 -->
<div class="flex-1 overflow-auto">
<div
v-if="!selectedItem"
class="flex h-full items-center justify-center text-gray-400"
>
请先选择左侧列表中的分析任务
</div>
<template v-else>
<div <div
v-show="activeTab === 'detail'" v-for="item in list"
class="flex h-full flex-col gap-4" :key="item.v_id"
@click="selectItem(item)"
class="cursor-pointer rounded border p-3 hover:bg-gray-100"
:class="{ 'bg-gray-100': item.v_id === selectedItem?.v_id }"
> >
<!-- 主内容区域左右结构 --> <div class="text-base font-medium">{{ item.v_name }}</div>
<div class="flex flex-1 gap-4"> <div class="text-sm text-gray-400">{{ item.v_a_time }}</div>
<!-- 左侧 --> </div>
<div class="flex w-72 flex-col gap-4"> </div>
<!-- 视频基础信息展示 --> </div>
<div
class="w-full rounded border bg-white p-4" <!-- 右侧Tab 内容区 -->
id="video_base_info" <div class="flex flex-1 flex-col overflow-hidden p-6">
> <!-- Tab 标题 -->
<div class="mb-4 flex shrink-0 space-x-4 border-b">
<button
v-for="tab in tabs"
:key="tab.key"
@click="activeTab = tab.key as 'detail' | 'video'"
class="px-4 py-2"
:class="[
activeTab === tab.key
? 'border-primary text-primary border-b-2'
: 'hover:text-primary text-gray-500',
]"
>
{{ tab.label }}
</button>
</div>
<!-- Tab 内容滚动区域 -->
<div class="flex-1 overflow-auto">
<div
v-if="!selectedItem"
class="flex h-full items-center justify-center text-gray-400"
>
请先选择左侧列表中的分析任务
</div>
<template v-else>
<div
v-show="activeTab === 'detail'"
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 <div
v-for="(value, key) in showInfoStr" class="w-full rounded border bg-white p-4"
:key="key" id="video_base_info"
class="mb-2 flex text-sm text-gray-700"
> >
<div class="w-32 font-medium text-gray-900"> <div
{{ key }} v-for="(value, key) in showInfoStr"
</div> :key="key"
<div class="flex-1 break-all text-gray-600"> class="mb-2 flex text-sm text-gray-700"
{{ value || '—' }} >
<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> </div>
<!-- 空白卡片 -->
<div class="h-[300px] flex-1 rounded border bg-white p-4">
<EchartsUI ref="chartRef2" />
</div>
</div> </div>
<!-- 空白卡片 --> <!-- 右侧 -->
<div class="h-[300px] flex-1 rounded border bg-white p-4"> <div class="flex flex-1 flex-col gap-4">
<EchartsUI ref="chartRef2" /> <!-- 四个统计卡片 -->
</div> <AnalysisOverview
</div> :items="overviewItems"
class="grid grid-cols-4 gap-4"
<!-- 右侧 --> />
<div class="flex flex-1 flex-col gap-4"> <!-- 折线图区域 -->
<!-- 四个统计卡片 --> <div class="flex-1 rounded border bg-white p-4">
<AnalysisOverview <EchartsUI ref="chartRef1" />
:items="overviewItems" </div>
class="grid grid-cols-4 gap-4"
/>
<!-- 折线图区域 -->
<div class="flex-1 rounded border bg-white p-4">
<EchartsUI ref="chartRef1" />
</div> </div>
</div> </div>
</div> </div>
</div> <div v-show="activeTab === 'video'" class="flex h-full space-x-4">
<div v-show="activeTab === 'video'" class="flex h-full space-x-4"> <!-- 左侧视频区域 -->
<!-- 左侧视频区域 --> <div class="flex-1 overflow-hidden rounded bg-black">
<div class="flex-1 overflow-hidden rounded bg-black"> <video
<video ref="videoEl"
ref="videoEl" class="video-js vjs-default-skin h-full w-full"
class="video-js vjs-default-skin h-full w-full" preload="auto"
preload="auto" controls
controls ></video>
></video> </div>
</div>
<!-- 右侧时间点列表 --> <!-- 右侧时间点列表 -->
<div
class="flex w-1/4 flex-col overflow-auto rounded-md border bg-white"
>
<!-- 列表标题 -->
<div <div
class="flex justify-between border-b p-3 text-sm font-medium text-gray-700" class="flex w-1/4 flex-col overflow-auto rounded-md border bg-white"
> >
<span>事件</span> <!-- 列表标题 -->
<span>时间点</span>
</div>
<!-- 列表内容 -->
<div class="flex-1">
<div <div
v-for="(video, index) in detailList" class="flex justify-between border-b p-3 text-sm font-medium text-gray-700"
:key="index"
@click="onListItemClick(video)"
class="flex cursor-pointer justify-between border-b p-3 text-sm hover:bg-gray-100"
> >
<span>{{ video.action }}</span> <span>事件</span>
<span>{{ video.time }}</span> <span>时间点</span>
</div>
<!-- 列表内容 -->
<div class="flex-1">
<div
v-for="(video, index) in detailList"
:key="index"
@click="onListItemClick(video)"
class="flex cursor-pointer justify-between border-b p-3 text-sm hover:bg-gray-100"
>
<span>{{ video.action }}</span>
<span>{{ video.time }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </template>
</template> </div>
</div> </div>
</div> </div>
</div> </div>
+106 -91
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { EchartsUIType } from '@vben/plugins/echarts'; import type { EchartsUIType } from '@vben/plugins/echarts';
import { onMounted, ref, watch } from 'vue'; import { onActivated, onDeactivated, onMounted, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
@@ -144,112 +144,127 @@ function refreshLineChart() {
}, },
}); });
} }
onDeactivated(() => {
// 离开路由时清理状态
selectedItem.value = null;
showInfoStr.value = {};
});
onActivated(() => {
// 回来的时候重新刷新一次列表
loadList();
});
</script> </script>
<template> <template>
<BaseModal /> <div class="flex h-full w-full flex-col">
<CreateYSATaskModal /> <BaseModal />
<div class="flex h-full w-full bg-gray-50"> <CreateYSATaskModal />
<!-- 左侧筛选 + 列表 --> <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="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"> <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> </Button>
</div> <Button @click="refreshList" class="flex-1"> 刷新列表 </Button>
<!-- 筛选框 --> </div>
<input <!-- 筛选框 -->
v-model="filterKeyword" <input
placeholder="筛选分析任务" v-model="filterKeyword"
class="focus:ring-primary mb-4 rounded-md border px-3 py-2 focus:outline-none focus:ring-2" placeholder="筛选分析任务"
/> class="focus:ring-primary mb-4 rounded-md border px-3 py-2 focus:outline-none focus:ring-2"
<!-- 列表 --> />
<div class="flex-1 space-y-2 overflow-auto"> <!-- 列表 -->
<div <div class="flex-1 space-y-2 overflow-auto">
v-for="item in list" <div
:key="item.v_id" v-for="item in list"
@click="selectItem(item)" :key="item.v_id"
class="cursor-pointer rounded border p-3 hover:bg-gray-100" @click="selectItem(item)"
:class="{ 'bg-gray-100': item.id === selectedItem?.id }" 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 class="text-base font-medium">{{ item.name }}</div>
<div class="text-sm text-gray-400">{{ item.upload_datetime }}</div>
</div>
</div> </div>
</div> </div>
</div>
<!-- 右侧Tab 内容区 --> <!-- 右侧Tab 内容区 -->
<div class="flex flex-1 flex-col overflow-hidden p-6"> <div class="flex flex-1 flex-col overflow-hidden p-6">
<div <div
v-if="!selectedItem" v-if="!selectedItem"
class="flex h-full items-center justify-center text-gray-400" class="flex h-full items-center justify-center text-gray-400"
> >
请先选择左侧列表中的分析任务 请先选择左侧列表中的分析任务
</div> </div>
<template v-else> <template v-else>
<div class="flex h-full flex-col gap-4"> <div class="flex h-full flex-col gap-4">
<!-- 主内容区域左右结构 --> <!-- 主内容区域左右结构 -->
<div class="flex flex-1 gap-4"> <div class="flex flex-1 gap-4">
<!-- 左侧 --> <!-- 左侧 -->
<div class="flex w-72 flex-col 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 <div
v-for="(value, key) in showInfoStr" class="w-full rounded border bg-white p-4"
:key="key" id="video_base_info"
class="mb-2 flex text-sm text-gray-700"
> >
<div class="w-32 font-medium text-gray-900">{{ key }}</div> <div
<div class="flex-1 break-all text-gray-600"> v-for="(value, key) in showInfoStr"
{{ value || '—' }} :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> </div>
</div>
<!-- 空白卡片 --> <!-- 空白卡片 -->
<div class="flex-1 rounded border bg-white p-4"> <div class="flex-1 rounded border bg-white p-4">
<EchartsUI ref="chartRef2" /> <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="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="object-contain"
/>
</div> </div>
</div> </div>
<!-- 柱状图区域 -->
<div class="flex-1 rounded border bg-white p-4"> <!-- 右侧 -->
<EchartsUI ref="chartRef1" /> <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="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="object-contain"
/>
</div>
</div>
<!-- 柱状图区域 -->
<div class="flex-1 rounded border bg-white p-4">
<EchartsUI ref="chartRef1" />
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </template>
</template> </div>
</div> </div>
</div> </div>
</template> </template>
@@ -46,7 +46,7 @@
"sendResetLink": "发送重置链接", "sendResetLink": "发送重置链接",
"sendText": "{0}秒后重新获取", "sendText": "{0}秒后重新获取",
"signUp": "注册", "signUp": "注册",
"signUpSubtitle": "让您的应用程序管理变得简单而有趣", "signUpSubtitle": "让您的人工智能体验变得简单而有趣",
"terms": "条款", "terms": "条款",
"thirdPartyLogin": "其他登录方式", "thirdPartyLogin": "其他登录方式",
"username": "账号", "username": "账号",