Merge branch 'devlopment' of http://pms.dtyx.net:3000/wuxueyu/Industrial-image-management-system---web into devlopment
This commit is contained in:
commit
e3ec37fa79
|
@ -3,10 +3,15 @@
|
||||||
VITE_API_PREFIX = '/dev-api'
|
VITE_API_PREFIX = '/dev-api'
|
||||||
|
|
||||||
# 接口地址
|
# 接口地址
|
||||||
VITE_API_BASE_URL = 'http://localhost:8888/'
|
# VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/'
|
||||||
|
# VITE_API_BASE_URL = 'http://localhost:8888/'
|
||||||
|
VITE_API_BASE_URL = 'http://10.18.34.163:8888/'
|
||||||
|
# VITE_API_BASE_URL = 'http://10.18.34.213:8888/'
|
||||||
|
|
||||||
# 接口地址 (WebSocket)
|
# 接口地址 (WebSocket)
|
||||||
VITE_API_WS_URL = 'ws://localhost:8000'
|
# VITE_API_WS_URL = 'ws://localhost:8000'
|
||||||
|
VITE_API_WS_URL = 'ws://10.18.34.163:8000'
|
||||||
|
# VITE_API_WS_URL = 'ws://10.18.34.213:8000'
|
||||||
|
|
||||||
# 地址前缀
|
# 地址前缀
|
||||||
VITE_BASE = '/'
|
VITE_BASE = '/'
|
||||||
|
|
|
@ -14,4 +14,4 @@ VITE_BASE = '/'
|
||||||
VITE_APP_SETTING = true
|
VITE_APP_SETTING = true
|
||||||
|
|
||||||
# 客户端ID
|
# 客户端ID
|
||||||
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'
|
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 17 KiB |
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
<div class="folder-content">
|
<div class="folder-content">
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<a-skeleton :loading="loading && folderList.length === 0" :rows="5" v-if="loading" animation="pulse">
|
<a-skeleton :loading="loading && folderList.length === 0" :rows="5" v-if="loading" :animation="true">
|
||||||
<template #skeleton>
|
<template #skeleton>
|
||||||
<div class="skeleton-item flex items-center px-4 py-3" v-for="i in 5" :key="i">
|
<div class="skeleton-item flex items-center px-4 py-3" v-for="i in 5" :key="i">
|
||||||
<div class="w-6 h-6 rounded bg-gray-200 mr-3"></div>
|
<div class="w-6 h-6 rounded bg-gray-200 mr-3"></div>
|
||||||
|
@ -53,51 +53,53 @@
|
||||||
</a-typography-text>
|
</a-typography-text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a-empty v-if="!loading && folderList.length === 0" :description="searchKeyword ? '未找到匹配的文件夹' : '暂无文件夹'" />
|
<a-empty v-if="!loading && folderList.length === 0" :description="searchKeyword ? '未找到匹配的文件夹' : '暂无文件夹'" />
|
||||||
|
|
||||||
<a-list v-if="!loading && folderList.length > 0" class="folder-list">
|
<!-- 树形文件夹结构 -->
|
||||||
<!-- 文件夹列表 -->
|
<div v-if="!loading && folderList.length > 0" class="folder-tree-container">
|
||||||
<a-list-item
|
|
||||||
v-for="folder in folderList"
|
<a-tree
|
||||||
:key="folder.id"
|
:data="folderTreeData"
|
||||||
:class="['folder-list-item', { 'active': currentFolderId === folder.id }]"
|
:selected-keys="currentFolderId ? [currentFolderId] : []"
|
||||||
@click="handleFolderClick(folder.id)"
|
:field-names="{ key: 'key', title: 'title', children: 'children' }"
|
||||||
:tooltip="sidebarCollapsed ? folder.name : ''"
|
:show-line="!sidebarCollapsed"
|
||||||
>
|
:block-node="true"
|
||||||
<!-- 第一行:文件夹图标和名称 -->
|
:default-expand-all="true"
|
||||||
<div class="folder-main-info">
|
@select="handleFolderSelect"
|
||||||
<div class="folder-icon-wrapper">
|
@dblclick="handleFolderDoubleClick"
|
||||||
<IconFolder class="folder-icon" :style="{ color: folderColor }" />
|
class="folder-tree"
|
||||||
</div>
|
:class="{ 'collapsed': sidebarCollapsed }"
|
||||||
<span v-if="!sidebarCollapsed" class="folder-name">{{ folder.name }}</span>
|
/>
|
||||||
</div>
|
|
||||||
|
<!-- 文件夹操作按钮 -->
|
||||||
<!-- 第二行:文件夹操作按钮 -->
|
<div v-if="currentFolderId && currentFolderId !== '0'" class="folder-actions-bar" style="padding: 8px; border-top: 1px solid #e5e6eb; margin-top: 8px;">
|
||||||
<div v-if="!sidebarCollapsed" class="folder-actions-row">
|
<a-space>
|
||||||
<a-button
|
<a-button
|
||||||
type="text"
|
type="text"
|
||||||
shape="circle"
|
|
||||||
size="small"
|
size="small"
|
||||||
@click.stop="handleRenameFolder(folder, folder.id, folder.name)"
|
@click="handleRenameCurrentFolder"
|
||||||
tooltip="重命名"
|
tooltip="重命名"
|
||||||
class="action-btn"
|
|
||||||
>
|
>
|
||||||
<icon-edit />
|
<template #icon><icon-edit /></template>
|
||||||
|
重命名
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
type="text"
|
type="text"
|
||||||
shape="circle"
|
|
||||||
size="small"
|
size="small"
|
||||||
@click.stop="handleDeleteFolder(folder)"
|
@click="handleDeleteCurrentFolder"
|
||||||
tooltip="删除"
|
tooltip="删除"
|
||||||
status="danger"
|
status="danger"
|
||||||
class="action-btn"
|
|
||||||
>
|
>
|
||||||
<icon-delete />
|
<template #icon><icon-delete /></template>
|
||||||
|
删除
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</a-space>
|
||||||
</a-list-item>
|
</div>
|
||||||
</a-list>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 侧边栏底部分页控件 -->
|
<!-- 侧边栏底部分页控件 -->
|
||||||
|
@ -108,7 +110,8 @@
|
||||||
</a-typography-text>
|
</a-typography-text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pagination-controls">
|
<!-- 隐藏分页控件,因为现在获取所有文件夹 -->
|
||||||
|
<!-- <div class="pagination-controls">
|
||||||
<a-pagination
|
<a-pagination
|
||||||
:current="currentPage"
|
:current="currentPage"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
|
@ -120,7 +123,7 @@
|
||||||
size="small"
|
size="small"
|
||||||
show-total
|
show-total
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</a-layout-sider>
|
</a-layout-sider>
|
||||||
|
|
||||||
|
@ -128,8 +131,14 @@
|
||||||
<a-layout-header class="file-header">
|
<a-layout-header class="file-header">
|
||||||
<div class="breadcrumbs">
|
<div class="breadcrumbs">
|
||||||
<a-breadcrumb>
|
<a-breadcrumb>
|
||||||
<a-breadcrumb-item>知识库</a-breadcrumb-item>
|
<a-breadcrumb-item
|
||||||
<a-breadcrumb-item>{{ currentFolderName }}</a-breadcrumb-item>
|
v-for="(item, index) in breadcrumbPath"
|
||||||
|
:key="index"
|
||||||
|
:class="{ 'clickable': index < breadcrumbPath.length - 1 }"
|
||||||
|
@click="handleBreadcrumbClick(index)"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</a-breadcrumb-item>
|
||||||
</a-breadcrumb>
|
</a-breadcrumb>
|
||||||
<a-button
|
<a-button
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -369,20 +378,18 @@
|
||||||
<a-input v-model="folderForm.name" placeholder="输入文件夹名称" max-length="50" />
|
<a-input v-model="folderForm.name" placeholder="输入文件夹名称" max-length="50" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="父级目录" field="parentId" :rules="folderRules.parentId">
|
<a-form-item label="父级目录" field="parentId" :rules="folderRules.parentId">
|
||||||
<a-select
|
<a-tree-select
|
||||||
v-model="folderForm.parentId"
|
v-model="folderForm.parentId"
|
||||||
placeholder="请选择父级目录"
|
placeholder="请选择父级目录"
|
||||||
|
:data="folderTreeSelectData"
|
||||||
|
:field-names="{ key: 'id', title: 'name', children: 'children' }"
|
||||||
|
allow-clear
|
||||||
|
:tree-props="{ showLine: true }"
|
||||||
>
|
>
|
||||||
<a-option value="0">根目录</a-option>
|
<template #title="{ node }">
|
||||||
<a-option
|
<span>{{ node?.title || node?.name }}</span>
|
||||||
v-for="folder in folderList"
|
</template>
|
||||||
:key="folder.id"
|
</a-tree-select>
|
||||||
:value="folder.id"
|
|
||||||
v-if="!folderForm.id || folder.id !== folderForm.id"
|
|
||||||
>
|
|
||||||
{{ folder.name }}
|
|
||||||
</a-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
@ -592,7 +599,8 @@ import {
|
||||||
const folderList = ref([]);
|
const folderList = ref([]);
|
||||||
const fileList = ref([]);
|
const fileList = ref([]);
|
||||||
const currentFolderId = ref('');
|
const currentFolderId = ref('');
|
||||||
const currentFolderName = ref('');
|
// 移除currentFolderName,现在使用面包屑导航
|
||||||
|
// const currentFolderName = ref('');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const folderDialogVisible = ref(false);
|
const folderDialogVisible = ref(false);
|
||||||
const uploadDialogVisible = ref(false);
|
const uploadDialogVisible = ref(false);
|
||||||
|
@ -664,6 +672,111 @@ const canUpload = computed(() => {
|
||||||
return hasFiles.value && !uploading.value && uploadForm.folderId;
|
return hasFiles.value && !uploading.value && uploadForm.folderId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 计算属性:将平铺的文件夹数据转换为树形结构
|
||||||
|
const folderTreeData = computed(() => {
|
||||||
|
console.log('=== folderTreeData计算属性执行 ===');
|
||||||
|
console.log('folderList.value:', folderList.value);
|
||||||
|
console.log('folderList.value.length:', folderList.value?.length);
|
||||||
|
|
||||||
|
if (!folderList.value || folderList.value.length === 0) {
|
||||||
|
console.log('folderList为空,返回空数组');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建文件夹映射表
|
||||||
|
const folderMap = new Map();
|
||||||
|
const rootFolders = [];
|
||||||
|
|
||||||
|
console.log('=== 开始创建文件夹映射 ===');
|
||||||
|
// 首先创建所有文件夹的映射
|
||||||
|
folderList.value.forEach((folder, index) => {
|
||||||
|
console.log(`处理第${index + 1}个文件夹:`, folder);
|
||||||
|
// 确保文件夹数据完整
|
||||||
|
if (folder && folder.id && folder.name) {
|
||||||
|
const node = {
|
||||||
|
key: folder.id, // Tree组件需要的key字段
|
||||||
|
title: folder.name, // Tree组件需要的title字段
|
||||||
|
children: [], // Tree组件需要的children字段
|
||||||
|
// 保留原始字段用于其他功能
|
||||||
|
id: folder.id,
|
||||||
|
name: folder.name,
|
||||||
|
parentId: folder.parentId
|
||||||
|
};
|
||||||
|
folderMap.set(folder.id, node);
|
||||||
|
console.log(`✅ 成功添加文件夹到映射: ${folder.name} (ID: ${folder.id})`);
|
||||||
|
} else {
|
||||||
|
console.warn('❌ 跳过不完整的文件夹数据:', folder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('=== 开始构建树形结构 ===');
|
||||||
|
console.log('文件夹映射表大小:', folderMap.size);
|
||||||
|
|
||||||
|
// 构建树形结构
|
||||||
|
folderList.value.forEach((folder, index) => {
|
||||||
|
console.log(`构建第${index + 1}个文件夹的树形结构:`, folder);
|
||||||
|
// 确保文件夹数据完整
|
||||||
|
if (!folder || !folder.id || !folder.name) {
|
||||||
|
console.warn('❌ 跳过不完整的文件夹数据:', folder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = folderMap.get(folder.id);
|
||||||
|
if (!node) {
|
||||||
|
console.warn('❌ 找不到文件夹节点:', folder.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`处理文件夹: ${folder.name} (ID: ${folder.id}, ParentID: ${folder.parentId})`);
|
||||||
|
|
||||||
|
if (folder.parentId === '0' || folder.parentId === 0) {
|
||||||
|
// 根文件夹
|
||||||
|
rootFolders.push(node);
|
||||||
|
console.log(`✅ 添加为根文件夹: ${folder.name}`);
|
||||||
|
} else {
|
||||||
|
// 子文件夹
|
||||||
|
const parent = folderMap.get(folder.parentId);
|
||||||
|
if (parent) {
|
||||||
|
parent.children.push(node);
|
||||||
|
console.log(`✅ 添加为子文件夹: ${folder.name} -> ${parent.name}`);
|
||||||
|
} else {
|
||||||
|
// 如果找不到父文件夹,当作根文件夹处理
|
||||||
|
console.warn('⚠️ 找不到父文件夹,将文件夹作为根文件夹:', folder.name, folder.parentId);
|
||||||
|
rootFolders.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('=== 树形结构构建完成 ===');
|
||||||
|
console.log('根文件夹数量:', rootFolders.length);
|
||||||
|
console.log('构建的树形结构:', rootFolders);
|
||||||
|
|
||||||
|
// 验证树形结构中的节点数据
|
||||||
|
rootFolders.forEach((root, index) => {
|
||||||
|
console.log(`根文件夹${index + 1}:`, {
|
||||||
|
id: root.id,
|
||||||
|
name: root.name,
|
||||||
|
childrenCount: root.children?.length || 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return rootFolders;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:树形选择器数据(包含根目录选项)
|
||||||
|
const folderTreeSelectData = computed(() => {
|
||||||
|
const rootOption = {
|
||||||
|
key: '0', // Tree组件需要的key字段
|
||||||
|
title: '根目录', // Tree组件需要的title字段
|
||||||
|
children: [], // Tree组件需要的children字段
|
||||||
|
// 保留原始字段用于其他功能
|
||||||
|
id: '0',
|
||||||
|
name: '根目录'
|
||||||
|
};
|
||||||
|
|
||||||
|
return [rootOption, ...folderTreeData.value];
|
||||||
|
});
|
||||||
|
|
||||||
// 搜索相关
|
// 搜索相关
|
||||||
const searchKeyword = ref(''); // 文件夹搜索关键词
|
const searchKeyword = ref(''); // 文件夹搜索关键词
|
||||||
const fileSearchKeyword = ref(''); // 文件搜索关键词
|
const fileSearchKeyword = ref(''); // 文件搜索关键词
|
||||||
|
@ -682,7 +795,7 @@ const initData = async () => {
|
||||||
|
|
||||||
const apiParams = {
|
const apiParams = {
|
||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: 1000, // 修改为足够大的值,确保获取所有文件夹
|
||||||
folderName: searchKeyword.value.trim() || undefined
|
folderName: searchKeyword.value.trim() || undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -701,11 +814,43 @@ const initData = async () => {
|
||||||
|
|
||||||
// 根据后端返回的数据结构处理
|
// 根据后端返回的数据结构处理
|
||||||
if (folderRes.code === 200 && folderRes.data) {
|
if (folderRes.code === 200 && folderRes.data) {
|
||||||
const processedFolders = folderRes.data.rows.map(folder => ({
|
console.log('=== 开始处理数据 ===');
|
||||||
id: String(folder.folderId),
|
console.log('folderRes.data:', folderRes.data);
|
||||||
name: folder.folderName,
|
console.log('folderRes.data.rows:', folderRes.data.rows);
|
||||||
parentId: String(folder.parentId || 0)
|
console.log('folderRes.data.rows类型:', typeof folderRes.data.rows);
|
||||||
}));
|
console.log('folderRes.data.rows长度:', folderRes.data.rows?.length);
|
||||||
|
|
||||||
|
// 检查数据结构
|
||||||
|
if (!folderRes.data.rows || !Array.isArray(folderRes.data.rows)) {
|
||||||
|
console.error('API返回的数据结构不正确,rows字段不存在或不是数组');
|
||||||
|
console.log('可用的字段:', Object.keys(folderRes.data));
|
||||||
|
folderList.value = [];
|
||||||
|
totalFolders.value = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedFolders = folderRes.data.rows.map((folder, index) => {
|
||||||
|
console.log(`处理第${index + 1}个原始文件夹数据:`, folder);
|
||||||
|
console.log(`原始数据字段:`, Object.keys(folder));
|
||||||
|
console.log(`folderId:`, folder.folderId);
|
||||||
|
console.log(`folderName:`, folder.folderName);
|
||||||
|
console.log(`parentId:`, folder.parentId);
|
||||||
|
|
||||||
|
// 确保所有必需字段都存在
|
||||||
|
if (!folder.folderId || !folder.folderName) {
|
||||||
|
console.warn('❌ 跳过不完整的文件夹数据:', folder);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedFolder = {
|
||||||
|
id: String(folder.folderId),
|
||||||
|
name: String(folder.folderName),
|
||||||
|
parentId: String(folder.parentId || 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`✅ 处理后的文件夹数据:`, processedFolder);
|
||||||
|
return processedFolder;
|
||||||
|
}).filter(Boolean); // 过滤掉null值
|
||||||
|
|
||||||
folderList.value = processedFolders;
|
folderList.value = processedFolders;
|
||||||
totalFolders.value = Number(folderRes.data.total) || 0;
|
totalFolders.value = Number(folderRes.data.total) || 0;
|
||||||
|
@ -720,6 +865,8 @@ const initData = async () => {
|
||||||
totalFolders.value = 0;
|
totalFolders.value = 0;
|
||||||
console.log('API响应异常,清空列表');
|
console.log('API响应异常,清空列表');
|
||||||
console.log('响应码不是200或数据为空');
|
console.log('响应码不是200或数据为空');
|
||||||
|
console.log('folderRes.code:', folderRes.code);
|
||||||
|
console.log('folderRes.data:', folderRes.data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('初始化文件夹数据失败:', error);
|
console.error('初始化文件夹数据失败:', error);
|
||||||
|
@ -784,6 +931,7 @@ const handleSearchClear = () => {
|
||||||
}
|
}
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
console.log('重置页码为:', currentPage.value);
|
console.log('重置页码为:', currentPage.value);
|
||||||
|
// 清除搜索后立即刷新数据,显示所有文件夹
|
||||||
initData();
|
initData();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -854,13 +1002,6 @@ const loadFiles = async (folderId) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
currentFolderId.value = folderId;
|
currentFolderId.value = folderId;
|
||||||
|
|
||||||
if (folderId === '0') {
|
|
||||||
currentFolderName.value = '根目录';
|
|
||||||
} else {
|
|
||||||
const folder = folderList.value.find(f => f.id === folderId);
|
|
||||||
currentFolderName.value = folder ? folder.name : '未知文件夹';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载文件失败:', error);
|
console.error('加载文件失败:', error);
|
||||||
Message.error('服务开小差,请稍后再试');
|
Message.error('服务开小差,请稍后再试');
|
||||||
|
@ -873,14 +1014,141 @@ const loadFiles = async (folderId) => {
|
||||||
|
|
||||||
|
|
||||||
// 文件夹点击事件
|
// 文件夹点击事件
|
||||||
const handleFolderClick = (folderId) => {
|
// const handleFolderClick = (folderId) => {
|
||||||
const id = String(folderId);
|
// const id = String(folderId);
|
||||||
if (currentFolderId.value !== id) {
|
// if (currentFolderId.value !== id) {
|
||||||
fileCurrentPage.value = 1;
|
// fileCurrentPage.value = 1;
|
||||||
// 切换文件夹时清空文件搜索关键词
|
// // 切换文件夹时清空文件搜索关键词
|
||||||
fileSearchKeyword.value = '';
|
// fileSearchKeyword.value = '';
|
||||||
|
// }
|
||||||
|
// currentFolderId.value = id;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// 树形文件夹选择事件
|
||||||
|
const handleFolderSelect = (selectedKeys, info) => {
|
||||||
|
if (selectedKeys.length > 0) {
|
||||||
|
const folderId = selectedKeys[0];
|
||||||
|
if (currentFolderId.value !== folderId) {
|
||||||
|
fileCurrentPage.value = 1;
|
||||||
|
// 切换文件夹时清空文件搜索关键词
|
||||||
|
fileSearchKeyword.value = '';
|
||||||
|
}
|
||||||
|
currentFolderId.value = folderId;
|
||||||
|
loadFiles(folderId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 文件夹双击处理
|
||||||
|
const handleFolderDoubleClick = (info) => {
|
||||||
|
console.log('文件夹双击:', info);
|
||||||
|
|
||||||
|
const { node } = info;
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
// 显示操作选项
|
||||||
|
Modal.confirm({
|
||||||
|
title: '文件夹操作',
|
||||||
|
content: `请选择对文件夹"${node.title}"的操作`,
|
||||||
|
okText: '重命名',
|
||||||
|
cancelText: '删除',
|
||||||
|
onOk: () => handleRenameFolder(node),
|
||||||
|
onCancel: () => handleDeleteFolder(node)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重命名当前选中的文件夹
|
||||||
|
const handleRenameCurrentFolder = () => {
|
||||||
|
if (!currentFolderId.value || currentFolderId.value === '0') {
|
||||||
|
Message.warning('请先选择一个文件夹');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从folderList中找到当前选中的文件夹
|
||||||
|
const currentFolder = folderList.value.find(folder => folder.id === currentFolderId.value);
|
||||||
|
if (!currentFolder) {
|
||||||
|
Message.error('找不到当前文件夹信息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造node对象
|
||||||
|
const node = {
|
||||||
|
key: currentFolder.id,
|
||||||
|
title: currentFolder.name,
|
||||||
|
id: currentFolder.id,
|
||||||
|
name: currentFolder.name
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRenameFolder(node);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除当前选中的文件夹
|
||||||
|
const handleDeleteCurrentFolder = () => {
|
||||||
|
if (!currentFolderId.value || currentFolderId.value === '0') {
|
||||||
|
Message.warning('请先选择一个文件夹');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从folderList中找到当前选中的文件夹
|
||||||
|
const currentFolder = folderList.value.find(folder => folder.id === currentFolderId.value);
|
||||||
|
if (!currentFolder) {
|
||||||
|
Message.error('找不到当前文件夹信息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造node对象
|
||||||
|
const node = {
|
||||||
|
key: currentFolder.id,
|
||||||
|
title: currentFolder.name,
|
||||||
|
id: currentFolder.id,
|
||||||
|
name: currentFolder.name
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDeleteFolder(node);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 计算属性:面包屑导航路径
|
||||||
|
const breadcrumbPath = computed(() => {
|
||||||
|
if (!currentFolderId.value || currentFolderId.value === '0') {
|
||||||
|
return ['知识库', '根目录'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = ['知识库'];
|
||||||
|
let currentId = currentFolderId.value;
|
||||||
|
|
||||||
|
// 从当前文件夹向上查找父级路径
|
||||||
|
while (currentId && currentId !== '0') {
|
||||||
|
const folder = folderList.value.find(f => f.id === currentId);
|
||||||
|
if (folder) {
|
||||||
|
path.unshift(folder.name);
|
||||||
|
currentId = folder.parentId;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 面包屑点击事件处理
|
||||||
|
const handleBreadcrumbClick = (index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
// 点击"知识库",回到根目录
|
||||||
|
currentFolderId.value = '0';
|
||||||
|
loadFiles('0');
|
||||||
|
} else {
|
||||||
|
// 点击其他路径项,需要找到对应的文件夹ID
|
||||||
|
const targetPath = breadcrumbPath.value.slice(0, index + 1);
|
||||||
|
const targetFolderName = targetPath[targetPath.length - 1];
|
||||||
|
|
||||||
|
// 查找对应的文件夹
|
||||||
|
const targetFolder = folderList.value.find(folder => folder.name === targetFolderName);
|
||||||
|
if (targetFolder) {
|
||||||
|
currentFolderId.value = targetFolder.id;
|
||||||
|
loadFiles(targetFolder.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
currentFolderId.value = id;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 重命名对话框状态
|
// 重命名对话框状态
|
||||||
|
@ -901,22 +1169,42 @@ const renameFileForm = reactive({
|
||||||
});
|
});
|
||||||
|
|
||||||
// 文件夹重命名处理函数
|
// 文件夹重命名处理函数
|
||||||
const handleRenameFolder = (folder, folderId, currentName) => {
|
const handleRenameFolder = (folder) => {
|
||||||
console.log('handleRenameFolder 被调用:', { folder, folderId, currentName });
|
console.log('重命名文件夹:', folder);
|
||||||
|
|
||||||
|
if (!folder) {
|
||||||
|
Message.error('文件夹信息不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderId = folder.key || folder.id;
|
||||||
|
const currentName = folder.title || folder.name;
|
||||||
|
|
||||||
// 验证参数
|
|
||||||
if (!folderId) {
|
if (!folderId) {
|
||||||
console.error('folderId 为空');
|
|
||||||
Message.error('文件夹ID不能为空');
|
Message.error('文件夹ID不能为空');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentName) {
|
if (!currentName) {
|
||||||
console.error('currentName 为空');
|
Message.error('文件夹名称不能为空');
|
||||||
Message.error('当前文件夹名称不能为空');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!currentName) {
|
||||||
|
console.error('❌ currentName 为空');
|
||||||
|
console.error('尝试从folder对象获取名称...');
|
||||||
|
const fallbackName = folder?.title || folder?.name;
|
||||||
|
console.error('fallbackName:', fallbackName);
|
||||||
|
|
||||||
|
if (!fallbackName) {
|
||||||
|
Message.error('当前文件夹名称不能为空');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.log('✅ 使用fallbackName:', fallbackName);
|
||||||
|
currentName = fallbackName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 先显示一个简单的提示,确认函数被调用
|
// 先显示一个简单的提示,确认函数被调用
|
||||||
Message.info('重命名功能被触发');
|
Message.info('重命名功能被触发');
|
||||||
|
|
||||||
|
@ -961,13 +1249,14 @@ const confirmRename = async () => {
|
||||||
if (result && result.code === 200) {
|
if (result && result.code === 200) {
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
Message.success('根目录重命名成功');
|
Message.success('根目录重命名成功');
|
||||||
currentFolderName.value = newName.trim();
|
// 移除对currentFolderName的设置,现在使用面包屑导航
|
||||||
|
// currentFolderName.value = newName.trim();
|
||||||
} else {
|
} else {
|
||||||
Message.success('文件夹重命名成功');
|
Message.success('文件夹重命名成功');
|
||||||
// 如果重命名的是当前选中的文件夹,更新显示名称
|
// 移除对currentFolderName的设置,现在使用面包屑导航
|
||||||
if (currentFolderId.value === folderId) {
|
// if (currentFolderId.value === folderId) {
|
||||||
currentFolderName.value = newName.trim();
|
// currentFolderName.value = newName.trim();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
initData(); // 刷新文件夹列表
|
initData(); // 刷新文件夹列表
|
||||||
|
@ -1015,6 +1304,10 @@ const handleFilePageSizeChange = (current, size) => {
|
||||||
const refreshData = async () => {
|
const refreshData = async () => {
|
||||||
refreshing.value = true;
|
refreshing.value = true;
|
||||||
try {
|
try {
|
||||||
|
// 强制清空搜索关键词,确保显示所有文件夹
|
||||||
|
searchKeyword.value = '';
|
||||||
|
currentPage.value = 1;
|
||||||
|
|
||||||
await initData();
|
await initData();
|
||||||
if (currentFolderId.value) {
|
if (currentFolderId.value) {
|
||||||
await loadFiles(currentFolderId.value);
|
await loadFiles(currentFolderId.value);
|
||||||
|
@ -1035,14 +1328,22 @@ const submitFolderForm = async () => {
|
||||||
await updateFolderApi(folderForm.id, folderForm.name);
|
await updateFolderApi(folderForm.id, folderForm.name);
|
||||||
Message.success('文件夹重命名成功');
|
Message.success('文件夹重命名成功');
|
||||||
} else {
|
} else {
|
||||||
await createFolderApi({
|
const result = await createFolderApi({
|
||||||
name: folderForm.name,
|
name: folderForm.name,
|
||||||
parentId: folderForm.parentId
|
parentId: folderForm.parentId
|
||||||
});
|
});
|
||||||
Message.success('文件夹创建成功');
|
Message.success('文件夹创建成功');
|
||||||
|
|
||||||
|
// 新建文件夹后,刷新数据并自动选中新建的文件夹
|
||||||
|
await initData();
|
||||||
|
|
||||||
|
// 如果有返回新建文件夹的ID,自动选中它
|
||||||
|
if (result.data && result.data.folderId) {
|
||||||
|
currentFolderId.value = String(result.data.folderId);
|
||||||
|
loadFiles(currentFolderId.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
folderDialogVisible.value = false;
|
folderDialogVisible.value = false;
|
||||||
initData();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('文件夹操作失败:', error);
|
console.error('文件夹操作失败:', error);
|
||||||
Message.error(folderForm.id ? '重命名失败' : '创建失败');
|
Message.error(folderForm.id ? '重命名失败' : '创建失败');
|
||||||
|
@ -1669,7 +1970,7 @@ const showAudioPreview = (url, fileName) => {
|
||||||
marginBottom: '20px',
|
marginBottom: '20px',
|
||||||
color: '#165DFF'
|
color: '#165DFF'
|
||||||
}
|
}
|
||||||
}, '🎵'),
|
}, '<EFBFBD><EFBFBD>'),
|
||||||
|
|
||||||
// 音频播放器
|
// 音频播放器
|
||||||
h('audio', {
|
h('audio', {
|
||||||
|
@ -1883,16 +2184,17 @@ const confirmRenameFile = async () => {
|
||||||
const handleDeleteFolder = (folder) => {
|
const handleDeleteFolder = (folder) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
content: `确定要删除文件夹「${folder.name}」吗?删除后无法恢复,文件夹内的所有文件也将被删除。`,
|
content: `确定要删除文件夹「${folder.title || folder.name}」吗?删除后无法恢复,文件夹内的所有文件也将被删除。`,
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
try {
|
try {
|
||||||
const result = await deleteFolderApi(folder.id);
|
const result = await deleteFolderApi(folder.key || folder.id);
|
||||||
if (result.code === 200) {
|
if (result.code === 200) {
|
||||||
Message.success('文件夹删除成功');
|
Message.success('文件夹删除成功');
|
||||||
// 如果删除的是当前选中的文件夹,切换到根目录
|
// 如果删除的是当前选中的文件夹,切换到根目录
|
||||||
if (currentFolderId.value === folder.id) {
|
if (currentFolderId.value === (folder.key || folder.id)) {
|
||||||
currentFolderId.value = '0';
|
currentFolderId.value = '0';
|
||||||
currentFolderName.value = '根目录';
|
// 移除对currentFolderName的设置,现在使用面包屑导航
|
||||||
|
// currentFolderName.value = '根目录';
|
||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
totalFiles.value = 0;
|
totalFiles.value = 0;
|
||||||
}
|
}
|
||||||
|
@ -2067,6 +2369,8 @@ onMounted(() => {
|
||||||
background-color: var(--color-bg-2);
|
background-color: var(--color-bg-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* 侧边栏样式 */
|
/* 侧边栏样式 */
|
||||||
.folder-sidebar {
|
.folder-sidebar {
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
@ -3208,4 +3512,110 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 树形文件夹结构 */
|
||||||
|
.folder-tree-container {
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--color-bg-2);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-name {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-content:hover .folder-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-actions .action-btn {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
background: var(--color-fill-3);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-tree {
|
||||||
|
:deep(.arco-tree-node-content) {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-fill-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-tree-node-selected .arco-tree-node-content) {
|
||||||
|
background-color: var(--color-primary-light-1);
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-tree-node-indent) {
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
:deep(.arco-tree-node-content) {
|
||||||
|
padding: 8px 4px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-tree-node-title) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-tree-node-switcher) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-tree-node-indent) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 面包屑导航样式 */
|
||||||
|
.breadcrumbs {
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-primary);
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary-light-1);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-tab-pane key="props" tap="形变" title="形变原数据">
|
||||||
|
<div class="tab-content">
|
||||||
|
<raw-data>
|
||||||
|
|
||||||
|
</raw-data>
|
||||||
|
</div>
|
||||||
|
</a-tab-pane>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import rawData from './raw-data.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.data-storage-container {
|
||||||
|
padding: 16px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-section {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #595959;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-files-section {
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.image-preview,
|
||||||
|
.video-preview {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #595959;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-tabs-nav) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-tabs-tab) {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-table-th) {
|
||||||
|
background-color: #fafafa;
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-table-td) {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-tag) {
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-btn-size-small) {
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-upload-drag:hover) {
|
||||||
|
border-color: #1890ff;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,590 @@
|
||||||
|
<template>
|
||||||
|
<GiPageLayout>
|
||||||
|
<div class="raw-data-container">
|
||||||
|
<!-- <div class="page-header">
|
||||||
|
<div class="page-title">原始数据管理</div>
|
||||||
|
<div class="page-subtitle">管理和分析原始视频数据</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="action-bar">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a-button type="primary" @click="showUploadModal = true">
|
||||||
|
<template #icon>
|
||||||
|
<IconUpload />
|
||||||
|
</template>
|
||||||
|
上传视频
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" @click="handleBatchAnalysis">
|
||||||
|
<template #icon>
|
||||||
|
<IconPlayCircle />
|
||||||
|
</template>
|
||||||
|
批量分析
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" @click="handleExportData">
|
||||||
|
<template #icon>
|
||||||
|
<IconDownload />
|
||||||
|
</template>
|
||||||
|
导出数据
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<div class="filter-section">
|
||||||
|
<a-form :model="filterForm" layout="inline">
|
||||||
|
<a-form-item label="项目">
|
||||||
|
<a-select v-model="filterForm.projectId" placeholder="请选择项目">
|
||||||
|
<a-option value="project-1">风电场A区</a-option>
|
||||||
|
<a-option value="project-2">风电场B区</a-option>
|
||||||
|
<a-option value="project-3">风电场C区</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="机组号">
|
||||||
|
<a-input v-model="filterForm.unitNumber" placeholder="请输入机组号" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="状态">
|
||||||
|
<a-select v-model="filterForm.status" placeholder="请选择状态">
|
||||||
|
<a-option value="completed">已完成</a-option>
|
||||||
|
<a-option value="pending">待分析</a-option>
|
||||||
|
<a-option value="analyzing">分析中</a-option>
|
||||||
|
<a-option value="failed">失败</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-button type="primary" @click="handleFilterChange">查询</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="project-sections">
|
||||||
|
<div v-for="project in filteredProjects" :key="project.id" class="project-section">
|
||||||
|
<div class="project-header">
|
||||||
|
<div class="project-title">{{ project.name }}</div>
|
||||||
|
<div class="project-stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<IconVideoCamera />
|
||||||
|
{{ project.totalVideos }} 个视频
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<IconCheckCircle />
|
||||||
|
{{ project.completedCount }} 个已完成
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<IconClockCircle />
|
||||||
|
{{ project.pendingCount }} 个待分析
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="units-grid">
|
||||||
|
<div v-for="unit in project.units" :key="unit.id" class="unit-card">
|
||||||
|
<div class="unit-header">
|
||||||
|
<div class="unit-title">{{ unit.number }}</div>
|
||||||
|
<div class="unit-actions">
|
||||||
|
<a-button type="primary" size="small" @click="handleViewUnitVideos(unit)">查看全部</a-button>
|
||||||
|
<a-button type="primary" size="small" @click="handleAnalyzeUnit(unit)">{{
|
||||||
|
getAnalysisButtonText(unit.status)
|
||||||
|
}}</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="videos-list">
|
||||||
|
<div v-for="video in unit.videos" :key="video.id" class="video-item">
|
||||||
|
<div class="video-thumbnail">
|
||||||
|
<img :src="video.thumbnail" alt="Video Thumbnail" />
|
||||||
|
<div class="video-overlay" @click="handlePlayVideo(video)">
|
||||||
|
<IconPlayArrowFill style="font-size: 24px; color: #fff;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="video-info">
|
||||||
|
<div class="video-name">{{ video.name }}</div>
|
||||||
|
<div class="video-meta">
|
||||||
|
<span>{{ video.duration }}</span>
|
||||||
|
<span>{{ video.angle }}°</span>
|
||||||
|
</div>
|
||||||
|
<div class="video-status">
|
||||||
|
<a-tag :color="getStatusColor(video.status)">{{ getStatusText(video.status) }}</a-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="analysis-progress">
|
||||||
|
<div class="progress-info">
|
||||||
|
<span>分析进度</span>
|
||||||
|
<span>{{ unit.progress }}%</span>
|
||||||
|
</div>
|
||||||
|
<a-progress :percent="unit.progress" :show-text="false" status="active" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 视频播放模态框 -->
|
||||||
|
<a-modal v-model:visible="videoModalVisible" title="原始视频播放" width="900px" @ok="videoModalVisible = false"
|
||||||
|
@cancel="videoModalVisible = false">
|
||||||
|
<video v-if="selectedVideo" :src="selectedVideo.url" controls
|
||||||
|
style="width: 100%; height: 480px; border-radius: 8px; background: #000;"></video>
|
||||||
|
<div v-if="selectedVideo" class="video-meta-info">
|
||||||
|
<p>项目:{{ selectedVideo.projectName }}</p>
|
||||||
|
<p>机组号:{{ selectedVideo.unitNumber }}</p>
|
||||||
|
<p>采集人:{{ selectedVideo.collector }}</p>
|
||||||
|
<p>风速:{{ selectedVideo.windSpeed }} m/s</p>
|
||||||
|
<p>转速:{{ selectedVideo.rpm }} rpm</p>
|
||||||
|
<p>采集时间:{{ selectedVideo.time }}</p>
|
||||||
|
<p>角度:{{ selectedVideo.angle }}°</p>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 上传视频模态框 -->
|
||||||
|
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" @ok="handleUpload"
|
||||||
|
@cancel="showUploadModal = false">
|
||||||
|
<a-form :model="uploadForm" layout="vertical">
|
||||||
|
<a-form-item label="项目" required>
|
||||||
|
<a-select v-model="uploadForm.projectId" placeholder="请选择项目">
|
||||||
|
<a-option value="project-1">风电场A区</a-option>
|
||||||
|
<a-option value="project-2">风电场B区</a-option>
|
||||||
|
<a-option value="project-3">风电场C区</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="机组号" required>
|
||||||
|
<a-input v-model="uploadForm.unitNumber" placeholder="请输入机组号" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="采集人" required>
|
||||||
|
<a-input v-model="uploadForm.collector" placeholder="请输入采集人姓名" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="风速 (m/s)">
|
||||||
|
<a-input-number v-model="uploadForm.windSpeed" :min="0" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="转速 (rpm)">
|
||||||
|
<a-input-number v-model="uploadForm.rpm" :min="0" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="采集时间" required>
|
||||||
|
<a-date-picker v-model="uploadForm.time" show-time format="YYYY-MM-DD HH:mm" style="width: 100%;" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="视频文件(可多选,建议3个角度)" required>
|
||||||
|
<a-upload v-model:file-list="uploadForm.fileList" :multiple="true" :limit="3" accept="video/*"
|
||||||
|
:auto-upload="false" list-type="picture-card">
|
||||||
|
<template #upload-button>
|
||||||
|
<a-button>选择视频</a-button>
|
||||||
|
</template>
|
||||||
|
</a-upload>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</GiPageLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import {
|
||||||
|
IconUpload,
|
||||||
|
IconPlayCircle,
|
||||||
|
IconDownload,
|
||||||
|
IconVideoCamera,
|
||||||
|
IconCheckCircle,
|
||||||
|
IconClockCircle,
|
||||||
|
IconPlayArrowFill
|
||||||
|
} from '@arco-design/web-vue/es/icon'
|
||||||
|
|
||||||
|
const showUploadModal = ref(false)
|
||||||
|
const videoModalVisible = ref(false)
|
||||||
|
const selectedVideo = ref<any>(null)
|
||||||
|
|
||||||
|
const filterForm = reactive({
|
||||||
|
projectId: '',
|
||||||
|
unitNumber: '',
|
||||||
|
status: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const uploadForm = reactive({
|
||||||
|
projectId: '',
|
||||||
|
unitNumber: '',
|
||||||
|
collector: '',
|
||||||
|
windSpeed: null,
|
||||||
|
rpm: null,
|
||||||
|
time: '',
|
||||||
|
fileList: []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 示例数据结构
|
||||||
|
const projects = ref([
|
||||||
|
{
|
||||||
|
id: 'project-1',
|
||||||
|
name: '风电场A区',
|
||||||
|
totalVideos: 6,
|
||||||
|
completedCount: 4,
|
||||||
|
pendingCount: 2,
|
||||||
|
units: [
|
||||||
|
{
|
||||||
|
id: 'A-001',
|
||||||
|
number: 'A-001',
|
||||||
|
status: 'completed',
|
||||||
|
progress: 100,
|
||||||
|
videos: [
|
||||||
|
{
|
||||||
|
id: 'v1',
|
||||||
|
name: 'A-001-正面',
|
||||||
|
url: '/videos/A-001-front.mp4',
|
||||||
|
thumbnail: '/images/A-001-front.jpg',
|
||||||
|
angle: 0,
|
||||||
|
duration: '00:30',
|
||||||
|
status: 'completed',
|
||||||
|
projectName: '风电场A区',
|
||||||
|
unitNumber: 'A-001',
|
||||||
|
collector: '张三',
|
||||||
|
windSpeed: 8.2,
|
||||||
|
rpm: 15,
|
||||||
|
time: '2023-11-05 08:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'v2',
|
||||||
|
name: 'A-001-侧面',
|
||||||
|
url: '/videos/A-001-side.mp4',
|
||||||
|
thumbnail: '/images/A-001-side.jpg',
|
||||||
|
angle: 90,
|
||||||
|
duration: '00:30',
|
||||||
|
status: 'completed',
|
||||||
|
projectName: '风电场A区',
|
||||||
|
unitNumber: 'A-001',
|
||||||
|
collector: '张三',
|
||||||
|
windSpeed: 8.2,
|
||||||
|
rpm: 15,
|
||||||
|
time: '2023-11-05 08:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'v3',
|
||||||
|
name: 'A-001-背面',
|
||||||
|
url: '/videos/A-001-back.mp4',
|
||||||
|
thumbnail: '/images/A-001-back.jpg',
|
||||||
|
angle: 180,
|
||||||
|
duration: '00:30',
|
||||||
|
status: 'pending',
|
||||||
|
projectName: '风电场A区',
|
||||||
|
unitNumber: 'A-001',
|
||||||
|
collector: '张三',
|
||||||
|
windSpeed: 8.2,
|
||||||
|
rpm: 15,
|
||||||
|
time: '2023-11-05 08:00'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'A-002',
|
||||||
|
number: 'A-002',
|
||||||
|
status: 'analyzing',
|
||||||
|
progress: 60,
|
||||||
|
videos: [
|
||||||
|
{
|
||||||
|
id: 'v4',
|
||||||
|
name: 'A-002-正面',
|
||||||
|
url: '/videos/A-002-front.mp4',
|
||||||
|
thumbnail: '/images/A-002-front.jpg',
|
||||||
|
angle: 0,
|
||||||
|
duration: '00:28',
|
||||||
|
status: 'analyzing',
|
||||||
|
projectName: '风电场A区',
|
||||||
|
unitNumber: 'A-002',
|
||||||
|
collector: '李四',
|
||||||
|
windSpeed: 7.9,
|
||||||
|
rpm: 14,
|
||||||
|
time: '2023-11-05 12:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'v5',
|
||||||
|
name: 'A-002-侧面',
|
||||||
|
url: '/videos/A-002-side.mp4',
|
||||||
|
thumbnail: '/images/A-002-side.jpg',
|
||||||
|
angle: 90,
|
||||||
|
duration: '00:28',
|
||||||
|
status: 'pending',
|
||||||
|
projectName: '风电场A区',
|
||||||
|
unitNumber: 'A-002',
|
||||||
|
collector: '李四',
|
||||||
|
windSpeed: 7.9,
|
||||||
|
rpm: 14,
|
||||||
|
time: '2023-11-05 12:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'v6',
|
||||||
|
name: 'A-002-背面',
|
||||||
|
url: '/videos/A-002-back.mp4',
|
||||||
|
thumbnail: '/images/A-002-back.jpg',
|
||||||
|
angle: 180,
|
||||||
|
duration: '00:28',
|
||||||
|
status: 'pending',
|
||||||
|
projectName: '风电场A区',
|
||||||
|
unitNumber: 'A-002',
|
||||||
|
collector: '李四',
|
||||||
|
windSpeed: 7.9,
|
||||||
|
rpm: 14,
|
||||||
|
time: '2023-11-05 12:00'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
// ... 其他项目
|
||||||
|
])
|
||||||
|
|
||||||
|
const filteredProjects = computed(() => {
|
||||||
|
// 按筛选条件过滤
|
||||||
|
return projects.value
|
||||||
|
.filter(p => !filterForm.projectId || p.id === filterForm.projectId)
|
||||||
|
.map(project => ({
|
||||||
|
...project,
|
||||||
|
units: project.units
|
||||||
|
.filter(u => !filterForm.unitNumber || u.number === filterForm.unitNumber)
|
||||||
|
.map(unit => ({
|
||||||
|
...unit,
|
||||||
|
videos: unit.videos.filter(v => !filterForm.status || v.status === filterForm.status)
|
||||||
|
}))
|
||||||
|
.filter(u => u.videos.length > 0)
|
||||||
|
}))
|
||||||
|
.filter(p => p.units.length > 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleFilterChange() {
|
||||||
|
// 这里可以加API请求
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePlayVideo(video: any) {
|
||||||
|
selectedVideo.value = video
|
||||||
|
videoModalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleViewUnitVideos(unit: any) {
|
||||||
|
// 可扩展为弹窗或跳转页面,展示该机组所有视频
|
||||||
|
Message.info(`查看机组 ${unit.number} 的所有视频`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAnalyzeUnit(unit: any) {
|
||||||
|
// 触发分析流程
|
||||||
|
Message.success(`已提交机组 ${unit.number} 的分析任务`)
|
||||||
|
// 实际应调用API并刷新状态
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusColor(status: string) {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed': return 'green'
|
||||||
|
case 'pending': return 'gray'
|
||||||
|
case 'analyzing': return 'blue'
|
||||||
|
case 'failed': return 'red'
|
||||||
|
default: return 'gray'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getStatusText(status: string) {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed': return '已完成'
|
||||||
|
case 'pending': return '待分析'
|
||||||
|
case 'analyzing': return '分析中'
|
||||||
|
case 'failed': return '失败'
|
||||||
|
default: return '未知'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getAnalysisButtonText(status: string) {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed': return '重新分析'
|
||||||
|
case 'pending': return '分析'
|
||||||
|
case 'analyzing': return '分析中...'
|
||||||
|
case 'failed': return '重新分析'
|
||||||
|
default: return '分析'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBatchAnalysis() {
|
||||||
|
Message.success('批量分析任务已提交')
|
||||||
|
}
|
||||||
|
function handleExportData() {
|
||||||
|
Message.success('数据导出成功')
|
||||||
|
}
|
||||||
|
function handleUpload() {
|
||||||
|
Message.success('上传成功')
|
||||||
|
showUploadModal.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.raw-data-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #86909c;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
margin-left: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-sections {
|
||||||
|
.project-section {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px #f0f1f2;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.project-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.units-grid {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.unit-card {
|
||||||
|
background: #fafbfc;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
width: 360px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.unit-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.unit-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.videos-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.video-item {
|
||||||
|
width: 100px;
|
||||||
|
|
||||||
|
.video-thumbnail {
|
||||||
|
position: relative;
|
||||||
|
width: 100px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-info {
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
.video-name {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1d2129;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #86909c;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-status {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-progress {
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #86909c;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-meta-info {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4e5969;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<GiPageLayout>
|
<GiPageLayout>
|
||||||
<!-- 页面标题 -->
|
<!-- 页面标题 -->
|
||||||
<!-- <div class="page-header">
|
<!-- <div class="page-header">
|
||||||
<h2 class="page-title">图像音频关联查看</h2>
|
<h2 class="page-title">图像音频关联查看</h2>
|
||||||
</div>-->
|
</div>-->
|
||||||
|
|
||||||
|
@ -17,62 +17,32 @@
|
||||||
<!-- 项目选择 -->
|
<!-- 项目选择 -->
|
||||||
<div class="filter-item">
|
<div class="filter-item">
|
||||||
<span class="filter-label">项目:</span>
|
<span class="filter-label">项目:</span>
|
||||||
<a-select
|
<a-select v-model="filterParams.project" placeholder="请选择项目" :options="projectOptions" allow-search
|
||||||
v-model="filterParams.project"
|
allow-clear :loading="loading.project" style="width: 200px" @change="handleFilterChange" />
|
||||||
placeholder="请选择项目"
|
|
||||||
:options="projectOptions"
|
|
||||||
allow-search
|
|
||||||
allow-clear
|
|
||||||
:loading="loading.project"
|
|
||||||
style="width: 200px"
|
|
||||||
@change="handleFilterChange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 机组选择 -->
|
<!-- 机组选择 -->
|
||||||
<div class="filter-item">
|
<div class="filter-item">
|
||||||
<span class="filter-label">机组:</span>
|
<span class="filter-label">机组:</span>
|
||||||
<a-select
|
<a-select v-model="filterParams.unit" placeholder="请先选择项目" :options="unitOptions" allow-search
|
||||||
v-model="filterParams.unit"
|
allow-clear :disabled="!filterParams.project" :loading="loading.unit" style="width: 200px"
|
||||||
placeholder="请先选择项目"
|
@change="handleFilterChange" />
|
||||||
:options="unitOptions"
|
|
||||||
allow-search
|
|
||||||
allow-clear
|
|
||||||
:disabled="!filterParams.project"
|
|
||||||
:loading="loading.unit"
|
|
||||||
style="width: 200px"
|
|
||||||
@change="handleFilterChange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 部件选择 -->
|
<!-- 部件选择 -->
|
||||||
<div class="filter-item">
|
<div class="filter-item">
|
||||||
<span class="filter-label">部件:</span>
|
<span class="filter-label">部件:</span>
|
||||||
<a-select
|
<a-select v-model="filterParams.component" placeholder="请先选择机组" :options="componentOptions"
|
||||||
v-model="filterParams.component"
|
allow-search allow-clear :disabled="!filterParams.unit" :loading="loading.component"
|
||||||
placeholder="请先选择机组"
|
style="width: 200px" @change="handleFilterChange" />
|
||||||
:options="componentOptions"
|
|
||||||
allow-search
|
|
||||||
allow-clear
|
|
||||||
:disabled="!filterParams.unit"
|
|
||||||
:loading="loading.component"
|
|
||||||
style="width: 200px"
|
|
||||||
@change="handleFilterChange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</a-space>
|
</a-space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 已上传数据列表 -->
|
<!-- 已上传数据列表 -->
|
||||||
<div class="uploaded-files-section">
|
<div class="uploaded-files-section">
|
||||||
<a-table
|
<a-table :columns="fileColumns" :data="imageList" :pagination="false"
|
||||||
:columns="fileColumns"
|
:scroll="{ x: '100%', y: 'calc(100vh - 380px)' }" :loading="loading.image" class="scrollable-table">
|
||||||
:data="imageList"
|
|
||||||
:pagination="false"
|
|
||||||
:scroll="{ x: '100%', y: 'calc(100vh - 380px)' }"
|
|
||||||
:loading="loading.image"
|
|
||||||
class="scrollable-table"
|
|
||||||
>
|
|
||||||
<!-- 文件类型 -->
|
<!-- 文件类型 -->
|
||||||
<template #type="{ record }">
|
<template #type="{ record }">
|
||||||
<a-tag :color="getFileTypeColor(record.type)" size="small">
|
<a-tag :color="getFileTypeColor(record.type)" size="small">
|
||||||
|
@ -82,7 +52,7 @@
|
||||||
|
|
||||||
<!-- 文件大小 -->
|
<!-- 文件大小 -->
|
||||||
<template #size="{ record }">
|
<template #size="{ record }">
|
||||||
<span>{{ record.imageTypeLabel}}</span>
|
<span>{{ record.imageTypeLabel }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 状态 -->
|
<!-- 状态 -->
|
||||||
|
@ -103,6 +73,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="props" tap="形变" title="形变原数据">
|
||||||
|
<div class="tab-content">
|
||||||
|
<raw-data>
|
||||||
|
|
||||||
|
</raw-data>
|
||||||
|
</div>
|
||||||
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -112,10 +89,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed,onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import type { TableColumnData } from '@arco-design/web-vue'
|
import type { TableColumnData } from '@arco-design/web-vue'
|
||||||
|
import PreviewModal from './components/PreviewModal.vue'
|
||||||
|
import rawData from './components/raw-data.vue'
|
||||||
import {
|
import {
|
||||||
getProjectList,
|
getProjectList,
|
||||||
getTurbineList,
|
getTurbineList,
|
||||||
|
@ -127,6 +105,7 @@ import {
|
||||||
batchUploadImages,
|
batchUploadImages,
|
||||||
uploadImageToPartV2
|
uploadImageToPartV2
|
||||||
} from '@/apis/industrial-image'
|
} from '@/apis/industrial-image'
|
||||||
|
import DeformationTap from './components/DeformationTap.vue'
|
||||||
|
|
||||||
// 预览弹窗引用(待重新设计)
|
// 预览弹窗引用(待重新设计)
|
||||||
// const previewModal = ref()
|
// const previewModal = ref()
|
||||||
|
@ -141,9 +120,9 @@ const filterParams = reactive({
|
||||||
})
|
})
|
||||||
|
|
||||||
// 筛选选项
|
// 筛选选项
|
||||||
const projectOptions = ref<Array<{label: string, value: string}>>([])
|
const projectOptions = ref<Array<{ label: string, value: string }>>([])
|
||||||
const unitOptions = ref<Array<{label: string, value: string}>>([])
|
const unitOptions = ref<Array<{ label: string, value: string }>>([])
|
||||||
const componentOptions = ref<Array<{label: string, value: string}>>([])
|
const componentOptions = ref<Array<{ label: string, value: string }>>([])
|
||||||
|
|
||||||
// 图像列表
|
// 图像列表
|
||||||
const imageList = ref<Array<{
|
const imageList = ref<Array<{
|
||||||
|
@ -251,37 +230,32 @@ const fetchPartList = async (projectId: string, turbineId: string) => {
|
||||||
|
|
||||||
// 处理筛选变化,获取图像列表
|
// 处理筛选变化,获取图像列表
|
||||||
const handleFilterChange = async () => {
|
const handleFilterChange = async () => {
|
||||||
// if (!filterParams.project) return
|
// if (!filterParams.project) return
|
||||||
|
|
||||||
loading.image = true
|
loading.image = true
|
||||||
try {
|
try {
|
||||||
let params = {
|
const params: any = {
|
||||||
projectId: filterParams.project
|
projectId: filterParams.project,
|
||||||
}
|
}
|
||||||
if(filterParams.unit){
|
|
||||||
params = {
|
if (filterParams.unit) {
|
||||||
projectId: filterParams.project,
|
params.turbineId = filterParams.unit
|
||||||
turbineId: filterParams.unit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if(filterParams.component){
|
|
||||||
params = {
|
if (filterParams.component) {
|
||||||
projectId: filterParams.project,
|
params.partId = filterParams.component
|
||||||
turbineId: filterParams.unit,
|
|
||||||
partId: filterParams.component
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await getImageList(params)
|
const res = await getImageList(params)
|
||||||
imageList.value = res.data.map((item: any) => ({
|
imageList.value = res.data.map((item: any) => ({
|
||||||
id: item.imageId,
|
id: item.imageId,
|
||||||
name: item.imageName,
|
name: item.imageName,
|
||||||
type: item.imageType?item.imageType:"未指定类型",
|
type: item.imageType ? item.imageType : "未指定类型",
|
||||||
imageTypeLabel: item.imageTypeLabel,
|
imageTypeLabel: item.imageTypeLabel,
|
||||||
shootingTime: item.shootingTime,
|
shootingTime: item.shootingTime,
|
||||||
preTreatment: item.preTreatment?"已审核":"未审核",
|
preTreatment: item.preTreatment ? "已审核" : "未审核",
|
||||||
imagePath: item.imagePath,
|
imagePath: item.imagePath,
|
||||||
audioList:item.audioList
|
audioList: item.audioList
|
||||||
}))
|
}))
|
||||||
Message.success(`获取到 ${imageList.value.length} 条图像数据`)
|
Message.success(`获取到 ${imageList.value.length} 条图像数据`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -313,14 +287,6 @@ const fileColumns: TableColumnData[] = [
|
||||||
{ title: '操作', slotName: 'action', width: 150, fixed: 'right' }
|
{ title: '操作', slotName: 'action', width: 150, fixed: 'right' }
|
||||||
]
|
]
|
||||||
|
|
||||||
// 过滤后的文件列表
|
|
||||||
const filteredFiles = computed(() => {
|
|
||||||
return uploadedFiles.value.filter(file =>
|
|
||||||
(filterParams.project === null || file.project === filterParams.project) &&
|
|
||||||
(filterParams.unit === null || file.unit === filterParams.unit) &&
|
|
||||||
(filterParams.component === null || file.component === filterParams.component)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取文件类型颜色
|
// 获取文件类型颜色
|
||||||
const getFileTypeColor = (type: string) => {
|
const getFileTypeColor = (type: string) => {
|
||||||
|
@ -360,6 +326,9 @@ const getImageUrl = (imagePath: string): string => {
|
||||||
|
|
||||||
// 预览文件(待重新设计预览弹窗)
|
// 预览文件(待重新设计预览弹窗)
|
||||||
const previewFile = (file: any) => {
|
const previewFile = (file: any) => {
|
||||||
|
/* previewFileData.value = file
|
||||||
|
previewModalVisible.value = true*/
|
||||||
|
|
||||||
const fileObj = {
|
const fileObj = {
|
||||||
id: file.id,
|
id: file.id,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
|
@ -373,12 +342,7 @@ const previewFile = (file: any) => {
|
||||||
|
|
||||||
// 删除文件
|
// 删除文件
|
||||||
const deleteFile = (file: any) => {
|
const deleteFile = (file: any) => {
|
||||||
console.log(index);
|
|
||||||
const index = uploadedFiles.value.findIndex(f => f.id === file.id)
|
|
||||||
if (index > -1) {
|
|
||||||
uploadedFiles.value.splice(index, 1)
|
|
||||||
Message.success('文件已删除')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
5. 导入导出团队成员数据
|
5. 导入导出团队成员数据
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<GiPageLayout>
|
<GiPageLayout class="construction-personnel-page">
|
||||||
<!-- 页面头部 -->
|
<!-- 页面头部 -->
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
|
@ -828,15 +828,22 @@ onMounted(() => {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
max-height: calc(100vh - 300px);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
.arco-table-container {
|
.arco-table-container {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: visible;
|
overflow-y: auto;
|
||||||
|
max-height: calc(100vh - 350px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-table {
|
.arco-table {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.arco-table-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -917,6 +924,28 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页面滚动修复
|
||||||
|
.construction-personnel-page {
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
:deep(.gi-page-layout) {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.gi-page-layout-content) {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 响应式设计
|
// 响应式设计
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.page-header {
|
.page-header {
|
||||||
|
|
|
@ -237,31 +237,37 @@
|
||||||
|
|
||||||
<!-- 团队成员明细 -->
|
<!-- 团队成员明细 -->
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<div class="section-header">
|
<div class="section-header team-members-header">
|
||||||
<h3>团队成员明细</h3>
|
<div class="header-left">
|
||||||
<a-button size="small" @click="openPersonnelManagement">
|
<h3>团队成员明细</h3>
|
||||||
|
<span class="member-count" v-if="currentProject.teamMembers && currentProject.teamMembers.length > 0">
|
||||||
|
({{ currentProject.teamMembers.length }}人)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<a-button size="small" @click="openPersonnelManagement" class="add-member-btn">
|
||||||
<template #icon><icon-user-group /></template>
|
<template #icon><icon-user-group /></template>
|
||||||
添加成员
|
添加成员
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="team-members">
|
<div class="team-members">
|
||||||
<div
|
<div
|
||||||
v-for="member in currentProject.teamMembers"
|
v-for="(member, index) in currentProject.teamMembers"
|
||||||
:key="member.id"
|
:key="member.id"
|
||||||
class="member-item"
|
class="member-item"
|
||||||
|
:style="{ animationDelay: `${index * 0.1}s` }"
|
||||||
@click="editMemberPosition(member)"
|
@click="editMemberPosition(member)"
|
||||||
>
|
>
|
||||||
<div class="member-avatar">
|
<div class="member-avatar">
|
||||||
<icon-user />
|
<icon-user />
|
||||||
</div>
|
</div>
|
||||||
<div class="member-info">
|
<div class="member-info">
|
||||||
<div class="member-name">{{ member.name }}</div>
|
<div class="member-name">{{ member.name || '未设置姓名' }}</div>
|
||||||
<div class="member-position">{{ member.position }}</div>
|
<div class="member-position">{{ member.position || '未设置岗位' }}</div>
|
||||||
<div class="member-details">
|
<div class="member-details">
|
||||||
<span class="member-status" :class="member.status">
|
<span class="member-status" :class="member.status">
|
||||||
{{ member.status === 'available' ? '在线' : '离线' }}
|
{{ member.status === 'available' ? '在线' : '离线' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="member-date">入职: {{ member.joinDate }}</span>
|
<span class="member-date">入职: {{ member.joinDate || '未设置' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="member-actions">
|
<div class="member-actions">
|
||||||
|
@ -453,15 +459,18 @@ const mapProjectRespToProjectCard = (projectResp: any): any => {
|
||||||
// 处理团队成员数据 - 使用后端返回的真实数据
|
// 处理团队成员数据 - 使用后端返回的真实数据
|
||||||
const teamMembers = projectResp.teamMembers ? projectResp.teamMembers.map((member: any) => {
|
const teamMembers = projectResp.teamMembers ? projectResp.teamMembers.map((member: any) => {
|
||||||
console.log('处理团队成员数据:', member) // 添加调试日志
|
console.log('处理团队成员数据:', member) // 添加调试日志
|
||||||
|
console.log('成员userName字段:', member.userName)
|
||||||
|
console.log('成员name字段:', member.name)
|
||||||
|
|
||||||
const mappedMember = {
|
const mappedMember = {
|
||||||
id: member.memberId,
|
id: member.memberId,
|
||||||
name: member.name, // 修复:使用正确的姓名字段
|
name: member.userName || member.name || '未设置姓名', // 修复:优先使用userName字段
|
||||||
position: member.roleTypeDesc || member.jobCodeDesc,
|
position: member.roleTypeDesc || member.jobCodeDesc || '未设置岗位',
|
||||||
phone: member.phone || '', // 后端数据中的电话字段
|
phone: member.phone || '', // 后端数据中的电话字段
|
||||||
email: member.email || '', // 后端数据中的邮箱字段
|
email: member.email || '', // 后端数据中的邮箱字段
|
||||||
status: member.status === 'ACTIVE' ? 'available' : 'offline',
|
status: member.status === 'ACTIVE' ? 'available' : 'offline',
|
||||||
skills: [], // 后端数据中没有技能字段
|
skills: [], // 后端数据中没有技能字段
|
||||||
joinDate: member.joinDate,
|
joinDate: member.joinDate || '未设置',
|
||||||
remark: member.remark || member.jobDesc || '',
|
remark: member.remark || member.jobDesc || '',
|
||||||
// 保留原始数据用于后续处理
|
// 保留原始数据用于后续处理
|
||||||
originalData: member
|
originalData: member
|
||||||
|
@ -533,18 +542,32 @@ const loadKanbanData = async () => {
|
||||||
...(response.data.pendingProjects || [])
|
...(response.data.pendingProjects || [])
|
||||||
]
|
]
|
||||||
|
|
||||||
|
console.log('后端返回的所有项目数据:', allProjects)
|
||||||
|
console.log('后端返回的preparingProjects:', response.data.preparingProjects)
|
||||||
|
console.log('后端返回的ongoingProjects:', response.data.ongoingProjects)
|
||||||
|
console.log('后端返回的inProgressProjects:', response.data.inProgressProjects)
|
||||||
|
console.log('后端返回的pendingProjects:', response.data.pendingProjects)
|
||||||
|
|
||||||
// 根据项目状态分类
|
// 根据项目状态分类
|
||||||
allProjects.forEach(project => {
|
allProjects.forEach(project => {
|
||||||
|
console.log('处理项目:', project.projectName || project.name)
|
||||||
|
console.log('项目团队成员:', project.teamMembers)
|
||||||
|
console.log('项目状态:', project.status, typeof project.status)
|
||||||
|
|
||||||
const mappedProject = mapProjectRespToProjectCard(project)
|
const mappedProject = mapProjectRespToProjectCard(project)
|
||||||
|
console.log('映射后的项目:', mappedProject.name)
|
||||||
|
console.log('映射后的团队成员:', mappedProject.teamMembers)
|
||||||
|
|
||||||
// 确保状态比较时类型一致,将字符串转换为数字进行比较
|
// 确保状态比较时类型一致,将字符串转换为数字进行比较
|
||||||
const status = typeof project.status === 'string' ? parseInt(project.status) : project.status
|
const status = typeof project.status === 'string' ? parseInt(project.status) : project.status
|
||||||
|
|
||||||
if (status === 0) {
|
if (status === 0) {
|
||||||
// status: 0 表示准备中
|
// status: 0 表示准备中
|
||||||
|
console.log('添加到准备中项目:', mappedProject.name)
|
||||||
preparingProjects.value.push(mappedProject)
|
preparingProjects.value.push(mappedProject)
|
||||||
} else if (status === 1) {
|
} else if (status === 1) {
|
||||||
// status: 1 表示进行中
|
// status: 1 表示进行中
|
||||||
|
console.log('添加到进行中项目:', mappedProject.name)
|
||||||
ongoingProjects.value.push(mappedProject)
|
ongoingProjects.value.push(mappedProject)
|
||||||
}
|
}
|
||||||
// status: 其他值表示未开工,我们不需要显示
|
// status: 其他值表示未开工,我们不需要显示
|
||||||
|
@ -629,10 +652,12 @@ const openProjectDetail = async (project: any) => {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
console.log('正在获取项目详情,项目ID:', project.id)
|
console.log('正在获取项目详情,项目ID:', project.id)
|
||||||
|
console.log('传入的项目数据:', project)
|
||||||
|
|
||||||
// 首先检查传入的项目数据是否已经包含团队成员信息
|
// 首先检查传入的项目数据是否已经包含团队成员信息
|
||||||
if (project.teamMembers && project.teamMembers.length > 0) {
|
if (project.teamMembers && project.teamMembers.length > 0) {
|
||||||
console.log('项目数据已包含团队成员信息,直接使用')
|
console.log('项目数据已包含团队成员信息,直接使用')
|
||||||
|
console.log('团队成员数据:', project.teamMembers)
|
||||||
currentProject.value = project
|
currentProject.value = project
|
||||||
projectDetailVisible.value = true
|
projectDetailVisible.value = true
|
||||||
return
|
return
|
||||||
|
@ -643,7 +668,9 @@ const openProjectDetail = async (project: any) => {
|
||||||
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
// 使用映射函数处理后端数据
|
// 使用映射函数处理后端数据
|
||||||
|
console.log('API返回的原始数据:', response.data)
|
||||||
currentProject.value = mapProjectRespToProjectCard(response.data)
|
currentProject.value = mapProjectRespToProjectCard(response.data)
|
||||||
|
console.log('映射后的项目数据:', currentProject.value)
|
||||||
projectDetailVisible.value = true
|
projectDetailVisible.value = true
|
||||||
} else {
|
} else {
|
||||||
// 如果API没有返回数据,使用传入的项目数据作为后备
|
// 如果API没有返回数据,使用传入的项目数据作为后备
|
||||||
|
@ -1135,98 +1162,238 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.team-members-header {
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #1d2129;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-count {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-member-btn {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.team-members {
|
.team-members {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 8px;
|
||||||
|
|
||||||
|
// 自定义滚动条样式
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.member-item {
|
.member-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
padding: 12px;
|
padding: 16px;
|
||||||
border: 1px solid #f0f0f0;
|
border: 1px solid #e5e6eb;
|
||||||
border-radius: 6px;
|
border-radius: 12px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
animation: fadeInUp 0.6s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #f8f9fa;
|
background: linear-gradient(135deg, #f8f9ff 0%, #e8f2ff 100%);
|
||||||
border-color: #667eea;
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.15);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-avatar {
|
.member-avatar {
|
||||||
width: 40px;
|
width: 48px;
|
||||||
height: 40px;
|
height: 48px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #f0f0f0;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #86909c;
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
right: -2px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #52c41a;
|
||||||
|
border: 2px solid white;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-info {
|
.member-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
.member-name {
|
.member-name {
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
color: #1d2129;
|
color: #1d2129;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-position {
|
.member-position {
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
color: #86909c;
|
color: #4e5969;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: linear-gradient(135deg, #f0f2f5 0%, #e5e6eb 100%);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-details {
|
.member-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #86909c;
|
color: #86909c;
|
||||||
|
|
||||||
.member-status {
|
.member-status {
|
||||||
padding: 2px 6px;
|
padding: 4px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
&.available {
|
&.available {
|
||||||
background: #e3f2fd;
|
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
|
||||||
color: #2196f3;
|
color: #1890ff;
|
||||||
|
border: 1px solid #91d5ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.offline {
|
&.offline {
|
||||||
background: #f8f9fa;
|
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
|
||||||
color: #868e96;
|
color: #8c8c8c;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.member-date {
|
||||||
|
background: #f7f8fa;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e5e6eb;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-actions {
|
.member-actions {
|
||||||
.arco-btn {
|
.arco-btn {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px 0;
|
padding: 60px 20px;
|
||||||
color: #86909c;
|
color: #86909c;
|
||||||
|
background: linear-gradient(135deg, #fafbfc 0%, #f0f2f5 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px dashed #d9d9d9;
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
.empty-icon {
|
.empty-icon {
|
||||||
font-size: 48px;
|
font-size: 64px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 20px;
|
||||||
|
color: #bfbfbf;
|
||||||
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-text {
|
.empty-text {
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 12px;
|
||||||
|
color: #595959;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-desc {
|
.empty-desc {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue