Compare commits
8 Commits
e076e42b86
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f68199b7da | |||
| 9a12ef5e2e | |||
| aa763a0ee6 | |||
| 7a8f25cf65 | |||
| ab86729e33 | |||
| f9eaa441a3 | |||
| 891bffba3b | |||
| 07f8ef90bb |
@@ -3,7 +3,11 @@
|
|||||||
"allow": [
|
"allow": [
|
||||||
"Bash(tree:*)",
|
"Bash(tree:*)",
|
||||||
"Bash(mvn clean package:*)",
|
"Bash(mvn clean package:*)",
|
||||||
"Bash(echo:*)"
|
"Bash(echo:*)",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git commit:*)",
|
||||||
|
"Bash(ls:*)",
|
||||||
|
"Bash(grep:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 页面标题
|
# 页面标题
|
||||||
VITE_APP_TITLE = RAR巡检管理平台
|
VITE_APP_TITLE = AR巡检管理平台
|
||||||
VITE_APP_LOGO_TITLE = AR巡检管理平台
|
VITE_APP_LOGO_TITLE = AR巡检管理平台
|
||||||
|
|
||||||
# 生产环境配置
|
# 生产环境配置
|
||||||
|
|||||||
@@ -40,7 +40,8 @@
|
|||||||
"vue-json-pretty": "2.4.0",
|
"vue-json-pretty": "2.4.0",
|
||||||
"vue-router": "4.5.0",
|
"vue-router": "4.5.0",
|
||||||
"vue-types": "6.0.0",
|
"vue-types": "6.0.0",
|
||||||
"vxe-table": "4.13.7"
|
"vxe-table": "4.13.7",
|
||||||
|
"cos-js-sdk-v5": "^1.8.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/json": "^2.2.276",
|
"@iconify/json": "^2.2.276",
|
||||||
|
|||||||
69
plus-ui/src/api/inspection/cos/index.ts
Normal file
69
plus-ui/src/api/inspection/cos/index.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import { AxiosPromise } from 'axios';
|
||||||
|
import COS from 'cos-js-sdk-v5';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* COS临时凭证响应
|
||||||
|
*/
|
||||||
|
export interface CosCredentialResponse {
|
||||||
|
credentials: {
|
||||||
|
tmpSecretId: string;
|
||||||
|
tmpSecretKey: string;
|
||||||
|
sessionToken: string;
|
||||||
|
};
|
||||||
|
startTime: number;
|
||||||
|
expiredTime: number;
|
||||||
|
expiration: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* COS配置响应
|
||||||
|
*/
|
||||||
|
export interface CosConfigResponse {
|
||||||
|
bucket: string;
|
||||||
|
region: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取COS临时凭证
|
||||||
|
*/
|
||||||
|
export const getCosCredential = (): AxiosPromise<CosCredentialResponse> => {
|
||||||
|
return request({
|
||||||
|
url: '/inspection/cos/credential',
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取COS配置信息
|
||||||
|
*/
|
||||||
|
export const getCosConfig = (): AxiosPromise<CosConfigResponse> => {
|
||||||
|
return request({
|
||||||
|
url: '/inspection/cos/config',
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建COS实例(包含自动获取凭证)
|
||||||
|
*/
|
||||||
|
export const createCosInstance = (): COS => {
|
||||||
|
return new COS({
|
||||||
|
getAuthorization: (options, callback) => {
|
||||||
|
getCosCredential()
|
||||||
|
.then((res) => {
|
||||||
|
const credentials = res.data.credentials;
|
||||||
|
callback({
|
||||||
|
TmpSecretId: credentials.tmpSecretId,
|
||||||
|
TmpSecretKey: credentials.tmpSecretKey,
|
||||||
|
SecurityToken: credentials.sessionToken,
|
||||||
|
StartTime: res.data.startTime,
|
||||||
|
ExpiredTime: res.data.expiredTime
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('获取COS临时凭证失败:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
18
plus-ui/src/api/inspection/cos/types.ts
Normal file
18
plus-ui/src/api/inspection/cos/types.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* COS上传回调参数
|
||||||
|
*/
|
||||||
|
export interface CosUploadCallbackData {
|
||||||
|
Location: string; // 完整URL(不含协议)
|
||||||
|
statusCode: number;
|
||||||
|
headers: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* COS上传进度回调
|
||||||
|
*/
|
||||||
|
export interface CosUploadProgress {
|
||||||
|
loaded: number; // 已上传字节数
|
||||||
|
total: number; // 总字节数
|
||||||
|
speed: number; // 上传速度(字节/秒)
|
||||||
|
percent: number; // 上传进度(0-1)
|
||||||
|
}
|
||||||
275
plus-ui/src/components/AudioUpload/index.vue
Normal file
275
plus-ui/src/components/AudioUpload/index.vue
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
<template>
|
||||||
|
<div class="audio-upload-container">
|
||||||
|
<el-upload
|
||||||
|
ref="audioUploadRef"
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
:accept="fileAccept"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:disabled="disabled || uploading"
|
||||||
|
>
|
||||||
|
<el-button type="primary" :icon="Upload" :disabled="disabled || uploading">
|
||||||
|
{{ uploading ? '上传中...' : '选择音频文件' }}
|
||||||
|
</el-button>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
|
<!-- 上传提示 -->
|
||||||
|
<div v-if="showTip" class="el-upload__tip">
|
||||||
|
支持格式: <b style="color: #f56c6c">{{ fileType.join('、') }}</b>,
|
||||||
|
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上传进度 -->
|
||||||
|
<div v-if="uploading" class="upload-progress">
|
||||||
|
<el-progress :percentage="uploadPercent" :status="uploadStatus" />
|
||||||
|
<span class="progress-text">{{ progressText }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已上传文件显示 -->
|
||||||
|
<div v-if="currentFile && !uploading" class="file-preview">
|
||||||
|
<div class="file-info">
|
||||||
|
<el-icon class="file-icon"><Headset /></el-icon>
|
||||||
|
<span class="file-name">{{ currentFile.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="file-actions">
|
||||||
|
<el-button v-if="currentFile.url" type="primary" link @click="previewAudio">试听</el-button>
|
||||||
|
<el-button v-if="!disabled" type="danger" link @click="handleRemove">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 音频预览对话框 -->
|
||||||
|
<el-dialog v-model="previewVisible" title="音频预览" width="500px" append-to-body>
|
||||||
|
<audio v-if="previewUrl" :src="previewUrl" controls style="width: 100%"></audio>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, getCurrentInstance, onMounted } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { Upload, Headset } from '@element-plus/icons-vue';
|
||||||
|
import { createCosInstance, getCosConfig } from '@/api/inspection/cos';
|
||||||
|
import type { CosUploadProgress } from '@/api/inspection/cos/types';
|
||||||
|
import type { ComponentInternalInstance } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: string; // v-model绑定的URL
|
||||||
|
fileSize?: number; // 文件大小限制(MB)
|
||||||
|
fileType?: string[]; // 支持的文件类型
|
||||||
|
disabled?: boolean; // 是否禁用
|
||||||
|
showTip?: boolean; // 是否显示提示
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: '',
|
||||||
|
fileSize: 10,
|
||||||
|
fileType: () => ['mp3', 'wav', 'm4a', 'aac', 'ogg'],
|
||||||
|
disabled: false,
|
||||||
|
showTip: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string];
|
||||||
|
'upload-success': [url: string];
|
||||||
|
'upload-error': [error: any];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
|
|
||||||
|
const audioUploadRef = ref();
|
||||||
|
const currentFile = ref<{ name: string; url?: string } | null>(null);
|
||||||
|
const uploading = ref(false);
|
||||||
|
const uploadPercent = ref(0);
|
||||||
|
const uploadStatus = ref<'success' | 'exception' | 'warning'>('success');
|
||||||
|
const previewVisible = ref(false);
|
||||||
|
const previewUrl = ref('');
|
||||||
|
|
||||||
|
// COS配置
|
||||||
|
const cosConfig = ref<{ bucket: string; region: string }>();
|
||||||
|
|
||||||
|
// 计算accept属性
|
||||||
|
const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','));
|
||||||
|
|
||||||
|
// 进度文本
|
||||||
|
const progressText = computed(() => {
|
||||||
|
if (uploadPercent.value < 100) {
|
||||||
|
return `正在上传: ${uploadPercent.value}%`;
|
||||||
|
}
|
||||||
|
return '上传完成';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听modelValue变化
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(url) => {
|
||||||
|
if (url) {
|
||||||
|
// 从URL提取文件名
|
||||||
|
const fileName = url.substring(url.lastIndexOf('/') + 1);
|
||||||
|
currentFile.value = { name: decodeURIComponent(fileName), url };
|
||||||
|
} else {
|
||||||
|
currentFile.value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化COS配置
|
||||||
|
const initCosConfig = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getCosConfig();
|
||||||
|
cosConfig.value = res.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取COS配置失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时初始化
|
||||||
|
onMounted(() => {
|
||||||
|
initCosConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 文件选择变化
|
||||||
|
const handleFileChange = (file: any) => {
|
||||||
|
// 校验文件类型
|
||||||
|
const fileName = file.name;
|
||||||
|
const fileExt = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
|
||||||
|
if (!props.fileType.includes(fileExt)) {
|
||||||
|
proxy?.$modal.msgError(`文件格式不正确,请上传${props.fileType.join('、')}格式的音频文件!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验文件大小
|
||||||
|
const fileSizeMB = file.size / 1024 / 1024;
|
||||||
|
if (fileSizeMB > props.fileSize) {
|
||||||
|
proxy?.$modal.msgError(`文件大小不能超过${props.fileSize}MB!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始上传
|
||||||
|
uploadToCos(file.raw, fileName);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传到COS
|
||||||
|
const uploadToCos = async (file: File, fileName: string) => {
|
||||||
|
if (!cosConfig.value) {
|
||||||
|
ElMessage.error('COS配置未加载,请稍后重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploading.value = true;
|
||||||
|
uploadPercent.value = 0;
|
||||||
|
uploadStatus.value = 'success';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cos = createCosInstance();
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const key = `audio/${timestamp}_${fileName}`;
|
||||||
|
|
||||||
|
cos.uploadFile(
|
||||||
|
{
|
||||||
|
Bucket: cosConfig.value.bucket,
|
||||||
|
Region: cosConfig.value.region,
|
||||||
|
Key: key,
|
||||||
|
Body: file,
|
||||||
|
SliceSize: 1024 * 1024 * 100, // 大于100MB使用分块上传
|
||||||
|
onProgress: (progressData: CosUploadProgress) => {
|
||||||
|
uploadPercent.value = Math.round(progressData.percent * 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err: any, data: any) => {
|
||||||
|
uploading.value = false;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
uploadStatus.value = 'exception';
|
||||||
|
ElMessage.error('上传失败: ' + err.message);
|
||||||
|
emit('upload-error', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传成功
|
||||||
|
const url = 'https://' + data.Location;
|
||||||
|
currentFile.value = { name: fileName, url };
|
||||||
|
emit('update:modelValue', url);
|
||||||
|
emit('upload-success', url);
|
||||||
|
ElMessage.success('上传成功');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
uploading.value = false;
|
||||||
|
uploadStatus.value = 'exception';
|
||||||
|
ElMessage.error('上传失败: ' + error);
|
||||||
|
emit('upload-error', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除文件
|
||||||
|
const handleRemove = () => {
|
||||||
|
currentFile.value = null;
|
||||||
|
uploadPercent.value = 0;
|
||||||
|
emit('update:modelValue', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 预览音频
|
||||||
|
const previewAudio = () => {
|
||||||
|
if (currentFile.value?.url) {
|
||||||
|
previewUrl.value = currentFile.value.url;
|
||||||
|
previewVisible.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.audio-upload-container {
|
||||||
|
.el-upload__tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress {
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -27,8 +27,8 @@
|
|||||||
<svg-icon class-name="search-icon" icon-class="search" />
|
<svg-icon class-name="search-icon" icon-class="search" />
|
||||||
</div>
|
</div>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<!-- 消息 -->
|
<!-- 消息 - 已隐藏 -->
|
||||||
<el-tooltip :content="proxy.$t('navbar.message')" effect="dark" placement="bottom">
|
<el-tooltip v-if="false" :content="proxy.$t('navbar.message')" effect="dark" placement="bottom">
|
||||||
<div>
|
<div>
|
||||||
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
|
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
@@ -42,25 +42,6 @@
|
|||||||
</el-popover>
|
</el-popover>
|
||||||
</div>
|
</div>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip content="Github" effect="dark" placement="bottom">
|
|
||||||
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
|
|
||||||
</el-tooltip>
|
|
||||||
|
|
||||||
<el-tooltip :content="proxy.$t('navbar.document')" effect="dark" placement="bottom">
|
|
||||||
<ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
|
|
||||||
</el-tooltip>
|
|
||||||
|
|
||||||
<el-tooltip :content="proxy.$t('navbar.full')" effect="dark" placement="bottom">
|
|
||||||
<screenfull id="screenfull" class="right-menu-item hover-effect" />
|
|
||||||
</el-tooltip>
|
|
||||||
|
|
||||||
<el-tooltip :content="proxy.$t('navbar.language')" effect="dark" placement="bottom">
|
|
||||||
<lang-select id="lang-select" class="right-menu-item hover-effect" />
|
|
||||||
</el-tooltip>
|
|
||||||
|
|
||||||
<el-tooltip :content="proxy.$t('navbar.layoutSize')" effect="dark" placement="bottom">
|
|
||||||
<size-select id="size-select" class="right-menu-item hover-effect" />
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
</template>
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand">
|
<el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand">
|
||||||
@@ -281,6 +262,7 @@ watch(
|
|||||||
|
|
||||||
.avatar-container {
|
.avatar-container {
|
||||||
margin-right: 40px;
|
margin-right: 40px;
|
||||||
|
margin-left: 20px;
|
||||||
|
|
||||||
.avatar-wrapper {
|
.avatar-wrapper {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
@@ -290,7 +272,7 @@ watch(
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 10px;
|
border-radius: 20px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,12 +34,6 @@
|
|||||||
<el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange" />
|
<el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="drawer-item">
|
|
||||||
<span>深色模式</span>
|
|
||||||
<span class="comp-style">
|
|
||||||
<el-switch v-model="isDark" class="drawer-switch" @change="toggleDark" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider />
|
<el-divider />
|
||||||
|
|
||||||
@@ -114,22 +108,6 @@ const sideTheme = ref(settingsStore.sideTheme);
|
|||||||
const storeSettings = computed(() => settingsStore);
|
const storeSettings = computed(() => settingsStore);
|
||||||
const predefineColors = ref(['#409EFF', '#ff4500', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585']);
|
const predefineColors = ref(['#409EFF', '#ff4500', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585']);
|
||||||
|
|
||||||
// 是否暗黑模式
|
|
||||||
const isDark = useDark({
|
|
||||||
storageKey: 'useDarkKey',
|
|
||||||
valueDark: 'dark',
|
|
||||||
valueLight: 'light'
|
|
||||||
});
|
|
||||||
// 匹配菜单颜色
|
|
||||||
watch(isDark, () => {
|
|
||||||
if (isDark.value) {
|
|
||||||
settingsStore.sideTheme = SideThemeEnum.DARK;
|
|
||||||
} else {
|
|
||||||
settingsStore.sideTheme = sideTheme.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const toggleDark = () => useToggle(isDark);
|
|
||||||
|
|
||||||
const topNavChange = (val: any) => {
|
const topNavChange = (val: any) => {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
appStore.toggleSideBarHide(false);
|
appStore.toggleSideBarHide(false);
|
||||||
@@ -148,11 +126,6 @@ const themeChange = (val: string) => {
|
|||||||
};
|
};
|
||||||
const handleTheme = (val: string) => {
|
const handleTheme = (val: string) => {
|
||||||
sideTheme.value = val;
|
sideTheme.value = val;
|
||||||
if (isDark.value && val === SideThemeEnum.LIGHT) {
|
|
||||||
// 暗黑模式颜色不变
|
|
||||||
settingsStore.sideTheme = SideThemeEnum.DARK;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
settingsStore.sideTheme = val;
|
settingsStore.sideTheme = val;
|
||||||
};
|
};
|
||||||
const saveSetting = () => {
|
const saveSetting = () => {
|
||||||
|
|||||||
@@ -107,7 +107,13 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="区域" prop="regionId">
|
<el-form-item label="区域" prop="regionId">
|
||||||
<el-select v-model="form.regionId" placeholder="请选择区域" filterable style="width: 100%">
|
<el-select
|
||||||
|
v-model="form.regionId"
|
||||||
|
placeholder="请选择任务模板后自动填充"
|
||||||
|
filterable
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled="!!form.taskId && !form.id"
|
||||||
|
>
|
||||||
<el-option v-for="region in regionOptions" :key="region.id" :label="region.regionName" :value="region.id" />
|
<el-option v-for="region in regionOptions" :key="region.id" :label="region.regionName" :value="region.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -121,7 +127,8 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<!-- 仅编辑时显示状态字段 -->
|
||||||
|
<el-col v-if="form.id" :span="12">
|
||||||
<el-form-item label="执行状态" prop="status">
|
<el-form-item label="执行状态" prop="status">
|
||||||
<el-select v-model="form.status" placeholder="请选择状态" style="width: 100%">
|
<el-select v-model="form.status" placeholder="请选择状态" style="width: 100%">
|
||||||
<el-option label="待执行" value="pending" />
|
<el-option label="待执行" value="pending" />
|
||||||
@@ -134,34 +141,90 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-divider content-position="left">执行角色</el-divider>
|
<el-divider content-position="left">执行角色</el-divider>
|
||||||
|
|
||||||
|
<!-- 操作人和监护人 -->
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="操作人" prop="operatorName">
|
<el-form-item label="操作人" prop="operatorName">
|
||||||
<el-input v-model="form.operatorName" placeholder="请输入操作人姓名" />
|
<el-input
|
||||||
|
v-model="form.operatorName"
|
||||||
|
placeholder="请选择操作人"
|
||||||
|
readonly
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button icon="User" @click="operatorSelectRef.open()" />
|
||||||
|
<el-button v-if="form.operatorName" icon="Close" @click="clearUser('operator')" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="监护人" prop="custodianName">
|
<el-form-item label="监护人" prop="custodianName">
|
||||||
<el-input v-model="form.custodianName" placeholder="请输入监护人姓名" />
|
<el-input
|
||||||
|
v-model="form.custodianName"
|
||||||
|
placeholder="请选择监护人"
|
||||||
|
readonly
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button icon="User" @click="custodianSelectRef.open()" />
|
||||||
|
<el-button v-if="form.custodianName" icon="Close" @click="clearUser('custodian')" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 送电人和受电人 -->
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="送电人" prop="senderName">
|
<el-form-item label="送电人" prop="senderName">
|
||||||
<el-input v-model="form.senderName" placeholder="请输入送电人姓名" />
|
<el-input
|
||||||
|
v-model="form.senderName"
|
||||||
|
placeholder="请选择送电人"
|
||||||
|
readonly
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button icon="User" @click="senderSelectRef.open()" />
|
||||||
|
<el-button v-if="form.senderName" icon="Close" @click="clearUser('sender')" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="受电人" prop="recipientName">
|
<el-form-item label="受电人" prop="recipientName">
|
||||||
<el-input v-model="form.recipientName" placeholder="请输入受电人姓名" />
|
<el-input
|
||||||
|
v-model="form.recipientName"
|
||||||
|
placeholder="请选择受电人"
|
||||||
|
readonly
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button icon="User" @click="recipientSelectRef.open()" />
|
||||||
|
<el-button v-if="form.recipientName" icon="Close" @click="clearUser('recipient')" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 指挥人 -->
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="指挥人" prop="commanderName">
|
<el-form-item label="指挥人" prop="commanderName">
|
||||||
<el-input v-model="form.commanderName" placeholder="请输入指挥人姓名" />
|
<el-input
|
||||||
|
v-model="form.commanderName"
|
||||||
|
placeholder="请选择指挥人"
|
||||||
|
readonly
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button icon="User" @click="commanderSelectRef.open()" />
|
||||||
|
<el-button v-if="form.commanderName" icon="Close" @click="clearUser('commander')" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -173,18 +236,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 用户选择组件 -->
|
||||||
|
<UserSelect ref="operatorSelectRef" :multiple="false" @confirmCallBack="handleOperatorSelect" />
|
||||||
|
<UserSelect ref="custodianSelectRef" :multiple="false" @confirmCallBack="handleCustodianSelect" />
|
||||||
|
<UserSelect ref="senderSelectRef" :multiple="false" @confirmCallBack="handleSenderSelect" />
|
||||||
|
<UserSelect ref="recipientSelectRef" :multiple="false" @confirmCallBack="handleRecipientSelect" />
|
||||||
|
<UserSelect ref="commanderSelectRef" :multiple="false" @confirmCallBack="handleCommanderSelect" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup name="ArExecution" lang="ts">
|
<script setup name="ArExecution" lang="ts">
|
||||||
import { listArExecution, getArExecution, delArExecution, addArExecution, updateArExecution } from '@/api/inspection/execution';
|
import { listArExecution, getArExecution, delArExecution, addArExecution, updateArExecution } from '@/api/inspection/execution';
|
||||||
import { ArExecutionVO, ArExecutionQuery, ArExecutionForm } from '@/api/inspection/execution/types';
|
import { ArExecutionVO, ArExecutionQuery, ArExecutionForm } from '@/api/inspection/execution/types';
|
||||||
import { listArTask } from '@/api/inspection/task';
|
import { listArTask, getArTask } from '@/api/inspection/task';
|
||||||
import { ArTaskVO } from '@/api/inspection/task/types';
|
import { ArTaskVO } from '@/api/inspection/task/types';
|
||||||
import { listArRegion } from '@/api/inspection/region';
|
import { listArRegion } from '@/api/inspection/region';
|
||||||
import { ArRegionVO } from '@/api/inspection/region/types';
|
import { ArRegionVO } from '@/api/inspection/region/types';
|
||||||
import { listArDevice } from '@/api/inspection/device';
|
import { listArDevice } from '@/api/inspection/device';
|
||||||
import { ArDeviceVO } from '@/api/inspection/device/types';
|
import { ArDeviceVO } from '@/api/inspection/device/types';
|
||||||
|
import { UserVO } from '@/api/system/user/types';
|
||||||
|
import UserSelect from '@/components/UserSelect/index.vue';
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
|
|
||||||
@@ -203,6 +275,12 @@ const total = ref(0);
|
|||||||
const queryFormRef = ref<ElFormInstance>();
|
const queryFormRef = ref<ElFormInstance>();
|
||||||
const executionFormRef = ref<ElFormInstance>();
|
const executionFormRef = ref<ElFormInstance>();
|
||||||
|
|
||||||
|
const operatorSelectRef = ref();
|
||||||
|
const custodianSelectRef = ref();
|
||||||
|
const senderSelectRef = ref();
|
||||||
|
const recipientSelectRef = ref();
|
||||||
|
const commanderSelectRef = ref();
|
||||||
|
|
||||||
const dialog = reactive<DialogOption>({
|
const dialog = reactive<DialogOption>({
|
||||||
visible: false,
|
visible: false,
|
||||||
title: ''
|
title: ''
|
||||||
@@ -369,6 +447,90 @@ const handleExport = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 监听任务模板变化,自动填充区域 */
|
||||||
|
watch(() => form.value.taskId, async (newTaskId) => {
|
||||||
|
if (newTaskId && !form.value.id) { // 仅新增时自动填充
|
||||||
|
try {
|
||||||
|
const res = await getArTask(newTaskId);
|
||||||
|
if (res.data && res.data.regionId) {
|
||||||
|
form.value.regionId = res.data.regionId;
|
||||||
|
const region = regionOptions.value.find(r => r.id === res.data.regionId);
|
||||||
|
if (region) {
|
||||||
|
proxy?.$modal.msgSuccess(`已自动填充区域:${region.regionName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取任务模板详情失败', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 操作人选择回调 */
|
||||||
|
const handleOperatorSelect = (users: UserVO[]) => {
|
||||||
|
if (users && users.length > 0) {
|
||||||
|
form.value.operatorId = users[0].userId;
|
||||||
|
form.value.operatorName = users[0].nickName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 监护人选择回调 */
|
||||||
|
const handleCustodianSelect = (users: UserVO[]) => {
|
||||||
|
if (users && users.length > 0) {
|
||||||
|
form.value.custodianId = users[0].userId;
|
||||||
|
form.value.custodianName = users[0].nickName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 送电人选择回调 */
|
||||||
|
const handleSenderSelect = (users: UserVO[]) => {
|
||||||
|
if (users && users.length > 0) {
|
||||||
|
form.value.senderId = users[0].userId;
|
||||||
|
form.value.senderName = users[0].nickName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 受电人选择回调 */
|
||||||
|
const handleRecipientSelect = (users: UserVO[]) => {
|
||||||
|
if (users && users.length > 0) {
|
||||||
|
form.value.recipientId = users[0].userId;
|
||||||
|
form.value.recipientName = users[0].nickName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 指挥人选择回调 */
|
||||||
|
const handleCommanderSelect = (users: UserVO[]) => {
|
||||||
|
if (users && users.length > 0) {
|
||||||
|
form.value.commanderId = users[0].userId;
|
||||||
|
form.value.commanderName = users[0].nickName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 清空人员选择 */
|
||||||
|
const clearUser = (role: 'operator' | 'custodian' | 'sender' | 'recipient' | 'commander') => {
|
||||||
|
switch (role) {
|
||||||
|
case 'operator':
|
||||||
|
form.value.operatorId = undefined;
|
||||||
|
form.value.operatorName = undefined;
|
||||||
|
break;
|
||||||
|
case 'custodian':
|
||||||
|
form.value.custodianId = undefined;
|
||||||
|
form.value.custodianName = undefined;
|
||||||
|
break;
|
||||||
|
case 'sender':
|
||||||
|
form.value.senderId = undefined;
|
||||||
|
form.value.senderName = undefined;
|
||||||
|
break;
|
||||||
|
case 'recipient':
|
||||||
|
form.value.recipientId = undefined;
|
||||||
|
form.value.recipientName = undefined;
|
||||||
|
break;
|
||||||
|
case 'commander':
|
||||||
|
form.value.commanderId = undefined;
|
||||||
|
form.value.commanderName = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getOptions();
|
getOptions();
|
||||||
getList();
|
getList();
|
||||||
|
|||||||
@@ -140,8 +140,8 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="内容语音URL" prop="contentVoice">
|
<el-form-item label="内容语音" prop="contentVoice">
|
||||||
<el-input v-model="form.contentVoice" placeholder="请输入内容语音URL" />
|
<AudioUpload v-model="form.contentVoice" :file-size="10" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -172,8 +172,8 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="复述语音URL" prop="rephraseVoice">
|
<el-form-item label="复述语音" prop="rephraseVoice">
|
||||||
<el-input v-model="form.rephraseVoice" placeholder="语音URL" />
|
<AudioUpload v-model="form.rephraseVoice" :file-size="10" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -189,8 +189,8 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="确认语音URL" prop="confirmVoice">
|
<el-form-item label="确认语音" prop="confirmVoice">
|
||||||
<el-input v-model="form.confirmVoice" placeholder="语音URL" />
|
<AudioUpload v-model="form.confirmVoice" :file-size="10" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -243,6 +243,7 @@ import { listArTask } from '@/api/inspection/task';
|
|||||||
import { ArTaskVO } from '@/api/inspection/task/types';
|
import { ArTaskVO } from '@/api/inspection/task/types';
|
||||||
import { listArPoint } from '@/api/inspection/point';
|
import { listArPoint } from '@/api/inspection/point';
|
||||||
import { ArPointVO } from '@/api/inspection/point/types';
|
import { ArPointVO } from '@/api/inspection/point/types';
|
||||||
|
import AudioUpload from '@/components/AudioUpload/index.vue';
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">{{ proxy.$t('login.rememberPassword') }}</el-checkbox>
|
<el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">{{ proxy.$t('login.rememberPassword') }}</el-checkbox>
|
||||||
<el-form-item style="float: right">
|
<el-form-item v-if="false" style="float: right">
|
||||||
<el-button circle :title="proxy.$t('login.social.wechat')" @click="doSocialLogin('wechat')">
|
<el-button circle :title="proxy.$t('login.social.wechat')" @click="doSocialLogin('wechat')">
|
||||||
<svg-icon icon-class="wechat" />
|
<svg-icon icon-class="wechat" />
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
<!-- 底部 -->
|
<!-- 底部 -->
|
||||||
<div class="el-login-footer">
|
<div class="el-login-footer">
|
||||||
<span>Copyright © 2018-2025 疯狂的狮子Li All Rights Reserved.</span>
|
<span>Copyright © 2021-2025 NJCQ Tech All Rights Reserved.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -96,8 +96,8 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
const loginForm = ref<LoginData>({
|
const loginForm = ref<LoginData>({
|
||||||
tenantId: '000000',
|
tenantId: '000000',
|
||||||
username: 'admin',
|
username: '',
|
||||||
password: 'admin123',
|
password: '',
|
||||||
rememberMe: false,
|
rememberMe: false,
|
||||||
code: '',
|
code: '',
|
||||||
uuid: ''
|
uuid: ''
|
||||||
@@ -236,8 +236,9 @@ onMounted(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-image: url('../assets/images/login-background.jpg');
|
// background-image: url('../assets/images/login-background.jpg');
|
||||||
background-size: cover;
|
background-color: #28273a;
|
||||||
|
// background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-box {
|
.title-box {
|
||||||
|
|||||||
@@ -103,13 +103,14 @@ public class AuthController {
|
|||||||
// 登录
|
// 登录
|
||||||
LoginVo loginVo = IAuthStrategy.login(body, client, grantType);
|
LoginVo loginVo = IAuthStrategy.login(body, client, grantType);
|
||||||
|
|
||||||
Long userId = LoginHelper.getUserId();
|
// 注释掉登录欢迎消息推送 - UI定制需求
|
||||||
scheduledExecutorService.schedule(() -> {
|
// Long userId = LoginHelper.getUserId();
|
||||||
SseMessageDto dto = new SseMessageDto();
|
// scheduledExecutorService.schedule(() -> {
|
||||||
dto.setMessage("欢迎登录RuoYi-Vue-Plus后台管理系统");
|
// SseMessageDto dto = new SseMessageDto();
|
||||||
dto.setUserIds(List.of(userId));
|
// dto.setMessage("欢迎登录RuoYi-Vue-Plus后台管理系统");
|
||||||
SseMessageUtils.publishMessage(dto);
|
// dto.setUserIds(List.of(userId));
|
||||||
}, 5, TimeUnit.SECONDS);
|
// SseMessageUtils.publishMessage(dto);
|
||||||
|
// }, 5, TimeUnit.SECONDS);
|
||||||
return R.ok(loginVo);
|
return R.ok(loginVo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -268,3 +268,13 @@ justauth:
|
|||||||
client-id: 10**********6
|
client-id: 10**********6
|
||||||
client-secret: 1f7d08**********5b7**********29e
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
redirect-uri: ${justauth.address}/social-callback?source=gitea
|
redirect-uri: ${justauth.address}/social-callback?source=gitea
|
||||||
|
|
||||||
|
--- # 腾讯云COS配置
|
||||||
|
tencent:
|
||||||
|
cos:
|
||||||
|
secret-id: AKIDBDu22pdIn8Tjx9D6nGWt68wY0JQJ0T3U
|
||||||
|
secret-key: HJ6i6MtHRP9fzDD3f3EBuPjqmUzGJ8qK
|
||||||
|
duration-seconds: 1800
|
||||||
|
bucket: nc-1375092979
|
||||||
|
region: ap-nanjing
|
||||||
|
app-id: 1375092979
|
||||||
|
|||||||
@@ -270,3 +270,13 @@ justauth:
|
|||||||
client-id: 10**********6
|
client-id: 10**********6
|
||||||
client-secret: 1f7d08**********5b7**********29e
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
redirect-uri: ${justauth.address}/social-callback?source=gitea
|
redirect-uri: ${justauth.address}/social-callback?source=gitea
|
||||||
|
|
||||||
|
--- # 腾讯云COS配置
|
||||||
|
tencent:
|
||||||
|
cos:
|
||||||
|
secret-id: AKIDBDu22pdIn8Tjx9D6nGWt68wY0JQJ0T3U
|
||||||
|
secret-key: HJ6i6MtHRP9fzDD3f3EBuPjqmUzGJ8qK
|
||||||
|
duration-seconds: 1800
|
||||||
|
bucket: nc-1375092979
|
||||||
|
region: ap-nanjing
|
||||||
|
app-id: 1375092979
|
||||||
|
|||||||
@@ -195,16 +195,16 @@ springdoc:
|
|||||||
enabled: true
|
enabled: true
|
||||||
info:
|
info:
|
||||||
# 标题
|
# 标题
|
||||||
title: '标题:RuoYi-Vue-Plus多租户管理系统_接口文档'
|
title: 'AR智能巡检平台-接口文档'
|
||||||
# 描述
|
# 描述
|
||||||
description: '描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...'
|
description: 'AR智能巡检平台 接口文档'
|
||||||
# 版本
|
# 版本
|
||||||
version: '版本号: ${project.version}'
|
version: '版本号: ${project.version}'
|
||||||
# 作者信息
|
# 作者信息
|
||||||
contact:
|
contact:
|
||||||
name: Lion Li
|
name: YANG JIANKUAN
|
||||||
email: crazylionli@163.com
|
email: tim.yee@hotmail.com
|
||||||
url: https://gitee.com/dromara/RuoYi-Vue-Plus
|
url: https://www.njcqit.com
|
||||||
#这里定义了两个分组,可定义多个,也可以不定义
|
#这里定义了两个分组,可定义多个,也可以不定义
|
||||||
group-configs:
|
group-configs:
|
||||||
- group: 1.演示模块
|
- group: 1.演示模块
|
||||||
|
|||||||
@@ -78,6 +78,13 @@
|
|||||||
<artifactId>ruoyi-common-oss</artifactId>
|
<artifactId>ruoyi-common-oss</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 腾讯云COS临时密钥生成 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.qcloud</groupId>
|
||||||
|
<artifactId>cos-sts_api</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package org.dromara.inspection.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 腾讯云COS配置属性
|
||||||
|
*
|
||||||
|
* @author LionLi
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "tencent.cos")
|
||||||
|
public class TencentCosProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 腾讯云SecretId
|
||||||
|
*/
|
||||||
|
private String secretId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 腾讯云SecretKey
|
||||||
|
*/
|
||||||
|
private String secretKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 临时密钥有效期(秒),默认1800秒=30分钟
|
||||||
|
*/
|
||||||
|
private Integer durationSeconds = 1800;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储桶名称
|
||||||
|
*/
|
||||||
|
private String bucket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储桶所在地域
|
||||||
|
*/
|
||||||
|
private String region;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 腾讯云AppId
|
||||||
|
*/
|
||||||
|
private String appId;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.dromara.inspection.controller;
|
package org.dromara.inspection.controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.dev33.satoken.annotation.SaIgnore;
|
||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
import org.dromara.common.core.validate.AddGroup;
|
import org.dromara.common.core.validate.AddGroup;
|
||||||
import org.dromara.common.core.validate.EditGroup;
|
import org.dromara.common.core.validate.EditGroup;
|
||||||
@@ -13,6 +14,8 @@ import org.dromara.common.excel.utils.ExcelUtil;
|
|||||||
import org.dromara.common.log.annotation.Log;
|
import org.dromara.common.log.annotation.Log;
|
||||||
import org.dromara.common.log.enums.BusinessType;
|
import org.dromara.common.log.enums.BusinessType;
|
||||||
import org.dromara.inspection.domain.bo.ArExecutionBo;
|
import org.dromara.inspection.domain.bo.ArExecutionBo;
|
||||||
|
import org.dromara.inspection.domain.bo.ArExecutionSubmitBo;
|
||||||
|
import org.dromara.inspection.domain.vo.ArExecutionDetailVo;
|
||||||
import org.dromara.inspection.domain.vo.ArExecutionVo;
|
import org.dromara.inspection.domain.vo.ArExecutionVo;
|
||||||
import org.dromara.inspection.service.IArExecutionService;
|
import org.dromara.inspection.service.IArExecutionService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -61,15 +64,16 @@ public class ArExecutionController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取任务执行记录详细信息
|
* 获取任务执行记录详细信息(包含关联对象和步骤树)
|
||||||
*
|
*
|
||||||
* @param id 执行ID
|
* @param id 执行ID
|
||||||
*/
|
*/
|
||||||
@SaCheckPermission("inspection:execution:query")
|
@SaCheckPermission("inspection:execution:query")
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public R<ArExecutionVo> getInfo(@NotNull(message = "执行ID不能为空")
|
@SaIgnore
|
||||||
|
public R<ArExecutionDetailVo> getInfo(@NotNull(message = "执行ID不能为空")
|
||||||
@PathVariable("id") Long id) {
|
@PathVariable("id") Long id) {
|
||||||
return R.ok(arExecutionService.queryById(id));
|
return R.ok(arExecutionService.queryDetailById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,6 +98,18 @@ public class ArExecutionController extends BaseController {
|
|||||||
return toAjax(arExecutionService.updateByBo(bo));
|
return toAjax(arExecutionService.updateByBo(bo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量提交任务执行结果
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("inspection:execution:submit")
|
||||||
|
@Log(title = "提交任务执行结果", businessType = BusinessType.UPDATE)
|
||||||
|
@RepeatSubmit
|
||||||
|
@SaIgnore
|
||||||
|
@PostMapping("/submit")
|
||||||
|
public R<Void> submit(@Validated @RequestBody ArExecutionSubmitBo bo) {
|
||||||
|
return toAjax(arExecutionService.submitExecution(bo));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除任务执行记录
|
* 删除任务执行记录
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -40,9 +40,11 @@ public class ArStepRecordController extends BaseController {
|
|||||||
|
|
||||||
private final IArStepRecordService arStepRecordService;
|
private final IArStepRecordService arStepRecordService;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询步骤执行记录列表
|
* 查询步骤执行记录列表
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@SaCheckPermission("inspection:stepRecord:list")
|
@SaCheckPermission("inspection:stepRecord:list")
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public TableDataInfo<ArStepRecordVo> list(@Validated(QueryGroup.class) ArStepRecordBo bo, PageQuery pageQuery) {
|
public TableDataInfo<ArStepRecordVo> list(@Validated(QueryGroup.class) ArStepRecordBo bo, PageQuery pageQuery) {
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package org.dromara.inspection.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import com.tencent.cloud.CosStsClient;
|
||||||
|
import com.tencent.cloud.Policy;
|
||||||
|
import com.tencent.cloud.Response;
|
||||||
|
import com.tencent.cloud.Statement;
|
||||||
|
import com.tencent.cloud.cos.util.Jackson;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.dromara.common.core.domain.R;
|
||||||
|
import org.dromara.common.log.annotation.Log;
|
||||||
|
import org.dromara.common.log.enums.BusinessType;
|
||||||
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
|
import org.dromara.inspection.config.TencentCosProperties;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 腾讯云COS临时凭证Controller
|
||||||
|
*
|
||||||
|
* @author LionLi
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Validated
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/inspection/cos")
|
||||||
|
public class TencentCosController {
|
||||||
|
|
||||||
|
private final TencentCosProperties cosProperties;
|
||||||
|
|
||||||
|
private static final String CACHE_KEY = "inspection:cos:credential";
|
||||||
|
private static final int CACHE_EXPIRE_SECONDS = 1500; // 缓存25分钟,临时密钥30分钟过期
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取腾讯云COS临时上传凭证
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("inspection:step:edit")
|
||||||
|
@Log(title = "获取COS临时凭证", businessType = BusinessType.OTHER)
|
||||||
|
@GetMapping("/credential")
|
||||||
|
public R<Response> getCredential() {
|
||||||
|
// 先从缓存获取
|
||||||
|
Response cachedCredential = RedisUtils.getCacheObject(CACHE_KEY);
|
||||||
|
if (cachedCredential != null) {
|
||||||
|
return R.ok(cachedCredential);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建配置参数
|
||||||
|
TreeMap<String, Object> config = new TreeMap<>();
|
||||||
|
config.put("secretId", cosProperties.getSecretId());
|
||||||
|
config.put("secretKey", cosProperties.getSecretKey());
|
||||||
|
config.put("durationSeconds", cosProperties.getDurationSeconds());
|
||||||
|
config.put("bucket", cosProperties.getBucket());
|
||||||
|
config.put("region", cosProperties.getRegion());
|
||||||
|
|
||||||
|
// 初始化 policy
|
||||||
|
Policy policy = new Policy();
|
||||||
|
|
||||||
|
// 开始构建一条 statement
|
||||||
|
Statement statement = new Statement();
|
||||||
|
// 声明设置的结果是允许操作
|
||||||
|
statement.setEffect("allow");
|
||||||
|
|
||||||
|
// 添加操作权限
|
||||||
|
statement.addActions(new String[]{
|
||||||
|
// 简单上传
|
||||||
|
"cos:PutObject",
|
||||||
|
"cos:PostObject",
|
||||||
|
// 分块上传
|
||||||
|
"cos:InitiateMultipartUpload",
|
||||||
|
"cos:ListMultipartUploads",
|
||||||
|
"cos:ListParts",
|
||||||
|
"cos:UploadPart",
|
||||||
|
"cos:CompleteMultipartUpload"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置允许操作的资源路径(限定只能上传到audio目录)
|
||||||
|
// 格式: qcs::cos:{region}:uid/{appid}:{bucket}/{path}
|
||||||
|
statement.addResources(new String[]{
|
||||||
|
"qcs::cos:" + cosProperties.getRegion() +
|
||||||
|
":uid/" + cosProperties.getAppId() +
|
||||||
|
":" + cosProperties.getBucket() +
|
||||||
|
"/audio/*"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 把一条 statement 添加到 policy
|
||||||
|
policy.addStatement(statement);
|
||||||
|
|
||||||
|
// 将 Policy 实例转化成 String
|
||||||
|
config.put("policy", Jackson.toJsonPrettyString(policy));
|
||||||
|
|
||||||
|
// 获取临时密钥
|
||||||
|
Response response = CosStsClient.getCredential(config);
|
||||||
|
|
||||||
|
// 缓存凭证(25分钟,临时密钥30分钟过期)
|
||||||
|
RedisUtils.setCacheObject(CACHE_KEY, response, Duration.ofSeconds(CACHE_EXPIRE_SECONDS));
|
||||||
|
|
||||||
|
return R.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取临时COS凭证失败", e);
|
||||||
|
return R.fail("获取临时凭证失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取COS配置信息(供前端使用)
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("inspection:step:edit")
|
||||||
|
@GetMapping("/config")
|
||||||
|
public R<Map<String, String>> getConfig() {
|
||||||
|
Map<String, String> config = new HashMap<>();
|
||||||
|
config.put("bucket", cosProperties.getBucket());
|
||||||
|
config.put("region", cosProperties.getRegion());
|
||||||
|
return R.ok(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package org.dromara.inspection.domain.bo;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务执行提交业务对象
|
||||||
|
*
|
||||||
|
* @author Lion Li
|
||||||
|
* @date 2025-01-13
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ArExecutionSubmitBo implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行ID
|
||||||
|
*/
|
||||||
|
@NotNull(message = "执行ID不能为空")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态(pending/in_progress/completed/cancelled)
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始时间
|
||||||
|
*/
|
||||||
|
private Date startTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束时间
|
||||||
|
*/
|
||||||
|
private Date endTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤执行树
|
||||||
|
*/
|
||||||
|
@Valid
|
||||||
|
private List<ArStepRecordTreeBo> stepTree;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package org.dromara.inspection.domain.bo;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤执行记录树形业务对象
|
||||||
|
*
|
||||||
|
* @author Lion Li
|
||||||
|
* @date 2025-01-13
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ArStepRecordTreeBo implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤ID(仅用于引用)
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行记录ID(用于更新现有记录)
|
||||||
|
*/
|
||||||
|
private Long recordId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行状态(pending/completed/skipped)
|
||||||
|
*/
|
||||||
|
private String recordStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否完成(0否 1是)
|
||||||
|
*/
|
||||||
|
private String isDone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始时间
|
||||||
|
*/
|
||||||
|
private Date startTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成时间
|
||||||
|
*/
|
||||||
|
private Date completionTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 耗时(秒)
|
||||||
|
*/
|
||||||
|
private Integer duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本反馈
|
||||||
|
*/
|
||||||
|
private String textFeedback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语音识别文本
|
||||||
|
*/
|
||||||
|
private String voiceText;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI识别结果(JSON)
|
||||||
|
*/
|
||||||
|
private String aiResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行人ID
|
||||||
|
*/
|
||||||
|
private Long executorId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行人姓名
|
||||||
|
*/
|
||||||
|
private String executorName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 子步骤列表
|
||||||
|
*/
|
||||||
|
@Valid
|
||||||
|
private List<ArStepRecordTreeBo> children;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package org.dromara.inspection.domain.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务执行详情视图对象
|
||||||
|
*
|
||||||
|
* @author Lion Li
|
||||||
|
* @date 2025-01-13
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ArExecutionDetailVo implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行基本信息
|
||||||
|
*/
|
||||||
|
private ArExecutionVo execution;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务模板信息
|
||||||
|
*/
|
||||||
|
private ArTaskVo task;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 区域信息
|
||||||
|
*/
|
||||||
|
private ArRegionVo region;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备信息
|
||||||
|
*/
|
||||||
|
private ArDeviceVo device;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤执行树
|
||||||
|
*/
|
||||||
|
private List<ArStepRecordTreeVo> stepTree;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package org.dromara.inspection.domain.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤执行记录树形视图对象
|
||||||
|
*
|
||||||
|
* @author Lion Li
|
||||||
|
* @date 2025-01-13
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ArStepRecordTreeVo implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
// ========== 步骤模板信息(来自 ar_step) ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤ID
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所属任务ID
|
||||||
|
*/
|
||||||
|
private Long taskId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 父步骤ID(0为顶级)
|
||||||
|
*/
|
||||||
|
private Long parentId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 祖级列表
|
||||||
|
*/
|
||||||
|
private String ancestors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤名称
|
||||||
|
*/
|
||||||
|
private String stepName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤内容描述
|
||||||
|
*/
|
||||||
|
private String stepContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤语音文本
|
||||||
|
*/
|
||||||
|
private String contentVoice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序号
|
||||||
|
*/
|
||||||
|
private Integer orderNum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联点位ID
|
||||||
|
*/
|
||||||
|
private Long pointId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需要语音朗读(0否 1是)
|
||||||
|
*/
|
||||||
|
private String needVoiceRead;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需要用户复述(0否 1是)
|
||||||
|
*/
|
||||||
|
private String needVoiceRephrase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复述提示文本
|
||||||
|
*/
|
||||||
|
private String rephraseContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复述语音文本
|
||||||
|
*/
|
||||||
|
private String rephraseVoice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需要确认(0否 1是)
|
||||||
|
*/
|
||||||
|
private String needVoiceConfirm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认提示文本
|
||||||
|
*/
|
||||||
|
private String confirmContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认语音文本
|
||||||
|
*/
|
||||||
|
private String confirmVoice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认词
|
||||||
|
*/
|
||||||
|
private String confirmWord;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需要AI识别(0否 1是)
|
||||||
|
*/
|
||||||
|
private String needAi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI目标名称
|
||||||
|
*/
|
||||||
|
private String aiTargetName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI配置数据(预留)
|
||||||
|
*/
|
||||||
|
private Map<String, Object> aiData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否操作步骤(0否 1是)
|
||||||
|
*/
|
||||||
|
private String isOperation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否叶子节点(0否 1是)
|
||||||
|
*/
|
||||||
|
private String isLeaf;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
// ========== 执行记录信息(来自 ar_step_record) ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行记录ID
|
||||||
|
*/
|
||||||
|
private Long recordId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行状态(pending/completed/skipped)
|
||||||
|
*/
|
||||||
|
private String recordStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否完成(0否 1是)
|
||||||
|
*/
|
||||||
|
private String isDone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始时间
|
||||||
|
*/
|
||||||
|
private Date startTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成时间
|
||||||
|
*/
|
||||||
|
private Date completionTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 耗时(秒)
|
||||||
|
*/
|
||||||
|
private Integer duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本反馈
|
||||||
|
*/
|
||||||
|
private String textFeedback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语音识别文本
|
||||||
|
*/
|
||||||
|
private String voiceText;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI识别结果(JSON)
|
||||||
|
*/
|
||||||
|
private String aiResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行人ID
|
||||||
|
*/
|
||||||
|
private Long executorId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行人姓名
|
||||||
|
*/
|
||||||
|
private String executorName;
|
||||||
|
|
||||||
|
// ========== 树形结构 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 子步骤列表
|
||||||
|
*/
|
||||||
|
private List<ArStepRecordTreeVo> children;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ package org.dromara.inspection.service;
|
|||||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||||
import org.dromara.inspection.domain.bo.ArExecutionBo;
|
import org.dromara.inspection.domain.bo.ArExecutionBo;
|
||||||
|
import org.dromara.inspection.domain.bo.ArExecutionSubmitBo;
|
||||||
|
import org.dromara.inspection.domain.vo.ArExecutionDetailVo;
|
||||||
import org.dromara.inspection.domain.vo.ArExecutionVo;
|
import org.dromara.inspection.domain.vo.ArExecutionVo;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -23,6 +25,22 @@ public interface IArExecutionService {
|
|||||||
*/
|
*/
|
||||||
ArExecutionVo queryById(Long id);
|
ArExecutionVo queryById(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询任务执行详情(包含关联对象和步骤树)
|
||||||
|
*
|
||||||
|
* @param id 执行ID
|
||||||
|
* @return 任务执行详情VO
|
||||||
|
*/
|
||||||
|
ArExecutionDetailVo queryDetailById(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量提交任务执行结果
|
||||||
|
*
|
||||||
|
* @param bo 提交业务对象
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
Boolean submitExecution(ArExecutionSubmitBo bo);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询列表
|
* 查询列表
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,16 +10,21 @@ import org.dromara.common.core.utils.StringUtils;
|
|||||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||||
import org.dromara.inspection.domain.ArExecution;
|
import org.dromara.inspection.domain.ArExecution;
|
||||||
|
import org.dromara.inspection.domain.ArStep;
|
||||||
|
import org.dromara.inspection.domain.ArStepRecord;
|
||||||
import org.dromara.inspection.domain.bo.ArExecutionBo;
|
import org.dromara.inspection.domain.bo.ArExecutionBo;
|
||||||
import org.dromara.inspection.domain.vo.ArExecutionVo;
|
import org.dromara.inspection.domain.bo.ArExecutionSubmitBo;
|
||||||
import org.dromara.inspection.mapper.ArExecutionMapper;
|
import org.dromara.inspection.domain.bo.ArStepRecordTreeBo;
|
||||||
|
import org.dromara.inspection.domain.vo.*;
|
||||||
|
import org.dromara.inspection.mapper.*;
|
||||||
import org.dromara.inspection.service.IArExecutionService;
|
import org.dromara.inspection.service.IArExecutionService;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.*;
|
||||||
import java.util.Date;
|
import java.util.stream.Collectors;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 任务执行记录Service业务层处理
|
* 任务执行记录Service业务层处理
|
||||||
@@ -32,6 +37,11 @@ import java.util.Map;
|
|||||||
public class ArExecutionServiceImpl implements IArExecutionService {
|
public class ArExecutionServiceImpl implements IArExecutionService {
|
||||||
|
|
||||||
private final ArExecutionMapper baseMapper;
|
private final ArExecutionMapper baseMapper;
|
||||||
|
private final ArTaskMapper taskMapper;
|
||||||
|
private final ArRegionMapper regionMapper;
|
||||||
|
private final ArDeviceMapper deviceMapper;
|
||||||
|
private final ArStepMapper stepMapper;
|
||||||
|
private final ArStepRecordMapper stepRecordMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ArExecutionVo queryById(Long id) {
|
public ArExecutionVo queryById(Long id) {
|
||||||
@@ -141,4 +151,253 @@ public class ArExecutionServiceImpl implements IArExecutionService {
|
|||||||
}
|
}
|
||||||
return baseMapper.deleteByIds(ids) > 0;
|
return baseMapper.deleteByIds(ids) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ArExecutionDetailVo queryDetailById(Long id) {
|
||||||
|
// 1. 查询执行记录
|
||||||
|
ArExecutionVo execution = baseMapper.selectVoById(id);
|
||||||
|
if (execution == null) {
|
||||||
|
throw new ServiceException("任务执行记录不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查询关联对象
|
||||||
|
ArTaskVo task = execution.getTaskId() != null
|
||||||
|
? taskMapper.selectVoById(execution.getTaskId()) : null;
|
||||||
|
ArRegionVo region = execution.getRegionId() != null
|
||||||
|
? regionMapper.selectVoById(execution.getRegionId()) : null;
|
||||||
|
ArDeviceVo device = execution.getDeviceId() != null
|
||||||
|
? deviceMapper.selectVoById(execution.getDeviceId()) : null;
|
||||||
|
|
||||||
|
// 3. 查询步骤模板
|
||||||
|
List<ArStepVo> allSteps = new ArrayList<>();
|
||||||
|
if (execution.getTaskId() != null) {
|
||||||
|
LambdaQueryWrapper<ArStep> stepWrapper = Wrappers.lambdaQuery();
|
||||||
|
stepWrapper.eq(ArStep::getTaskId, execution.getTaskId());
|
||||||
|
stepWrapper.orderByAsc(ArStep::getOrderNum);
|
||||||
|
allSteps = stepMapper.selectVoList(stepWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 查询执行记录
|
||||||
|
LambdaQueryWrapper<ArStepRecord> recordWrapper = Wrappers.lambdaQuery();
|
||||||
|
recordWrapper.eq(ArStepRecord::getExecutionId, id);
|
||||||
|
List<ArStepRecordVo> allRecords = stepRecordMapper.selectVoList(recordWrapper);
|
||||||
|
|
||||||
|
// 5. 构建步骤记录 Map
|
||||||
|
Map<Long, ArStepRecordVo> recordMap = allRecords.stream()
|
||||||
|
.collect(Collectors.toMap(ArStepRecordVo::getStepId, r -> r, (r1, r2) -> r1));
|
||||||
|
|
||||||
|
// 6. 构建步骤树
|
||||||
|
List<ArStepRecordTreeVo> stepTree = buildStepRecordTree(allSteps, recordMap, 0L);
|
||||||
|
|
||||||
|
// 7. 组装返回对象
|
||||||
|
ArExecutionDetailVo detail = new ArExecutionDetailVo();
|
||||||
|
detail.setExecution(execution);
|
||||||
|
detail.setTask(task);
|
||||||
|
detail.setRegion(region);
|
||||||
|
detail.setDevice(device);
|
||||||
|
detail.setStepTree(stepTree);
|
||||||
|
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归构建步骤执行记录树
|
||||||
|
*
|
||||||
|
* @param allSteps 所有步骤模板
|
||||||
|
* @param recordMap 执行记录Map (stepId -> record)
|
||||||
|
* @param parentId 父步骤ID
|
||||||
|
* @return 树形步骤执行记录列表
|
||||||
|
*/
|
||||||
|
private List<ArStepRecordTreeVo> buildStepRecordTree(
|
||||||
|
List<ArStepVo> allSteps,
|
||||||
|
Map<Long, ArStepRecordVo> recordMap,
|
||||||
|
Long parentId) {
|
||||||
|
|
||||||
|
List<ArStepRecordTreeVo> tree = new ArrayList<>();
|
||||||
|
for (ArStepVo step : allSteps) {
|
||||||
|
if (step.getParentId().equals(parentId)) {
|
||||||
|
ArStepRecordTreeVo treeNode = new ArStepRecordTreeVo();
|
||||||
|
|
||||||
|
// 复制步骤模板信息
|
||||||
|
BeanUtils.copyProperties(step, treeNode);
|
||||||
|
|
||||||
|
// 合并执行记录信息
|
||||||
|
ArStepRecordVo record = recordMap.get(step.getId());
|
||||||
|
if (record != null) {
|
||||||
|
treeNode.setRecordId(record.getId());
|
||||||
|
treeNode.setRecordStatus(record.getStatus());
|
||||||
|
treeNode.setIsDone(record.getIsDone());
|
||||||
|
treeNode.setStartTime(record.getStartTime());
|
||||||
|
treeNode.setCompletionTime(record.getCompletionTime());
|
||||||
|
treeNode.setDuration(record.getDuration());
|
||||||
|
treeNode.setTextFeedback(record.getTextFeedback());
|
||||||
|
treeNode.setVoiceText(record.getVoiceText());
|
||||||
|
treeNode.setAiResult(record.getAiResult());
|
||||||
|
treeNode.setExecutorId(record.getExecutorId());
|
||||||
|
treeNode.setExecutorName(record.getExecutorName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归查找子节点
|
||||||
|
List<ArStepRecordTreeVo> children = buildStepRecordTree(allSteps, recordMap, step.getId());
|
||||||
|
if (!children.isEmpty()) {
|
||||||
|
treeNode.setChildren(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.add(treeNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean submitExecution(ArExecutionSubmitBo bo) {
|
||||||
|
// 1. 校验执行记录是否存在
|
||||||
|
ArExecution execution = baseMapper.selectById(bo.getId());
|
||||||
|
if (execution == null) {
|
||||||
|
throw new ServiceException("任务执行记录不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 收集所有需要更新的步骤记录
|
||||||
|
List<ArStepRecord> recordsToUpdate = new ArrayList<>();
|
||||||
|
if (bo.getStepTree() != null && !bo.getStepTree().isEmpty()) {
|
||||||
|
collectRecordsToUpdate(bo.getStepTree(), bo.getId(), recordsToUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 批量更新步骤记录
|
||||||
|
if (!recordsToUpdate.isEmpty()) {
|
||||||
|
boolean updateSuccess = stepRecordMapper.updateBatchById(recordsToUpdate);
|
||||||
|
if (!updateSuccess) {
|
||||||
|
throw new ServiceException("步骤记录更新失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 计算总步骤数和已完成步骤数
|
||||||
|
StepCountResult countResult = countSteps(bo.getStepTree());
|
||||||
|
|
||||||
|
// 5. 更新执行记录
|
||||||
|
ArExecution updateExecution = new ArExecution();
|
||||||
|
updateExecution.setId(bo.getId());
|
||||||
|
|
||||||
|
// 设置状态
|
||||||
|
if (StringUtils.isNotBlank(bo.getStatus())) {
|
||||||
|
updateExecution.setStatus(bo.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置时间
|
||||||
|
if (bo.getStartTime() != null) {
|
||||||
|
updateExecution.setStartTime(bo.getStartTime());
|
||||||
|
}
|
||||||
|
if (bo.getEndTime() != null) {
|
||||||
|
updateExecution.setEndTime(bo.getEndTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动设置开始时间
|
||||||
|
if ("in_progress".equals(updateExecution.getStatus()) && execution.getStartTime() == null) {
|
||||||
|
updateExecution.setStartTime(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动设置结束时间
|
||||||
|
if (("completed".equals(updateExecution.getStatus()) || "cancelled".equals(updateExecution.getStatus()))
|
||||||
|
&& execution.getEndTime() == null) {
|
||||||
|
updateExecution.setEndTime(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置步骤统计
|
||||||
|
updateExecution.setTotalSteps(countResult.getTotal());
|
||||||
|
updateExecution.setCompletedSteps(countResult.getCompleted());
|
||||||
|
|
||||||
|
return baseMapper.updateById(updateExecution) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归收集所有需要更新的步骤记录
|
||||||
|
*
|
||||||
|
* @param stepTree 步骤树
|
||||||
|
* @param executionId 执行ID
|
||||||
|
* @param result 结果列表
|
||||||
|
*/
|
||||||
|
private void collectRecordsToUpdate(List<ArStepRecordTreeBo> stepTree, Long executionId, List<ArStepRecord> result) {
|
||||||
|
if (stepTree == null || stepTree.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ArStepRecordTreeBo stepBo : stepTree) {
|
||||||
|
// 只更新有 recordId 的记录
|
||||||
|
if (stepBo.getRecordId() != null) {
|
||||||
|
ArStepRecord record = new ArStepRecord();
|
||||||
|
record.setId(stepBo.getRecordId());
|
||||||
|
record.setExecutionId(executionId);
|
||||||
|
record.setStepId(stepBo.getId());
|
||||||
|
record.setStatus(stepBo.getRecordStatus());
|
||||||
|
record.setIsDone(stepBo.getIsDone());
|
||||||
|
record.setStartTime(stepBo.getStartTime());
|
||||||
|
record.setCompletionTime(stepBo.getCompletionTime());
|
||||||
|
record.setTextFeedback(stepBo.getTextFeedback());
|
||||||
|
record.setVoiceText(stepBo.getVoiceText());
|
||||||
|
record.setAiResult(stepBo.getAiResult());
|
||||||
|
record.setExecutorId(stepBo.getExecutorId());
|
||||||
|
record.setExecutorName(stepBo.getExecutorName());
|
||||||
|
|
||||||
|
// 自动计算耗时
|
||||||
|
if (stepBo.getStartTime() != null && stepBo.getCompletionTime() != null) {
|
||||||
|
long durationMillis = stepBo.getCompletionTime().getTime() - stepBo.getStartTime().getTime();
|
||||||
|
record.setDuration((int) (durationMillis / 1000));
|
||||||
|
} else if (stepBo.getDuration() != null) {
|
||||||
|
record.setDuration(stepBo.getDuration());
|
||||||
|
}
|
||||||
|
|
||||||
|
result.add(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理子步骤
|
||||||
|
if (stepBo.getChildren() != null && !stepBo.getChildren().isEmpty()) {
|
||||||
|
collectRecordsToUpdate(stepBo.getChildren(), executionId, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归统计步骤数
|
||||||
|
*
|
||||||
|
* @param stepTree 步骤树
|
||||||
|
* @return 统计结果
|
||||||
|
*/
|
||||||
|
private StepCountResult countSteps(List<ArStepRecordTreeBo> stepTree) {
|
||||||
|
int totalCount = 0;
|
||||||
|
int completedCount = 0;
|
||||||
|
|
||||||
|
if (stepTree == null || stepTree.isEmpty()) {
|
||||||
|
return new StepCountResult(totalCount, completedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ArStepRecordTreeBo stepBo : stepTree) {
|
||||||
|
// 只统计有 recordId 的步骤
|
||||||
|
if (stepBo.getRecordId() != null) {
|
||||||
|
totalCount++;
|
||||||
|
if ("1".equals(stepBo.getIsDone()) || "completed".equals(stepBo.getRecordStatus())) {
|
||||||
|
completedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归统计子步骤
|
||||||
|
if (stepBo.getChildren() != null && !stepBo.getChildren().isEmpty()) {
|
||||||
|
StepCountResult childCounts = countSteps(stepBo.getChildren());
|
||||||
|
totalCount += childCounts.getTotal();
|
||||||
|
completedCount += childCounts.getCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepCountResult(totalCount, completedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤统计结果内部类
|
||||||
|
*/
|
||||||
|
@lombok.Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
private static class StepCountResult {
|
||||||
|
private int total;
|
||||||
|
private int completed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user