first commit

This commit is contained in:
2025-08-19 10:19:29 +08:00
commit 3b61e84a7f
3014 changed files with 2640574 additions and 0 deletions

View File

@ -0,0 +1,308 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="模板名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入模板名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['cory:contactformtemplate:add']">新增</el-button>
</el-col>
<!-- <el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['cory:contactformtemplate:edit']"
>修改</el-button
>
</el-col> -->
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['cory:contactformtemplate:remove']"
>删除</el-button
>
</el-col>
<!-- <el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['cory:contactformtemplate:export']">导出</el-button>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="contactformtemplateList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="ID" align="center" type="index" />
<el-table-column label="模板名称" align="center" prop="name" />
<el-table-column label="缩略图" align="center" prop="thumbnailUrl" width="100">
<template #default="scope">
<image-preview :src="scope.row.thumbnail" :width="50" :height="50" />
</template>
</el-table-column>
<!-- <el-table-column label="模板路径" align="center" prop="path">
<template #default="scope">
<span class="text-blue cursor-pointer" @click="openDoc(scope.row)">{{ scope.row.path }}</span></template
>
</el-table-column> -->
<!-- <el-table-column label="备注" align="center" prop="remark" /> -->
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<!-- <el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['cory:contactformtemplate:edit']"></el-button>
</el-tooltip> -->
<el-tooltip content="查看" placement="top">
<el-button link type="primary" icon="View" @click="openDoc(scope.row)" v-hasPermi="['cory:contactformtemplate:edit']"></el-button>
</el-tooltip>
<el-tooltip content="下载" placement="top">
<el-button
link
type="primary"
icon="Download"
@click="handleDownload(scope.row)"
v-hasPermi="['cory:contactformtemplate:edit']"
></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
link
type="primary"
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['cory:contactformtemplate:remove']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改联系单模板对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body destroy-on-close>
<el-form ref="contactformtemplateFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="模板名称" prop="name">
<el-input v-model="form.name" placeholder="请输入模板名称" />
</el-form-item>
<el-form-item label="模板路径" prop="file">
<file-upload
v-model="form.file"
:fileType="['doc', 'docx']"
:autoUpload="false"
ref="fileUploadRef"
:data="{ name: form.name, projectId: currentProject.id }"
uploadUrl="/cory/contactformtemplate"
:limit="1"
:onUploadSuccess="handleUploadSuccess"
showFileList
/>
</el-form-item>
<!-- <el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item> -->
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
<documentDetailVue ref="documentDetailRef" v-if="showDocumentDetail" @onClose="showDocumentDetail = false"></documentDetailVue>
</div>
</template>
<script setup name="Contactformtemplate" lang="ts">
import {
listContactformtemplate,
getContactformtemplate,
delContactformtemplate,
addContactformtemplate,
updateContactformtemplate
} from '@/api/cory/contactformtemplate';
import { ContactformtemplateVO, ContactformtemplateQuery, ContactformtemplateForm } from '@/api/cory/contactformtemplate/types';
import { useUserStoreHook } from '@/store/modules/user';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import documentDetailVue from '@/views/safety/knowledgeDocument/component/documentsDeails.vue';
const contactformtemplateList = ref<ContactformtemplateVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const fileUploadRef = ref();
const queryFormRef = ref<ElFormInstance>();
const contactformtemplateFormRef = ref<ElFormInstance>();
const showDocumentDetail = ref(false);
const documentDetailRef = ref();
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const uploadData = reactive({
bo: {
name: '12'
}
});
const initFormData: ContactformtemplateForm = {
id: undefined,
name: undefined,
file: undefined,
remark: undefined
};
const data = reactive<PageData<ContactformtemplateForm, ContactformtemplateQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
name: undefined,
path: undefined,
thumbnail: undefined,
params: {}
},
rules: {
id: [{ required: true, message: '自增ID不能为空', trigger: 'blur' }],
name: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询联系单模板列表 */
const getList = async () => {
loading.value = true;
const res = await listContactformtemplate(queryParams.value);
contactformtemplateList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
contactformtemplateFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: ContactformtemplateVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加联系单模板';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: ContactformtemplateVO) => {
reset();
const _id = row?.id || ids.value[0];
const res = await getContactformtemplate(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改联系单模板';
};
const handleDownload = async (row) => {
window.open(row.path);
};
const openDoc = (row) => {
showDocumentDetail.value = true;
nextTick(() => {
let data = {
id: row.id,
fileSuffix: 'doc',
fileName: row.name,
fileUrl: row.path
};
documentDetailRef.value.openDialog(data);
});
};
/** 提交按钮 */
const submitForm = () => {
contactformtemplateFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
// if (form.value.id) {
// await updateContactformtemplate(form.value).finally(() => (buttonLoading.value = false));
// } else {
// await addContactformtemplate(form.value).finally(() => (buttonLoading.value = false));
// }
uploadData.bo.name = form.value.name;
fileUploadRef.value!.submitUpload();
}
});
};
const handleUploadSuccess = () => {
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
buttonLoading.value = false;
getList();
};
/** 删除按钮操作 */
const handleDelete = async (row?: ContactformtemplateVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除联系单模板编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
await delContactformtemplate(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'cory/contactformtemplate/export',
{
...queryParams.value
},
`contactformtemplate_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});
</script>

View File

@ -0,0 +1,263 @@
<template>
<div class="content-box">
<el-table :data="data" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column align="center" prop="projectName" label="工程名称" />
<el-table-column align="center" prop="serialNumber" label="编号" />
<el-table-column align="center" prop="to" label="致" />
<el-table-column align="center" prop="subject" label="主题" />
<el-table-column align="center" prop="content" label="内容">
<template #default="{ row }">
<div style="white-space: pre-line">{{ row.content }}</div>
</template>
</el-table-column>
<!-- <el-table-column align="center" label="附件">
<template #default="{ row }">
<el-link type="primary" :underline="false" @click="openFile(row.attachments)" target="_blank">查看附件</el-link>
</template>
</el-table-column> -->
<!-- 分段标题施工项目部 -->
<el-table-column label="施工项目部" align="center">
<el-table-column align="center" prop="contractorLeader" label="项目负责人" />
<el-table-column align="center" prop="contractorDate" label="日期">
<template #default="{ row }">
{{ dayjs(row.contractorDate).format('YYYY-MM-DD') }}
</template>
</el-table-column>
</el-table-column>
<!-- 分段标题项目监理机构 -->
<el-table-column label="项目监理机构" align="center">
<el-table-column align="center" prop="supervisorLeader" label="总监理工程师" />
<el-table-column align="center" prop="supervisorDate" label="日期">
<template #default="{ row }">
{{ dayjs(row.supervisorDate).format('YYYY-MM-DD') }}
</template>
</el-table-column>
</el-table-column>
<!-- 分段标题建设单位 -->
<el-table-column label="建设单位" align="center">
<el-table-column align="center" prop="ownerRep" label="业主代表" />
<el-table-column align="center" prop="ownerDate" label="日期">
<template #default="{ row }">
{{ dayjs(row.ownerDate).format('YYYY-MM-DD') }}
</template>
</el-table-column>
</el-table-column>
<el-table-column align="center" prop="content" label="操作" width="160">
<template #default="scope">
<el-button link type="success" icon="View" @click="handleDetail(scope.row)" class="ml-3"> 详情 </el-button>
<!-- <el-button link type="primary" icon="Download" @click="handleDownload()"> 下载 </el-button> -->
<!-- <el-button link type="warning" icon="Edit" @click="handleDelete(scope.row)"> 修改 </el-button> -->
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
<!-- 详情 -->
<el-dialog title="外部联系单详情" v-model="detailVisible" width="1000">
<div class="w[850px] ma word-export-wrapper" ref="exportRef">
<div class="w80% ma">
<h2 style="text-align: center; margin-top: 5px; font-weight: bold">外部联系单</h2>
<el-row>
<el-col :span="12" style="text-align: left">工程名称{{ tableDetail.projectName }}</el-col>
<el-col :span="12" style="text-align: right">编号{{ tableDetail.serialNumber }}</el-col>
</el-row>
<el-descriptions :column="2" border style="margin-top: 8px" label-width="160px" size="large">
<el-descriptions-item label-align="center" label="致:" :span="2" class-name="zebra"> {{ tableDetail.to }}</el-descriptions-item>
<el-descriptions-item label-align="center" label="主题" :span="2" label-class-name="white">
{{ tableDetail.subject }}</el-descriptions-item
>
<el-descriptions-item label-align="center" label="内容" :span="2" class-name="zebra">
{{ tableDetail.content }}
</el-descriptions-item>
<el-descriptions-item label-align="center" label="附件" :span="2" label-class-name="white">
<el-link type="primary" :underline="false" :href="tableDetail.url" target="_blank">{{ tableDetail.originalName }}</el-link>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border direction="vertical" size="large">
<el-descriptions-item label-align="center" label="施工项目部" class-name="none"></el-descriptions-item>
</el-descriptions>
<el-descriptions :column="2" border label-width="160px" size="large">
<el-descriptions-item label-align="center" label="项目负责人" label-class-name="white">
{{ tableDetail.contractorLeader }}</el-descriptions-item
>
<el-descriptions-item label-align="center" label="日期" label-class-name="white">
{{ dayjs(tableDetail.contractorDate).format('YYYY-MM-DD') }}</el-descriptions-item
>
</el-descriptions>
<el-descriptions border direction="vertical" size="large">
<el-descriptions-item label-align="center" label="项目监理机构" class-name="none"> </el-descriptions-item>
</el-descriptions>
<el-descriptions :column="2" border label-width="160px" size="large">
<el-descriptions-item label-align="center" label="项目负责人" label-class-name="white">
{{ tableDetail.supervisorLeader }}</el-descriptions-item
>
<el-descriptions-item label-align="center" label="日期" label-class-name="white">
{{ dayjs(tableDetail.supervisorDate).format('YYYY-MM-DD') }}</el-descriptions-item
>
</el-descriptions>
<el-descriptions border direction="vertical" size="large">
<el-descriptions-item label-align="center" label="建设单位" class-name="none"></el-descriptions-item>
</el-descriptions>
<el-descriptions :column="2" border label-width="160px" size="large">
<el-descriptions-item label-align="center" label="项目负责人" label-class-name="white"> {{ tableDetail.ownerRep }}</el-descriptions-item>
<el-descriptions-item label-align="center" label="日期" label-class-name="white">
{{ dayjs(tableDetail.ownerDate).format('YYYY-MM-DD') }}</el-descriptions-item
>
</el-descriptions>
</div>
<div class="dialog-footer">
<div class="btn-item" @click="handleDownload">
<img src="@/assets/icons/svg/derived.png" />
<span>下载</span>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { listByIds } from '@/api/system/oss';
import { saveAs } from 'file-saver';
import { dayjs } from 'element-plus';
const exportRef = ref<HTMLElement>();
const emit = defineEmits(['selection-change', 'delete']);
const props = defineProps({
data: {
type: Array,
default: () => []
}
});
const form = reactive({
id: '1',
projectName: '',
serialNumber: '',
to: '',
subject: '',
content: '',
attachments: '', // 或 URL
contractorLeader: '',
contractorDate: '',
supervisorLeader: '',
supervisorDate: '',
ownerRep: '',
ownerDate: '',
pageNum: 1,
pageSize: 10
});
const tableDetail = ref<any>({});
const total = ref(0);
const detailVisible = ref(false);
/** 多选框选中数据 */
const handleSelectionChange = (selection: any) => {
emit('selection-change', selection);
};
const handleDetail = async (row) => {
const res = await listByIds(row.id);
tableDetail.value = {
...row,
...res.data[0]
};
detailVisible.value = true;
};
const handleDownload = () => {
const style = `
<style>
.white { background: #fff !important; }
.none { display: none !important; }
.zebra { background: #f5f7fa !important; }
.el-descriptions__table { table-layout: fixed !important; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #333; padding: 6px; font-size: 14px; }
</style>
`;
const el = exportRef.value;
if (!el) return;
// 拷贝 DOM避免修改原内容
const clone = el.cloneNode(true) as HTMLElement;
applyInlineTableStyles(clone);
// 包裹 HTML 内容
const html = `
<html>
<head>
<meta charset="utf-8">
${style}
</head>
<body>
${clone.innerHTML}
</body>
</html>
`;
const blob = (window as any).htmlDocx.asBlob(html);
saveAs(blob, '外部联系单.docx');
};
const applyInlineTableStyles = (rootEl: HTMLElement) => {
rootEl.querySelectorAll('table').forEach((table) => {
table.setAttribute('style', 'width:100%; border-collapse:collapse; table-layout:fixed; border:1px solid #333;');
});
rootEl.querySelectorAll('th, td').forEach((cell) => {
cell.setAttribute('style', 'border:1px solid #333; padding:6px; font-size:14px; word-break:break-all;');
});
};
const handleDelete = (row) => {
emit('delete', row.id);
};
const openFile = async (url: string) => {
const res = await listByIds(url);
window.open(res.data[0].url);
};
</script>
<style lang="scss" scoped>
:deep(.white) {
background: #fff !important;
}
:deep(.none) {
display: none !important;
}
:deep(.zebra) {
background: #f5f7fa;
}
.dialog-footer {
height: 100px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: absolute;
top: 14%;
right: 6%;
background: #fff;
box-shadow: 0 0 10px #ddd;
text-align: center;
padding: 20px 10px;
.btn-item {
display: flex;
flex-direction: column;
justify-content: center;
cursor: pointer;
}
}
</style>

View File

@ -0,0 +1,396 @@
<template>
<div class="content-box">
<el-table :data="data" style="width: 100%" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column align="center" prop="projectName" label="工程名称" />
<el-table-column align="center" prop="submitUnit" label="提出单位" />
<el-table-column align="center" prop="specialty" label="专业">
<template #default="{ row }">
<dict-tag :options="des_user_major" :value="row.specialty" />
</template>
</el-table-column>
<el-table-column align="center" prop="submitDate" label="提出日期">
<template #default="{ row }">
{{ formatDate(row.submitDate) }}
</template>
</el-table-column>
<el-table-column align="center" prop="volumeName" label="卷册名称" />
<el-table-column align="center" prop="volumeNumber" label="卷册号" />
<el-table-column align="center" prop="content" label="变更内容" />
<el-table-column align="center" prop="costEstimation" label="变更费用估算" />
<el-table-column label="流程状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="wf_business_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column align="center" prop="content" label="操作" width="240">
<template #default="scope">
<el-button link type="warning" v-if="scope.row.status === 'draft'" icon="Edit" @click="handleUpdate(scope.row)" class="ml-3"
>审批
</el-button>
<el-button link type="primary" icon="View" @click="handleViewInfo(scope.row)" class="ml-3"> 查看流程 </el-button>
<el-button link type="success" icon="View" @click="handleDetail(scope.row)" class="ml-3"> 详情 </el-button>
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
<!-- 详情 -->
<el-dialog title="变更单详情" v-model="detailVisible" width="1000">
<div class="w[850px] ma word-export-wrapper" ref="exportRef">
<div class="w80% ma">
<h2 style="text-align: center; margin-top: 5px; font-weight: bold">变更单</h2>
<el-row>
<el-col :span="12">编号{{ tableDetail.id }}</el-col>
</el-row>
<el-descriptions :column="2" border style="margin-top: 8px" label-width="160px" size="large">
<el-descriptions-item label-align="center" label="工程名称" class-name="zebra"> {{ tableDetail.projectName }} </el-descriptions-item>
<el-descriptions-item label-align="center" label="提出单位" class-name="zebra"> {{ tableDetail.submitUnit }} </el-descriptions-item>
<el-descriptions-item label-align="center" label="专业" label-class-name="white"
><dict-tag :options="des_user_major" :value="tableDetail.specialty" />
</el-descriptions-item>
<el-descriptions-item label-align="center" label="提出日期" label-class-name="white">
{{ dayjs(tableDetail.submitDate).format('YYYY-MM-DD') }}
</el-descriptions-item>
<el-descriptions-item label-align="center" label="卷册名称" class-name="zebra"> {{ tableDetail.volumeName }} </el-descriptions-item>
<el-descriptions-item label-align="center" label="卷册号" class-name="zebra"> {{ tableDetail.volumeNumber }} </el-descriptions-item>
<el-descriptions-item label-align="center" label="附图" :span="2" label-class-name="white">
<img :src="item.url" v-for="item in tableDetail.attachmentsImgList" alt="" style="width: 200px; height: auto" />
</el-descriptions-item>
<el-descriptions-item label-align="center" label="变更原因" :span="2" class-name="zebra">
<el-checkbox-group v-model="tableDetail.changeReasons">
<el-checkbox
v-for="(item, index) in radioList"
:class="index % 2 == 0 ? 'w45%' : ''"
:label="item.label"
:value="item.label"
disabled
/>
</el-checkbox-group>
</el-descriptions-item>
<el-descriptions-item label-align="center" label="内容" :span="2" label-class-name="white">
{{ tableDetail.content }}
</el-descriptions-item>
<el-descriptions-item label-align="center" label="附件" :span="2" label-class-name="white">
<el-link type="primary" :underline="false" :href="tableDetail.attachmentsList?.url" target="_blank">{{
tableDetail.attachmentsList?.originalName
}}</el-link>
</el-descriptions-item>
<el-descriptions-item label-align="center" label="变更费用估算" :span="2" class-name="zebra">
{{ tableDetail.costEstimation }}
</el-descriptions-item>
</el-descriptions>
<el-descriptions border direction="vertical" size="large">
<el-descriptions-item label-align="center" label="施工承包单位" class-name="none"></el-descriptions-item>
</el-descriptions>
<el-descriptions :column="2" border label-width="160px" size="large">
<el-descriptions-item label-align="center" label="项目经理 " label-class-name="white">
{{ tableDetail.contractorLeader }}
</el-descriptions-item>
<el-descriptions-item label-align="center" label="日期" label-class-name="white">
{{ dayjs(tableDetail.contractorDate).format('YYYY-MM-DD') }}
</el-descriptions-item>
</el-descriptions>
<!-- <el-descriptions border direction="vertical" size="large">
<el-descriptions-item label-align="center" label="总承包单位" class-name="none"></el-descriptions-item>
</el-descriptions>
<el-descriptions :column="2" border label-width="160px" size="large">
<el-descriptions-item label-align="center" label="项目技术负责人" label-class-name="white">{{
tableDetail.bsupervisorLeader
}}</el-descriptions-item>
<el-descriptions-item label-align="center" label="日期" label-class-name="white">
{{ dayjs(tableDetail.bsupervisorDate).format('YYYY-MM-DD') }}
</el-descriptions-item>
</el-descriptions>
<el-descriptions border direction="vertical" size="large">
<el-descriptions-item label-align="center" label="设计单位" class-name="none"></el-descriptions-item>
</el-descriptions>
<el-descriptions :column="2" border label-width="160px" size="large">
<el-descriptions-item label-align="center" label="设计代表" label-class-name="white">{{
tableDetail.csupervisorLeader
}}</el-descriptions-item>
<el-descriptions-item label-align="center" label="日期" label-class-name="white">
{{ dayjs(tableDetail.csupervisorDate).format('YYYY-MM-DD') }}
</el-descriptions-item>
</el-descriptions> -->
<el-descriptions border direction="vertical" size="large">
<el-descriptions-item label-align="center" label="项目监理单位" class-name="none"></el-descriptions-item>
</el-descriptions>
<!-- 单独插入整行占用的内容 -->
<el-descriptions :column="2" border label-width="160px" size="large">
<el-descriptions-item label="总监理工程师" label-align="center" label-class-name="white">
{{ tableDetail.supervisorLeader }}
</el-descriptions-item>
<el-descriptions-item label="日期" label-align="center" label-class-name="white">
{{ dayjs(tableDetail.supervisorDate).format('YYYY-MM-DD') }}
</el-descriptions-item>
</el-descriptions>
<!-- 一组完整两列 -->
<!-- <el-descriptions :column="2" border label-width="160px" size="large">
<el-descriptions-item label="监理工程师" label-align="center" label-class-name="white">
{{ tableDetail.dsupervisorLeader }}
</el-descriptions-item>
<el-descriptions-item label="日期" label-align="center" label-class-name="white">
{{ dayjs(tableDetail.supervisorDate).format('YYYY-MM-DD') }}
</el-descriptions-item>
</el-descriptions> -->
<el-descriptions border direction="vertical" size="large">
<el-descriptions-item label-align="center" label="建设单位" class-name="none"></el-descriptions-item>
</el-descriptions>
<el-descriptions :column="2" border label-width="160px" size="large">
<!-- <el-descriptions-item label-align="center" label="会签" :span="2" label-class-name="white"> {{ tableDetail.esupervisorLeader }} </el-descriptions-item> -->
<el-descriptions-item label-align="center" label="负责人" label-class-name="white">
{{ tableDetail.ownerRep }}
</el-descriptions-item>
<el-descriptions-item label-align="center" label="日期" label-class-name="white">
{{ dayjs(tableDetail.ownerDate).format('YYYY-MM-DD') }}
</el-descriptions-item>
</el-descriptions>
</div>
<div class="dialog-footer">
<div class="btn-item" @click="handleDownload">
<img src="@/assets/icons/svg/derived.png" />
<span>下载</span>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { listByIds } from '@/api/system/oss';
import { dayjs } from 'element-plus';
import { saveAs } from 'file-saver';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
const props = defineProps({
data: {
type: Array,
default: () => []
},
thumbnail: {
type: String,
default: ''
}
});
const tableDetail = ref<any>({ attachmentsImgList: [], attachmentsList: [] });
const exportRef = ref<HTMLElement>();
const radioList = ref([
{ value: 0, label: '设计漏项' },
{ value: 1, label: '设计改进' },
{ value: 2, label: '设计差错' },
{ value: 3, label: '接口差错' },
{ value: 4, label: '业主要求' },
{ value: 5, label: '施工承包商要求' },
{ value: 6, label: '外部资料与最终情况不符' },
{ value: 7, label: '材料代用及其他' }
]);
const detailVisible = ref(false);
const formatDate = (val: string | Date) => (val ? dayjs(val).format('YYYY-MM-DD') : '');
const { des_user_major } = toRefs<any>(proxy?.useDict('des_user_major'));
const handleDetail = async (row) => {
tableDetail.value = { ...row };
if (row.attachmentsImg) {
const res = await listByIds(row.attachmentsImg);
tableDetail.value.attachmentsImgList = res.data;
}
if (row.attachments) {
const res = await listByIds(row.attachments);
tableDetail.value.attachmentsList = res.data[0];
}
// tableDetail.value = {
// ...row,
// hasAttachmentList: res.data
// };
detailVisible.value = true;
};
/** 多选框选中数据 */
const emit = defineEmits(['selection-change', 'delete']);
const handleSelectionChange = (selection: any) => {
emit('selection-change', selection);
};
const handleDelete = (row) => {
emit('delete', row.id);
};
const handleUpdate = (row) => {
// 添加审批
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/changeContact/indexEdit`,
query: {
thumbnailUrl: props.thumbnail,
row: JSON.stringify(row),
id: row.id,
type: 'update'
}
});
};
const handleViewInfo = (row) => {
// 添加审批
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/changeContact/indexEdit`,
query: {
thumbnailUrl: props.thumbnail,
row: JSON.stringify(row),
id: row.id,
type: 'view'
}
});
};
const handleDownload = async () => {
const style = `
<style>
.white { background: #fff !important; }
.none { display: none !important; }
.zebra { background: #f5f7fa !important; }
.el-descriptions__table { table-layout: fixed !important; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #333; padding: 6px; font-size: 14px; }
</style>
`;
const el = exportRef.value;
if (!el) return;
// 拷贝 DOM避免影响原内容
const clone = el.cloneNode(true) as HTMLElement;
// 删除 .dialog-footer
const footer = clone.querySelector('.dialog-footer');
if (footer) {
footer.remove();
}
// 工具函数:图片转成指定宽度的 base64
const resizeImageToBase64 = (img: HTMLImageElement, maxWidth = 500) => {
return new Promise<string>((resolve) => {
const image = new Image();
image.crossOrigin = 'anonymous';
image.src = img.src;
image.onload = () => {
const scale = Math.min(1, maxWidth / image.naturalWidth);
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth * scale;
canvas.height = image.naturalHeight * scale;
const ctx = canvas.getContext('2d');
ctx!.drawImage(image, 0, 0, canvas.width, canvas.height);
resolve(canvas.toDataURL('image/png'));
};
image.onerror = () => {
resolve(img.src); // 如果加载失败就用原地址
};
});
};
// 找到所有图片并替换成 base64顺序执行以避免并发问题
const imgs = Array.from(clone.querySelectorAll('img'));
for (let img of imgs) {
const base64 = await resizeImageToBase64(img, 200);
img.src = base64;
}
// 应用表格的内联样式
applyInlineTableStyles(clone);
// 拼接 HTML
const html = `
<html>
<head>
<meta charset="utf-8">
${style}
</head>
<body>
${clone.innerHTML}
</body>
</html>
`;
const blob = (window as any).htmlDocx.asBlob(html);
saveAs(blob, '变更单.docx');
};
const applyInlineTableStyles = (rootEl: HTMLElement) => {
rootEl.querySelectorAll('table').forEach((table) => {
table.setAttribute('style', 'width:100%; border-collapse:collapse; table-layout:fixed; border:1px solid #333;');
});
rootEl.querySelectorAll('th, td').forEach((cell) => {
cell.setAttribute('style', 'border:1px solid #333; padding:6px; font-size:14px; word-break:break-all;');
});
};
</script>
<style lang="scss" scoped>
:deep(.white) {
background: #fff !important;
}
:deep(.none) {
display: none !important;
}
:deep(.zebra) {
background: #f5f7fa;
}
:deep(.el-descriptions__table) {
table-layout: fixed !important;
}
.word-export-wrapper {
table {
width: 100% !important;
border-collapse: collapse;
table-layout: fixed;
word-break: break-all;
}
th,
td {
border: 1px solid #333;
padding: 8px;
font-size: 14px;
text-align: left;
vertical-align: middle;
}
/* 确保不超出 Word 页边距 */
margin: 0 auto;
max-width: 100%;
overflow: hidden;
}
.el-descriptions__label {
font-weight: bold;
background-color: #f2f2f2;
}
.dialog-footer {
height: 100px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: absolute;
top: 14%;
right: 6%;
background: #fff;
box-shadow: 0 0 10px #ddd;
text-align: center;
padding: 20px 10px;
.btn-item {
display: flex;
flex-direction: column;
justify-content: center;
cursor: pointer;
img {
transform: rotateZ(180deg);
}
}
}
</style>

View File

@ -0,0 +1,296 @@
<template>
<div class="p-2">
<el-tabs type="border-card" v-model="activeName" @tab-click="handleClick">
<el-tab-pane label="变更单" name="1"
><el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" :disabled="addSingle" @click="handleAddApp" v-hasPermi="['quality:qualityInspection:add']"
>上传变更单模版</el-button
>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<EngineeringChangeApplicationForm
@selection-change="handleSelectionChange"
:data="tableData"
:thumbnail="projectTypeOptions[1].thumbnail"
@delete="handleDelete"
></EngineeringChangeApplicationForm>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/> </el-card
></el-tab-pane>
<el-tab-pane label="外部联系单" name="0"
><el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" :disabled="addSingle" @click="handleAdd" v-hasPermi="['quality:qualityInspection:add']"
>上传外部联系单模版</el-button
>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<Contactform @selection-change="handleSelectionChange" :data="tableData" @delete="handleDelete"></Contactform>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/> </el-card
></el-tab-pane>
</el-tabs>
<el-dialog title="新增模板" v-model="dialogVisible" width="800">
<el-form :model="form" :rules="rules" ref="formRef" label-width="110px">
<div class="flex">
<div><image-preview :src="thumbnailUrl" width="150px"></image-preview></div>
<div>
<el-form-item label="工程名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入工程名称" />
</el-form-item>
<el-form-item label="编号" prop="serialNumber">
<el-input v-model="form.serialNumber" placeholder="请输入编号" />
</el-form-item>
<el-form-item label="致" prop="to">
<el-input v-model="form.to" placeholder="致:" />
</el-form-item>
<el-form-item label="主题" prop="subject">
<el-input v-model="form.subject" placeholder="请输入主题" />
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input v-model="form.content" type="textarea" :rows="6" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="附件" prop="attachments">
<file-upload v-model="form.attachments" :limit="1" :file-type="['pdf', 'png', 'jpg', 'jpeg', 'gif', 'bmp']"></file-upload>
</el-form-item>
<el-divider class="mb-10! mt-10!">施工项目部</el-divider>
<el-form-item label="项目负责人" prop="contractorLeader">
<el-input v-model="form.contractorLeader" placeholder="请输入负责人姓名" />
</el-form-item>
<el-form-item label="日期" prop="contractorDate">
<el-date-picker v-model="form.contractorDate" type="date" placeholder="选择日期" style="width: 100%" />
</el-form-item>
<el-divider class="mb-10! mt-10!">项目监理机构</el-divider>
<el-form-item label="总监理工程师" prop="supervisorLeader">
<el-input v-model="form.supervisorLeader" placeholder="请输入总监理工程师姓名" />
</el-form-item>
<el-form-item label="日期" prop="supervisorDate">
<el-date-picker v-model="form.supervisorDate" type="date" placeholder="选择日期" style="width: 100%" />
</el-form-item>
<el-divider class="mb-10! mt-10!">建设单位</el-divider>
<el-form-item label="业主代表" prop="ownerRep">
<el-input v-model="form.ownerRep" placeholder="请输入业主代表" />
</el-form-item>
<el-form-item label="日期" prop="ownerDate">
<el-date-picker v-model="form.ownerDate" type="date" placeholder="选择日期" style="width: 100%" />
</el-form-item>
</div>
</div>
</el-form>
<template #footer>
<span>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { useUserStoreHook } from '@/store/modules/user';
import type { FormInstance, FormRules } from 'element-plus';
import Contactform from './components/contactform.vue';
import EngineeringChangeApplicationForm from './components/engineeringChangeApplicationForm.vue';
import { listContactTypeformtemplate } from '@/api/cory/contactformtemplate';
import { addContactnotice, delContactnotice, listContactnotice } from '@/api/cory/contactnotice';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const thumbnailUrl = ref('');
const tableData = ref([]);
const total = ref(0);
const activeName = ref('1');
const formRef = ref<FormInstance>();
const dialogVisible = ref<boolean>(false);
const showSearch = ref(true);
const multiple = ref<boolean>(true);
const addSingle = ref<boolean>(false);
const single = ref<boolean>(true);
const ids = ref<Array<string | number>>([]);
const projectTypeOptions = ref<any>([
{
value: '0',
label: '外部联系单'
},
{
value: '1',
label: '变更单'
},
{
value: '2',
label: '通知单'
},
{
value: '3',
label: '回复单'
},
{
value: '4',
label: '签证单'
}
]);
const initFormData = {
projectType: '',
projectName: '',
serialNumber: '',
to: '',
subject: '',
content: '',
attachments: '',
contractorLeader: '',
contractorDate: '',
supervisorLeader: '',
supervisorDate: '',
ownerRep: '',
ownerDate: '',
unitName: '',
profession: '',
applyDate: '',
bookName: '',
bookNo: '',
hasAttachment: '',
changeReasons: [],
changeContent: '',
costEstimate: ''
};
const data = reactive<PageData<any, any>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
inspectionType: undefined,
inspectionStatus: undefined,
teamId: undefined,
type: undefined,
params: {},
// ids: ['1942107830848872449', '1942107931415699457'],
projectType: undefined
},
rules: {
projectName: [{ required: true, message: '请输入工程名称', trigger: 'blur' }],
projectType: [{ required: true, message: '请选择模板类型', trigger: 'blur' }],
serialNumber: [{ required: true, message: '请输入编号', trigger: 'blur' }],
to: [{ required: true, message: '请输入接收方', trigger: 'blur' }],
subject: [{ required: true, message: '请输入主题', trigger: 'blur' }],
content: [{ required: true, message: '请输入内容', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
const submitForm = () => {
formRef.value?.validate(async (valid) => {
if (valid) {
let data = {
type: queryParams.value.type,
projectId: currentProject.value?.id,
detail: JSON.stringify(form.value)
};
console.log('提交表单:', form);
const res = await addContactnotice(data);
if (res.code == 200) {
proxy.$modal.msgSuccess('添加成功');
dialogVisible.value = false;
formRef.value.resetFields();
getList();
}
}
});
};
const handleAdd = () => {
dialogVisible.value = true;
};
const getList = async () => {
if (!queryParams.value.projectType) {
const res = await listContactTypeformtemplate(queryParams.value);
projectTypeOptions.value = res.data;
queryParams.value.projectType = res.data[1].name;
thumbnailUrl.value = res.data[1].thumbnail;
queryParams.value.type = res.data[1].id as string;
}
const res = await listContactnotice(queryParams.value);
res.rows = res.rows.map((item) => {
return {
...item,
...JSON.parse(item.detail),
status: item.status
};
});
tableData.value = res.rows;
console.log('🚀 ~ getList ~ tableData.value:', tableData.value);
total.value = res.total || 0;
};
const handleDelete = async (id?: string) => {
const _ids = id || ids.value;
await proxy?.$modal.confirm('是否确认删除识别记录编号为"' + _ids + '"的数据项?').finally();
const res = await delContactnotice(_ids);
if (res.code == 200) {
proxy.$modal.msgSuccess('删除成功');
getList();
}
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: any) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
const selectType = (value: string) => {
queryParams.value.projectType = value;
thumbnailUrl.value = projectTypeOptions.value.filter((item) => item.name == value)[0].thumbnail;
queryParams.value.type = projectTypeOptions.value.filter((item) => item.name == value)[0].id;
getList();
};
const handleClick = (val) => {
console.log(val);
queryParams.value.projectType = val.props.name;
getList();
};
const handleAddApp = (row) => {
// 添加审批
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/changeContact/indexEdit`,
query: {
thumbnailUrl: projectTypeOptions.value[1].thumbnail,
id: projectTypeOptions.value[1].id,
row,
type: 'add'
}
});
};
onMounted(() => {
getList();
});
</script>

View File

@ -0,0 +1,515 @@
<template>
<div class="p-4 bg-gray-50">
<div class="max-w-4xl mx-auto">
<!-- 顶部按钮区域 -->
<el-card class="mb-4 rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.id"
:status="form.status"
:pageType="routeParams.type"
/>
</el-card>
<!-- 表单区域 -->
<el-card class="rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md overflow-hidden">
<div class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-800">变更联系单</h3>
</div>
<div class="p-6">
<el-form
ref="leaveFormRef"
v-loading="loading"
:disabled="routeParams.type === 'view' || form.status == 'waiting'"
:model="form"
:rules="rules"
label-width="100px"
class="space-y-4"
>
<div class="grid grid-cols-1 gap-4">
<div class="flex">
<div v-if="thumbnailUrl"><image-preview :src="thumbnailUrl" width="150px"></image-preview></div>
<div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="工程名称">
<el-input v-model="form.projectName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="提出单位">
<el-input v-model="form.submitUnit" />
</el-form-item>
</el-col>
</el-row>
<!-- 专业 & 提出日期 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="专业">
<el-select v-model="form.specialty" placeholder="">
<el-option v-for="item in des_user_major" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="提出日期">
<el-date-picker v-model="form.submitDate" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
</el-form-item>
</el-col>
</el-row>
<!-- 卷册名称 & 附图 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="卷册名称">
<el-input v-model="form.volumeName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="附图">
<file-Upload v-model="form.attachmentsImg" :file-type="['pdf', 'png', 'jpg', 'jpeg', 'gif', 'bmp']">
<el-button type="primary">上传附件</el-button>
</file-Upload>
</el-form-item>
</el-col>
</el-row>
<!-- 卷册号 -->
<el-form-item label="卷册号">
<el-input v-model="form.volumeNumber" />
</el-form-item>
<!-- 变更原因 -->
<el-form-item label="变更原因">
<el-checkbox-group v-model="form.changeReasons">
<el-checkbox label="设计漏项" />
<el-checkbox label="设计改进" />
<el-checkbox label="设计差错" />
<el-checkbox label="接口差错" />
<el-checkbox label="业主要求" />
<el-checkbox label="施工承包商要求" />
<el-checkbox label="外部资料与最终情况不符" />
<el-checkbox label="材料代用及其他" />
</el-checkbox-group>
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input v-model="form.content" type="textarea" :rows="6" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="附件" prop="attachments">
<file-upload v-model="form.attachments" :limit="1" :file-type="['pdf']"></file-upload>
</el-form-item>
<el-form-item label="变更费用估算" prop="costEstimation">
<el-input v-model="form.costEstimation" :rows="6" placeholder="请输入变更费用估算" />
</el-form-item>
<el-divider class="mb-10! mt-10!">施工项目部</el-divider>
<el-form-item label="项目负责人" prop="contractorLeader">
<el-input v-model="form.contractorLeader" placeholder="请输入负责人姓名" />
</el-form-item>
<el-form-item label="日期" prop="contractorDate">
<el-date-picker v-model="form.contractorDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
<el-divider class="mb-10! mt-10!">项目监理机构</el-divider>
<el-form-item label="总监理工程师" prop="supervisorLeader">
<el-input v-model="form.supervisorLeader" placeholder="请输入总监理工程师姓名" />
</el-form-item>
<el-form-item label="日期" prop="supervisorDate">
<el-date-picker v-model="form.supervisorDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
<el-divider class="mb-10! mt-10!">建设单位</el-divider>
<el-form-item label="业主代表" prop="ownerRep">
<el-input v-model="form.ownerRep" placeholder="请输入业主代表" />
</el-form-item>
<el-form-item label="日期" prop="ownerDate">
<el-date-picker v-model="form.ownerDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
</div>
</div>
</div>
</el-form>
</div>
</el-card>
<!-- 提交组件 -->
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<approvalRecord ref="approvalRecordRef"></approvalRecord>
<!-- 流程选择对话框 -->
<el-dialog
draggable
v-model="dialogVisible.visible"
:title="dialogVisible.title"
:before-close="handleClose"
width="500"
class="rounded-lg shadow-lg"
>
<div class="p-4">
<p class="text-gray-600 mb-4">请选择要启动的流程</p>
<el-select v-model="flowCode" placeholder="请选择流程" style="width: 100%">
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<template #footer>
<div class="dialog-footer p-4 border-t border-gray-100 flex justify-end space-x-3">
<el-button @click="handleClose" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
>取消</el-button
>
<el-button type="primary" @click="submitFlow()" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
>确认</el-button
>
</div>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup name="Leave" lang="ts">
import { LeaveForm, LeaveQuery, LeaveVO } from '@/api/workflow/leave/types';
import { startWorkFlow } from '@/api/workflow/task';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { useUserStoreHook } from '@/store/modules/user';
import { listByIds } from '@/api/system/oss';
import { addContactnotice, getContactnotice, updateContactnotice } from '@/api/cory/contactnotice';
const { des_user_major } = toRefs(proxy?.useDict('des_user_major'));
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const buttonLoading = ref(false);
const loading = ref(true);
const thumbnailUrl = ref('');
//路由参数
const routeParams = ref<Record<string, any>>({});
const flowCodeOptions = [
{
value: currentProject.value?.id + '_changecontact',
label: '变更联系单审批'
}
];
const flowCode = ref<string>('');
const status = ref<string>('');
const dialogVisible = reactive<DialogOption>({
visible: false,
title: '流程定义'
});
//提交组件
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
//审批记录组件
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
const leaveFormRef = ref<ElFormInstance>();
const dialog = reactive({
visible: false,
title: '',
isEdit: false
});
const submitFormData = ref<StartProcessBo>({
businessId: '',
flowCode: '',
variables: {}
});
const taskVariables = ref<Record<string, any>>({});
const initFormData = {
id: undefined,
projectId: currentProject.value?.id,
projectType: '',
projectName: '',
serialNumber: '',
to: '',
subject: '',
costEstimation: '',
content: '',
attachments: '',
contractorLeader: '',
contractorDate: '',
supervisorLeader: '',
supervisorDate: '',
ownerRep: '',
ownerDate: '',
unitName: '',
profession: '',
applyDate: '',
bookName: '',
bookNo: '',
hasAttachment: '',
changeReasons: [],
changeContent: '',
costEstimate: '',
status: ''
};
const data = reactive({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
fileName: undefined,
fileType: undefined,
fileSuffix: undefined,
fileStatus: undefined,
originalName: undefined,
newest: undefined,
params: {}
},
rules: {
projectName: [{ required: true, message: '请输入工程名称', trigger: 'blur' }],
projectType: [{ required: true, message: '请选择模板类型', trigger: 'blur' }],
serialNumber: [{ required: true, message: '请输入编号', trigger: 'blur' }],
to: [{ required: true, message: '请输入接收方', trigger: 'blur' }],
subject: [{ required: true, message: '请输入主题', trigger: 'blur' }],
content: [{ required: true, message: '请输入内容', trigger: 'blur' }]
}
});
const handleClose = () => {
dialogVisible.visible = false;
flowCode.value = '';
buttonLoading.value = false;
};
const { form, rules } = toRefs(data);
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
leaveFormRef.value?.resetFields();
};
/** 获取详情 */
const getInfo = () => {
loading.value = true;
buttonLoading.value = false;
nextTick(async () => {
const res = await getContactnotice(routeParams.value.id);
var data = {
// ...JSON.parse(routeParams.value.row),
...JSON.parse(res.data.detail),
...res.data
};
console.log(routeParams.value);
Object.assign(form.value, data);
loading.value = false;
buttonLoading.value = false;
});
};
/** 提交按钮 */
const submitForm = (status1: string) => {
status.value = status1;
leaveFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
let data = {
detail: JSON.stringify(form.value),
type: routeParams.value.id,
projectId: currentProject.value?.id
};
var res;
if (form.value.id) {
res = await updateContactnotice(data).finally(() => (buttonLoading.value = false));
Object.assign(form.value, res.data);
} else {
res = await addContactnotice(data).finally(() => (buttonLoading.value = false));
Object.assign(form.value, res.data);
}
if (res.code == 200) {
dialog.visible = false;
submit(status.value, form.value);
}
}
});
};
const submitFlow = async () => {
console.log('form.value', form.value);
handleStartWorkFlow(form.value);
dialogVisible.visible = false;
};
//提交申请
const handleStartWorkFlow = async (data: LeaveForm) => {
console.log('data', data);
try {
submitFormData.value.flowCode = flowCode.value;
submitFormData.value.businessId = data.id;
//流程变量
taskVariables.value = {
// leave4/5 使用的流程变量
userList: ['1', '3', '4']
};
submitFormData.value.variables = taskVariables.value;
const resp = await startWorkFlow(submitFormData.value);
if (submitVerifyRef.value) {
buttonLoading.value = false;
submitVerifyRef.value.openDialog(resp.data.taskId);
}
} finally {
buttonLoading.value = false;
}
};
//审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value.init(form.value.id);
};
//提交回调
const submitCallback = async () => {
await proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
};
//审批
const approvalVerifyOpen = async () => {
taskVariables.value = {
// leave4/5 使用的流程变量
costEstimation: form.value.costEstimation
};
submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
// 图纸上传成功之后 开始提交
const submit = async (status, data) => {
form.value = data;
if (status === 'draft') {
buttonLoading.value = false;
proxy?.$modal.msgSuccess('暂存成功');
proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
} else {
if ((form.value.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
flowCode.value = flowCodeOptions[0].value;
dialogVisible.visible = true;
return;
}
//说明启动过先随意穿个参数
if (flowCode.value === '' || flowCode.value === null) {
flowCode.value = 'xx';
}
console.log('data', data);
await handleStartWorkFlow(data);
}
};
onMounted(() => {
nextTick(async () => {
routeParams.value = proxy.$route.query;
thumbnailUrl.value = proxy.$route.query.thumbnailUrl;
reset();
loading.value = false;
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
getInfo();
}
});
});
</script>
<style scoped lang="scss">
/* 全局样式 */
:root {
--primary: #409eff;
--primary-light: #66b1ff;
--primary-dark: #3a8ee6;
--success: #67c23a;
--warning: #e6a23c;
--danger: #f56c6c;
--info: #909399;
}
/* 表单样式优化 */
.el-form-item {
.el-form-item__label {
color: #606266;
font-weight: 500;
}
.el-input__inner,
.el-select .el-input__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
.el-textarea__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
}
/* 按钮样式优化 */
.el-button {
border-radius: 4px;
transition: all 0.2s;
&.is-primary {
background-color: var(--primary);
border-color: var(--primary);
&:hover {
background-color: var(--primary-light);
border-color: var(--primary-light);
}
&:active {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
}
&.is-text {
color: var(--primary);
&:hover {
color: var(--primary-light);
background-color: rgba(64, 158, 255, 0.05);
}
}
}
/* 卡片样式优化 */
.el-card {
transition: all 0.3s ease;
&:hover {
/* transform: translateY(-2px); */
}
}
/* 对话框样式优化 */
.el-dialog {
.el-dialog__header {
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
padding: 15px 20px;
}
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-dialog__footer {
padding: 15px 20px;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@ -0,0 +1,255 @@
<template>
<div class="content-box">
<el-table v-loading="loading" :data="data" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="处理状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="safety_inspection_type" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="检查人" align="center" prop="correctorName" />
<el-table-column label="检查时间" align="center" prop="rectificationDeadline" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.rectificationDeadline, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="检查类型" align="center" prop="checkType">
<template #default="scope">
<dict-tag :options="safety_inspection_check_type" :value="scope.row.checkType" />
</template>
</el-table-column>
<el-table-column label="违章类型" align="center" prop="violationType">
<template #default="scope">
<dict-tag :options="safety_inspection_violation_type" :value="scope.row.violationType" />
</template>
</el-table-column>
<el-table-column label="巡检结果" align="center" prop="inspectionResult">
<template #default="scope">
<el-tooltip placement="top" effect="dark">
<template #content>
<div class="max-w-670px">{{ scope.row.inspectionResult }}</div>
</template>
<el-text truncated>
{{ scope.row.inspectionResult }}
</el-text>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="整改人" align="center" prop="correctorName" />
<el-table-column label="复查状态" align="center" prop="reviewType">
<template #default="scope">
<dict-tag :options="review_type" :value="scope.row.reviewType" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
<template #default="scope">
<el-space>
<el-button link type="primary" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['safety:safetyInspection:query']">
详情
</el-button>
<!-- <el-button link type="success" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['safety:safetyInspection:edit']">修改 </el-button> -->
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['safety:safetyInspection:remove']">
删除
</el-button>
</el-space>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="form.pageNum" v-model:limit="form.pageSize" @pagination="getList" />
<!-- 详情 -->
<el-dialog title="联系单详情" v-model="detailVisible" width="60vw">
<div class="w80% ma">
<h2 style="text-align: center; margin-top: 5px; font-weight: bold">通知单</h2>
<el-row>
<el-col :span="12" style="text-align: left">填报人{{ safetyInspectionDetail?.creatorName }}</el-col>
<el-col :span="12" style="text-align: right">填报时间{{ safetyInspectionDetail?.createTime }}</el-col>
</el-row>
<el-descriptions :column="2" border style="margin-top: 8px" label-width="160px" size="large">
<el-descriptions-item label-align="center" label="检查项目" :span="2" class-name="zebra">{{ currentProject?.name }} </el-descriptions-item>
<el-descriptions-item label-align="center" label="检查类型" label-class-name="white">
<dict-tag :options="safety_inspection_check_type" :value="safetyInspectionDetail?.checkType" />
</el-descriptions-item>
<el-descriptions-item label-align="center" label="违章类型" label-class-name="white">
<dict-tag :options="safety_inspection_violation_type" :value="safetyInspectionDetail?.violationType" />
</el-descriptions-item>
<el-descriptions-item label-align="center" label="检查时间" class-name="zebra"
>{{ safetyInspectionDetail?.checkTime }}
</el-descriptions-item>
<el-descriptions-item label-align="center" label="检查人" class-name="zebra"
>{{ safetyInspectionDetail?.creatorName }}
</el-descriptions-item>
<el-descriptions-item label-align="center" label="整改人" label-class-name="white"
>{{ safetyInspectionDetail?.correctorName }}
</el-descriptions-item>
<el-descriptions-item label-align="center" label="要求整改期限" label-class-name="white">
{{ dayjs(safetyInspectionDetail?.rectificationDeadline).format('YYYY 年 MM 月 DD 日') }}
</el-descriptions-item>
</el-descriptions>
<el-descriptions border direction="vertical" size="large">
<el-descriptions-item label-align="center" label="巡检结果" class-name="none"></el-descriptions-item>
</el-descriptions>
<el-descriptions :column="2" border label-width="160px" size="large">
<el-descriptions-item label-align="center" label="内容" :span="2" label-class-name="white"
>{{ safetyInspectionDetail?.hiddenDanger }}
</el-descriptions-item>
<el-descriptions-item label-align="center" label="检查附件" :span="2" label-class-name="white">
<el-space wrap>
<div v-for="item in checkFileList" :key="item.ossId">
<span v-if="['.png', '.jpg', '.jpeg'].includes(item.fileSuffix)">
<image-preview :src="item.url" width="200px" />
</span>
<span v-else>
<el-link :href="`${item.url}`" type="primary" :underline="false" target="_blank">
<span> {{ item.originalName }} </span>
</el-link>
</span>
</div>
</el-space>
</el-descriptions-item>
<el-descriptions-item label-align="center" label="检查状态" :span="2" label-class-name="white">
<el-steps style="max-width: 200px" :active="Number(safetyInspectionDetail?.status)" finish-status="finish">
<el-step v-for="item in safety_inspection_type" :key="item.value" :title="item.label" />
</el-steps>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border direction="vertical" size="large">
<el-descriptions-item label-align="center" label="整改情况" class-name="none"></el-descriptions-item>
</el-descriptions>
<el-descriptions :column="2" border label-width="160px" size="large">
<el-descriptions-item label-align="center" label="班组" label-class-name="white"
>{{ safetyInspectionDetail?.teamName }}
</el-descriptions-item>
<el-descriptions-item label-align="center" label="整改日期" label-class-name="white"
>{{ safetyInspectionDetail?.rectificationTime }}
</el-descriptions-item>
<el-descriptions-item label-align="center" label="整改措施及完成情况" :span="2" label-class-name="white">
{{ safetyInspectionDetail?.measure }}
</el-descriptions-item>
<el-descriptions-item label-align="center" label="整改附件" :span="2" label-class-name="white">
<el-space wrap>
<div v-for="item in rectificationFileList" :key="item.ossId">
<span v-if="['.png', '.jpg', '.jpeg'].includes(item.fileSuffix)">
<image-preview :src="item.url" width="200px" />
</span>
<span v-else>
<el-link :href="`${item.url}`" :underline="false" target="_blank">
<span> {{ item.originalName }} </span>
</el-link>
</span>
</div>
</el-space>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border direction="vertical" size="large">
<el-descriptions-item label-align="center" label="复查结果" class-name="none"></el-descriptions-item>
</el-descriptions>
<el-descriptions :column="2" border label-width="160px" size="large">
<el-descriptions-item label-align="center" label="复查人" label-class-name="white"
>{{ safetyInspectionDetail?.creatorName }}
</el-descriptions-item>
<el-descriptions-item label-align="center" label="复查日期" label-class-name="white"
>{{ safetyInspectionDetail?.reviewTime }}
</el-descriptions-item>
<el-descriptions-item label-align="center" label="复查情况" :span="2" label-class-name="white"
>{{ safetyInspectionDetail?.review }}
</el-descriptions-item>
</el-descriptions>
</div>
<template #footer>
<!-- <span>
<el-button @click="detailVisible = false">Cancel</el-button>
<el-button type="primary" @click="">OK</el-button>
</span> -->
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { SafetyInspectionVO } from '@/api/safety/safetyInspection/types';
import { listByIds } from '@/api/system/oss';
import { OssVO } from '@/api/system/oss/types';
import { useUserStoreHook } from '@/store/modules/user';
import { useDict } from '@/utils/dict';
import { dayjs } from 'element-plus';
const rectificationFileList = ref<OssVO[]>();
const checkFileList = ref<OssVO[]>();
const emit = defineEmits(['selection-change']);
const props = defineProps({
data: {
type: Array,
default: () => []
}
});
console.log(props.data);
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const { safety_inspection_violation_type, safety_inspection_check_type, review_type, safety_inspection_type } = toRefs<any>(
useDict('safety_inspection_violation_type', 'safety_inspection_check_type', 'review_type', 'safety_inspection_type')
);
const safetyInspectionDetail = ref<SafetyInspectionVO>();
const form = reactive({
id: '1',
projectName: '',
serialNumber: '',
to: '',
subject: '',
content: '',
attachments: '', // 或 URL
contractorLeader: '',
contractorDate: '',
supervisorLeader: '',
supervisorDate: '',
ownerRep: '',
ownerDate: '',
pageNum: 1,
pageSize: 10
});
const total = ref(0);
const loading = ref(false);
const detailVisible = ref(false);
/** 多选框选中数据 */
const handleSelectionChange = (selection: any) => {
emit('selection-change', selection);
};
const handleDetail = async (row) => {
detailVisible.value = true;
safetyInspectionDetail.value = row;
if (row.checkFile) {
const checkFileRes = await listByIds(row.checkFile.split(','));
checkFileList.value = checkFileRes.data;
}
if (row.rectificationFile) {
const rectificationFileRes = await listByIds(row.rectificationFile.split(','));
rectificationFileList.value = rectificationFileRes.data;
}
};
const getList = (row) => {};
const handleDelete = (row) => {};
</script>
<style lang="scss" scoped>
:deep(.white) {
background: #fff !important;
}
:deep(.none) {
display: none !important;
}
:deep(.zebra) {
background: #f5f7fa;
}
</style>

View File

@ -0,0 +1,325 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="模板类型" prop="projectType">
<el-select v-model="queryParams.projectType" placeholder="请选择模板类型" clearable filterable @change="selectType">
<el-option v-for="item in projectTypeOptions" :key="item.name" :label="item.name" :value="item.name"> </el-option>
</el-select>
</el-form-item>
<!-- <el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item> -->
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" :disabled="addSingle" @click="handleAdd" v-hasPermi="['quality:qualityInspection:add']"
>新增</el-button
>
</el-col>
<!-- <el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['quality:qualityInspection:edit']"
>修改</el-button
>
</el-col> -->
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete()"
v-hasPermi="['quality:qualityInspection:remove']"
>删除</el-button
>
</el-col>
<!-- <el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['quality:qualityInspection:export']">导出</el-button>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<!-- card body -->
<Notice @selection-change="handleSelectionChange" :data="tableData"></Notice>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<el-dialog title="新增模板" v-model="dialogVisible" width="800">
<el-form :model="form" :rules="rules" ref="formRef" label-width="110px">
<div class="flex">
<!-- <img :src="thumbnailUrl" alt="" style="width: 150px;" /> -->
<div><image-preview :src="thumbnailUrl" width="150px"></image-preview></div>
<div>
<el-form-item label="检查类型" prop="checkType">
<el-select v-model="form.checkType" placeholder="请选择检查类型">
<el-option v-for="dict in safety_inspection_check_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="违章类型" prop="violationType">
<el-select v-model="form.violationType" placeholder="请选择违章类型">
<el-option v-for="dict in safety_inspection_violation_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="巡检结果" prop="inspectionResult">
<el-input v-model="form.inspectionResult" placeholder="请输入巡检结果" />
</el-form-item>
<el-form-item label="整改班组" prop="teamId">
<el-select v-model="form.teamId" placeholder="请选择整改班组">
<el-option v-for="item in teamOpt" :key="item.value" :label="item.label" :value="item.value" @click="changeForeman(item.value)" />
</el-select>
</el-form-item>
<el-form-item label="整改人" prop="correctorId">
<el-select v-model="form.correctorId" placeholder="请选择整改人" :disabled="!form.teamId">
<el-option v-for="item in foremanOpt" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="问题隐患" prop="hiddenDanger">
<el-input v-model="form.hiddenDanger" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="整改措施" prop="measure">
<el-input v-model="form.measure" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="要求整改期限" prop="rectificationDeadline">
<el-date-picker clearable v-model="form.rectificationDeadline" type="date" value-format="YYYY-MM-DD" placeholder="选择要求整改期限" />
</el-form-item>
<el-form-item label="检查附件" prop="checkFile">
<file-upload v-model="form.checkFile" :file-size="20" :file-type="['doc', 'docx', 'pdf', 'png', 'jpg', 'jpeg']" />
</el-form-item>
<el-form-item label="整改附件" prop="rectificationFile">
<file-upload v-model="form.rectificationFile" :file-size="20" :file-type="['doc', 'docx', 'pdf', 'png', 'jpg', 'jpeg']" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</div>
</div>
</el-form>
<template #footer>
<span>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { useUserStoreHook } from '@/store/modules/user';
import type { FormInstance, FormRules } from 'element-plus';
import Notice from './components/notice.vue';
import { listContactTypeformtemplate } from '@/api/cory/contactformtemplate';
import { addContactnotice, delContactnotice, listContactnotice } from '@/api/cory/contactnotice';
import { listProjectTeamForeman } from '@/api/project/projectTeam';
import { foremanQuery, ProjectTeamForemanResp } from '@/api/project/projectTeam/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const { safety_inspection_violation_type, safety_inspection_check_type } = toRefs<any>(
proxy?.useDict('safety_inspection_violation_type', 'safety_inspection_check_type')
);
const teamOpt = ref([]);
const foremanOpt = ref([]);
const teamList = ref<ProjectTeamForemanResp[]>();
const thumbnailUrl = ref('');
const tableData = ref([]);
const total = ref(0);
const formRef = ref<FormInstance>();
const dialogVisible = ref<boolean>(false);
const showSearch = ref(true);
const multiple = ref<boolean>(true);
const addSingle = ref<boolean>(false);
const single = ref<boolean>(true);
const ids = ref<Array<string | number>>([]);
const projectTypeOptions = ref<any>([
{
value: '0',
label: '外部联系单'
},
{
value: '1',
label: '变更单'
},
{
value: '2',
label: '通知单'
},
{
value: '3',
label: '回复单'
},
{
value: '4',
label: '签证单'
}
]);
const initFormData = {
projectType: '',
projectName: '',
serialNumber: '',
to: '',
subject: '',
content: '',
attachments: '',
contractorLeader: '',
contractorDate: '',
supervisorLeader: '',
supervisorDate: '',
ownerRep: '',
ownerDate: '',
unitName: '',
profession: '',
applyDate: '',
bookName: '',
bookNo: '',
hasAttachment: '',
changeReasons: [],
changeContent: '',
costEstimate: ''
};
const data = reactive<PageData<any, any>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
inspectionType: undefined,
inspectionStatus: undefined,
teamId: undefined,
type: undefined,
params: {},
ids: ['1942115670472814593', '1942115720766713857'],
projectType: undefined
},
rules: {
projectName: [{ required: true, message: '请输入工程名称', trigger: 'blur' }],
projectType: [{ required: true, message: '请选择模板类型', trigger: 'blur' }],
serialNumber: [{ required: true, message: '请输入编号', trigger: 'blur' }],
to: [{ required: true, message: '请输入接收方', trigger: 'blur' }],
subject: [{ required: true, message: '请输入主题', trigger: 'blur' }],
content: [{ required: true, message: '请输入内容', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
const submitForm = () => {
formRef.value?.validate(async (valid) => {
if (valid) {
let data = {
type: queryParams.value.type,
projectId: currentProject.value?.id,
detail: JSON.stringify(form.value)
};
console.log('提交表单:', form);
const res = await addContactnotice(data);
if (res.code == 200) {
proxy.$modal.msgSuccess('添加成功');
dialogVisible.value = false;
formRef.value.resetFields();
getList();
}
}
});
};
const handleAdd = () => {
dialogVisible.value = true;
};
const changeForeman = (value: string | number) => {
const team = teamList.value.filter((team) => team.id === value)[0];
foremanOpt.value = team.foremanList?.map((foreman: foremanQuery) => ({
label: foreman.foremanName,
value: foreman.foremanId
}));
form.value.correctorId = '';
};
const handleQuery = () => {};
const resetQuery = () => {};
const getList = async () => {
if (!queryParams.value.projectType) {
const res = await listContactTypeformtemplate(queryParams.value);
projectTypeOptions.value = res.data;
queryParams.value.projectType = res.data[0].name;
thumbnailUrl.value = res.data[0].thumbnail;
queryParams.value.type = res.data[0].id as string;
}
const res = await listContactnotice(queryParams.value);
res.rows = res.rows.map((item) => {
return {
...item,
...JSON.parse(item.detail)
};
});
tableData.value = res.rows;
total.value = res.total || 0;
// 获取项目班组信息
const teamRes = await listProjectTeamForeman(currentProject.value?.id);
teamList.value = teamRes.data;
teamOpt.value = teamList.value.map((team: ProjectTeamForemanResp) => ({
label: team.teamName,
value: team.id
}));
};
const handleDelete = async (id?: string) => {
const _ids = id || ids.value;
await proxy?.$modal.confirm('是否确认删除识别记录编号为"' + _ids + '"的数据项?').finally();
const res = await delContactnotice(_ids);
if (res.code == 200) {
proxy.$modal.msgSuccess('删除成功');
getList();
}
};
const handleUpdate = () => {};
/** 多选框选中数据 */
const handleSelectionChange = (selection: any) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
const selectType = (value: string) => {
queryParams.value.projectType = value;
thumbnailUrl.value = projectTypeOptions.value.filter((item) => item.name == value)[0].thumbnail;
queryParams.value.type = projectTypeOptions.value.filter((item) => item.name == value)[0].id;
getList();
};
const resetForm = () => {
formRef.value?.resetFields();
};
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
queryParams.value.projectId = nid;
form.value.projectId = nid;
getList();
}
);
onUnmounted(() => {
listeningProject();
});
onMounted(() => {
getList();
});
</script>

View File

@ -0,0 +1,244 @@
<template>
<div class="p-6 bg-gray-50 Professional">
<transition>
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="never">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="电话" prop="phone">
<el-input v-model="queryParams.phone" placeholder="请输入电话" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="professionalList">
<el-table-column label="序号" align="center" type="index" width="100" />
<el-table-column label="提资人" align="center" prop="userName" />
<el-table-column label="专业" align="center" prop="userMajorName" />
<el-table-column label="电话" align="center" prop="phone" />
<el-table-column label="邮箱" align="center" prop="email" />
<el-table-column label="流程状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="wf_business_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-row :gutter="10" class="mb8">
<el-col :span="1.5" v-if="scope.row.status === 'draft' || scope.row.status === 'cancel' || scope.row.status === 'back'">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['design:extract:query']">审核</el-button>
</el-col>
<el-col :span="1.5" v-if="scope.row.status === 'finish'">
<el-button link type="primary" icon="Download" @click="handleDownload(scope.row)" v-hasPermi="['design:extract:export']"
>导出</el-button
>
</el-col>
<el-col :span="1.5">
<el-button
link
type="warning"
icon="View"
v-if="scope.row.status != 'draft'"
v-hasPermi="['design:extract:query']"
@click="handleViewInfo(scope.row)"
>查看流程</el-button
>
</el-col>
<el-col :span="1.5">
<el-button
link
type="warning"
icon="View"
v-if="scope.row.status != 'draft'"
v-hasPermi="['design:extract:query']"
@click="handleFile(scope.row)"
>查看文件</el-button
>
</el-col>
</el-row>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<el-dialog title="提取文件列表" v-model="viewVisible" width="500px">
<el-table :loading="loadingFlie" :data="fileList" style="width: 100%" border>
<el-table-column prop="fileName" label="文件名称" align="center">
<template #default="scope">
<el-link
:key="scope.row.fileId"
:href="scope.row.fileUrl"
target="_blank"
:type="scope.row.status == '1' ? 'primary' : 'info'"
:underline="false"
>
{{ scope.row.fileName }}
</el-link>
</template>
</el-table-column>
<el-table-column label="是否变更" align="center" width="120">
<template #default="scope">
<el-tag :type="scope.row.type == 1 ? 'success' : 'info'">{{ scope.row.type == 1 ? '否' : '是' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="120" align="center">
<template #default="scope">
<el-tag :type="scope.row.status == 1 ? 'success' : 'info'">{{ scope.row.status == 1 ? '使用中' : '已作废' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="170" align="center">
<template #default="scope">
<el-button type="success" link icon="View" @click="handleViewFile(scope.row)"> 查看 </el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<span>
<el-button type="primary" @click="viewVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup name="DataCollectionForm" lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { useUserStoreHook } from '@/store/modules/user';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
import { extractList, getFileList } from '@/api/design/Professional';
// 获取用户 store
const userStore = useUserStoreHook();
const total = ref(0);
// 从 store 中获取当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const professionalList = ref([]);
const showSearch = ref(true);
const loading = ref(false);
const loadingFlie = ref(false);
const viewVisible = ref(false); //文件列表展示
const fileList = ref([]);
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
phone: undefined,
status: undefined,
params: {}
}
});
const queryFormRef = ref<ElFormInstance>();
const { queryParams } = toRefs(data);
// 查询提资清单表
const getList = async () => {
let res = await extractList({ ...queryParams.value });
if (res.code == 200) {
professionalList.value = res.rows;
total.value = res.total; //默认第一个
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 新增按钮操作 */
const handleAdd = (row) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/Professional/indexEdit`,
query: {
id: row.id,
type: 'add'
}
});
};
const handleViewInfo = (row) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/Professional/indexEdit`,
query: {
id: row.id,
type: 'view'
}
});
};
const handleUpdate = (row) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/Professional/indexEdit`,
query: {
id: row.id,
type: 'update'
}
});
};
const handleDownload = (row) => {
// 导出接口
proxy?.download(
'design/extract/exportWord',
{
id: row.id
},
`互提资料.zip`
);
};
const handleViewFile = (row) => {
window.open(row.docUrl, '_blank');
};
const handleFile = async (row) => {
// 查看文件
viewVisible.value = true;
loadingFlie.value = true;
let res = await getFileList(row.id);
if (res.code == 200) {
fileList.value = res.data;
}
loadingFlie.value = false;
};
// 页面挂载时初始化数据
onMounted(() => {
getList();
});
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
queryParams.value.projectId = nid;
getList();
}
);
onUnmounted(() => {
listeningProject();
});
</script>
<style lang="scss">
.Professional {
.el-tabs__header {
height: 84vh !important;
}
}
</style>

View File

@ -0,0 +1,571 @@
<template>
<div class="p-4 bg-gray-50">
<div class="max-w-4xl mx-auto">
<!-- 顶部按钮区域 -->
<el-card class="mb-4 rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.id"
:status="form.status"
:pageType="routeParams.type"
/>
</el-card>
<!-- 表单区域 -->
<el-card class="rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md overflow-hidden">
<div class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-800">专业互提资料</h3>
</div>
<div class="p-6">
<div class="appWidth mx-auto bg-white rounded-xl shadow-sm overflow-hidden transition-all duration-300 hover:shadow-md">
<!-- 表单内容区域 -->
<el-form :disabled="disableAll" ref="mainFormRef" :model="form" :rules="mainRules" label-width="120px" class="p-6">
<!-- 基本信息区域 -->
<div class="bg-blue-50 p-4 rounded-lg mb-6">
<h3 class="text-lg font-semibold text-blue-700 mb-4">基本信息</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<el-form-item label="提资人" prop="userId" class="mb-4">
<el-select
v-model="form.userId"
placeholder="请选择提资人"
class="w-full transition-all duration-300 border-gray-300 focus:border-blue-400 focus:ring-1 focus:ring-blue-400"
>
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" />
</el-select>
</el-form-item>
<el-form-item label="专业" prop="user_major" class="mb-4">
<el-select
v-model="form.user_major"
placeholder="请选择专业"
class="transition-all duration-300 border-gray-300"
:rules="{ required: true, message: '请选择专业', trigger: 'change' }"
>
<el-option v-for="item in des_user_major" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="电话" prop="phone" class="mb-4">
<el-input placeholder="请输入电话" v-model="form.phone" autocomplete="off" />
</el-form-item>
<el-form-item label="邮箱" prop="email" class="mb-4">
<el-input placeholder="请输入邮箱" v-model="form.email" autocomplete="off" />
</el-form-item>
</div>
</div>
<!-- 资料文件区域 -->
<div class="mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-blue-700">资料文件清单</h3>
<el-button type="primary" size="small" @click="addDocumentItem" icon="Plus"> 添加资料 </el-button>
</div>
<el-form :disabled="disableAll" ref="documentsFormRef" :model="form" class="space-y-4">
<div
v-for="(item, index) in form.documents"
:key="item.id"
class="bg-gray-50 p-4 rounded-lg transition-all duration-200 hover:shadow-sm"
>
<div class="flex justify-between items-start mb-2">
<span class="text-sm font-medium text-gray-600">资料 {{ index + 1 }}</span>
<el-button
type="text"
size="small"
text-color="#ff4d4f"
@click="removeDocumentItem(index)"
icon="el-icon-delete"
v-if="form.documents.length > 1"
>
删除
</el-button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<el-form-item
label="资料名称"
:prop="`documents.${index}.catalogueName`"
:rules="[{ required: true, message: '请输入文件资料名称', trigger: 'blur' }]"
class="mb-4"
>
<el-select
id="projectSelect"
v-model="item.volumeCatalogId"
placeholder="请选择资料"
clearable
filterable
@change="handleSelect($event, item)"
style="width: 150px; margin-right: 20px"
>
<el-option
v-for="project in volumeCatalogList"
:key="project.design"
:label="project.documentName"
:value="project.design"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" :prop="`documents.${index}.remark`" class="mb-4">
<el-input placeholder="请输入备注" v-model="item.remark" autocomplete="off" />
</el-form-item>
</div>
</div>
</el-form>
</div>
</el-form>
</div>
</div>
</el-card>
<!-- 提交组件 -->
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<approvalRecord ref="approvalRecordRef"></approvalRecord>
<!-- 流程选择对话框 -->
<el-dialog
draggable
v-model="dialogVisible.visible"
:title="dialogVisible.title"
:before-close="handleClose"
width="500"
class="rounded-lg shadow-lg"
>
<div class="p-4">
<p class="text-gray-600 mb-4">请选择要启动的流程</p>
<el-select v-model="flowCode" placeholder="请选择流程" style="width: 100%">
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<template #footer>
<div class="dialog-footer p-4 border-t border-gray-100 flex justify-end space-x-3">
<el-button @click="handleClose" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
>取消</el-button
>
<el-button type="primary" @click="submitFlow()" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
>确认</el-button
>
</div>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup name="Leave" lang="ts">
import { LeaveForm } from '@/api/workflow/leave/types';
import { startWorkFlow } from '@/api/workflow/task';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { useUserStoreHook } from '@/store/modules/user';
import { systemUserList } from '@/api/design/appointment';
import { extractBatch, extractDetail } from '@/api/design/Professional';
import { listVolumeCatalog } from '@/api/design/volumeCatalog';
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const { des_user_major } = toRefs<any>(proxy?.useDict('des_user_major'));
const buttonLoading = ref(false);
const loading = ref(true);
const disableAll = ref(false);
const volumeCatalogList = ref([]);
let volumeMap = new Map();
//路由参数
const routeParams = ref<Record<string, any>>({});
const flowCodeOptions = [
{
value: currentProject.value?.id + '_extract',
label: '互提资料清单'
}
];
const flowCode = ref<string>('');
const status = ref<string>('');
const dialogVisible = reactive<DialogOption>({
visible: false,
title: '流程定义'
});
//提交组件
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
//审批记录组件
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
//按钮组件
const approvalButtonRef = ref<InstanceType<typeof ApprovalButton>>();
const leaveFormRef = ref<ElFormInstance>();
const dialog = reactive({
visible: false,
title: '',
isEdit: false
});
const submitFormData = ref<StartProcessBo>({
businessId: '',
flowCode: '',
variables: {}
});
const taskVariables = ref<Record<string, any>>({});
// 用户列表
const userList = ref([]);
const userMap = new Map();
// 表单引用
const mainFormRef = ref();
const handleClose = () => {
dialogVisible.visible = false;
flowCode.value = '';
buttonLoading.value = false;
};
// 表单数据
const form = reactive({
projectId: currentProject.value?.id,
userId: '', // 收资人
user_major: '', // 专业
phone: '', // 电话
email: '', // 邮箱
id: '',
status: '',
documents: [
{
id: Date.now(),
catalogueName: '', // 卷册目录名称
remark: '', // 备注
volumeCatalogId: '' //卷册目录ID
}
] as Array<{
id: number;
catalogueName: string;
remark: string;
volumeCatalogId: string;
}>
});
// 主表单验证规则
const mainRules = reactive({
userId: [{ required: true, message: '请输入收资人', trigger: 'blur' }],
user_major: [{ required: true, message: '请选择专业', trigger: 'change' }],
phone: [
{ required: true, message: '请输入电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
]
});
/** 表单重置 */
const reset = () => {
if (mainFormRef.value) {
mainFormRef.value.resetFields();
}
// 重置资料列表,保留一个空项
form.documents = [
{
id: Date.now(),
catalogueName: '',
remark: '',
volumeCatalogId: ''
}
];
leaveFormRef.value?.resetFields();
};
// 添加资料项
const addDocumentItem = () => {
form.documents.push({
id: Date.now(),
catalogueName: '',
remark: '',
volumeCatalogId: ''
});
};
// 删除资料项
const removeDocumentItem = (index: number) => {
form.documents.splice(index, 1);
};
/** 提交按钮 */
const submitForm = (status1: string) => {
status.value = status1;
buttonLoading.value = true;
dialog.visible = false;
// 验证表单数据
mainFormRef.value.validate(async (valid) => {
if (valid) {
console.log('验证成功');
form.documents.map((item, i) => {
item.num = i + 1;
});
let body = {
desExtractBo: {
projectId: currentProject.value?.id,
userId: form.userId, // 收资人
userMajor: form.user_major, // 专业
id: form.id,
phone: form.phone, // 电话
email: form.email, // 邮箱
userName: userMap.get(form.userId)
},
catalogueList: form.documents
};
let res = await extractBatch(body);
if (res.code == 200) {
buttonLoading.value = false;
dialog.visible = false;
} else {
ElMessage.error(res.msg);
}
// 表单验证通过,执行提交逻辑
form.id = res.data;
submit(status.value, form);
} else {
// 表单验证失败,提示用户并关闭加载状态
proxy?.$modal.msgError('请完善必填信息后再提交');
buttonLoading.value = false;
return false;
}
});
};
const submitFlow = async () => {
handleStartWorkFlow(form);
dialogVisible.visible = false;
};
//提交申请
const handleStartWorkFlow = async (data: LeaveForm) => {
try {
submitFormData.value.flowCode = flowCode.value;
submitFormData.value.businessId = data.id;
//流程变量
taskVariables.value = {
// leave4/5 使用的流程变量
userList: ['1', '3', '4']
};
submitFormData.value.variables = taskVariables.value;
const resp = await startWorkFlow(submitFormData.value);
if (submitVerifyRef.value) {
buttonLoading.value = false;
submitVerifyRef.value.openDialog(resp.data.taskId);
}
} finally {
buttonLoading.value = false;
}
};
//审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value.init(form.id);
};
//提交回调
const submitCallback = async () => {
await proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
};
//审批
const approvalVerifyOpen = async () => {
submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
const submit = async (status, data) => {
if (status === 'draft') {
buttonLoading.value = false;
proxy?.$modal.msgSuccess('暂存成功');
proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
} else {
if ((form.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
flowCode.value = flowCodeOptions[0].value;
dialogVisible.visible = true;
return;
}
//说明启动过先随意穿个参数
if (flowCode.value === '' || flowCode.value === null) {
flowCode.value = 'xx';
}
await handleStartWorkFlow(data);
}
};
/** 查询当前部门的所有用户 */
const getDeptAllUser = async (deptId: any) => {
try {
const res = await systemUserList({ deptId });
// 实际项目中使用接口返回的数据
userList.value = res.rows;
userList.value.forEach((user) => {
userMap.set(user.userId, user.nickName);
});
} catch (error) {
ElMessage.error('获取用户列表失败');
}
};
// 查询数据 再次回显
const byProjectIdAll = async () => {
loading.value = true;
buttonLoading.value = false;
// 调用接口获取数据
const res = await extractDetail(routeParams.value.id);
if (res.code === 200 && res.data) {
const data = res.data;
// 回显基本信息
form.userId = data.userId || '';
form.user_major = data.userMajor || '';
form.phone = data.phone || '';
form.email = data.email || '';
form.id = data.id || '';
form.status = data.status || '';
if (data.status != 'draft') {
disableAll.value = true;
}
// 回显资料文件列表
if (data.catalogueList && data.catalogueList.length > 0) {
// 清空现有列表
form.documents = [];
// 填充新数据
data.catalogueList.forEach((item: any, index: number) => {
form.documents.push({
id: item.id || Date.now() + index, // 确保id唯一
catalogueName: item.catalogueName || '',
remark: item.remark || '',
volumeCatalogId: item.volumeCatalogId
});
});
} else {
// 如果没有资料,保持一个空项
form.documents = [
{
id: Date.now(),
catalogueName: '',
remark: '',
volumeCatalogId: ''
}
];
}
}
loading.value = false;
buttonLoading.value = false;
};
/** 查询卷册目录列表 */
const getList = async () => {
const res = await listVolumeCatalog({ projectId: currentProject.value?.id, auditStatus: 'finish' });
volumeCatalogList.value = res.rows;
volumeCatalogList.value.forEach((e) => {
volumeMap.set(e.design, e);
});
};
const handleSelect = (val, item) => {
item.catalogueName = volumeMap.get(val).documentName;
};
onMounted(() => {
nextTick(async () => {
routeParams.value = proxy.$route.query;
reset();
loading.value = false;
getList(); // 获取卷册目录
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
getDeptAllUser(userStore.deptId).then(() => {
byProjectIdAll();
});
} else {
getDeptAllUser(userStore.deptId);
}
});
});
</script>
<style scoped lang="scss">
/* 全局样式 */
:root {
--primary: #409eff;
--primary-light: #66b1ff;
--primary-dark: #3a8ee6;
--success: #67c23a;
--warning: #e6a23c;
--danger: #f56c6c;
--info: #909399;
}
/* 表单样式优化 */
.el-form-item {
.el-form-item__label {
color: #606266;
font-weight: 500;
}
.el-input__inner,
.el-select .el-input__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
.el-textarea__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
}
/* 按钮样式优化 */
.el-button {
border-radius: 4px;
transition: all 0.2s;
&.is-primary {
background-color: var(--primary);
border-color: var(--primary);
&:hover {
background-color: var(--primary-light);
border-color: var(--primary-light);
}
&:active {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
}
&.is-text {
color: var(--primary);
&:hover {
color: var(--primary-light);
background-color: rgba(64, 158, 255, 0.05);
}
}
}
/* 卡片样式优化 */
.el-card {
transition: all 0.3s ease;
&:hover {
/* transform: translateY(-2px); */
}
}
/* 对话框样式优化 */
.el-dialog {
.el-dialog__header {
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
padding: 15px 20px;
}
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-dialog__footer {
padding: 15px 20px;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@ -0,0 +1,805 @@
<template>
<div class="p-6 bg-gray-50">
<div class="appWidth mx-auto bg-white rounded-xl shadow-sm overflow-hidden transition-all duration-300 hover:shadow-md">
<!-- 表单标题区域 -->
<div class="bg-gradient-to-r from-blue-500 to-blue-600 text-white p-6">
<h2 class="text-2xl font-bold flex items-center"><i class="el-icon-user-circle mr-3"></i>人员配置</h2>
<p class="text-blue-100 mt-2 opacity-90">请配置项目相关负责人员信息</p>
<el-button @click="disabledForm = false" class="px-8 py-2.5 transition-all duration-300 font-medium" v-if="disabledForm">
点击编辑
</el-button>
</div>
<!-- 表单内容区域 -->
<el-form ref="leaveFormRef" :model="form" :disabled="disabledForm" :rules="rules" label-width="120px" class="p-6 space-y-6">
<!-- 设计负责人 -->
<div class="fonts">
<el-row>
<el-col :span="8"
><el-form-item label="设计负责人" prop="designLeader" class="mb-4">
<el-select
v-model="form.designLeader"
placeholder="请选择设计负责人"
class="w-full transition-all duration-300 border-gray-300 focus:border-blue-400 focus:ring-1 focus:ring-blue-400"
>
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 专业人员配置专业 + 设计人员 + 校审人员 横向排列 -->
<div class="border border-gray-200 rounded-lg p-5 transition-all duration-300 hover:shadow-md bg-gray-50">
<div class="flex justify-between items-center mb-5">
<h3 class="text-lg font-semibold text-gray-700 flex items-center"><i class="el-icon-users mr-2 text-blue-500"></i>专业人员配置</h3>
<div class="flex gap-3">
<!-- 新增专业按钮 -->
<el-button type="primary" size="small" :disabled="disabledForm" @click="addMajor">
<i class="el-icon-plus mr-1"></i>新增专业
</el-button>
</div>
</div>
<!-- 表头 -->
<el-row :gutter="20" class="mb-3 font-medium text-gray-700">
<el-col :span="6" :xs="24" :sm="8">专业</el-col>
<el-col :span="9" :xs="24" :sm="8">设计人员可多选</el-col>
<el-col :span="9" :xs="24" :sm="8">校审人员可多选</el-col>
</el-row>
<!-- 分割线 -->
<el-divider class="my-4" />
<!-- 专业配置行专业+ 设计人员+ 校审人员 横向排列 -->
<div
v-for="(majorConfig, configIndex) in combinedConfigs"
:key="configIndex"
style="background: aliceblue; border-radius: 10px"
class="mb-5 animate-fadeIn"
>
<el-row :gutter="20" class="items-top">
<!-- 左侧专业选择 -->
<el-col :span="6" :xs="24" :sm="8" class="mb-4 sm:mb-0" style="margin-top: 8px">
<el-form-item
:prop="`designers.${configIndex}.userMajor`"
:rules="{ required: true, message: '请选择专业', trigger: 'change' }"
class="mb-0"
label-width="80px"
label="专业"
>
<!-- 专业选择下拉框 -->
<el-select
v-model="form.designers[configIndex].userMajor"
placeholder="请选择专业"
class="w-full transition-all duration-300 border-gray-300"
@change="(val) => handleMajorChange(val, configIndex)"
>
<!-- 临时添加调试显示 -->
<template v-if="des_user_major && des_user_major.length > 0">
<el-option v-for="item in des_user_major" :key="item.value" :label="item.label" :value="item.value" />
</template>
<template v-else>
<el-option label="无专业数据" value="" disabled />
</template>
</el-select>
</el-form-item>
</el-col>
<!-- 中间设计人员 -->
<el-col :span="9" :xs="24" :sm="8" class="mb-4 sm:mb-0">
<div class="pl-0 sm:pl-4 border-l-0 sm:border-l-2 border-blue-200 py-0 sm:py-2">
<!-- 设计人员列表 -->
<div class="space-y-3">
<div v-for="(person, personIndex) in majorConfig.designPersons" :key="personIndex" class="flex items-center">
<el-form-item
:prop="`designers.${configIndex}.persons.${personIndex}.userId`"
:rules="{ required: true, message: '请选择人员', trigger: 'change' }"
class="flex-1 mr-3 mb-0"
label="设计人员"
label-width="80px"
>
<el-select
v-model="person.userId"
placeholder="请选择设计人员"
class="w-full transition-all duration-300 border-gray-300"
@change="() => checkDuplicate(person, 'designers', configIndex, personIndex)"
>
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" />
</el-select>
</el-form-item>
<div>
<el-button
type="danger"
size="small"
@click="removePerson('designers', configIndex, personIndex)"
class="transition-all duration-300 hover:bg-red-600"
:disabled="majorConfig.designPersons.length <= 1 || disabledForm"
>
<el-icon :size="16">
<Delete />
</el-icon>
</el-button>
<el-button
type="success"
size="small"
@click="addPerson('designers', configIndex)"
class="transition-all duration-300 transform hover:scale-105"
:disabled="!majorConfig.userMajor || disabledForm"
>
<el-icon :size="16">
<Plus />
</el-icon>
</el-button>
</div>
</div>
</div>
<!-- 空状态提示 -->
<div
v-if="majorConfig.designPersons.length == 0"
class="text-gray-500 text-sm py-2 bg-gray-100 rounded-lg border border-dashed border-gray-200 mt-1"
>
暂无设计人员请点击"添加设计人员"
</div>
</div>
</el-col>
<!-- 右侧校审人员 -->
<el-col :span="9" :xs="24" :sm="8">
<div class="pl-0 sm:pl-4 border-l-0 sm:border-l-2 border-green-200 py-0 sm:py-2">
<!-- 校审人员列表 -->
<div class="space-y-3">
<div v-for="(person, personIndex) in majorConfig.reviewPersons" :key="personIndex" class="flex items-center">
<el-form-item
:prop="`reviewers.${configIndex}.persons.${personIndex}.userId`"
:rules="{ required: true, message: '请选择人员', trigger: 'change' }"
class="flex-1 mr-3 mb-0"
label="校审人员"
label-width="80px"
>
<el-select
v-model="person.userId"
placeholder="请选择校审人员"
class="w-full transition-all duration-300 border-gray-300"
@change="() => checkDuplicate(person, 'reviewers', configIndex, personIndex)"
>
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" />
</el-select>
</el-form-item>
<div>
<el-button
type="danger"
size="small"
@click="removePerson('reviewers', configIndex, personIndex)"
class="transition-all duration-300 hover:bg-red-600"
:disabled="majorConfig.reviewPersons.length <= 1 || disabledForm"
>
<el-icon :size="16">
<Delete />
</el-icon>
</el-button>
<el-button
type="success"
size="small"
@click="addPerson('reviewers', configIndex)"
class="transition-all duration-300 transform hover:scale-105"
:disabled="!majorConfig.userMajor || disabledForm"
>
<el-icon :size="16">
<Plus />
</el-icon>
</el-button>
</div>
</div>
</div>
<!-- 空状态提示 -->
<div
v-if="majorConfig.reviewPersons.length == 0"
class="text-gray-500 text-sm py-2 bg-gray-100 rounded-lg border border-dashed border-gray-200 mt-1"
>
暂无校审人员请点击"添加校审人员"
</div>
</div>
</el-col>
</el-row>
<!-- 删除专业配置行 -->
<el-row class="mt-2">
<el-col :span="24" class="text-right pr-4">
<el-button
type="text"
class="text-red-500 hover:text-red-700 transition-colors"
@click="removeMajor(configIndex)"
:disabled="combinedConfigs.length <= 1 || disabledForm"
>
<i class="el-icon-delete mr-1"></i>删除专业
</el-button>
</el-col>
</el-row>
</div>
</div>
<!-- 提交按钮区域 -->
<div class="flex justify-center space-x-6 mt-8 pt-6 border-t border-gray-100">
<el-button
type="primary"
size="large"
v-hasPermi="['design:user:batch']"
@click="submitForm"
class="px-8 py-2.5 transition-all duration-300 transform hover:scale-105 bg-blue-500 hover:bg-blue-600 text-white font-medium"
:disabled="disabledForm"
>
<i class="el-icon-check mr-2"></i>确认提交
</el-button>
<el-button
size="large"
@click="resetForm"
class="px-8 py-2.5 transition-all duration-300 border-gray-300 hover:bg-gray-100 font-medium"
:disabled="disabledForm"
>
<i class="el-icon-refresh mr-2"></i>重置
</el-button>
</div>
</el-form>
</div>
</div>
</template>
<script setup name="PersonnelForm" lang="ts">
import { ref, reactive, computed, onMounted, toRefs, watch, WatchStopHandle } from 'vue';
import { getCurrentInstance } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { useUserStoreHook } from '@/store/modules/user';
import { ElMessage, ElLoading } from 'element-plus';
import { Delete, Plus } from '@element-plus/icons-vue'; // 修复添加Plus图标导入
import { designUserAdd, designUserList, systemUserList } from '@/api/design/appointment';
// 获取当前实例
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
// 专业字典数据 - 增加默认空数组避免undefined
const { des_user_major = ref([]) } = toRefs<any>(proxy?.useDict('des_user_major') || {});
// 调试:打印专业数据
onMounted(() => {
console.log('专业数据:', des_user_major.value);
});
// 表单数据:保持原有数据结构不变
interface MajorGroup {
userMajor: string | null; // 专业
persons: Array<{ userId: number | null }>; // 该专业下的多个人员
}
const form = reactive({
projectId: currentProject.value?.id,
designLeader: null, // 设计负责人
designers: [] as MajorGroup[], // 设计人员:按专业分组,每组含多个人员
reviewers: [] as MajorGroup[] // 校审人员:按专业分组,每组含多个人员
});
// 组合配置用于视图展示(专业+设计人员+校审人员)
const combinedConfigs = computed(() => {
// 确保designers和reviewers数组长度一致
const maxLength = Math.max(form.designers.length, form.reviewers.length);
while (form.designers.length < maxLength) {
form.designers.push(createEmptyMajorGroup());
}
while (form.reviewers.length < maxLength) {
form.reviewers.push(createEmptyMajorGroup());
}
// 组合数据用于视图展示
return form.designers.map((designerGroup, index) => ({
userMajor: designerGroup.userMajor,
designPersons: designerGroup.persons,
reviewPersons: form.reviewers[index].persons
}));
});
// 表单验证规则
const rules = reactive({
designLeader: [{ required: true, message: '请选择设计负责人', trigger: 'change' }]
});
// 用户列表
const userList = ref([]);
// 表单引用
const leaveFormRef = ref();
const disabledForm = ref(false); //控制提交按钮状态
/** 查询当前部门的所有用户 */
const getDeptAllUser = async (deptId: any) => {
try {
const res = await systemUserList({ deptId });
userList.value = res.rows;
} catch (error) {
ElMessage.error('获取用户列表失败');
}
};
/** 查询当前表单数据并回显 */
const designUser = async () => {
if (!currentProject.value?.id) return;
const loading = ElLoading.service({
lock: true,
text: '加载配置数据中...',
background: 'rgba(255, 255, 255, 0.7)'
});
try {
const res = await designUserList({ projectId: currentProject.value?.id });
// 清空现有数据
form.designLeader = null;
form.designers = [];
form.reviewers = [];
if (res.code == 200 && res.rows && res.rows.length > 0) {
disabledForm.value = true;
// 1. 分类整理数据(按用户类型)
const designLeader = res.rows.find((item) => item.userType == 1);
const designerItems = res.rows.filter((item) => item.userType == 2);
const reviewerItems = res.rows.filter((item) => item.userType == 3);
// 2. 回显设计负责人
if (designLeader) form.designLeader = designLeader.userId;
// 3. 回显设计人员(按专业分组)
form.designers = groupPersonByMajor(designerItems);
// 4. 回显校审人员(按专业分组)
form.reviewers = groupPersonByMajor(reviewerItems);
}
// 补全默认空项至少1个专业分组每组至少1个人员
if (form.designers.length == 0) form.designers.push(createEmptyMajorGroup());
if (form.reviewers.length == 0) form.reviewers.push(createEmptyMajorGroup());
} catch (error) {
ElMessage.error('获取配置数据失败');
// 异常时初始化默认空项
form.designers = [createEmptyMajorGroup()];
form.reviewers = [createEmptyMajorGroup()];
} finally {
loading.close();
}
};
/** 辅助函数创建空的专业分组含1个空人员 */
const createEmptyMajorGroup = (): MajorGroup => ({
userMajor: null,
persons: [{ userId: null }]
});
/** 辅助函数:按专业分组整理人员数据(用于回显) */
const groupPersonByMajor = (items: any[]): MajorGroup[] => {
const groupMap: Record<string, MajorGroup> = {};
items.forEach((item) => {
const major = item.userMajor || '未分类';
// 不存在该专业分组则创建
if (!groupMap[major]) {
groupMap[major] = { userMajor: item.userMajor, persons: [] };
}
// 添加当前人员到专业分组
groupMap[major].persons.push({ userId: item.userId });
});
// 处理空分组确保每组至少1个人员
Object.values(groupMap).forEach((group) => {
if (group.persons.length == 0) group.persons.push({ userId: null });
});
return Object.values(groupMap);
};
/** 新增专业配置行 */
const addMajor = () => {
form.designers.push(createEmptyMajorGroup());
form.reviewers.push(createEmptyMajorGroup());
// 滚动到新增的专业配置行
setTimeout(() => {
const groups = document.querySelectorAll(`[data-v-${proxy?.$options.__scopeId}] .animate-fadeIn`);
if (groups.length > 0) {
groups[groups.length - 1].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, 100);
};
/** 删除专业配置行 */
const removeMajor = (configIndex: number) => {
if (form.designers.length <= 1) {
ElMessage.warning('至少保留一个专业配置');
return;
}
form.designers.splice(configIndex, 1);
form.reviewers.splice(configIndex, 1);
};
/** 给指定专业配置行添加人员 */
const addPerson = (type: 'designers' | 'reviewers', configIndex: number) => {
form[type][configIndex].persons.push({ userId: null });
// 滚动到新增的人员选择框
setTimeout(() => {
const personSelects = document.querySelectorAll(`[data-v-${proxy?.$options.__scopeId}] .el-select`);
if (personSelects.length > 0) {
personSelects[personSelects.length - 1].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, 100);
};
/** 从指定专业配置行删除人员 */
const removePerson = (type: 'designers' | 'reviewers', configIndex: number, personIndex: number) => {
const targetGroup = form[type][configIndex];
if (targetGroup.persons.length <= 1) {
ElMessage.warning(`该专业至少保留一个${type == 'designers' ? '设计' : '校审'}人员`);
return;
}
targetGroup.persons.splice(personIndex, 1);
};
/** 专业变更时:清空当前专业下的人员(避免专业与人员不匹配) */
const handleMajorChange = (newMajor: string, configIndex: number) => {
// 直接修改原始数据源,确保响应式生效
form.designers[configIndex].userMajor = newMajor;
form.reviewers[configIndex].userMajor = newMajor;
form.designers[configIndex].persons = [{ userId: null }];
form.reviewers[configIndex].persons = [{ userId: null }];
// ElMessage.info(`已重置「${getMajorLabel(newMajor)}」专业下的人员,请重新选择`);
};
// ========== 核心:重复校验逻辑 ==========
/**
* 校验同一角色内(设计/校审)的「专业+人员」组合唯一性
*/
const checkDuplicate = (current: { userId: number | null }, role: 'designers' | 'reviewers', configIndex: number, personIndex: number) => {
console.log(`校验触发 - 角色: ${role}, 专业索引: ${configIndex}, 人员索引: ${personIndex}, 人员ID: ${current.userId}`);
console.log(form);
const currentGroup = form[role][configIndex];
// 未选专业/人员时不校验
if (!currentGroup.userMajor || !current.userId) return;
// 生成当前「专业+人员」唯一标识
const currentKey = `${currentGroup.userMajor}-${current.userId}`;
let duplicateItem = null;
// 1. 检查当前专业配置行内是否有重复人员
duplicateItem = currentGroup.persons.find((item, idx) => {
return idx !== personIndex && item.userId == current.userId;
});
if (duplicateItem) {
ElMessage.warning(`当前专业下「${getUserName(current.userId)}」已存在,请重新选择`);
current.userId = null;
return;
}
// 2. 检查同一角色内其他专业配置行是否有重复(专业+人员唯一)
form[role].forEach((group, gIdx) => {
if (gIdx == configIndex) return; // 跳过当前配置行
group.persons.forEach((item) => {
if (`${group.userMajor}-${item.userId}` == currentKey) {
duplicateItem = item;
}
});
});
if (duplicateItem) {
ElMessage.warning(`${getMajorLabel(currentGroup.userMajor)}+${getUserName(current.userId)}」组合已存在,请重新选择`);
current.userId = null;
}
};
/** 辅助函数:通过专业值获取专业名称 */
const getMajorLabel = (majorValue: string | null) => {
if (!majorValue || !des_user_major.value) return '';
const major = des_user_major.value.find((item: any) => item.value == majorValue);
return major ? major.label : majorValue;
};
/** 辅助函数通过用户ID获取用户名 */
const getUserName = (userId: number | null) => {
if (!userId || !userList.value.length) return '';
const user = userList.value.find((item: any) => item.userId == userId);
return user ? user.nickName : userId;
};
/** 提交表单(保持原有数据结构) */
const submitForm = async () => {
if (!leaveFormRef.value) return;
try {
// 1. 基础表单验证
await leaveFormRef.value.validate();
// 2. 提交前二次校验:「专业+人员」组合唯一性
let hasDuplicate = false;
const allKeys: string[] = [];
// 收集所有「专业+人员」组合(设计+校审分开校验)
const collectKeys = (roleGroups: MajorGroup[], roleName: string) => {
roleGroups.forEach((group) => {
if (!group.userMajor) return;
group.persons.forEach((person) => {
if (!person.userId) return;
const key = `${group.userMajor}-${person.userId}`;
if (allKeys.includes(key)) {
hasDuplicate = true;
ElMessage.error(`${roleName}中存在重复的「专业+人员」组合,请检查`);
}
allKeys.push(key);
});
});
};
// 校验设计人员
collectKeys(form.designers, '设计人员');
if (hasDuplicate) return;
// 清空临时数组,校验校审人员(不校验设计与校审之间)
allKeys.length = 0;
collectKeys(form.reviewers, '校审人员');
if (hasDuplicate) return;
// 3. 构建提交数据(适配后端原有数据格式)
const submitData = {
projectId: form.projectId,
personnel: [
// 设计负责人
{
userId: form.designLeader,
userType: 'designLeader',
userMajor: null
},
// 设计人员:展开专业分组,每个人员单独作为一条数据
...form.designers.flatMap((group) =>
group.persons.map((person) => ({
userId: person.userId,
userType: 'designer',
userMajor: group.userMajor
}))
),
// 校审人员:展开专业分组,每个人员单独作为一条数据
...form.reviewers.flatMap((group) =>
group.persons.map((person) => ({
userId: person.userId,
userType: 'reviewer',
userMajor: group.userMajor
}))
)
]
};
// 4. 数据处理(保持原有逻辑不变)
const arr = [];
userList.value.forEach((item) => {
submitData.personnel.forEach((item1) => {
if (item1.userId == item.userId) {
let userType = 1;
if (item1.userType == 'designer') userType = 2;
else if (item1.userType == 'reviewer') userType = 3;
arr.push({
userName: item.nickName,
projectId: submitData.projectId,
userId: item1.userId,
userType: userType,
userMajor: item1.userMajor
});
}
});
});
// 5. 提交到后端(保持原有逻辑不变)
const loading = ElLoading.service({ text: '提交中...', background: 'rgba(255,255,255,0.7)' });
const res = await designUserAdd({
list: arr,
projectId: currentProject.value?.id
});
if (res.code == 200) {
disabledForm.value = true;
ElMessage.success('提交成功');
} else {
ElMessage.error(res.msg || '提交失败');
}
} catch (error) {
ElMessage.error('请完善表单信息后再提交');
} finally {
ElLoading.service().close();
}
};
/** 重置表单(适配新数据结构) */
const resetForm = () => {
if (leaveFormRef.value) {
leaveFormRef.value.resetFields();
// 重置为默认空状态1个专业分组每组1个空人员
form.designers = [createEmptyMajorGroup()];
form.reviewers = [createEmptyMajorGroup()];
ElMessage.info('表单已重置');
}
};
// 监听项目ID刷新数据
const listeningProject: WatchStopHandle = watch(
() => currentProject.value?.id,
() => {
getDeptAllUser(userStore.deptId).then(() => {
designUser();
});
}
);
// 页面生命周期
onUnmounted(() => {
listeningProject();
});
onMounted(() => {
getDeptAllUser(userStore.deptId).then(() => {
designUser();
});
});
</script>
<style lang="scss" scoped>
.appWidth {
width: 70vw;
max-width: 1600px;
.el-select__wrapper {
width: 100% !important;
}
.el-button--small {
margin-bottom: 0;
}
.fonts {
.el-form-item--default .el-form-item__label {
font-size: 18px !important;
}
}
}
// 自定义动画
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fadeIn {
animation: fadeIn 0.3s ease-out forwards;
}
// 表单样式优化
::v-deep .el-form {
--el-form-item-margin-bottom: 0;
}
::v-deep .el-form-item {
margin-bottom: 0;
&__label {
font-weight: 500;
color: #4e5969;
}
&__content {
padding: 0;
}
}
::v-deep .el-select {
width: 100%;
.el-input__inner {
border-radius: 6px;
transition: all 0.3s ease;
}
&:hover .el-input__inner {
border-color: #66b1ff;
}
&.el-select-focus .el-input__inner {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
}
::v-deep .el-button {
border-radius: 6px;
padding: 8px 16px;
&--primary {
background-color: #409eff;
border-color: #409eff;
&:hover {
background-color: #66b1ff;
border-color: #66b1ff;
}
}
&--success {
background-color: #67c23a;
border-color: #67c23a;
&:hover {
background-color: #85ce61;
border-color: #85ce61;
}
&:disabled {
background-color: #b3e099;
border-color: #b3e099;
}
}
&--danger {
background-color: #f56c6c;
border-color: #f56c6c;
&:hover {
background-color: #f78989;
border-color: #f78989;
}
&:disabled {
background-color: #ffcccc;
border-color: #ffbbbb;
cursor: not-allowed;
}
}
&--text {
color: #f56c6c;
&:hover {
color: #f78989;
background-color: rgba(245, 108, 108, 0.05);
}
}
}
// 响应式网格布局
.grid {
display: grid;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.gap-4 {
gap: 1rem;
}
// 适配小屏幕小于768px时垂直排列
@media (max-width: 768px) {
.appWidth {
width: 95vw;
}
::v-deep .el-form {
padding: 4px;
}
::v-deep .el-form-item__label {
width: 100px;
}
// 小屏幕下各列上下间距
::v-deep .el-col-xs-24 + .el-col-xs-24 {
margin-top: 12px;
}
}
</style>

View File

@ -0,0 +1,342 @@
<template>
<div class="billof-quantities">
<!-- tabPosition="left" -->
<el-tabs type="border-card" @tab-change="handleTabChange">
<el-tab-pane v-for="(item, index) in work_order_type" :key="item.value" :label="item.label">
<el-card v-if="index < 3" shadow="always">
<el-form :model="state.queryForm" :inline="true">
<el-form-item label="版本号" prop="versions">
<el-select v-model="state.queryForm.versions" placeholder="选择版本号">
<el-option v-for="item in state.options" :key="item.versions" :label="item.versions" :value="item.versions" />
</el-select>
</el-form-item>
<el-form-item label="表名" prop="sheet">
<el-select v-model="state.queryForm.sheet" placeholder="选择表名" @change="handleChange">
<el-option v-for="item in state.sheets" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="openTable(true, index)">一键展开</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="openTable(false, index)">一键收起</el-button>
</el-form-item>
<el-form-item>
<el-upload ref="uploadRef" class="upload-demo" :http-request="importExcel" :show-file-list="false">
<template #trigger>
<el-button type="primary">导入excel</el-button>
</template>
</el-upload>
</el-form-item>
</el-form>
</el-card>
<el-card v-if="index == 3" shadow="always">
<el-form :model="state.queryForm" :inline="true">
<el-form-item label="版本号" prop="versions">
<el-select v-model="state.queryForm.versions" placeholder="选择版本号" @change="handleChangeVersion">
<el-option v-for="item in state.options" :key="item.versions" :label="item.versions" :value="item.versions" />
</el-select>
</el-form-item>
<el-form-item>
<el-upload
ref="uploadRef"
class="upload-demo"
:http-request="importExcel"
style="margin-right: 12px"
v-if="Object.keys(state.versionsData).length === 0 || state.versionsData.status == 'cancel'"
:show-file-list="false"
>
<template #trigger>
<el-button type="primary">导入excel</el-button>
</template>
</el-upload>
<el-button v-if="state.versionsData.status == 'draft'" type="primary" con="edit" @click="clickApprovalSheet()">审核</el-button>
<el-button
v-if="state.versionsData.status == 'waiting' || state.versionsData.status == 'finish'"
icon="view"
@click="lookApprovalFlow()"
type="warning"
>查看流程</el-button
>
</el-form-item>
</el-form>
</el-card>
<el-table
v-if="index < 3"
:ref="(el) => (tableRef[index] = el)"
:data="state.tableData"
v-loading="state.loading.list"
stripe
:row-class-name="state.tableData.length === 0 ? 'table-null' : ''"
style="width: 100%; margin-bottom: 20px; height: calc(100vh - 305px)"
row-key="id"
lazy
border
:default-expand-all="true"
>
<el-table-column prop="num" label="编号" />
<el-table-column prop="name" label="工程或费用名称" />
<el-table-column prop="unit" label="单位" />
<el-table-column prop="quantity" label="数量" />
<el-table-column prop="remark" label="备注" />
</el-table>
<el-table v-if="index == 3" :data="state.tableData" style="width: 100%; margin-bottom: 20px; height: calc(100vh - 305px)" row-key="id" border>
<el-table-column prop="num" label="编号" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="specification" label="规格" />
<el-table-column prop="unit" label="单位" />
<el-table-column prop="quantity" label="数量" />
<el-table-column prop="remark" label="备注" />
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup name="billofQuantities">
import { ref, reactive, onMounted, computed, toRefs, getCurrentInstance, nextTick } from 'vue';
import {
obtainAllVersionNumbers,
importExcelFile,
obtainTheList,
sheetList,
detailsMaterialAndEquipmentApproval
} from '@/api/design/billofQuantities/index';
import { useUserStoreHook } from '@/store/modules/user';
const userStore = useUserStoreHook();
const currentProject = computed(() => userStore.selectedProject);
const { proxy } = getCurrentInstance();
const { work_order_type } = toRefs(proxy?.useDict('work_order_type'));
const tableRef = ref({});
console.log(currentProject.value);
// tableData
// 版本号
const state = reactive({
work_order_type: 0,
// 版本号
version_num: '',
options: [], // 版本号选项
sheets: [], // sheet选项
queryForm: {
projectId: currentProject.value?.id,
versions: '',
sheet: '',
pageSize: 20,
pageNum: 1
},
loading: {
versions: false,
sheets: false,
list: false
},
error: null,
// 前三个
tableData: [],
// 版本号
versionsData: {}
});
// tab切换
const handleTabChange = (tab) => {
state.tableData = [];
state.options = [];
state.sheets = [];
state.queryForm.sheet = '';
state.queryForm.versions = '';
state.work_order_type = tab;
if (tab <= 2) {
getVersionNums();
} else {
getVersionNums(false);
}
};
onMounted(async () => {
await getVersionNums();
});
// 获取版本号
async function getVersionNums(isSheet = true) {
try {
state.loading.versions = true;
state.error = null;
const res = await obtainAllVersionNumbers({
projectId: currentProject.value?.id,
workOrderType: state.work_order_type,
pageSize: 1000,
pageNum: 1
});
const { code, rows } = res || {};
if (code === 200) {
state.options = rows || [];
if (state.options.length > 0) {
state.queryForm.versions = state.options[0].versions;
if (state.work_order_type == 3) {
state.versionsData = state.options[0] || [];
console.log('state.versionsData', state.versionsData);
}
// 等待表名加载完成
console.log(isSheet, state.sheets.length);
if (isSheet) {
await handleSheetName();
} else {
await handleQueryList(isSheet);
}
}
} else {
await handleQueryList(isSheet);
}
} catch (err) {
state.error = `获取版本号时发生错误: ${err.message}`;
console.error(state.error, err);
} finally {
state.loading.versions = false;
}
}
// 获取表名
async function handleSheetName() {
if (!state.queryForm.versions) {
console.warn('版本号不存在,无法获取表名');
return;
}
try {
state.loading.sheets = true;
state.error = null;
const { data } = await sheetList({
projectId: currentProject.value?.id,
workOrderType: state.work_order_type,
versions: state.queryForm.versions
});
state.sheets = data || [];
if (state.sheets.length > 0) {
state.queryForm.sheet = state.sheets[0];
}
// 获取列表数据
await handleQueryList();
} catch (err) {
state.error = `获取表名时发生错误: ${err.message}`;
console.error(state.error, err);
} finally {
state.loading.sheets = false;
}
}
// 获取列表
async function handleQueryList(isSheet = true) {
if (isSheet && !state.queryForm.sheet) {
console.warn('表名不存在,无法获取列表数据');
return;
}
try {
state.loading.list = true;
state.error = null;
const result = await obtainTheList(state.queryForm);
if (result?.code === 200) {
state.tableData = result.data || [];
} else {
state.error = `获取列表数据失败: 错误码 ${result?.code}`;
console.error(state.error);
}
} catch (err) {
state.error = `获取列表数据时发生错误: ${err.message}`;
console.error(state.error, err);
} finally {
state.loading.list = false;
}
}
// 上传excel
function importExcel(options) {
console.log(options);
let formData = new FormData();
formData.append('file', options.file);
state.loading.list = true;
importExcelFile({ workOrderType: state.work_order_type, projectId: currentProject.value?.id }, formData)
.then((res) => {
const { code } = res;
if (code == 200) {
proxy.$modal.msgSuccess(res.msg || '导入成功');
// 更新列表
if (state.work_order_type == 3) {
getVersionNums(false);
} else {
getVersionNums();
}
} else {
proxy.$modal.msgError(res.msg || '导入失败');
}
})
.catch((err) => {
proxy.$modal.msgError(err.msg || '导入失败');
})
.finally(() => {
state.loading.list = false;
});
}
// 切换表名
function handleChange(sheet) {
state.queryForm.sheet = sheet;
handleQueryList();
}
// 切换版本号
function handleChangeVersion(versions) {
state.queryForm.versions = versions;
state.versionsData = state.options.find((e) => e.versions == versions);
console.log('state.versionsData', state.versionsData);
state.sheets = [];
handleQueryList();
}
// 在 openTable 方法中通过索引获取对应的表格实例
function openTable(flag, index) {
nextTick(() => {
// 通过索引获取当前标签页的表格实例
const currentTable = tableRef.value[index];
console.log(currentTable, index);
if (currentTable) {
handleArr(state.tableData, flag, currentTable);
}
});
}
function handleArr(arr, flag, table) {
arr.forEach((i) => {
table.toggleRowExpansion(i, flag);
if (i.children) {
handleArr(i.children, flag, table);
}
});
}
// 审批
function clickApprovalSheet(row) {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/billofQuantities/indexEdit`,
query: {
id: state.queryForm.versions,
type: 'update'
}
});
}
// 审核流程
function lookApprovalFlow(row) {
proxy.$router.push({
path: `/approval/billofQuantities/indexEdit`,
query: {
id: state.queryForm.versions,
type: 'view'
}
});
}
</script>
<style>
.billof-quantities {
padding: 20px;
}
</style>

View File

@ -0,0 +1,357 @@
<template>
<div class="p-4 bg-gray-50">
<div class="max-w-4xl mx-auto">
<!-- 顶部按钮区域 -->
<el-card class="mb-4 rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.versions"
:status="form.status"
:pageType="routeParams.type"
/>
</el-card>
<!-- 表单区域 -->
<el-card class="rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md overflow-hidden">
<div class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-800">物资设备清单</h3>
</div>
<div class="p-6">
<el-form
ref="leaveFormRef"
v-loading="loading"
:disabled="routeParams.type === 'view' || form.status == 'waiting'"
:model="form"
:rules="rules"
label-width="100px"
class="space-y-4"
>
<div class="grid grid-cols-1 gap-4">
<el-row>
<el-col :span="12">
<el-form-item label="版本号" prop="formNo">
<el-input disabled v-model="form.versions" placeholder="请输入文件名称" />
</el-form-item>
</el-col>
</el-row>
</div>
</el-form>
<el-table :data="tableData" style="width: 100%; margin-bottom: 20px; height: calc(100vh - 305px)" row-key="id" border>
<el-table-column prop="num" label="编号" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="specification" label="规格" />
<el-table-column prop="unit" label="单位" />
<el-table-column prop="quantity" label="数量" />
<el-table-column prop="remark" label="备注" />
</el-table>
</div>
</el-card>
<!-- 提交组件 -->
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<approvalRecord ref="approvalRecordRef"></approvalRecord>
<!-- 流程选择对话框 -->
<el-dialog
draggable
v-model="dialogVisible.visible"
:title="dialogVisible.title"
:before-close="handleClose"
width="500"
class="rounded-lg shadow-lg"
>
<div class="p-4">
<p class="text-gray-600 mb-4">请选择要启动的流程</p>
<el-select v-model="flowCode" placeholder="请选择流程" style="width: 100%">
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<template #footer>
<div class="dialog-footer p-4 border-t border-gray-100 flex justify-end space-x-3">
<el-button @click="handleClose" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
>取消</el-button
>
<el-button type="primary" @click="submitFlow()" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
>确认</el-button
>
</div>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup name="Leave" lang="ts">
import { LeaveForm, LeaveQuery, LeaveVO } from '@/api/workflow/leave/types';
import { startWorkFlow } from '@/api/workflow/task';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { useUserStoreHook } from '@/store/modules/user';
const { design_change_reason_type } = toRefs<any>(proxy?.useDict('design_change_reason_type'));
import { detailsMaterialAndEquipmentApproval } from '@/api/design/billofQuantities/index';
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const buttonLoading = ref(false);
const loading = ref(true);
//路由参数
const routeParams = ref<Record<string, any>>({});
const flowCode = ref<string>('');
const status = ref<string>('');
const dialogVisible = reactive<DialogOption>({
visible: false,
title: '流程定义'
});
//提交组件
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
//审批记录组件
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
//按钮组件
const flowCodeOptions = [
{
value: currentProject.value?.id + '_equipmentList',
label: '工程量清单审核'
}
];
const leaveFormRef = ref<ElFormInstance>();
const dialog = reactive({
visible: false,
title: '',
isEdit: false
});
const submitFormData = ref<StartProcessBo>({
businessId: '',
flowCode: '',
variables: {}
});
const taskVariables = ref<Record<string, any>>({});
const initFormData = {
id: undefined,
fileName: undefined,
fileUrl: undefined,
status: undefined,
originalName: undefined
};
const data = reactive({
form: { ...initFormData },
tableData: [],
rules: {}
});
const handleClose = () => {
dialogVisible.visible = false;
flowCode.value = '';
buttonLoading.value = false;
};
const { form, rules, tableData } = toRefs(data);
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
leaveFormRef.value?.resetFields();
};
/** 获取详情 */
const getInfo = () => {
loading.value = true;
buttonLoading.value = false;
nextTick(async () => {
const res = await detailsMaterialAndEquipmentApproval(routeParams.value.id);
console.log('res.data', res.data);
Object.assign(form.value, res.data);
console.log('form', form.value);
tableData.value = res.data.auditData;
loading.value = false;
buttonLoading.value = false;
});
};
/** 提交按钮 */
const submitForm = (status1: string) => {
status.value = status1;
submit(status.value, form.value);
};
const submitFlow = async () => {
handleStartWorkFlow(form.value);
dialogVisible.visible = false;
};
//提交申请
const handleStartWorkFlow = async (data: LeaveForm) => {
try {
submitFormData.value.flowCode = flowCode.value;
submitFormData.value.businessId = data.versions;
//流程变量
taskVariables.value = {
// leave4/5 使用的流程变量
userList: ['1', '3', '4']
};
submitFormData.value.variables = taskVariables.value;
const resp = await startWorkFlow(submitFormData.value);
if (submitVerifyRef.value) {
buttonLoading.value = false;
submitVerifyRef.value.openDialog(resp.data.taskId);
}
} finally {
buttonLoading.value = false;
}
};
//审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value.init(form.value.versions);
};
//提交回调
const submitCallback = async () => {
await proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
};
//审批
const approvalVerifyOpen = async () => {
submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
// 图纸上传成功之后 开始提交
const submit = async (status, data) => {
form.value = data;
if (status === 'draft') {
buttonLoading.value = false;
proxy?.$modal.msgSuccess('暂存成功');
proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
} else {
if ((form.value.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
flowCode.value = flowCodeOptions[0].value;
dialogVisible.visible = true;
return;
}
//说明启动过先随意穿个参数
if (flowCode.value === '' || flowCode.value === null) {
flowCode.value = 'xx';
}
await handleStartWorkFlow(data);
}
};
onMounted(() => {
nextTick(async () => {
routeParams.value = proxy.$route.query;
reset();
loading.value = false;
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
getInfo();
console.log('routeParams.value', routeParams.value);
}
});
});
</script>
<style scoped lang="scss">
/* 全局样式 */
:root {
--primary: #409eff;
--primary-light: #66b1ff;
--primary-dark: #3a8ee6;
--success: #67c23a;
--warning: #e6a23c;
--danger: #f56c6c;
--info: #909399;
}
/* 表单样式优化 */
.el-form-item {
.el-form-item__label {
color: #606266;
font-weight: 500;
}
.el-input__inner,
.el-select .el-input__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
.el-textarea__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
}
/* 按钮样式优化 */
.el-button {
border-radius: 4px;
transition: all 0.2s;
&.is-primary {
background-color: var(--primary);
border-color: var(--primary);
&:hover {
background-color: var(--primary-light);
border-color: var(--primary-light);
}
&:active {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
}
&.is-text {
color: var(--primary);
&:hover {
color: var(--primary-light);
background-color: rgba(64, 158, 255, 0.05);
}
}
}
/* 卡片样式优化 */
.el-card {
transition: all 0.3s ease;
&:hover {
/* transform: translateY(-2px); */
}
}
/* 对话框样式优化 */
.el-dialog {
.el-dialog__header {
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
padding: 15px 20px;
}
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-dialog__footer {
padding: 15px 20px;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@ -0,0 +1,163 @@
<template>
<div>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<div class="box_btn">
<file-upload :limit="1" :uploadUrl="uploadUrl" :params="uploadParams" :on-upload-success="uploadFile" :fileType="[]">
<el-button type="primary" style="float: left">
<el-icon size="small"><Upload /></el-icon>上传文件
</el-button>
</file-upload>
</div>
</el-col>
<right-toolbar @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table :data="FileList" style="width: 100%" height="64vh">
<el-table-column type="index" align="center" label="序号" width="180" />
<el-table-column prop="fileName" align="center" label="文件名称" />
<el-table-column label="流程状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="wf_business_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button link type="primary" icon="Download" @click="onExport(scope.row.fileUrl)">下载</el-button>
<el-button type="success" link icon="edit" v-show="scope.row.status == 'draft'" @click="onUpdate(scope.row)">审核</el-button>
<el-button link type="warning" v-show="scope.row.status != 'draft'" icon="View" @click="onView(scope.row)">查看流程</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
</div>
</template>
<script setup name="DataCollectionForm" lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { useUserStoreHook } from '@/store/modules/user';
import { collectFileList } from '@/api/design/condition';
const { proxy } = getCurrentInstance() as any;
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const uploadUrl = computed(() => {
return `/design/collectFile/upload`;
});
// 父组件传递的参数接受
const props = defineProps({
catalogueId: {
type: Number
}
});
const uploadParams = ref({
catalogueId: '',
projectId: currentProject.value?.id
});
const total = ref(0);
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
formNo: undefined,
projectName: undefined,
submitUnit: undefined,
specialty: undefined,
submitDate: undefined,
volumeName: undefined,
volumeNo: undefined,
changeReason: undefined,
status: undefined,
params: {},
catalogueId: undefined
}
});
const { queryParams } = toRefs(data);
const FileList = ref([]);
// 查询收资清单目录列表
const getList = async () => {
let res = await collectFileList(queryParams.value);
if (res.code == 200) {
FileList.value = res.rows;
total.value = res.total;
}
};
// 上传文件
const uploadFile = (files) => {
proxy.$modal.success('上传成功');
console.log(files);
getList();
};
const onUpdate = (row) => {
// 审核
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/condition/indexEdit`,
query: {
id: row.id,
type: 'update'
}
});
};
const onView = (row) => {
// 查看流程
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/condition/indexEdit`,
query: {
id: row.id,
type: 'view'
}
});
};
const onExport = (fileUrl) => {
if (!fileUrl) {
proxy.$modal.error('文件地址不存在,无法下载');
return;
}
try {
// 创建一个隐藏的a标签
const link = document.createElement('a');
// 设置下载地址
link.href = fileUrl;
// 从URL中提取文件名作为下载文件名
const fileName = fileUrl.split('/').pop();
link.download = fileName || 'download file';
// 触发点击事件
link.click();
// 下载后移除a标签
document.body.removeChild(link);
// 显示下载成功提示
proxy.$modal.success('文件开始下载');
} catch (error) {
// proxy.$modal.error('下载失败,请稍后重试');
}
};
// 页面挂载时初始化数据
onMounted(() => {
getList();
});
const getInfo = (id) => {
queryParams.value.catalogueId = id;
uploadParams.value.catalogueId = id;
getList();
};
defineExpose({
getInfo
});
</script>
<style lang="scss">
.condition {
.el-tabs__header {
height: 84vh !important;
}
}
</style>

View File

@ -0,0 +1,156 @@
<template>
<div class="p-6 bg-gray-50 condition">
<div class="file-category">
<span style="color: #757575; display: inline-block; margin-bottom: 15px">文件分类</span>
<!-- 优化了图标与文字的对齐和间距 -->
<div v-for="(item, i) of FolderList" :key="i" :class="{ active: currentActive === i }" @click="handleClick(item, i)" class="category-item">
<el-icon :size="20" class="folder-icon">
<Folder />
</el-icon>
<span class="folder-name">
{{ item.catalogueName }}
</span>
</div>
</div>
<div class="boxs">
<filePage :catalogueId="catalogueId" ref="filePageRef"></filePage>
</div>
</div>
</template>
<script setup name="DataCollectionForm" lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useUserStoreHook } from '@/store/modules/user';
import { collectCatalogueList } from '@/api/design/condition';
import filePage from './comm/filePage.vue';
const filePageRef = ref<typeof filePage | null>(null);
const activeName = ref('');
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const FolderList = ref([]);
const catalogueId = ref(0);
// 查询收资清单目录列表
const getList = async () => {
let res = await collectCatalogueList({ projectId: currentProject.value?.id });
if (res.code == 200) {
FolderList.value = res.rows;
catalogueId.value = res.rows[0].id; //默认第一个
nextTick(() => {
filePageRef.value?.getInfo(catalogueId.value); //重新加载当前页
});
}
};
// 当前激活项的索引
const currentActive = ref(0);
// 点击事件处理函数
const handleClick = (item, index) => {
currentActive.value = index;
catalogueId.value = item.id;
nextTick(() => {
filePageRef.value?.getInfo(item.id); //重新加载当前页
});
};
// 页面挂载时初始化数据
onMounted(() => {
getList();
});
</script>
<style lang="scss">
.condition {
display: flex;
.el-tabs__header {
height: 90vh !important;
}
.file-category {
width: 200px;
display: flex;
flex-direction: column;
/* 移除固定宽度,让容器根据内容自适应 */
background-color: #ffffff;
padding: 10px;
border-radius: 6px;
/* 限制最大宽度,防止内容过长 */
/* max-width: 200px; */
margin-right: 10px;
}
.file-category > div {
cursor: pointer;
padding: 8px 12px;
margin-bottom: 4px;
border-radius: 4px;
display: flex;
/* 文本不换行,确保宽度由内容决定 */
white-space: nowrap;
transition: all 0.2s ease;
> span {
margin-left: 6px;
/* color: #676767;
font-size: 18px; */
}
}
.file-category {
width: 200px;
display: flex;
flex-direction: column;
background-color: #ffffff;
padding: 10px;
border-radius: 8px;
margin-right: 10px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
// 分类项样式优化
.category-item {
cursor: pointer;
padding: 10px 12px;
margin-bottom: 4px;
border-radius: 6px;
display: inline-flex;
align-items: center; /* 垂直居中对齐 */
white-space: nowrap;
transition: all 0.25s ease;
font-size: 14px;
color: #334155;
line-height: 1; /* 确保行高一致 */
&:hover {
background-color: #f1f5f9;
transform: translateX(2px);
}
}
// 图标样式
.folder-icon {
color: #94a3b8;
transition: color 0.25s ease;
}
// 文件夹名称样式
.folder-name {
margin-left: 8px; /* 增加图标与文字间距 */
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - 30px); /* 限制最大宽度,防止溢出 */
}
// 活跃状态样式
.category-item.active {
background-color: #eff6ff;
color: #2563eb;
font-weight: 500;
.folder-icon {
color: #2563eb;
}
}
.boxs {
width: calc(100% - 220px);
}
}
</style>

View File

@ -0,0 +1,353 @@
<template>
<div class="p-4 bg-gray-50">
<div class="max-w-4xl mx-auto">
<!-- 顶部按钮区域 -->
<el-card class="mb-4 rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.id"
:status="form.status"
:pageType="routeParams.type"
/>
</el-card>
<!-- 表单区域 -->
<el-card class="rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md overflow-hidden">
<div class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-800">设计输入条件</h3>
</div>
<div class="p-6">
<el-form
ref="leaveFormRef"
v-loading="loading"
:disabled="routeParams.type === 'view' || form.status == 'waiting'"
:model="form"
:rules="rules"
label-width="100px"
class="space-y-4"
>
<div class="grid grid-cols-1 gap-4">
<el-row>
<el-col :span="12">
<el-form-item label="文件名称" prop="formNo">
<el-input disabled v-model="form.fileName" placeholder="请输入文件名称" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="文件" prop="formNo">
<div style="display: flex">
<span style="color: rgb(50, 142, 248)" @click="onOpen">点击打开</span>
</div>
</el-form-item>
</el-col>
</el-row>
</div>
</el-form>
</div>
</el-card>
<!-- 提交组件 -->
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<approvalRecord ref="approvalRecordRef"></approvalRecord>
<!-- 流程选择对话框 -->
<el-dialog
draggable
v-model="dialogVisible.visible"
:title="dialogVisible.title"
:before-close="handleClose"
width="500"
class="rounded-lg shadow-lg"
>
<div class="p-4">
<p class="text-gray-600 mb-4">请选择要启动的流程</p>
<el-select v-model="flowCode" placeholder="请选择流程" style="width: 100%">
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<template #footer>
<div class="dialog-footer p-4 border-t border-gray-100 flex justify-end space-x-3">
<el-button @click="handleClose" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
>取消</el-button
>
<el-button type="primary" @click="submitFlow()" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
>确认</el-button
>
</div>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup name="Leave" lang="ts">
import { LeaveForm, LeaveQuery, LeaveVO } from '@/api/workflow/leave/types';
import { startWorkFlow } from '@/api/workflow/task';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { useUserStoreHook } from '@/store/modules/user';
const { design_change_reason_type } = toRefs<any>(proxy?.useDict('design_change_reason_type'));
import { getKnowledgeDocument } from '@/api/design/technicalStandard';
import { getCollectFile } from '@/api/design/condition';
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const buttonLoading = ref(false);
const loading = ref(true);
//路由参数
const routeParams = ref<Record<string, any>>({});
const flowCode = ref<string>('');
const status = ref<string>('');
const dialogVisible = reactive<DialogOption>({
visible: false,
title: '流程定义'
});
//提交组件
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
//审批记录组件
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
//按钮组件
const flowCodeOptions = [
{
value: currentProject.value?.id + '_collectFile',
label: '设计输入文件'
}
];
const leaveFormRef = ref<ElFormInstance>();
const dialog = reactive({
visible: false,
title: '',
isEdit: false
});
const submitFormData = ref<StartProcessBo>({
businessId: '',
flowCode: '',
variables: {}
});
const taskVariables = ref<Record<string, any>>({});
const initFormData = {
id: undefined,
fileName: undefined,
fileUrl: undefined,
status: undefined,
originalName: undefined
};
const data = reactive({
form: { ...initFormData },
rules: {}
});
const onOpen = () => {
window.open(form.value.fileUrl, '_blank');
};
const handleClose = () => {
dialogVisible.visible = false;
flowCode.value = '';
buttonLoading.value = false;
};
const { form, rules } = toRefs(data);
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
leaveFormRef.value?.resetFields();
};
/** 获取详情 */
const getInfo = () => {
loading.value = true;
buttonLoading.value = false;
nextTick(async () => {
const res = await getCollectFile(routeParams.value.id);
Object.assign(form.value, res.data);
loading.value = false;
buttonLoading.value = false;
});
};
/** 提交按钮 */
const submitForm = (status1: string) => {
status.value = status1;
submit(status.value, form.value);
};
const submitFlow = async () => {
handleStartWorkFlow(form.value);
dialogVisible.visible = false;
};
//提交申请
const handleStartWorkFlow = async (data: LeaveForm) => {
try {
submitFormData.value.flowCode = flowCode.value;
submitFormData.value.businessId = data.id;
//流程变量
taskVariables.value = {
// leave4/5 使用的流程变量
userList: ['1', '3', '4']
};
submitFormData.value.variables = taskVariables.value;
const resp = await startWorkFlow(submitFormData.value);
if (submitVerifyRef.value) {
buttonLoading.value = false;
submitVerifyRef.value.openDialog(resp.data.taskId);
}
} finally {
buttonLoading.value = false;
}
};
//审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value.init(form.value.id);
};
//提交回调
const submitCallback = async () => {
await proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
};
//审批
const approvalVerifyOpen = async () => {
submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
// 图纸上传成功之后 开始提交
const submit = async (status, data) => {
form.value = data;
if (status === 'draft') {
buttonLoading.value = false;
proxy?.$modal.msgSuccess('暂存成功');
proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
} else {
if ((form.value.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
flowCode.value = flowCodeOptions[0].value;
dialogVisible.visible = true;
return;
}
//说明启动过先随意穿个参数
if (flowCode.value === '' || flowCode.value === null) {
flowCode.value = 'xx';
}
await handleStartWorkFlow(data);
}
};
onMounted(() => {
nextTick(async () => {
routeParams.value = proxy.$route.query;
reset();
loading.value = false;
console.log(routeParams.value.type);
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
getInfo();
}
});
});
</script>
<style scoped lang="scss">
/* 全局样式 */
:root {
--primary: #409eff;
--primary-light: #66b1ff;
--primary-dark: #3a8ee6;
--success: #67c23a;
--warning: #e6a23c;
--danger: #f56c6c;
--info: #909399;
}
/* 表单样式优化 */
.el-form-item {
.el-form-item__label {
color: #606266;
font-weight: 500;
}
.el-input__inner,
.el-select .el-input__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
.el-textarea__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
}
/* 按钮样式优化 */
.el-button {
border-radius: 4px;
transition: all 0.2s;
&.is-primary {
background-color: var(--primary);
border-color: var(--primary);
&:hover {
background-color: var(--primary-light);
border-color: var(--primary-light);
}
&:active {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
}
&.is-text {
color: var(--primary);
&:hover {
color: var(--primary-light);
background-color: rgba(64, 158, 255, 0.05);
}
}
}
/* 卡片样式优化 */
.el-card {
transition: all 0.3s ease;
&:hover {
/* transform: translateY(-2px); */
}
}
/* 对话框样式优化 */
.el-dialog {
.el-dialog__header {
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
padding: 15px 20px;
}
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-dialog__footer {
padding: 15px 20px;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@ -0,0 +1,285 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="110px">
<el-form-item label="申请单编号" prop="formNo">
<el-input v-model="queryParams.formNo" placeholder="请输入申请单编号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="工程名称" prop="projectName">
<el-input v-model="queryParams.projectName" placeholder="请输入工程名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="卷册号" prop="volumeNo">
<el-input v-model="queryParams.volumeNo" placeholder="请输入卷册号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery" v-hasPermi="['design:designChange:list']">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="ChatRound" @click="handleAdd" v-hasPermi="['design:designChange:add']">下发变更通知</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="designChangeList">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="申请单编号" align="center" prop="formNo" width="150" />
<el-table-column label="工程名称" align="center" prop="projectName" width="150" />
<el-table-column label="提出单位" align="center" prop="submitUnit" width="150" />
<el-table-column label="专业" align="center" prop="specialtyName" width="120" />
<el-table-column label="提出日期" align="center" prop="submitDate" width="150">
<template #default="scope">
<span>{{ parseTime(scope.row.submitDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="卷册号" align="center" prop="volumeNo" width="150" />
<el-table-column label="流程状态" align="center">
<template #default="scope">
<dict-tag v-if="scope.row.fileId != null" :options="wf_business_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="变更文件" align="center" width="150">
<template #default="scope">
<el-button
link
type="primary"
v-if="scope.row.ossVoList && scope.row.ossVoList.length > 0"
icon="View"
@click="handleDesignView(scope.row)"
>查看文件</el-button
>
</template>
</el-table-column>
<el-table-column label="变更原因" align="center" prop="changeReason" width="150">
<template #default="scope">
<dict-tag :options="design_change_reason_type" :value="scope.row.changeReason ? scope.row.changeReason.split(',') : []" />
</template>
</el-table-column>
<el-table-column label="变更内容" align="center" prop="changeContent" width="150" />
<el-table-column label="备注" align="center" prop="remark" width="150" />
<el-table-column label="操作" fixed="right" width="300">
<template #default="scope">
<el-button
type="primary"
link
icon="Upload"
@click="handleAddChange(scope.row)"
v-if="scope.row.status == 'draft' || scope.row.status == 'back'"
>上传</el-button
>
<el-button
type="success"
link
icon="View"
v-if="scope.row.status != 'draft'"
v-hasPermi="['design:designChange:query']"
@click="handleViewInfo(scope.row)"
>查看</el-button
>
<el-button type="success" link icon="View" v-hasPermi="['design:designChange:query']" @click="handleViewDetail(scope.row)"
>通知单</el-button
>
<el-button
type="warning"
link
icon="View"
v-hasPermi="['design:designChange:query']"
v-if="scope.row.status == 'back' || scope.row.status == 'termination'"
@click="handleViewHistory(scope.row)"
>查看单据</el-button
>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<wordDetial ref="wordDetialRef"></wordDetial>
<el-dialog draggable title="文件列表" v-model="viewVisible" width="500px">
<el-table v-if="fileList.length > 0" :data="fileList" style="width: 100%" border>
<el-table-column prop="fileName" label="文件名称" align="center">
<template #default="scope">
<el-link
:key="scope.row.fileId"
:href="scope.row.fileUrl"
target="_blank"
:type="scope.row.status == '1' ? 'primary' : 'info'"
:underline="false"
>
{{ scope.row.originalName }}
</el-link>
</template>
</el-table-column>
<el-table-column label="操作" width="170" align="center">
<template #default="scope">
<el-button type="success" link icon="View" @click="handleDownload(scope.row)"> 查看 </el-button>
</template>
</el-table-column>
</el-table>
<div v-else class="empty-list text-center">暂无文件</div>
<template #footer>
<span>
<el-button type="primary" @click="viewVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
<el-dialog title="单据" v-model="viewDetailVisible" width="800px">
<histroy ref="histroyRef"></histroy>
</el-dialog>
</div>
</template>
<script setup name="DesignChange" lang="ts">
import { listDesignChange, delDesignChange } from '@/api/design/designChange';
import { DesignChangeVO } from '@/api/design/designChange/types';
import { useUserStoreHook } from '@/store/modules/user';
import wordDetial from '@/components/wordDetial/index';
import histroy from '../volumeCatalog/comm/histroy.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { design_change_reason_type } = toRefs<any>(proxy?.useDict('design_change_reason_type'));
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const designChangeList = ref<DesignChangeVO[]>([]);
const wordDetialRef = ref<InstanceType<typeof wordDetial>>();
const histroyRef = ref<InstanceType<typeof histroy>>();
const loading = ref(true);
const showSearch = ref(true);
const total = ref(0);
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
const queryFormRef = ref<ElFormInstance>();
const uploadUrl = computed(() => {
return `/design/collectFile/upload`;
});
const viewVisible = ref(false);
const fileList = ref([]);
const ossid = ref(null);
const viewDetailVisible = ref(false);
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
formNo: undefined,
projectName: undefined,
submitUnit: undefined,
specialty: undefined,
submitDate: undefined,
volumeName: undefined,
volumeNo: undefined,
changeReason: undefined,
status: undefined,
params: {}
}
});
const { queryParams } = toRefs(data);
/** 查询设计变更管理列表 */
const getList = async () => {
loading.value = true;
const res = await listDesignChange(queryParams.value);
designChangeList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 下发通知 */
const handleAdd = () => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/designChange/indexEdit`,
query: {
type: 'add'
}
});
};
/** 查看详情 */
const handleViewDetail = (row) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/designChange/indexEdit`,
query: {
id: row.id,
type: 'view'
}
});
};
// 上传变更图纸
const handleAddChange = (row) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/drawing/indexEdit`,
query: {
id: row.id,
type: 'add'
}
});
};
/** 查看流程操作 */
const handleViewInfo = (row) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/drawing/indexEdit`,
query: {
id: row.id,
type: 'view'
}
});
};
/** 删除按钮操作 */
const handleDesignView = async (row?: DesignChangeVO) => {
fileList.value = row.ossVoList;
viewVisible.value = true;
};
const handleViewHistory = async (row) => {
// 查看历史流程记录
viewDetailVisible.value = true;
nextTick(() => {
histroyRef.value?.getList(row.id);
});
};
// 预览
const onOpen = (path: string) => {
window.open(path, '_blank');
};
const handleDownload = (row: any) => {
window.open(row.url, '_blank');
};
onMounted(() => {
getList();
});
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
queryParams.value.projectId = nid;
getList();
}
);
onUnmounted(() => {
listeningProject();
});
</script>

View File

@ -0,0 +1,437 @@
<template>
<div class="p-4 bg-gray-50 designChangeForm">
<div class="max-w-4xl mx-auto">
<!-- 表单区域 -->
<div v-if="routeParams.type == 'view'" style="width: 100%; text-align: right; margin-bottom: 10px">
<el-button @click="goBack" size="large" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
>返回</el-button
>
</div>
<el-card class="rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md overflow-hidden">
<div class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-800">下发变更通知信息</h3>
</div>
<div class="p-6">
<el-form
ref="leaveFormRef"
:disabled="routeParams.type === 'view' || form.status == 'waiting'"
:model="form"
:rules="rules"
label-width="100px"
class="space-y-4"
>
<div class="grid grid-cols-1 gap-4">
<el-row>
<el-col :span="12">
<el-form-item label="申请单编号" prop="formNo">
<el-input v-model="form.formNo" placeholder="请输入申请单编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工程名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入工程名称" /> </el-form-item
></el-col>
<el-col :span="12">
<el-form-item label="原卷册号" prop="volumeNo">
<el-select
id="projectSelect"
v-model="form.volumeNo"
placeholder="请选择原卷册号"
clearable
filterable
@change="handleSelect"
style="width: 150px; margin-right: 20px"
>
<el-option
v-for="project in volumeCatalogList"
:key="project.volumeNumber"
:label="project.volumeNumber"
:value="project.volumeNumber"
/>
</el-select> </el-form-item
></el-col>
<el-col :span="12">
<el-form-item label="提出单位" prop="submitUnit">
<el-input v-model="form.submitUnit" placeholder="请输入提出单位" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业" prop="specialtyName">
<el-input disabled v-model="form.specialtyName" placeholder="请输入专业" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="提出日期" prop="submitDate">
<el-date-picker clearable v-model="form.submitDate" type="date" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择提出日期">
</el-date-picker> </el-form-item
></el-col>
<!-- <el-col :span="12">
<el-form-item label="卷册名称" prop="volumeName"> <el-input v-model="form.volumeName" placeholder="请输入卷册名称" /> </el-form-item
></el-col> -->
<el-col :span="12">
<el-form-item label="子项名称" prop="subName">
<el-input disabled v-model="form.extendDetail.subName" placeholder="请输入子项名称" /> </el-form-item
></el-col>
<el-col :span="24">
<el-form-item label="原设计处置" prop="designDisposal">
<el-radio-group v-model="form.extendDetail.designDisposal">
<el-radio value="1" size="large">原图作废</el-radio>
<el-radio value="2" size="large">原图保留部分修改</el-radio>
<el-radio value="3" size="large">原图保留补充设计</el-radio>
</el-radio-group>
</el-form-item></el-col
>
<el-col :span="24" v-if="form.extendDetail.designDisposal == 2 && form.volumeNo">
<el-form-item label="选择保留文件" prop="designPhase">
<el-select
id="projectSelect"
v-model="form.saveFile"
placeholder="请选择保留文件"
multiple
style="width: 150px; margin-right: 20px"
>
<el-option v-for="project in fileVoList" :key="project.id" :label="project.fileName" :value="project.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设计阶段" prop="designPhase">
<el-input v-model="form.extendDetail.designPhase" placeholder="请输入设计阶段" /> </el-form-item
></el-col>
<el-col :span="24">
<el-form-item label="变更类别" prop="changeCategory">
<el-radio-group v-model="form.extendDetail.changeCategory">
<el-radio value="1" size="large">重大设计变更</el-radio>
<el-radio value="2" size="large">一般设计变更</el-radio>
</el-radio-group>
</el-form-item></el-col
>
<el-col :span="24">
<el-form-item label="实施程序" prop="ImpProcedure">
<el-radio-group v-model="form.extendDetail.ImpProcedure">
<el-radio value="1" size="large">建设单位重新申报初步设计审批</el-radio>
<el-radio value="2" size="large">建设单位送原施工图审查机构审查建设主管部分备案后交付实施</el-radio>
<el-radio value="3" size="large">建设单位确认后交付实施</el-radio>
</el-radio-group>
</el-form-item></el-col
>
<el-col :span="12">
<el-form-item label="更改相关专业" prop="involvingProfessions">
<el-input v-model="form.extendDetail.involvingProfessions" placeholder="请输入更改相关专业" /> </el-form-item
></el-col>
<el-col :span="24">
<el-form-item label="附图" prop="attachmentPic"> <image-upload v-model="form.attachmentPic" :fileSize="100" /> </el-form-item
></el-col>
<el-col :span="24">
<el-form-item label="变更原因" prop="changeReason">
<el-checkbox-group v-model="form.changeReason">
<el-checkbox v-for="dict in design_change_reason_type" :key="dict.value" :value="dict.value">
{{ dict.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item></el-col
>
<el-col :span="24">
<el-form-item label="变更内容" prop="changeContent">
<el-input v-model="form.changeContent" type="textarea" placeholder="请输入内容" /> </el-form-item
></el-col>
<el-col :span="12">
<el-form-item label="费用" prop="costEstimation">
<el-input v-model="form.costEstimation" type="number" placeholder="请输入费用" /> </el-form-item
></el-col>
<el-col :span="24">
<el-form-item label="变更费用估算表" label-width="110px" prop="costEstimationFile">
<file-upload v-model="form.costEstimationFile" :fileSize="100" /> </el-form-item
></el-col>
<!-- <el-col :span="24">
<el-form-item label="变更文件" prop="fileId"> <file-upload v-model="form.fileId" :fileSize="100" /> </el-form-item
></el-col> -->
<el-col :span="24"
><el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" /> </el-form-item
></el-col>
</el-row>
</div>
</el-form>
</div>
<div class="flex justify-center gap-4 mt-8">
<el-button
v-if="routeParams.type != 'view'"
@click="goBack"
size="large"
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
>返回</el-button
>
<el-button
type="primary"
size="large"
v-if="routeParams.type != 'view'"
@click="submitForm"
class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
>确认</el-button
>
</div>
</el-card>
</div>
</div>
</template>
<script setup name="Leave" lang="ts">
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { useUserStoreHook } from '@/store/modules/user';
import { addDesignChange, getDesignChange } from '@/api/design/designChange';
import { listVolumeCatalog } from '@/api/design/volumeCatalog';
const { design_change_reason_type } = toRefs<any>(proxy?.useDict('design_change_reason_type'));
const route = useRoute();
const router = useRouter();
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const buttonLoading = ref(false);
const volumeCatalogList = ref([]);
let volumeMap = new Map();
//路由参数
const routeParams = ref<Record<string, any>>({});
const leaveFormRef = ref<ElFormInstance>();
const dialog = reactive({
visible: false,
title: '',
isEdit: false
});
const fileVoList = ref([]);
const initFormData = {
id: undefined,
projectId: currentProject.value?.id,
formNo: undefined,
projectName: undefined,
submitUnit: undefined,
specialty: undefined,
specialtyName: undefined,
submitDate: undefined,
volumeName: undefined,
volumeNo: undefined,
attachmentPic: undefined,
changeReason: [],
changeContent: undefined,
costEstimation: undefined,
costEstimationFile: undefined,
fileId: undefined,
status: undefined,
remark: undefined,
saveFile: undefined,
extendDetail: {
changeCategory: undefined,
ImpProcedure: undefined,
involvingProfessions: undefined,
subName: undefined,
designDisposal: undefined,
designPhase: undefined
}
};
const data = reactive({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
fileName: undefined,
fileType: undefined,
fileSuffix: undefined,
fileStatus: undefined,
originalName: undefined,
newest: undefined,
params: {}
},
rules: {
// 卷册号
volumeNo: [{ required: true, message: '请请选择卷册号', trigger: 'change' }]
}
});
const { form, rules } = toRefs(data);
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
leaveFormRef.value?.resetFields();
};
//返回
const goBack = () => {
proxy.$tab.closePage(route);
router.go(-1);
};
/** 提交按钮 */
const submitForm = () => {
var changeReason = '';
if (form.value.changeReason && form.value.changeReason.length > 0) {
changeReason = form.value.changeReason.join(',');
}
var saveFile = '';
if (form.value.saveFile && form.value.saveFile.length > 0) {
saveFile = form.value.saveFile.join(',');
}
leaveFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
var res;
res = await addDesignChange({ ...form.value, changeReason, saveFile }).finally(() => (buttonLoading.value = false));
if (res.code == 200) {
ElMessage.success('通知成功');
goBack();
} else {
ElMessage.error(res.msg);
}
}
});
};
/** 查询卷册目录列表 */
const getList = async () => {
const res = await listVolumeCatalog({ projectId: currentProject.value?.id, auditStatus: 'finish' });
volumeCatalogList.value = res.rows;
volumeCatalogList.value.forEach((e) => {
volumeMap.set(e.volumeNumber, e);
});
};
const handleSelect = (val) => {
let obj = volumeMap.get(val);
console.log(obj);
fileVoList.value = obj.fileVoList;
form.value.volumeName = obj.volumeName;
form.value.specialty = obj.specialty;
form.value.specialtyName = obj.specialtyName;
form.value.extendDetail.subName = obj.designSubitem;
};
/** 获取详情 */
const getInfo = () => {
buttonLoading.value = false;
nextTick(async () => {
const res = await getDesignChange(routeParams.value.id);
Object.assign(form.value, res.data);
if (form.value.changeReason.length > 0) {
form.value.changeReason = form.value.changeReason.split(',');
}
buttonLoading.value = false;
});
};
onMounted(() => {
nextTick(async () => {
routeParams.value = proxy.$route.query;
reset();
getList();
if (routeParams.value.type != 'add') {
getInfo();
}
});
});
</script>
<style scoped lang="scss">
.designChangeForm {
.el-select {
width: 300px !important;
}
}
/* 全局样式 */
:root {
--primary: #409eff;
--primary-light: #66b1ff;
--primary-dark: #3a8ee6;
--success: #67c23a;
--warning: #e6a23c;
--danger: #f56c6c;
--info: #909399;
}
/* 表单样式优化 */
.el-form-item {
.el-form-item__label {
color: #606266;
font-weight: 500;
}
.el-input__inner,
.el-select .el-input__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
.el-textarea__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
}
/* 按钮样式优化 */
.el-button {
border-radius: 4px;
transition: all 0.2s;
&.is-primary {
background-color: var(--primary);
border-color: var(--primary);
&:hover {
background-color: var(--primary-light);
border-color: var(--primary-light);
}
&:active {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
}
&.is-text {
color: var(--primary);
&:hover {
color: var(--primary-light);
background-color: rgba(64, 158, 255, 0.05);
}
}
}
/* 卡片样式优化 */
.el-card {
transition: all 0.3s ease;
&:hover {
/* transform: translateY(-2px); */
}
}
/* 对话框样式优化 */
.el-dialog {
.el-dialog__header {
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
padding: 15px 20px;
}
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-dialog__footer {
padding: 15px 20px;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@ -0,0 +1,107 @@
<template>
<el-table v-loading="loading" :data="drawingList" height="74vh">
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column label="子项名称" align="center" prop="designSubitem" />
<el-table-column label="专业" align="center" prop="specialty">
<template #default="scope">
<dict-tag :options="des_user_major" :value="scope.row.specialty" />
</template>
</el-table-column>
<el-table-column label="负责人" align="center" prop="principal" />
<el-table-column label="卷册号" align="center" prop="volumeNumber" />
<el-table-column label="资料名称" align="center" prop="fileName">
<template #default="scope">
<el-link type="primary" :underline="false" :href="scope.row.url" target="_blank">{{ scope.row.fileName }}</el-link>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding " width="240">
<template #default="scope">
<el-button size="small" type="primary" icon="Download" @click="handleDownload(scope.row)">下载</el-button>
<el-button size="small" type="primary" icon="view" @click="handleViewHis(scope.row)">查阅记录</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog draggable title="文件列表" v-model="viewVisible" width="500px">
<el-table :data="histroyList" style="width: 100%" border>
<el-table-column type="index" label="序号" align="center" width="80"> </el-table-column>
<el-table-column prop="userName" label="用户名称" align="center"> </el-table-column>
<el-table-column prop="createTime" label="查阅时间" align="center"> </el-table-column>
</el-table>
<template #footer>
<span>
<el-button type="primary" @click="viewVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits } from 'vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { volumeFileViewer, volumeFileViewerList } from '@/api/design/drawing';
const { des_user_major } = toRefs(proxy?.useDict('des_user_major'));
const props = defineProps({
drawingList: {
type: Array,
required: true
},
loading: {
type: Boolean,
required: true
},
drawing_file_type: {
type: Array,
required: true
},
wf_business_status: {
type: Array,
required: true
}
});
const viewVisible = ref(false);
const histroyList = ref([]);
const emits = defineEmits(['selection-change', 'view', 'update', 'delete', 'view-info', 'cancel-process-apply']);
const handleSelectionChange = (selection) => {
emits('selection-change', selection);
};
const handleView = (row) => {
emits('view', row);
};
const handleUpdate = (row) => {
emits('update', row);
};
const handleDelete = (row) => {
emits('delete', row);
};
const handleViewInfo = (row) => {
emits('view-info', row);
};
const handleCancelProcessApply = (id) => {
emits('cancel-process-apply', id);
};
const handleViewHis = async (row) => {
viewVisible.value = true;
let res = await volumeFileViewerList(row.volumeFileId);
if (res.code == 200) {
histroyList.value = res.rows;
}
};
const handleDownload = (row) => {
getCheck(row);
proxy?.$download.oss(row.fileUrl);
};
// 调用查阅接口
const getCheck = async (row) => {
volumeFileViewer({ volumeFileId: row.volumeFileId });
};
</script>

View File

@ -0,0 +1,242 @@
<template>
<div class="p-2 volumeCatalog">
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="卷册号" prop="volumeNumber">
<el-input v-model="queryParams.volumeNumber" placeholder="请输入卷册号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="资料名称" prop="documentName">
<el-input v-model="queryParams.documentName" placeholder="请输入资料名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery" v-hasPermi="['design:volumeCatalog:query']">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery" v-hasPermi="['design:volumeCatalog:query']">重置</el-button>
</el-form-item>
</el-form>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="volumeCatalogList">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="子项名称" align="center" prop="designSubitem" />
<el-table-column label="专业" align="center" prop="specialtyName"> </el-table-column>
<el-table-column label="设计人员" align="center" prop="principalName" />
<el-table-column label="卷册号" align="center" prop="volumeNumber" />
<el-table-column label="资料名称" align="center" prop="documentName" />
<el-table-column label="图纸文件" align="center" prop="remark" width="150">
<template #default="scope">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['design:volumeFile:query']">查看文件</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 查看文件列表 -->
<el-dialog draggable title="文件列表" v-model="viewVisible" width="800px">
<el-table :data="fileList" style="width: 100%" border>
<el-table-column prop="fileName" label="文件" align="center">
<template #default="scope">
<el-link
:key="scope.row.fileId"
:href="scope.row.fileUrl"
target="_blank"
:type="scope.row.status == '1' ? 'primary' : 'info'"
:underline="false"
@click="handleBookFile(scope.row)"
>
{{ scope.row.fileName }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="size" label="状态" width="120" align="center">
<template #default="scope">
<el-tag :type="scope.row.status == 1 ? 'success' : 'info'">{{ scope.row.status == 1 ? '使用中' : '已作废' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="240" align="center">
<template #default="scope">
<el-button link type="primary" icon="view" @click="handleViewHis(scope.row)">查阅记录</el-button>
<el-button type="danger" link icon="Download" @click="handleDownload(scope.row)"> 下载 </el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<span>
<el-button type="primary" @click="viewVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
<el-dialog draggable title="文件列表" v-model="viewVisible1" width="500px">
<el-table :data="histroyList" style="width: 100%" border>
<el-table-column type="index" label="序号" align="center" width="80"> </el-table-column>
<el-table-column prop="userName" label="用户名称" align="center"> </el-table-column>
<el-table-column prop="createTime" label="查阅时间" align="center"> </el-table-column>
</el-table>
<template #footer>
<span>
<el-button type="primary" @click="viewVisible1 = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup name="VolumeCatalog" lang="ts">
import { listVolumeCatalog, addVolumeCatalog, updateVolumeCatalog } from '@/api/design/volumeCatalog';
import { VolumeCatalogVO } from '@/api/design/volumeCatalog/types';
import { useUserStoreHook } from '@/store/modules/user';
import { volumeFileViewer, volumeFileViewerList } from '@/api/design/drawing';
const fileList = ref([]);
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const volumeCatalogList = ref<VolumeCatalogVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const total = ref(0);
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const queryFormRef = ref<ElFormInstance>();
const volumeCatalogFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const uploadForm = reactive({
userIds: [],
volumeCatalogId: undefined,
fileId: undefined,
explainText: '',
fileList: [],
cancellationIds: [] // 用于存储已作废的文件ID
});
const initFormData: any = {
design: undefined,
projectId: currentProject.value?.id || '',
designSubitemId: undefined,
volumeNumber: undefined,
documentName: undefined,
designState: '2',
remark: undefined
};
const data = reactive({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
designSubitemId: undefined,
volumeNumber: undefined,
documentName: undefined,
params: {}
},
rules: {
design: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
projectId: [{ required: true, message: '项目ID不能为空', trigger: 'blur' }],
volumeNumber: [{ required: true, message: '卷册号不能为空', trigger: 'blur' }],
documentName: [{ required: true, message: '资料名称不能为空', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
const histroyList = ref([]);
/** 查询卷册目录列表 */
const getList = async () => {
loading.value = true;
try {
const res = await listVolumeCatalog(queryParams.value);
volumeCatalogList.value = res.rows;
total.value = res.total;
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
const handleView = (row?: any) => {
fileList.value = row.fileVoList;
viewVisible.value = true;
};
const viewVisible = ref(false);
const viewVisible1 = ref(false);
/** 重置上传表单 */
const resetUploadForm = () => {
uploadForm.userIds = [];
uploadForm.volumeCatalogId = undefined;
uploadForm.fileId = undefined;
uploadForm.explainText = '';
uploadForm.fileList = [];
uploadForm.cancellationIds = []; // 重置作废文件ID列表
};
/** 提交按钮 */
const submitForm = () => {
volumeCatalogFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.design) {
await updateVolumeCatalog(form.value).finally(() => (buttonLoading.value = false));
} else {
await addVolumeCatalog(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
const handleDownload = (row: any) => {
getCheck(row);
proxy?.$download.oss(row.fileId);
};
const handleBookFile = (row: any) => {
getCheck(row);
};
// 调用查阅接口
const getCheck = async (row) => {
volumeFileViewer({ volumeFileId: row.fileId });
};
const handleViewHis = async (row) => {
viewVisible1.value = true;
let res = await volumeFileViewerList(row.fileId);
if (res.code == 200) {
histroyList.value = res.rows;
}
};
onMounted(() => {
getList();
});
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
queryParams.value.projectId = nid;
form.value.projectId = nid;
getList();
}
);
onUnmounted(() => {
listeningProject();
});
</script>

View File

@ -0,0 +1,385 @@
<template>
<div class="p-4 bg-gray-50">
<div class="max-w-4xl mx-auto">
<!-- 顶部按钮区域 -->
<el-card class="mb-4 rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.id"
:status="form.status"
:pageType="routeParams.type"
/>
</el-card>
<!-- 表单区域 -->
<el-card class="rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md overflow-hidden">
<div class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-800">变更图纸信息</h3>
</div>
<div class="p-6">
<el-form
ref="leaveFormRef"
:disabled="routeParams.type === 'view' || form.status == 'waiting'"
:model="form"
:rules="rules"
label-width="100px"
class="space-y-4"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<el-form-item label="图纸文件" prop="fileId" class="mb-2 md:col-span-2">
<file-upload :fileType="['pdf']" v-model="form.fileId" class="w-full"></file-upload>
</el-form-item>
</div>
</el-form>
</div>
</el-card>
<!-- 提交组件 -->
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<approvalRecord ref="approvalRecordRef"></approvalRecord>
<!-- 流程选择对话框 -->
<el-dialog
draggable
v-model="dialogVisible.visible"
:title="dialogVisible.title"
:before-close="handleClose"
width="500"
class="rounded-lg shadow-lg"
>
<div class="p-4">
<p class="text-gray-600 mb-4">请选择要启动的流程</p>
<el-select v-model="flowCode" placeholder="请选择流程" style="width: 100%">
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<template #footer>
<div class="dialog-footer p-4 border-t border-gray-100 flex justify-end space-x-3">
<el-button @click="handleClose" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
>取消</el-button
>
<el-button type="primary" @click="submitFlow()" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
>确认</el-button
>
</div>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup name="Leave" lang="ts">
import { LeaveForm } from '@/api/workflow/leave/types';
import { startWorkFlow } from '@/api/workflow/task';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { useUserStoreHook } from '@/store/modules/user';
import { getDrawing } from '@/api/design/drawing';
import { updateDesignChange, getDesignChange } from '@/api/design/designChange';
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const buttonLoading = ref(false);
const loading = ref(true);
//路由参数
const routeParams = ref<Record<string, any>>({});
const flowCodeOptions = [
{
value: currentProject.value?.id + '_designchanged',
label: '变更图纸审批'
}
];
const flowCode = ref<string>('');
const status = ref<string>('');
const dialogVisible = reactive<DialogOption>({
visible: false,
title: '流程定义'
});
//提交组件
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
//审批记录组件
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
//按钮组件
const approvalButtonRef = ref<InstanceType<typeof ApprovalButton>>();
const leaveFormRef = ref<ElFormInstance>();
const dialog = reactive({
visible: false,
title: '',
isEdit: false
});
const submitFormData = ref<StartProcessBo>({
businessId: '',
flowCode: '',
variables: {}
});
const taskVariables = ref<Record<string, any>>({});
const initFormData = {
id: undefined,
projectId: currentProject.value?.id,
versionNumber: undefined,
fileName: undefined,
fileUrl: undefined,
fileType: undefined,
fileSuffix: undefined,
originalName: undefined,
remark: undefined,
fileId: undefined
};
const data = reactive({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
fileName: undefined,
fileType: undefined,
fileSuffix: undefined,
fileStatus: undefined,
originalName: undefined,
newest: undefined,
params: {}
},
rules: {
fileId: [
{
validator: (rule, value, callback) => {
// 新增时必须上传文件
if (!form.value.fileId) {
callback(new Error('请上传变更图纸文件'));
} else {
callback();
}
},
trigger: 'change'
}
]
}
});
const handleClose = () => {
dialogVisible.visible = false;
flowCode.value = '';
buttonLoading.value = false;
};
const { form, rules } = toRefs(data);
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
leaveFormRef.value?.resetFields();
};
/** 获取详情 */
const getInfo = () => {
loading.value = true;
buttonLoading.value = false;
nextTick(async () => {
const res = await getDesignChange(routeParams.value.id);
Object.assign(form.value, res.data);
loading.value = false;
buttonLoading.value = false;
});
};
/** 提交按钮 */
const submitForm = (status1: string) => {
status.value = status1;
leaveFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
var res;
// if (form.value.id) {
res = await updateDesignChange({ ...form.value, id: routeParams.value.id }).finally(() => (buttonLoading.value = false));
// }
if (res.code == 200) {
dialog.visible = false;
submit(status.value, res.data);
}
}
});
};
const submitFlow = async () => {
handleStartWorkFlow(form.value);
dialogVisible.visible = false;
};
//提交申请
const handleStartWorkFlow = async (data: LeaveForm) => {
try {
submitFormData.value.flowCode = flowCode.value;
submitFormData.value.businessId = data.id;
//流程变量
taskVariables.value = {
// leave4/5 使用的流程变量
userList: ['1', '3', '4']
};
submitFormData.value.variables = taskVariables.value;
const resp = await startWorkFlow(submitFormData.value);
if (submitVerifyRef.value) {
buttonLoading.value = false;
submitVerifyRef.value.openDialog(resp.data.taskId);
}
} finally {
buttonLoading.value = false;
}
};
//审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value.init(form.value.id);
};
//提交回调
const submitCallback = async () => {
await proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
};
//审批
const approvalVerifyOpen = async () => {
submitVerifyRef.value.openDialog(routeParams.value.taskId, true, routeParams.value.businessId);
// submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
// 图纸上传成功之后 开始提交
const submit = async (status, data) => {
form.value = data;
if (status === 'draft') {
buttonLoading.value = false;
proxy?.$modal.msgSuccess('暂存成功');
proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
} else {
if ((form.value.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
flowCode.value = flowCodeOptions[0].value;
dialogVisible.visible = true;
return;
}
//说明启动过先随意穿个参数
if (flowCode.value === '' || flowCode.value === null) {
flowCode.value = 'xx';
}
console.log(data);
await handleStartWorkFlow(data);
}
};
onMounted(() => {
nextTick(async () => {
routeParams.value = proxy.$route.query;
reset();
loading.value = false;
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
getInfo();
}
});
});
</script>
<style scoped lang="scss">
/* 全局样式 */
:root {
--primary: #409eff;
--primary-light: #66b1ff;
--primary-dark: #3a8ee6;
--success: #67c23a;
--warning: #e6a23c;
--danger: #f56c6c;
--info: #909399;
}
/* 表单样式优化 */
.el-form-item {
.el-form-item__label {
color: #606266;
font-weight: 500;
}
.el-input__inner,
.el-select .el-input__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
.el-textarea__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
}
/* 按钮样式优化 */
.el-button {
border-radius: 4px;
transition: all 0.2s;
&.is-primary {
background-color: var(--primary);
border-color: var(--primary);
&:hover {
background-color: var(--primary-light);
border-color: var(--primary-light);
}
&:active {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
}
&.is-text {
color: var(--primary);
&:hover {
color: var(--primary-light);
background-color: rgba(64, 158, 255, 0.05);
}
}
}
/* 卡片样式优化 */
.el-card {
transition: all 0.3s ease;
&:hover {
/* transform: translateY(-2px); */
}
}
/* 对话框样式优化 */
.el-dialog {
.el-dialog__header {
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
padding: 15px 20px;
}
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-dialog__footer {
padding: 15px 20px;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@ -0,0 +1,369 @@
<template>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" class="info-form">
<!-- 基本信息区域 -->
<div class="form-section">
<div class="section-title">
<el-divider content-position="left">
<span class="title-text">基本信息</span>
</el-divider>
</div>
<el-row :gutter="20" class="section-content">
<el-col :span="12">
<el-form-item label="编号" prop="num">
<el-input v-model="formData.num" placeholder="请输入编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业" prop="professional">
<el-input v-model="formData.professional" placeholder="请输入专业" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设计阶段" prop="stage">
<el-input v-model="formData.stage" placeholder="请输入设计阶段" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="卷册" prop="volume">
<el-input v-model="formData.volume" placeholder="请输入卷册" />
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 项目信息区域 -->
<div class="form-section">
<div class="section-title">
<el-divider content-position="left">
<span class="title-text">项目信息</span>
</el-divider>
</div>
<el-row :gutter="20" class="section-content">
<el-col :span="12">
<el-form-item label="项目" prop="projectName">
<el-input disabled v-model="formData.projectName" placeholder="请输入项目ID" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="子项目" prop="subprojectId">
<el-select v-model="formData.subprojectId" clearable placeholder="请选择子项目" style="width: 300px">
<el-option v-for="item in subProjectList" :key="item.id" :label="item.projectName" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 人员信息区域 -->
<div class="form-section">
<div class="section-title">
<el-divider content-position="left">
<span class="title-text">人员信息</span>
</el-divider>
</div>
<el-row :gutter="20" class="section-content">
<el-col :span="12">
<el-form-item label="设计人" prop="designer">
<el-input v-model="formData.designer" placeholder="请输入设计人" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="校审人员" prop="proofreading">
<el-input v-model="formData.proofreading" placeholder="请输入校审人员" />
</el-form-item>
</el-col>
<!-- <el-col :span="12">
<el-form-item label="校审人员ID" prop="proofreadingId">
<el-input v-model="formData.proofreadingId" placeholder="请输入校审人员ID" />
</el-form-item>
</el-col> -->
<el-col :span="12">
<el-form-item label="校审时间" prop="proofreadingDate">
<el-date-picker
v-model="formData.proofreadingDate"
type="date"
placeholder="选择校审时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="审核人员" prop="audit">
<el-input v-model="formData.audit" placeholder="请输入审核人员" />
</el-form-item>
</el-col>
<!-- <el-col :span="12">
<el-form-item label="审核人员ID" prop="auditId">
<el-input v-model="formData.auditId" placeholder="请输入审核人员ID" />
</el-form-item>
</el-col> -->
<el-col :span="12">
<el-form-item label="审核时间" prop="auditDate">
<el-date-picker v-model="formData.auditDate" type="date" placeholder="选择审核时间" format="YYYY-MM-DD" value-format="YYYY-MM-DD" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="执行人员" prop="executor">
<el-input v-model="formData.executor" placeholder="请输入执行人员" />
</el-form-item>
</el-col>
<!-- <el-col :span="12">
<el-form-item label="执行人员ID" prop="executorId">
<el-input v-model="formData.executorId" placeholder="请输入执行人员ID" />
</el-form-item>
</el-col> -->
<el-col :span="12">
<el-form-item label="执行时间" prop="executorDate">
<el-date-picker v-model="formData.executorDate" type="date" placeholder="选择执行时间" format="YYYY-MM-DD" value-format="YYYY-MM-DD" />
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 意见和内容区域 -->
<div class="form-section">
<div class="section-title">
<el-divider content-position="left">
<span class="title-text">意见和内容</span>
</el-divider>
</div>
<el-row :gutter="20" class="section-content">
<el-col :span="24">
<el-form-item label="验证内容" prop="verificationContent">
<el-input v-model="formData.verificationContent" type="textarea" placeholder="请输入验证内容" :rows="4" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="验证意见" prop="verificationOpinion">
<el-input v-model="formData.verificationOpinion" type="textarea" placeholder="请输入验证意见" :rows="4" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="执行意见" prop="executionOpinion">
<el-input v-model="formData.executionOpinion" type="textarea" placeholder="请输入执行意见" :rows="4" />
</el-form-item>
</el-col>
</el-row>
</div>
</el-form>
</template>
<script setup name="ExamineForm" lang="ts">
import { ref, watch, reactive } from 'vue';
import { fillOutTheDesignVerificationForm, drawingreviewReceipts } from '@/api/design/drawingreview';
import type { FormInstance, FormRules } from 'element-plus';
import { useUserStoreHook } from '@/store/modules/user';
import { computed } from 'vue';
import { subProjectListAll } from '@/api/design/drawingreview';
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
console.log(currentProject.value);
const subProjectList = ref([]);
let subProjectMap = new Map();
// 定义表单数据类型
interface FormData {
num: string;
professional: string;
stage: string;
volume: string;
projectId: string;
projectName: string;
subprojectId: string;
subprojectName: string;
designer: string;
proofreading: string;
proofreadingId: string;
proofreadingDate: string;
audit: string;
auditId: string;
auditDate: string;
executor: string;
executorId: string;
executorDate: string;
verificationContent: string;
verificationOpinion: string;
executionOpinion: string;
}
// 定义表单验证规则
const rules: FormRules = {
num: [{ required: true, message: '请输入编号', trigger: 'blur' }],
professional: [{ required: true, message: '请输入专业', trigger: 'blur' }]
};
// 表单数据 - 直接在组件内定义不再通过Props接收
const formData = reactive<FormData>({
num: '',
professional: '',
stage: '',
volume: '',
projectId: currentProject.value?.id || '',
projectName: currentProject.value?.name || '',
subprojectId: '',
subprojectName: '',
designer: '',
proofreading: '',
proofreadingId: '',
proofreadingDate: '',
audit: '',
auditId: '',
auditDate: '',
executor: '',
executorId: '',
executorDate: '',
verificationContent: '',
verificationOpinion: '',
executionOpinion: ''
});
// 表单引用
const formRef = ref<FormInstance | null>(null);
// 监听项目变化,可自动填充表单项目信息
watch(
currentProject,
(newVal) => {
if (newVal) {
// 根据实际项目结构调整赋值字段
formData.projectId = newVal.id || '';
formData.projectName = newVal.name || '';
}
},
{ immediate: true, deep: true }
);
// 通过项目获取子项目 - 监听项目 id 变化,可自动获取子项目
const getSubProject = async () => {
let res = await subProjectListAll(currentProject.value?.id);
subProjectList.value = res.data;
subProjectList.value.forEach((item: any) => {
subProjectMap.set(item.id, item.projectName);
});
};
// 验证表单
const validate = async () => {
if (formRef.value) {
return await formRef.value.validate();
}
return false;
};
// 重置表单
const resetFields = () => {
if (formRef.value) {
formRef.value.resetFields();
}
};
// 获取表单数据
const getFormData = (): FormData => {
return { ...formData };
};
// 设置表单数据
const setFormData = (data: Partial<FormData>) => {
Object.assign(formData, data);
};
// 提交表单
const submit = async (businessId) => {
try {
// 先验证表单
const isValid = await validate();
if (isValid) {
formData.subprojectName = subProjectMap.get(formData.subprojectId);
formData.drawingreviewId = businessId;
const res = await drawingreviewReceipts(formData);
if (res.code === 200) {
// // 提交成功处理逻辑
// console.log('提交成功');
}
}
} catch (error) {
console.error('提交失败:', error);
}
};
onMounted(() => {
getSubProject();
});
// 暴露方法给父组件(如果需要)
defineExpose({
validate,
resetFields,
getFormData,
setFormData,
submit
});
</script>
<style scoped>
/* 表单整体样式 */
.info-form {
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
/* 模块容器样式 */
.form-section {
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
/* 最后一个模块去掉底部边框 */
.form-section:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
/* 模块标题样式 */
.section-title {
margin-bottom: 20px;
}
.title-text {
font-size: 16px;
font-weight: 600;
color: #1f2329;
padding: 0 10px;
background-color: #fff;
position: relative;
}
/* 模块内容区域 */
.section-content {
margin-left: 10px;
}
/* 表单项间距调整 */
::v-deep .el-form-item {
margin-bottom: 18px;
}
/* 文本域样式优化 */
::v-deep .el-textarea__inner {
min-height: 100px;
resize: vertical;
}
/* 响应式调整 */
@media (max-width: 768px) {
.info-form {
padding: 15px;
}
.form-section {
margin-bottom: 20px;
}
.title-text {
font-size: 14px;
}
}
</style>

View File

@ -0,0 +1,384 @@
<template>
<div class="p-2 drawingreview">
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['design:drawingreview:add']">新增</el-button>
</el-col>
<right-toolbar @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="drawingreviewList">
<el-table-column type="index" label="序号" width="100" align="center" />
<el-table-column label="文件名称" align="center" prop="fileName" />
<el-table-column label="流程状态" align="center">
<template #default="scope">
<dict-tag :options="wf_business_status" :value="scope.row.auditType" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-row :gutter="10" class="mb8"
><el-col :span="1.5" v-if="scope.row.auditType === 'draft' || scope.row.auditType === 'cancel' || scope.row.auditType === 'back'">
<el-button size="small" type="primary" icon="Edit" @click="handleUpdate(scope.row)">审核</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" size="small" icon="View" v-if="scope.row.auditType != 'draft'" @click="handleViewInfo(scope.row)"
>查看流程</el-button
> </el-col
><el-col :span="1.5">
<el-button type="warning" size="small" icon="View" v-if="scope.row.auditType != 'draft'" @click="handleViewHistory(scope.row)"
>查看历史</el-button
>
</el-col></el-row
>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="drawingreviewFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="图纸文件" prop="auditType">
<el-upload ref="uploadRef" class="upload-demo" :http-request="importExcel">
<template #trigger>
<el-button type="primary">上传excel</el-button>
</template>
</el-upload>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialog.visible = false"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog draggable title="查看历史" v-model="dialogHistory" width="800px" append-to-body>
<div>
<span>选择历史退回记录</span>
<el-select v-model="hisId" placeholder="请选择" style="width: 240px" @change="handleShowInfo">
<el-option v-for="item in hisList" :key="item.id" :label="item.id" :value="item.id" />
</el-select>
<div style="margin-top: 20px">
<span style="color: #0d9df5">查看excel文件</span>
<div style="margin-top: 20px" class="table-content" id="table-content">
<el-row class="mb20" style="display: flex; justify-content: center">
<h2>设计验证表</h2>
</el-row>
<el-row class="mb10" style="display: flex; justify-content: space-between">
<div class="head-text">
<span>编号</span>
<span>{{ examineForm.num }}</span>
</div>
<div class="head-text"></div>
</el-row>
<table style="width: 100%" border="1" cellspacing="1">
<thead>
<tr>
<th colspan="2">工程名称</th>
<td class="th-bg" colspan="10">{{ examineForm.projectName }}</td>
</tr>
</thead>
<tbody>
<tr>
<th colspan="2">子项名称</th>
<td class="th-bg" colspan="6">{{ examineForm.subprojectName }}</td>
<th colspan="2">设计阶段</th>
<td class="th-bg" colspan="2">{{ examineForm.stage }}</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2">专业</th>
<td class="th-bg" colspan="2">{{ examineForm.professional }}</td>
<th colspan="2">卷册</th>
<td class="th-bg" colspan="2">{{ examineForm.volume }}</td>
<th colspan="2">设计人</th>
<td class="th-bg" colspan="2">{{ examineForm.designer }}</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2">验证内容</th>
<td class="th-bg" colspan="10">{{ examineForm.verificationContent }}</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="6">验证意见</th>
<th class="th-bg" colspan="6">执行意见</th>
</tr>
</tbody>
<tbody>
<tr>
<td colspan="6">
<div style="height: 400px">{{ examineForm.verificationOpinion }}</div>
</td>
<td class="th-bg" colspan="6">
<div style="height: 400px">{{ examineForm.executionOpinion }}</div>
</td>
</tr>
</tbody>
<tbody>
<tr>
<td colspan="6">
<div style="display: flex; align-items: center; justify-content: space-between">
<span> 校审 {{ examineForm.proofreading }} </span>
<span> {{ dateFormat(examineForm.proofreadingDate) }}</span>
</div>
</td>
<td rowspan="2" colspan="6">
<div>执行人 {{ examineForm.executor }}</div>
<div style="display: flex; align-items: center; justify-content: flex-end">
{{ dateFormat(examineForm.executorDate) }}
</div>
</td>
</tr>
<tr>
<td class="th-bg" colspan="6">
<div style="display: flex; align-items: center; justify-content: space-between">
<span>审核 {{ examineForm.audit }}</span>
<span> {{ dateFormat(examineForm.auditDate) }}</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup name="Drawingreview">
import { computed, getCurrentInstance, toRefs } from 'vue';
import {
listDrawingreview,
addDrawingreview,
updateDrawingreview,
ObtainHistoricalDesignDrawingsForReview,
drawingreview
} from '@/api/design/drawingreview';
import { useUserStoreHook } from '@/store/modules/user';
import { fromFile } from 'geotiff';
const { proxy } = getCurrentInstance();
const { wf_business_status } = toRefs(proxy?.useDict('wf_business_status'));
const userStore = useUserStoreHook();
const loading = ref(true);
const buttonLoading = ref(false);
const total = ref(0);
const drawingreviewList = ref([]);
const hisList = ref([]);
const hisId = ref('');
const fromData = new FormData();
const currentProject = computed(() => userStore.selectedProject);
const dialog = reactive({
visible: false,
title: ''
});
const dialogHistory = ref(false);
const initexamineForm = {
id: undefined,
projectId: currentProject.value?.id,
auditType: undefined,
remark: undefined
};
const drawingreviewFormRef = ref();
const data = reactive({
form: { ...initexamineForm },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
auditType: undefined,
params: {}
},
rules: {
id: [{ required: true, message: '项目ID不能为空', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
const examineForm = ref({
audit: '',
auditDate: '',
auditId: '',
designer: '',
executionOpinion: '',
executor: '',
executorDate: '',
executorId: '',
id: '1',
num: '',
professional: '',
projectId: '',
projectName: '',
proofreading: '',
proofreadingDate: '',
proofreadingId: '',
stage: '',
subprojectId: '',
subprojectName: '',
verificationContent: '',
verificationOpinion: '',
volume: ''
});
// 时间 格式化 YYYY-MM-DD 改为 YYYY年MM月DD日
const dateFormat = (v) => {
if (!v) return '-';
let time = new Date(v);
let y = time.getFullYear();
let MM = time.getMonth() + 1;
MM = MM < 10 ? '0' + MM : MM;
let d = time.getDate();
d = d < 10 ? '0' + d : d;
return y + '年' + MM + '月' + d + '日';
};
/** 查询设计-图纸评审列表 */
const getList = async () => {
loading.value = true;
const res = await listDrawingreview(queryParams.value);
drawingreviewList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加设计-图纸评审';
};
/** 表单重置 */
const reset = () => {
form.value = { ...initexamineForm };
drawingreviewFormRef.value?.resetFields();
};
function handleUpdate(row) {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/drawingreview/indexEdit`,
query: {
id: row.id,
type: 'update'
}
});
}
function handleViewInfo(row) {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/drawingreview/indexEdit`,
query: {
id: row.id,
type: 'view'
}
});
}
const handleViewHistory = async (row) => {
// 查看历史流程记录
dialogHistory.value = true;
let res = await ObtainHistoricalDesignDrawingsForReview(row.id);
console.log(res);
hisList.value = res;
hisId.value = hisList.value[0].id;
getDetails(hisId.value);
};
const handleShowInfo = (val) => {
getDetails(val);
};
const getDetails = async (id) => {
let res = await drawingreview(id);
examineForm.value = res.data;
};
/** 提交按钮 */
const submitForm = () => {
drawingreviewFormRef.value?.validate(async (valid) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateDrawingreview(form.value).finally(() => (buttonLoading.value = false));
} else {
await addDrawingreview(form.value, fromData).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
// 上传excel
function importExcel(options) {
fromData.append('file', options.file);
}
onMounted(() => {
getList();
});
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
queryParams.value.projectId = nid;
getList();
}
);
onUnmounted(() => {
listeningProject();
});
</script>
<style lang="scss">
.drawingreview {
.upload-demo {
width: 100% !important;
}
table {
border-collapse: collapse; //合并为一个单一的边框
border-color: rgba(199, 199, 199, 1); //边框颜色按实际自定义即可
}
thead {
tr {
th {
background-color: rgba(247, 247, 247, 1); //设置表格标题背景色
height: 35px; //设置单元格最小高度
text-align: center;
letter-spacing: 5px;
padding: 15px;
}
td {
text-align: left;
height: 35px; //设置单元格最小高度
padding: 15px;
}
.th-bg {
background-color: rgba(247, 247, 247, 1);
}
}
}
tbody {
tr {
td {
text-align: left;
height: 40px; //设置单元格最小高度
padding: 15px;
}
th {
height: 35px; //设置单元格最小高度
text-align: center;
letter-spacing: 5px;
padding: 15px;
}
}
}
.table-content {
box-shadow: 0px 0px 10px #ddd;
padding: 20px;
position: relative;
}
}
</style>

View File

@ -0,0 +1,341 @@
<template>
<div class="p-4 bg-gray-50">
<div class="max-w-4xl mx-auto">
<!-- 顶部按钮区域 -->
<el-card class="mb-4 rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.design"
:status="form.auditStatus"
:pageType="routeParams.type"
/>
</el-card>
<!-- 表单区域 -->
<el-card class="rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md overflow-hidden">
<div class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-800">图纸评审</h3>
</div>
<div class="p-6">
<div class="grid grid-cols-1 gap-4">
<el-form
ref="leaveFormRef"
:disabled="routeParams.type === 'view' || form.auditStatus == 'waiting'"
:model="form"
:rules="rules"
label-width="100px"
class="space-y-4"
>
<el-form-item label="图纸文件" v-for="value in form.fileVoList" :key="value.id" prop="fileId" class="mb-2 md:col-span-2">
<el-input v-model="value.fileName" disabled placeholder="图纸名称" />
</el-form-item>
</el-form>
</div>
</div>
</el-card>
<!-- 提交组件 -->
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<approvalRecord ref="approvalRecordRef"></approvalRecord>
<!-- 流程选择对话框 -->
<el-dialog
draggable
v-model="dialogVisible.visible"
:title="dialogVisible.title"
:before-close="handleClose"
width="500"
class="rounded-lg shadow-lg"
>
<div class="p-4">
<p class="text-gray-600 mb-4">请选择要启动的流程</p>
<el-select v-model="flowCode" placeholder="请选择流程" style="width: 100%">
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<template #footer>
<div class="dialog-footer p-4 border-t border-gray-100 flex justify-end space-x-3">
<el-button @click="handleClose" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
>取消</el-button
>
<el-button type="primary" @click="submitFlow()" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
>确认</el-button
>
</div>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup name="Leave" lang="ts">
import { LeaveForm, LeaveQuery, LeaveVO } from '@/api/workflow/leave/types';
import { startWorkFlow } from '@/api/workflow/task';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { useUserStoreHook } from '@/store/modules/user';
const { design_change_reason_type } = toRefs<any>(proxy?.useDict('design_change_reason_type'));
import { getVolumeCatalog } from '@/api/design/volumeCatalog';
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const buttonLoading = ref(false);
const loading = ref(true);
//路由参数
const routeParams = ref<Record<string, any>>({});
const flowCode = ref<string>('');
const status = ref<string>('');
const dialogVisible = reactive<DialogOption>({
visible: false,
title: '流程定义'
});
//提交组件
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
//审批记录组件
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
//按钮组件
const flowCodeOptions = [
{
value: currentProject.value?.id + '_designTheDiagram',
label: '图纸评审'
}
];
const leaveFormRef = ref<ElFormInstance>();
const dialog = reactive({
visible: false,
title: '',
isEdit: false
});
const submitFormData = ref<StartProcessBo>({
businessId: '',
flowCode: '',
variables: {}
});
const taskVariables = ref<Record<string, any>>({});
const initFormData = {
id: undefined,
fileName: undefined,
fileUrl: undefined,
status: undefined,
originalName: undefined,
fileVoList: []
};
const data = reactive({
form: { ...initFormData },
rules: {}
});
const onOpen = () => {
window.open(form.value.path, '_blank');
};
const handleClose = () => {
dialogVisible.visible = false;
flowCode.value = '';
buttonLoading.value = false;
};
const { form, rules } = toRefs(data);
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
leaveFormRef.value?.resetFields();
};
/** 获取详情 */
const getInfo = () => {
loading.value = true;
buttonLoading.value = false;
nextTick(async () => {
const res = await getVolumeCatalog(routeParams.value.id);
Object.assign(form.value, res.data);
loading.value = false;
buttonLoading.value = false;
});
};
/** 提交按钮 */
const submitForm = (status1: string) => {
status.value = status1;
submit(status.value, form.value);
};
const submitFlow = async () => {
handleStartWorkFlow(form.value);
dialogVisible.visible = false;
};
//提交申请
const handleStartWorkFlow = async (data: LeaveForm) => {
try {
submitFormData.value.flowCode = flowCode.value;
submitFormData.value.businessId = data.design;
//流程变量
taskVariables.value = {
// leave4/5 使用的流程变量
userList: ['1', '3', '4']
};
submitFormData.value.variables = taskVariables.value;
const resp = await startWorkFlow(submitFormData.value);
if (submitVerifyRef.value) {
buttonLoading.value = false;
submitVerifyRef.value.openDialog(resp.data.taskId);
}
} finally {
buttonLoading.value = false;
}
};
//审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value.init(form.value.design);
};
//提交回调
const submitCallback = async () => {
await proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
};
//审批
const approvalVerifyOpen = async () => {
// 图纸评审验证
submitVerifyRef.value.openDialog(routeParams.value.taskId, true, routeParams.value.businessId);
};
const submit = async (status, data) => {
form.value = data;
if (status === 'draft') {
buttonLoading.value = false;
proxy?.$modal.msgSuccess('暂存成功');
proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
} else {
if ((form.value.auditStatus === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
flowCode.value = flowCodeOptions[0].value;
dialogVisible.visible = true;
return;
}
//说明启动过先随意穿个参数
if (flowCode.value === '' || flowCode.value === null) {
flowCode.value = 'xx';
}
await handleStartWorkFlow(data);
}
};
onMounted(() => {
nextTick(async () => {
routeParams.value = proxy.$route.query;
reset();
loading.value = false;
console.log(routeParams.value.type);
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
getInfo();
}
});
});
</script>
<style scoped lang="scss">
/* 全局样式 */
:root {
--primary: #409eff;
--primary-light: #66b1ff;
--primary-dark: #3a8ee6;
--success: #67c23a;
--warning: #e6a23c;
--danger: #f56c6c;
--info: #909399;
}
/* 表单样式优化 */
.el-form-item {
.el-form-item__label {
color: #606266;
font-weight: 500;
}
.el-input__inner,
.el-select .el-input__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
.el-textarea__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
}
/* 按钮样式优化 */
.el-button {
border-radius: 4px;
transition: all 0.2s;
&.is-primary {
background-color: var(--primary);
border-color: var(--primary);
&:hover {
background-color: var(--primary-light);
border-color: var(--primary-light);
}
&:active {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
}
&.is-text {
color: var(--primary);
&:hover {
color: var(--primary-light);
background-color: rgba(64, 158, 255, 0.05);
}
}
}
/* 卡片样式优化 */
.el-card {
transition: all 0.3s ease;
&:hover {
/* transform: translateY(-2px); */
}
}
/* 对话框样式优化 */
.el-dialog {
.el-dialog__header {
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
padding: 15px 20px;
}
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-dialog__footer {
padding: 15px 20px;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@ -0,0 +1,234 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="文件名称" prop="fileName">
<el-input v-model="queryParams.fileName" placeholder="请输入文件名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery" v-hasPermi="['design:prelimScheme:list']">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['design:prelimScheme:add']">新增</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="schemeList" @selection-change="handleSelectionChange">
<el-table-column label="序号" type="index" width="55" align="center" />
<el-table-column label="文件名称" align="center" prop="fileName">
<template #default="scope">
<el-link type="primary" :href="scope.row.fileUrl" target="_blank" :underline="false">
{{ scope.row.fileName }}
</el-link>
</template>
</el-table-column>
<el-table-column label="审核状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="wf_business_status" :value="scope.row.status"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
link
type="primary"
v-if="scope.row.status !== 'draft'"
icon="Edit"
@click="handleView(scope.row)"
v-hasPermi="['design:PrelimScheme:query']"
>查看流程</el-button
>
<el-button
link
type="primary"
v-if="scope.row.status === 'draft'"
icon="Edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['design:PrelimScheme:edit']"
>修改</el-button
>
<!-- <el-button
link
type="primary"
v-if="scope.row.status === 'draft'"
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['design:PrelimScheme:remove']"
>删除</el-button
> -->
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
</div>
</template>
<script setup name="Scheme" lang="ts">
import { listPrelimScheme, getPrelimScheme, delPrelimScheme, addPrelimScheme, updatePrelimScheme } from '@/api/design/prelimScheme';
import { PrelimSchemeVO, PrelimSchemeQuery, PrelimSchemeForm } from '@/api/design/prelimScheme/types';
import { useUserStoreHook } from '@/store/modules/user';
import { useRoute } from 'vue-router';
const route = useRoute();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const schemeList = ref<PrelimSchemeVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const schemeFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: PrelimSchemeForm = {
id: undefined,
projectId: undefined,
ossId: undefined,
fileName: undefined,
fileUrl: undefined,
status: undefined
};
const data = reactive<PageData<PrelimSchemeForm, PrelimSchemeQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: undefined,
ossId: undefined,
fileName: undefined,
fileUrl: undefined,
status: undefined,
params: {}
},
rules: {
id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
projectId: [{ required: true, message: '项目id不能为空', trigger: 'blur' }],
ossId: [{ required: true, message: 'ossId不能为空', trigger: 'blur' }],
fileName: [{ required: true, message: '文件名称不能为空', trigger: 'blur' }],
status: [{ required: true, message: '审核状态不能为空', trigger: 'change' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询设计初步方案列表 */
const getList = async () => {
loading.value = true;
const res = await listPrelimScheme(queryParams.value);
schemeList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
schemeFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: PrelimSchemeVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
proxy.$tab.closePage(route);
proxy.$tab.openPage('/approval/prelimScheme/indexEdit', '', {
type: 'add'
});
};
/** 修改按钮操作 */
const handleUpdate = async (row?: PrelimSchemeVO) => {
proxy.$tab.closePage(route);
proxy.$tab.openPage(`/approval/prelimScheme/indexEdit`, '', {
id: row.id,
type: 'update'
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: PrelimSchemeVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除设计初步方案编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
await delPrelimScheme(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleView = (row?: PrelimSchemeVO) => {
proxy.$tab.closePage(route);
proxy.$tab.openPage(`/approval/prelimScheme/indexEdit`, '', {
id: row.id,
type: 'view'
});
};
onMounted(() => {
getList();
});
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
queryParams.value.projectId = nid;
getList();
}
);
onUnmounted(() => {
listeningProject();
});
</script>

View File

@ -0,0 +1,423 @@
<template>
<div class="p-4 bg-gray-50 min-h-screen">
<div class="max-w-4xl mx-auto">
<!-- 顶部按钮区域 -->
<el-card class="mb-4 rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.id"
:status="form.status"
:pageType="routeParams.type"
/>
</el-card>
<!-- 表单区域 -->
<el-card class="rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md overflow-hidden">
<div class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-800">设计初步方案信息</h3>
</div>
<div class="p-6">
<el-form
ref="leaveFormRef"
v-loading="loading"
:disabled="routeParams.type === 'view' || form.status == 'waiting' || form.status == 'waiting'"
:model="form"
:rules="rules"
label-width="120px"
class="space-y-4"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<el-form-item label="设计方案" prop="file" class="mb-2">
<file-upload
:limit="1"
:fileType="['pdf']"
:fileSize="100"
v-model="form.file"
ref="fileUploadRef"
class="w-full"
:auto-upload="false"
:data="{ projectId: form.projectId }"
:showFileList="showFileList"
:onUploadSuccess="handleUploadSuccess"
:uploadUrl="`${form.id ? '/design/prelimScheme/update/' + form.id : '/design/prelimScheme/upload'}`"
@handleChange="handleFileChange"
@handleRemove="handleFileRemove"
></file-upload>
</el-form-item>
</div>
</el-form>
</div>
</el-card>
<!-- 提交组件 -->
<approvalRecord ref="approvalRecordRef"></approvalRecord>
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<!-- 流程选择对话框 -->
<el-dialog
draggable
v-model="dialogVisible.visible"
:title="dialogVisible.title"
:before-close="handleClose"
width="500"
class="rounded-lg shadow-lg"
>
<div class="p-4">
<p class="text-gray-600 mb-4">请选择要启动的流程</p>
<el-select v-model="flowCode" placeholder="请选择流程" style="width: 100%">
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<template #footer>
<div class="dialog-footer p-4 border-t border-gray-100 flex justify-end space-x-3">
<el-button @click="handleClose" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
>取消</el-button
>
<el-button type="primary" @click="submitFlow()" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
>确认</el-button
>
</div>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup name="Leave" lang="ts">
import { LeaveForm, LeaveQuery } from '@/api/workflow/leave/types';
import { startWorkFlow } from '@/api/workflow/task';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { useUserStoreHook } from '@/store/modules/user';
import { useRoute, useRouter } from 'vue-router';
import { getPrelimScheme, updatePrelimScheme } from '@/api/design/prelimScheme';
const route = useRoute();
const router = useRouter();
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const buttonLoading = ref(false);
const loading = ref(true);
//路由参数
const routeParams = ref<Record<string, any>>({});
const flowCode = ref<string>('');
const status = ref<string>('');
const dialogVisible = reactive<DialogOption>({
visible: false,
title: '流程定义'
});
const showFileList = ref(true);
//提交组件
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
//审批记录组件
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
const fileUploadRef = ref<InstanceType<typeof fileUploadRef>>();
const leaveFormRef = ref<ElFormInstance>();
const dialog = reactive({
visible: false,
title: '',
isEdit: false
});
const submitFormData = ref<StartProcessBo>({
businessId: '',
flowCode: '',
variables: {}
});
const taskVariables = ref<Record<string, any>>({});
const flowCodeOptions = [
{
value: currentProject.value?.id + '_prelimScheme',
label: '设计初步方案审批'
}
];
const initFormData: any = {
projectId: currentProject.value?.id,
file: undefined
};
const data = reactive<any>({
form: { ...initFormData },
rules: {
file: [
{
required: true,
message: '请上传图纸文件',
trigger: 'change'
}
]
}
});
const { form, rules } = toRefs(data);
const handleClose = () => {
dialogVisible.visible = false;
flowCode.value = '';
buttonLoading.value = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
leaveFormRef.value?.resetFields();
};
/** 获取详情 */
const getInfo = () => {
loading.value = true;
buttonLoading.value = false;
nextTick(async () => {
const res = await getPrelimScheme(routeParams.value.id);
Object.assign(form.value, res.data);
form.value.file = res.data.ossId;
showFileList.value = false;
loading.value = false;
buttonLoading.value = false;
});
};
/** 提交按钮 */
const submitForm = async (status1: string) => {
status.value = status1;
var res;
if (form.value.id) {
if (!updateFileStatus.value) return proxy?.$modal.msgError('请上传图纸文件');
buttonLoading.value = true;
let data = { id: form.value.id, projectId: form.value.id, file: form.value.file };
if (form.value.file === form.value.ossId) {
data.file = '';
res = await updatePrelimScheme(data).finally(() => (buttonLoading.value = false));
if (res.code == 200) {
dialog.visible = false;
submit(status.value, form.value);
}
} else {
fileUploadRef.value.submitUpload();
}
} else {
if (!fileStatus.value) {
proxy?.$modal.msgError('请上传图纸文件');
return;
}
buttonLoading.value = true;
fileUploadRef.value.submitUpload();
}
};
const submitFlow = async () => {
handleStartWorkFlow(form.value);
dialogVisible.visible = false;
};
//提交申请
const handleStartWorkFlow = async (data: LeaveForm) => {
try {
submitFormData.value.flowCode = flowCode.value;
submitFormData.value.businessId = data.id;
//流程变量
taskVariables.value = {
// leave4/5 使用的流程变量
userList: ['1', '3', '4']
};
submitFormData.value.variables = taskVariables.value;
const resp = await startWorkFlow(submitFormData.value);
if (submitVerifyRef.value) {
buttonLoading.value = false;
submitVerifyRef.value.openDialog(resp.data.taskId);
}
} finally {
buttonLoading.value = false;
}
};
//审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value.init(form.value.id);
};
//提交回调
const submitCallback = async () => {
await proxy.$tab.closePage(route);
router.go(-1);
};
//审批
const approvalVerifyOpen = async () => {
submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
// 图纸上传成功之后 开始提交
const submit = async (status, data) => {
console.log('🚀 ~ submit ~ status, data:', status, data);
form.value = data;
if (status === 'draft') {
console.log(111);
buttonLoading.value = false;
proxy?.$modal.msgSuccess('暂存成功');
proxy.$tab.closePage(route);
router.go(-1);
} else {
if ((form.value.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
flowCode.value = flowCodeOptions[0].value;
dialogVisible.visible = true;
console.log(221);
return;
}
//说明启动过先随意穿个参数
if (flowCode.value === '' || flowCode.value === null) {
flowCode.value = 'xx';
}
console.log(data);
await handleStartWorkFlow(data);
}
};
const handleUploadSuccess = (list, res) => {
dialog.visible = false;
if (form.value.id) {
submit(status.value, form.value);
} else {
submit(status.value, { status: 'draft', id: res.data });
}
};
const fileStatus = ref(false);
const updateFileStatus = ref(true);
const handleFileChange = (file, fileList) => {
if (form.value.id) {
updateFileStatus.value = true;
}
fileStatus.value = true;
};
const handleFileRemove = (file, fileList) => {
if (form.value.id) {
updateFileStatus.value = false;
}
showFileList.value = true;
fileStatus.value = false;
};
onMounted(() => {
nextTick(async () => {
console.log(1111111111);
routeParams.value = route.query;
reset();
console.log(routeParams.value);
loading.value = false;
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
getInfo();
}
});
});
</script>
<style scoped lang="scss">
/* 全局样式 */
:root {
--primary: #409eff;
--primary-light: #66b1ff;
--primary-dark: #3a8ee6;
--success: #67c23a;
--warning: #e6a23c;
--danger: #f56c6c;
--info: #909399;
}
/* 表单样式优化 */
.el-form-item {
.el-form-item__label {
color: #606266;
font-weight: 500;
}
.el-input__inner,
.el-select .el-input__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
.el-textarea__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
}
/* 按钮样式优化 */
.el-button {
border-radius: 4px;
transition: all 0.2s;
&.is-primary {
background-color: var(--primary);
border-color: var(--primary);
&:hover {
background-color: var(--primary-light);
border-color: var(--primary-light);
}
&:active {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
}
&.is-text {
color: var(--primary);
&:hover {
color: var(--primary-light);
background-color: rgba(64, 158, 255, 0.05);
}
}
}
/* 卡片样式优化 */
.el-card {
transition: all 0.3s ease;
&:hover {
/* transform: translateY(-2px); */
}
}
/* 对话框样式优化 */
.el-dialog {
.el-dialog__header {
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
padding: 15px 20px;
}
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-dialog__footer {
padding: 15px 20px;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@ -0,0 +1,393 @@
<template>
<div class="p-6 bg-gray-50">
<div class="appWidth mx-auto bg-white rounded-xl shadow-sm overflow-hidden transition-all duration-300 hover:shadow-md">
<!-- 表单标题区域 -->
<div class="bg-gradient-to-r from-blue-500 to-blue-600 text-white p-6">
<h2 class="text-2xl font-bold flex items-center"><i class="el-icon-user-circle mr-3"></i>收集资料清单</h2>
<p class="text-blue-100 mt-2 opacity-90">请填写相关资料信息</p>
</div>
<!-- 表单内容区域 -->
<el-form ref="mainFormRef" :model="form" :rules="mainRules" label-width="120px" class="p-6">
<!-- 基本信息区域 -->
<div class="bg-blue-50 p-4 rounded-lg mb-6">
<h3 class="text-lg font-semibold text-blue-700 mb-4">基本信息</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<el-form-item label="收资人" prop="userId" class="mb-4">
<el-select
:disabled="disabledAll"
v-model="form.userId"
placeholder="请选择收资人"
class="w-full transition-all duration-300 border-gray-300 focus:border-blue-400 focus:ring-1 focus:ring-blue-400"
>
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" />
</el-select>
</el-form-item>
<el-form-item label="专业" prop="user_major" class="mb-4">
<el-select
:disabled="disabledAll"
v-model="form.user_major"
placeholder="请选择专业"
class="transition-all duration-300 border-gray-300"
:rules="{ required: true, message: '请选择专业', trigger: 'change' }"
>
<el-option v-for="item in des_user_major" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="电话" prop="phone" class="mb-4">
<el-input :disabled="disabledAll" placeholder="请输入电话" v-model="form.phone" autocomplete="off" />
</el-form-item>
<el-form-item label="邮箱" prop="email" class="mb-4">
<el-input :disabled="disabledAll" placeholder="请输入邮箱" v-model="form.email" autocomplete="off" />
</el-form-item>
</div>
</div>
<!-- 资料文件区域 -->
<div class="mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-blue-700">资料文件清单</h3>
<el-button type="primary" size="small" @click="addDocumentItem" v-if="!disabledAll" icon="Plus"> 添加资料 </el-button>
</div>
<el-form ref="documentsFormRef" :model="form" class="space-y-4">
<div v-for="(item, index) in form.documents" :key="item.id" class="bg-gray-50 p-4 rounded-lg transition-all duration-200 hover:shadow-sm">
<div class="flex justify-between items-start mb-2">
<span class="text-sm font-medium text-gray-600">资料 {{ index + 1 }}</span>
<el-button
type="text"
size="small"
text-color="#ff4d4f"
@click="removeDocumentItem(index)"
icon="el-icon-delete"
v-if="form.documents.length > 1 && !disabledAll"
>
删除
</el-button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<el-form-item
label="文件目录名称"
:prop="`documents.${index}.catalogueName`"
:rules="[{ required: true, message: '请输入文件目录名称', trigger: 'blur' }]"
class="mb-4"
>
<el-input :disabled="disabledAll" placeholder="请输入文件目录名称" v-model="item.catalogueName" autocomplete="off" />
</el-form-item>
<el-form-item label="备注" :prop="`documents.${index}.remark`" class="mb-4">
<el-input :disabled="disabledAll" placeholder="请输入备注" v-model="item.remark" autocomplete="off" />
</el-form-item>
</div>
</div>
</el-form>
</div>
<!-- 操作按钮区域 -->
<div class="flex justify-center gap-4 mt-8">
<el-button type="primary" @click="submitForm" v-hasPermi="['design:collect:add']" v-if="!form.id || form.status == 'draft'" size="large"
>确认提交</el-button
>
<el-button
type="primary"
@click="update"
v-hasPermi="['design:collect:query']"
v-show="form.id && form.status == 'draft'"
icon="Edit"
size="large"
>审核</el-button
>
<el-button type="primary" @click="update" v-hasPermi="['design:collect:query']" v-show="form.status == 'back'" size="large" icon="Edit"
>重新发起审核</el-button
>
<el-button
type="primary"
@click="onView"
v-hasPermi="['design:collect:query']"
v-show="form.id && form.status != 'draft'"
icon="view"
size="large"
>查看流程</el-button
>
<el-button
type="success"
v-hasPermi="['design:collect:export']"
@click="onLoad"
v-show="form.id && form.status != 'draft'"
icon="Download"
size="large"
>导出</el-button
>
</div>
</el-form>
</div>
</div>
</template>
<script setup name="DataCollectionForm" lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { useUserStoreHook } from '@/store/modules/user';
import { ElMessage, ElLoading } from 'element-plus';
import { systemUserList } from '@/api/design/appointment';
import { collectBatch, byProjectId, exportWord } from '@/api/design/received';
// 获取用户 store
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userStore = useUserStoreHook();
// 从 store 中获取当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const { des_user_major } = toRefs<any>(proxy?.useDict('des_user_major'));
// 表单引用
const mainFormRef = ref();
// 用户列表
const userList = ref([]);
const userMap = new Map();
const disabledAll = ref(false);
// 表单数据
const form = reactive({
projectId: currentProject.value?.id,
userId: '', // 收资人
user_major: '', // 专业
phone: '', // 电话
email: '', // 邮箱
id: '',
status: '',
documents: [
{
id: Date.now(),
catalogueName: '', // 文件目录名称
remark: '' // 备注
}
] as Array<{
id: number;
catalogueName: string;
remark: string;
}>
});
// 主表单验证规则
const mainRules = reactive({
userId: [{ required: true, message: '请输入收资人', trigger: 'blur' }],
user_major: [{ required: true, message: '请选择专业', trigger: 'change' }],
phone: [
{ required: true, message: '请输入电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
]
});
// 添加资料项
const addDocumentItem = () => {
form.documents.push({
id: Date.now(),
catalogueName: '',
remark: ''
});
};
// 删除资料项
const removeDocumentItem = (index: number) => {
form.documents.splice(index, 1);
};
// 查询数据 再次回显
const byProjectIdAll = async () => {
// 调用接口获取数据
const res = await byProjectId(currentProject.value?.id);
if (res.code === 200 && res.data) {
const data = res.data;
// 回显基本信息
form.userId = data.userId || '';
form.user_major = data.userMajor || '';
form.phone = data.phone || '';
form.email = data.email || '';
form.id = data.id || '';
form.status = data.status || '';
if (form.status == 'finish') {
// 表单全部禁用
disabledAll.value = true;
}
// 回显资料文件列表
if (data.catalogueList && data.catalogueList.length > 0) {
// 清空现有列表
form.documents = [];
// 填充新数据
data.catalogueList.forEach((item: any, index: number) => {
form.documents.push({
id: item.id || Date.now() + index, // 确保id唯一
catalogueName: item.catalogueName || '',
remark: item.remark || ''
});
});
} else {
// 如果没有资料,保持一个空项
form.documents = [
{
id: Date.now(),
catalogueName: '',
remark: ''
}
];
}
}
};
// 提交表单
const submitForm = async () => {
if (!mainFormRef.value) return;
try {
const valid = await mainFormRef.value.validate();
if (valid) {
// 这里可以添加提交逻辑
form.documents.map((item, i) => {
item.num = i + 1;
});
let body = {
desCollectBo: {
projectId: currentProject.value?.id,
userId: form.userId, // 收资人
userMajor: form.user_major, // 专业
id: form.id,
phone: form.phone, // 电话
email: form.email, // 邮箱
userName: userMap.get(form.userId)
},
catalogueList: form.documents
};
let res = await collectBatch(body);
if (res.code == 200) {
byProjectIdAll();
ElMessage.success('表单提交成功');
} else {
ElMessage.success(res.msg);
}
}
} catch (error) {
ElMessage.error('请完善表单信息后再提交');
}
};
// 重置表单
const resetForm = () => {
if (mainFormRef.value) {
mainFormRef.value.resetFields();
}
// 重置资料列表,保留一个空项
form.documents = [
{
id: Date.now(),
catalogueName: '',
remark: ''
}
];
};
/** 查询当前部门的所有用户 */
const getDeptAllUser = async (deptId: any) => {
try {
const res = await systemUserList({ deptId });
// 实际项目中使用接口返回的数据
userList.value = res.rows;
userList.value.forEach((user) => {
userMap.set(user.userId, user.nickName);
});
} catch (error) {
ElMessage.error('获取用户列表失败');
}
};
const update = () => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/received/indexEdit`,
query: {
id: form.id,
type: 'update'
}
});
};
const onView = () => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/received/indexEdit`,
query: {
id: form.id,
type: 'view'
}
});
};
// 页面挂载时初始化数据
onMounted(() => {
// 可以在这里添加初始化逻辑
getDeptAllUser(userStore.deptId).then(() => {
byProjectIdAll();
});
});
const onLoad = async () => {
// 导出接口
proxy?.download(
'design/collect/exportWord',
{
id: form.id
},
`收资清单.zip`
);
};
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
getDeptAllUser(userStore.deptId).then(() => {
byProjectIdAll();
});
}
);
onUnmounted(() => {
listeningProject();
});
</script>
<style lang="scss">
.appWidth {
width: 90%;
max-width: 1000px;
}
// 全局样式调整,使界面更柔和
::v-deep .el-input__inner,
::v-deep .el-select__input {
border-radius: 6px;
border-color: #dcdfe6;
transition: all 0.2s ease;
}
::v-deep .el-input__inner:focus,
::v-deep .el-select__input:focus {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
::v-deep .el-button {
border-radius: 6px;
transition: all 0.2s ease;
}
::v-deep .el-form-item__label {
font-weight: 500;
color: #606266;
}
// 响应式调整
@media (max-width: 768px) {
.appWidth {
width: 95%;
}
::v-deep .el-form-item {
margin-bottom: 16px;
}
::v-deep .el-form-item__label {
width: 100px;
}
}
</style>

View File

@ -0,0 +1,454 @@
<template>
<div class="p-4 bg-gray-50">
<div class="max-w-4xl mx-auto">
<!-- 顶部按钮区域 -->
<el-card class="mb-4 rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.id"
:status="form.status"
:pageType="routeParams.type"
/>
</el-card>
<!-- 表单区域 -->
<el-card class="rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md overflow-hidden">
<div class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-800">收集资料清单</h3>
</div>
<div class="p-6">
<div class="appWidth mx-auto bg-white rounded-xl shadow-sm overflow-hidden transition-all duration-300 hover:shadow-md">
<!-- 表单内容区域 -->
<el-form ref="mainFormRef" :model="form" label-width="120px" class="p-6">
<!-- 基本信息区域 -->
<div class="bg-blue-50 p-4 rounded-lg mb-6">
<h3 class="text-lg font-semibold text-blue-700 mb-4">基本信息</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<el-form-item label="收资人" prop="userId" class="mb-4">
<el-select
disabled
v-model="form.userId"
placeholder="请选择收资人"
class="w-full transition-all duration-300 border-gray-300 focus:border-blue-400 focus:ring-1 focus:ring-blue-400"
>
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" />
</el-select>
</el-form-item>
<el-form-item label="专业" prop="user_major" class="mb-4">
<el-select
disabled
v-model="form.user_major"
placeholder="请选择专业"
class="transition-all duration-300 border-gray-300"
:rules="{ required: true, message: '请选择专业', trigger: 'change' }"
>
<el-option v-for="item in des_user_major" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="电话" prop="phone" class="mb-4">
<el-input disabled placeholder="请输入电话" v-model="form.phone" autocomplete="off" />
</el-form-item>
<el-form-item label="邮箱" prop="email" class="mb-4">
<el-input disabled placeholder="请输入邮箱" v-model="form.email" autocomplete="off" />
</el-form-item>
</div>
</div>
<!-- 资料文件区域 -->
<div class="mb-6">
<el-form ref="documentsFormRef" :model="form" class="space-y-4">
<div
v-for="(item, index) in form.documents"
:key="item.id"
class="bg-gray-50 p-4 rounded-lg transition-all duration-200 hover:shadow-sm"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<el-form-item
label="文件目录名称"
:prop="`documents.${index}.catalogueName`"
:rules="[{ required: true, message: '请输入文件目录名称', trigger: 'blur' }]"
class="mb-4"
>
<el-input disabled placeholder="请输入文件目录名称" v-model="item.catalogueName" autocomplete="off" />
</el-form-item>
<el-form-item label="备注" :prop="`documents.${index}.remark`" class="mb-4">
<el-input disabled placeholder="请输入备注" v-model="item.remark" autocomplete="off" />
</el-form-item>
</div>
</div>
</el-form>
</div>
</el-form>
</div>
</div>
</el-card>
<!-- 提交组件 -->
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<approvalRecord ref="approvalRecordRef"></approvalRecord>
<!-- 流程选择对话框 -->
<el-dialog
draggable
v-model="dialogVisible.visible"
:title="dialogVisible.title"
:before-close="handleClose"
width="500"
class="rounded-lg shadow-lg"
>
<div class="p-4">
<p class="text-gray-600 mb-4">请选择要启动的流程</p>
<el-select v-model="flowCode" placeholder="请选择流程" style="width: 100%">
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<template #footer>
<div class="dialog-footer p-4 border-t border-gray-100 flex justify-end space-x-3">
<el-button @click="handleClose" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
>取消</el-button
>
<el-button type="primary" @click="submitFlow()" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
>确认</el-button
>
</div>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup name="Leave" lang="ts">
import { LeaveForm } from '@/api/workflow/leave/types';
import { startWorkFlow } from '@/api/workflow/task';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { useUserStoreHook } from '@/store/modules/user';
import { systemUserList } from '@/api/design/appointment';
import { collectBatch, byProjectId } from '@/api/design/received';
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const { des_user_major } = toRefs<any>(proxy?.useDict('des_user_major'));
const buttonLoading = ref(false);
const loading = ref(true);
//路由参数
const routeParams = ref<Record<string, any>>({});
const flowCodeOptions = [
{
value: currentProject.value?.id + '_collect',
label: '收资清单'
}
];
const flowCode = ref<string>('');
const status = ref<string>('');
const dialogVisible = reactive<DialogOption>({
visible: false,
title: '流程定义'
});
//提交组件
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
//审批记录组件
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
//按钮组件
const approvalButtonRef = ref<InstanceType<typeof ApprovalButton>>();
const leaveFormRef = ref<ElFormInstance>();
const dialog = reactive({
visible: false,
title: '',
isEdit: false
});
const submitFormData = ref<StartProcessBo>({
businessId: '',
flowCode: '',
variables: {}
});
const taskVariables = ref<Record<string, any>>({});
// 用户列表
const userList = ref([]);
const userMap = new Map();
// 表单引用
const mainFormRef = ref();
const handleClose = () => {
dialogVisible.visible = false;
flowCode.value = '';
buttonLoading.value = false;
};
// 表单数据
const form = reactive({
projectId: currentProject.value?.id,
userId: '', // 收资人
user_major: '', // 专业
phone: '', // 电话
email: '', // 邮箱
id: '',
status: '',
documents: [
{
id: Date.now(),
catalogueName: '', // 文件目录名称
remark: '' // 备注
}
] as Array<{
id: number;
catalogueName: string;
remark: string;
}>
});
/** 表单重置 */
const reset = () => {
if (mainFormRef.value) {
mainFormRef.value.resetFields();
}
// 重置资料列表,保留一个空项
form.documents = [
{
id: Date.now(),
catalogueName: '',
remark: ''
}
];
leaveFormRef.value?.resetFields();
};
/** 提交按钮 */
const submitForm = (status1: string) => {
status.value = status1;
buttonLoading.value = true;
dialog.visible = false;
submit(status.value, form);
};
const submitFlow = async () => {
handleStartWorkFlow(form);
dialogVisible.visible = false;
};
//提交申请
const handleStartWorkFlow = async (data: LeaveForm) => {
try {
submitFormData.value.flowCode = flowCode.value;
submitFormData.value.businessId = data.id;
//流程变量
taskVariables.value = {
// leave4/5 使用的流程变量
userList: ['1', '3', '4']
};
submitFormData.value.variables = taskVariables.value;
const resp = await startWorkFlow(submitFormData.value);
if (submitVerifyRef.value) {
buttonLoading.value = false;
submitVerifyRef.value.openDialog(resp.data.taskId);
}
} finally {
buttonLoading.value = false;
}
};
//审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value.init(form.id);
};
//提交回调
const submitCallback = async () => {
await proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
};
//审批
const approvalVerifyOpen = async () => {
submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
const submit = async (status, data) => {
if (status === 'draft') {
buttonLoading.value = false;
proxy?.$modal.msgSuccess('暂存成功');
proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
} else {
if ((form.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
flowCode.value = flowCodeOptions[0].value;
dialogVisible.visible = true;
return;
}
//说明启动过先随意穿个参数
if (flowCode.value === '' || flowCode.value === null) {
flowCode.value = 'xx';
}
await handleStartWorkFlow(data);
}
};
/** 查询当前部门的所有用户 */
const getDeptAllUser = async (deptId: any) => {
try {
const res = await systemUserList({ deptId });
// 实际项目中使用接口返回的数据
userList.value = res.rows;
userList.value.forEach((user) => {
userMap.set(user.userId, user.nickName);
});
} catch (error) {
ElMessage.error('获取用户列表失败');
}
};
// 查询数据 再次回显
const byProjectIdAll = async () => {
loading.value = true;
buttonLoading.value = false;
// 调用接口获取数据
const res = await byProjectId(currentProject.value?.id);
if (res.code === 200 && res.data) {
const data = res.data;
// 回显基本信息
form.userId = data.userId || '';
form.user_major = data.userMajor || '';
form.phone = data.phone || '';
form.email = data.email || '';
form.id = data.id || '';
form.status = data.status || '';
// 回显资料文件列表
if (data.catalogueList && data.catalogueList.length > 0) {
// 清空现有列表
form.documents = [];
// 填充新数据
data.catalogueList.forEach((item: any, index: number) => {
form.documents.push({
id: item.id || Date.now() + index, // 确保id唯一
catalogueName: item.catalogueName || '',
remark: item.remark || ''
});
});
} else {
// 如果没有资料,保持一个空项
form.documents = [
{
id: Date.now(),
catalogueName: '',
remark: ''
}
];
}
}
loading.value = false;
buttonLoading.value = false;
};
onMounted(() => {
nextTick(async () => {
routeParams.value = proxy.$route.query;
reset();
loading.value = false;
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
getDeptAllUser(userStore.deptId).then(() => {
byProjectIdAll();
});
}
});
});
</script>
<style scoped lang="scss">
/* 全局样式 */
:root {
--primary: #409eff;
--primary-light: #66b1ff;
--primary-dark: #3a8ee6;
--success: #67c23a;
--warning: #e6a23c;
--danger: #f56c6c;
--info: #909399;
}
/* 表单样式优化 */
.el-form-item {
.el-form-item__label {
color: #606266;
font-weight: 500;
}
.el-input__inner,
.el-select .el-input__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
.el-textarea__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
}
/* 按钮样式优化 */
.el-button {
border-radius: 4px;
transition: all 0.2s;
&.is-primary {
background-color: var(--primary);
border-color: var(--primary);
&:hover {
background-color: var(--primary-light);
border-color: var(--primary-light);
}
&:active {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
}
&.is-text {
color: var(--primary);
&:hover {
color: var(--primary-light);
background-color: rgba(64, 158, 255, 0.05);
}
}
}
/* 卡片样式优化 */
.el-card {
transition: all 0.3s ease;
&:hover {
/* transform: translateY(-2px); */
}
}
/* 对话框样式优化 */
.el-dialog {
.el-dialog__header {
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
padding: 15px 20px;
}
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-dialog__footer {
padding: 15px 20px;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@ -0,0 +1,232 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="文件名称" prop="fileName">
<el-input v-model="queryParams.fileName" placeholder="请输入文件名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" v-hasPermi="['design:scheme:add']" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['design:scheme:add']">新增</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="schemeList" @selection-change="handleSelectionChange">
<el-table-column label="序号" type="index" width="55" align="center" />
<el-table-column label="文件名称" align="center" prop="fileName">
<template #default="scope">
<el-link type="primary" :href="scope.row.fileUrl" target="_blank" :underline="false">
{{ scope.row.fileName }}
</el-link>
</template>
</el-table-column>
<el-table-column label="审核状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="wf_business_status" :value="scope.row.status"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
link
type="primary"
v-if="scope.row.status !== 'draft'"
icon="Edit"
@click="handleView(scope.row)"
v-hasPermi="['design:PrelimScheme:query']"
>查看流程</el-button
>
<el-button
link
type="primary"
v-if="scope.row.status === 'draft'"
icon="Edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['design:scheme:edit']"
>修改</el-button
>
<!-- <el-button
link
type="primary"
v-if="scope.row.status === 'draft'"
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['design:scheme:remove']"
>删除</el-button
> -->
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
</div>
</template>
<script setup name="Scheme" lang="ts">
import { listScheme, getScheme, delScheme, addScheme, updateScheme } from '@/api/design/scheme';
import { SchemeVO, SchemeQuery, SchemeForm } from '@/api/design/scheme/types';
import { useUserStoreHook } from '@/store/modules/user';
import { useRoute } from 'vue-router';
const route = useRoute();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const schemeList = ref<SchemeVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const schemeFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: SchemeForm = {
id: undefined,
projectId: undefined,
ossId: undefined,
fileName: undefined,
fileUrl: undefined,
status: undefined
};
const data = reactive<PageData<SchemeForm, SchemeQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: undefined,
ossId: undefined,
fileName: undefined,
fileUrl: undefined,
status: undefined,
params: {}
},
rules: {
id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
projectId: [{ required: true, message: '项目id不能为空', trigger: 'blur' }],
ossId: [{ required: true, message: 'ossId不能为空', trigger: 'blur' }],
fileName: [{ required: true, message: '文件名称不能为空', trigger: 'blur' }],
status: [{ required: true, message: '审核状态不能为空', trigger: 'change' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询设计初步方案列表 */
const getList = async () => {
loading.value = true;
const res = await listScheme(queryParams.value);
schemeList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
schemeFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: SchemeVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
proxy.$tab.closePage(route);
proxy.$tab.openPage('/approval/scheme/indexEdit', '', {
type: 'add'
});
};
/** 修改按钮操作 */
const handleUpdate = async (row?: SchemeVO) => {
proxy.$tab.closePage(route);
proxy.$tab.openPage(`/approval/scheme/indexEdit`, '', {
id: row.id,
type: 'update'
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: SchemeVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除设计初步方案编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
await delScheme(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
const handleView = (row?: SchemeVO) => {
proxy.$tab.closePage(route);
proxy.$tab.openPage(`/approval/scheme/indexEdit`, '', {
id: row.id,
type: 'view'
});
};
onMounted(() => {
getList();
});
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
queryParams.value.projectId = nid;
getList();
}
);
onUnmounted(() => {
listeningProject();
});
</script>

View File

@ -0,0 +1,431 @@
<template>
<div class="p-4 bg-gray-50 min-h-screen">
<div class="max-w-4xl mx-auto">
<!-- 顶部按钮区域 -->
<el-card class="mb-4 rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.id"
:status="form.status"
:pageType="routeParams.type"
/>
</el-card>
<!-- 表单区域 -->
<el-card class="rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md overflow-hidden">
<div class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-800">设计方案信息</h3>
</div>
<div class="p-6">
<el-form
ref="leaveFormRef"
v-loading="loading"
:disabled="routeParams.type === 'view' || form.status == 'waiting'"
:model="form"
:rules="rules"
label-width="120px"
class="space-y-4"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<el-form-item label="设计方案" prop="fileUrl" class="mb-2">
<file-upload
:limit="1"
:fileType="['pdf']"
:fileSize="100"
v-model="form.file"
ref="fileUploadRef"
class="w-full"
:auto-upload="false"
:data="{ projectId: currentProject.id }"
:showFileList="showFileList"
:onUploadSuccess="handleUploadSuccess"
:uploadUrl="`${form.id ? '/design/scheme/update/' + form.id : '/design/scheme/upload'}`"
@handleChange="handleFileChange"
@handleRemove="handleFileRemove"
></file-upload>
</el-form-item>
</div>
</el-form>
</div>
</el-card>
<!-- 提交组件 -->
<approvalRecord ref="approvalRecordRef"></approvalRecord>
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<!-- 流程选择对话框 -->
<el-dialog
draggable
v-model="dialogVisible.visible"
:title="dialogVisible.title"
:before-close="handleClose"
width="500"
class="rounded-lg shadow-lg"
>
<div class="p-4">
<p class="text-gray-600 mb-4">请选择要启动的流程</p>
<el-select v-model="flowCode" placeholder="请选择流程" style="width: 100%">
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<template #footer>
<div class="dialog-footer p-4 border-t border-gray-100 flex justify-end space-x-3">
<el-button @click="handleClose" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
>取消</el-button
>
<el-button type="primary" @click="submitFlow()" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
>确认</el-button
>
</div>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup name="Leave" lang="ts">
import { LeaveForm, LeaveQuery } from '@/api/workflow/leave/types';
import { startWorkFlow } from '@/api/workflow/task';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { DrawingForm } from '@/api/design/drawing/types';
import { useUserStoreHook } from '@/store/modules/user';
import { getSpecialScheme, addSpecialScheme, updateSpecialScheme } from '@/api/design/specialScheme';
import { useRoute, useRouter } from 'vue-router';
import { getScheme, updateScheme } from '@/api/design/scheme';
const route = useRoute();
const router = useRouter();
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const buttonLoading = ref(false);
const loading = ref(true);
//路由参数
const routeParams = ref<Record<string, any>>({});
const flowCode = ref<string>('');
const status = ref<string>('');
const dialogVisible = reactive<DialogOption>({
visible: false,
title: '流程定义'
});
const showFileList = ref(true);
//提交组件
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
//审批记录组件
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
const fileUploadRef = ref<InstanceType<typeof fileUploadRef>>();
const leaveFormRef = ref<ElFormInstance>();
const dialog = reactive({
visible: false,
title: '',
isEdit: false
});
const submitFormData = ref<StartProcessBo>({
businessId: '',
flowCode: '',
variables: {}
});
const taskVariables = ref<Record<string, any>>({});
const flowCodeOptions = [
{
value: currentProject.value?.id + '_completeScheme',
label: '设计方案审批'
}
];
const initFormData: any = {
projectId: currentProject.value?.id,
file: undefined
};
const data = reactive<any>({
form: { ...initFormData },
rules: {
file: [
{
validator: (rule, value, callback) => {
// 新增时必须上传文件
if (!form.value.file) {
callback(new Error('请上传图纸文件'));
} else {
callback();
}
},
trigger: 'change'
}
]
}
});
const { form, rules } = toRefs(data);
const handleClose = () => {
dialogVisible.visible = false;
flowCode.value = '';
buttonLoading.value = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
leaveFormRef.value?.resetFields();
};
/** 获取详情 */
const getInfo = () => {
loading.value = true;
buttonLoading.value = false;
nextTick(async () => {
const res = await getScheme(routeParams.value.id);
Object.assign(form.value, res.data);
form.value.file = res.data.ossId;
showFileList.value = false;
loading.value = false;
buttonLoading.value = false;
});
};
/** 提交按钮 */
const submitForm = async (status1: string) => {
status.value = status1;
var res;
if (form.value.id) {
if (!updateFileStatus.value) return proxy?.$modal.msgError('请上传图纸文件');
buttonLoading.value = true;
let data = { id: form.value.id, projectId: form.value.id, file: form.value.file };
if (form.value.file === form.value.ossId) {
data.file = '';
res = await updateScheme(data).finally(() => (buttonLoading.value = false));
if (res.code == 200) {
dialog.visible = false;
submit(status.value, form.value);
}
} else {
fileUploadRef.value.submitUpload();
}
} else {
if (!fileStatus.value) {
proxy?.$modal.msgError('请上传图纸文件');
return;
}
buttonLoading.value = true;
fileUploadRef.value.submitUpload();
}
};
const submitFlow = async () => {
handleStartWorkFlow(form.value);
dialogVisible.visible = false;
};
//提交申请
const handleStartWorkFlow = async (data: LeaveForm) => {
try {
submitFormData.value.flowCode = flowCode.value;
submitFormData.value.businessId = data.id;
//流程变量
taskVariables.value = {
// leave4/5 使用的流程变量
userList: ['1', '3', '4']
};
submitFormData.value.variables = taskVariables.value;
const resp = await startWorkFlow(submitFormData.value);
if (submitVerifyRef.value) {
buttonLoading.value = false;
submitVerifyRef.value.openDialog(resp.data.taskId);
}
} finally {
buttonLoading.value = false;
}
};
//审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value.init(form.value.id);
};
//提交回调
const submitCallback = async () => {
await proxy.$tab.closePage(route);
router.go(-1);
};
//审批
const approvalVerifyOpen = async () => {
submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
// 图纸上传成功之后 开始提交
const submit = async (status, data) => {
console.log('🚀 ~ submit ~ status, data:', status, data);
form.value = data;
if (status === 'draft') {
console.log(111);
buttonLoading.value = false;
proxy?.$modal.msgSuccess('暂存成功');
proxy.$tab.closePage(route);
router.go(-1);
} else {
if ((form.value.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
flowCode.value = flowCodeOptions[0].value;
dialogVisible.visible = true;
console.log(221);
return;
}
//说明启动过先随意穿个参数
if (flowCode.value === '' || flowCode.value === null) {
flowCode.value = 'xx';
}
console.log(data);
await handleStartWorkFlow(data);
}
};
const handleUploadSuccess = (list, res) => {
dialog.visible = false;
if (form.value.id) {
submit(status.value, form.value);
} else {
submit(status.value, { status: 'draft', id: res.data });
}
};
const fileStatus = ref(false);
const updateFileStatus = ref(true);
const handleFileChange = (file, fileList) => {
if (form.value.id) {
updateFileStatus.value = true;
}
fileStatus.value = true;
};
const handleFileRemove = (file, fileList) => {
if (form.value.id) {
updateFileStatus.value = false;
}
showFileList.value = true;
fileStatus.value = false;
};
onMounted(() => {
nextTick(async () => {
console.log(1111111111);
routeParams.value = route.query;
reset();
console.log(routeParams.value);
loading.value = false;
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
getInfo();
}
});
});
</script>
<style scoped lang="scss">
/* 全局样式 */
:root {
--primary: #409eff;
--primary-light: #66b1ff;
--primary-dark: #3a8ee6;
--success: #67c23a;
--warning: #e6a23c;
--danger: #f56c6c;
--info: #909399;
}
/* 表单样式优化 */
.el-form-item {
.el-form-item__label {
color: #606266;
font-weight: 500;
}
.el-input__inner,
.el-select .el-input__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
.el-textarea__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
}
/* 按钮样式优化 */
.el-button {
border-radius: 4px;
transition: all 0.2s;
&.is-primary {
background-color: var(--primary);
border-color: var(--primary);
&:hover {
background-color: var(--primary-light);
border-color: var(--primary-light);
}
&:active {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
}
&.is-text {
color: var(--primary);
&:hover {
color: var(--primary-light);
background-color: rgba(64, 158, 255, 0.05);
}
}
}
/* 卡片样式优化 */
.el-card {
transition: all 0.3s ease;
&:hover {
/* transform: translateY(-2px); */
}
}
/* 对话框样式优化 */
.el-dialog {
.el-dialog__header {
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
padding: 15px 20px;
}
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-dialog__footer {
padding: 15px 20px;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@ -0,0 +1,229 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="版本号" prop="versionNumber">
<el-input v-model="queryParams.versionNumber" placeholder="请输入版本号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="文件名称" prop="fileName">
<el-input v-model="queryParams.fileName" placeholder="请输入文件名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="原文件名" prop="originalName">
<el-input v-model="queryParams.originalName" placeholder="请输入原文件名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="审核状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择审核状态" clearable>
<el-option v-for="dict in wf_business_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" v-hasPermi="['design:prelimScheme:query']" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['design:specialScheme:add']">新增</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="specialSchemeList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="版本号" align="center" prop="versionNumber" />
<el-table-column label="文件名称" align="center" prop="fileName">
<template #default="scope">
<span style="color: #409eff" @click="handleView(scope.row)">{{ scope.row.fileName }}</span>
</template>
</el-table-column>
<el-table-column label="原文件名" align="center" prop="originalName" />
<el-table-column label="流程状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="wf_business_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="上传时间" align="center" prop="createTime" width="180"> </el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding">
<template #default="scope">
<el-row :gutter="10" class="mb8">
<el-col :span="1.5" v-if="scope.row.status === 'draft' || scope.row.status === 'cancel' || scope.row.status === 'back'">
<el-button v-hasPermi="['design:prelimScheme:edit']" size="small" type="primary" icon="Edit" @click="handleUpdate(scope.row)"
>修改</el-button
>
</el-col>
<!-- <el-col :span="1.5" v-if="scope.row.status === 'draft' || scope.row.status === 'cancel' || scope.row.status === 'back'">
<el-button v-hasPermi="['design:prelimScheme:remove']" size="small" type="primary" icon="Delete" @click="handleDelete(scope.row)"
>删除</el-button
>
</el-col> -->
</el-row>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
v-hasPermi="['design:prelimScheme:query']"
size="small"
icon="View"
v-if="scope.row.status != 'draft'"
@click="handleViewInfo(scope.row)"
>查看</el-button
>
</el-col>
<el-col :span="1.5" v-if="scope.row.status === 'waiting'">
<el-button size="small" type="primary" icon="Notification" @click="handleCancelProcessApply(scope.row.id)">撤销</el-button>
</el-col>
</el-row>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
</div>
</template>
<script setup name="SpecialScheme" lang="ts">
import { listSpecialScheme, delSpecialScheme } from '@/api/design/specialScheme';
import { SpecialSchemeVO } from '@/api/design/specialScheme/types';
import { useUserStoreHook } from '@/store/modules/user';
import { cancelProcessApply } from '@/api/workflow/instance';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const specialSchemeList = ref<SpecialSchemeVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
versionNumber: undefined,
fileName: undefined,
originalName: undefined,
status: undefined,
params: {}
}
});
const { queryParams } = toRefs(data);
/** 查询专项方案管理列表 */
const getList = async () => {
loading.value = true;
const res = await listSpecialScheme(queryParams.value);
specialSchemeList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: SpecialSchemeVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/specialScheme/indexEdit`,
query: {
type: 'add'
}
});
};
/** 修改按钮操作 */
const handleUpdate = async (row?: SpecialSchemeVO) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/specialScheme/indexEdit`,
query: {
id: row.id,
type: 'update'
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: SpecialSchemeVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除专项方案管理编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
await delSpecialScheme(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
const handleView = (row) => {
var url = row.file.url;
window.open(url, '_blank');
};
/** 查看按钮操作 */
const handleViewInfo = (row) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/specialScheme/indexEdit`,
query: {
id: row.id,
type: 'view'
}
});
};
/** 撤销按钮操作 */
const handleCancelProcessApply = async (id: string) => {
await proxy?.$modal.confirm('是否确认撤销当前单据?');
loading.value = true;
const data = {
businessId: id,
message: '申请人撤销流程!'
};
await cancelProcessApply(data).finally(() => (loading.value = false));
await getList();
proxy?.$modal.msgSuccess('撤销成功');
};
onMounted(() => {
getList();
});
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
queryParams.value.projectId = nid;
getList();
}
);
onUnmounted(() => {
listeningProject();
});
</script>

View File

@ -0,0 +1,397 @@
<template>
<div class="p-4 bg-gray-50 min-h-screen">
<div class="max-w-4xl mx-auto">
<!-- 顶部按钮区域 -->
<el-card class="mb-4 rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.id"
:status="form.status"
:pageType="routeParams.type"
/>
</el-card>
<!-- 表单区域 -->
<el-card class="rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md overflow-hidden">
<div class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-800">专项方案信息</h3>
</div>
<div class="p-6">
<el-form
ref="leaveFormRef"
v-loading="loading"
:disabled="routeParams.type === 'view' || form.status == 'waiting'"
:model="form"
:rules="rules"
label-width="120px"
class="space-y-4"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<el-form-item label="版本号" prop="versionNumber" class="mb-2">
<el-input v-model="form.versionNumber" placeholder="请输入版本号" class="w-full"></el-input>
</el-form-item>
<el-form-item label="文件名称" prop="fileName" class="mb-2">
<el-input v-model="form.fileName" placeholder="请输入文件名称" class="w-full"></el-input>
</el-form-item>
<el-form-item label="专项方案" prop="fileUrl" class="mb-2">
<file-upload :limit="1" :fileType="['pdf']" :fileSize="100" v-model="form.fileUrl" class="w-full"></file-upload>
</el-form-item>
<el-form-item label="备注" prop="remark" class="mb-2 md:col-span-2">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" :rows="4" class="w-full"></el-input>
</el-form-item>
</div>
</el-form>
</div>
</el-card>
<!-- 提交组件 -->
<approvalRecord ref="approvalRecordRef"></approvalRecord>
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<!-- 流程选择对话框 -->
<el-dialog
draggable
v-model="dialogVisible.visible"
:title="dialogVisible.title"
:before-close="handleClose"
width="500"
class="rounded-lg shadow-lg"
>
<div class="p-4">
<p class="text-gray-600 mb-4">请选择要启动的流程</p>
<el-select v-model="flowCode" placeholder="请选择流程" style="width: 100%">
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<template #footer>
<div class="dialog-footer p-4 border-t border-gray-100 flex justify-end space-x-3">
<el-button @click="handleClose" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
>取消</el-button
>
<el-button type="primary" @click="submitFlow()" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
>确认</el-button
>
</div>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup name="Leave" lang="ts">
import { LeaveForm, LeaveQuery } from '@/api/workflow/leave/types';
import { startWorkFlow } from '@/api/workflow/task';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { DrawingForm } from '@/api/design/drawing/types';
import { useUserStoreHook } from '@/store/modules/user';
import { getSpecialScheme, addSpecialScheme, updateSpecialScheme } from '@/api/design/specialScheme';
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const buttonLoading = ref(false);
const loading = ref(true);
//路由参数
const routeParams = ref<Record<string, any>>({});
const flowCode = ref<string>('');
const status = ref<string>('');
const dialogVisible = reactive<DialogOption>({
visible: false,
title: '流程定义'
});
//提交组件
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
//审批记录组件
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
const leaveFormRef = ref<ElFormInstance>();
const dialog = reactive({
visible: false,
title: '',
isEdit: false
});
const submitFormData = ref<StartProcessBo>({
businessId: '',
flowCode: '',
variables: {}
});
const taskVariables = ref<Record<string, any>>({});
const flowCodeOptions = [
{
value: currentProject.value?.id + '_specialScheme',
label: '专项方案审批'
}
];
const initFormData: DrawingForm = {
id: undefined,
projectId: currentProject.value?.id,
versionNumber: undefined,
fileName: undefined,
fileUrl: undefined,
fileType: undefined,
fileSuffix: undefined,
originalName: undefined,
remark: undefined,
file: undefined
};
const data = reactive<PageData<LeaveForm, LeaveQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
fileName: undefined,
fileType: undefined,
fileSuffix: undefined,
fileStatus: undefined,
originalName: undefined,
newest: undefined,
params: {}
},
rules: {
versionNumber: [{ required: true, message: '版本号不能为空', trigger: 'blur' }],
fileName: [{ required: true, message: '文件名称不能为空', trigger: 'blur' }],
fileType: [{ required: true, message: '文件类型不能为空', trigger: 'change' }],
fileUrl: [
{
validator: (rule, value, callback) => {
// 新增时必须上传文件
if (!form.value.fileUrl) {
callback(new Error('请上传图纸文件'));
} else {
callback();
}
},
trigger: 'change'
}
]
}
});
const handleClose = () => {
dialogVisible.visible = false;
flowCode.value = '';
buttonLoading.value = false;
};
const { form, rules } = toRefs(data);
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
leaveFormRef.value?.resetFields();
};
/** 获取详情 */
const getInfo = () => {
loading.value = true;
buttonLoading.value = false;
nextTick(async () => {
const res = await getSpecialScheme(routeParams.value.id);
Object.assign(form.value, res.data);
loading.value = false;
buttonLoading.value = false;
});
};
/** 提交按钮 */
const submitForm = (status1: string) => {
status.value = status1;
leaveFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
var res;
if (form.value.id) {
res = await updateSpecialScheme(form.value).finally(() => (buttonLoading.value = false));
} else {
res = await addSpecialScheme(form.value).finally(() => (buttonLoading.value = false));
}
if (res.code == 200) {
dialog.visible = false;
submit(status.value, res.data);
}
}
});
};
const submitFlow = async () => {
handleStartWorkFlow(form.value);
dialogVisible.visible = false;
};
//提交申请
const handleStartWorkFlow = async (data: LeaveForm) => {
try {
submitFormData.value.flowCode = flowCode.value;
submitFormData.value.businessId = data.id;
//流程变量
taskVariables.value = {
// leave4/5 使用的流程变量
userList: ['1', '3', '4']
};
submitFormData.value.variables = taskVariables.value;
const resp = await startWorkFlow(submitFormData.value);
if (submitVerifyRef.value) {
buttonLoading.value = false;
submitVerifyRef.value.openDialog(resp.data.taskId);
}
} finally {
buttonLoading.value = false;
}
};
//审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value.init(form.value.id);
};
//提交回调
const submitCallback = async () => {
await proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
};
//审批
const approvalVerifyOpen = async () => {
submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
// 图纸上传成功之后 开始提交
const submit = async (status, data) => {
form.value = data;
if (status === 'draft') {
buttonLoading.value = false;
proxy?.$modal.msgSuccess('暂存成功');
proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
} else {
if ((form.value.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
flowCode.value = flowCodeOptions[0].value;
dialogVisible.visible = true;
return;
}
//说明启动过先随意穿个参数
if (flowCode.value === '' || flowCode.value === null) {
flowCode.value = 'xx';
}
console.log(data);
await handleStartWorkFlow(data);
}
};
onMounted(() => {
nextTick(async () => {
console.log(1111111111);
routeParams.value = proxy.$route.query;
reset();
console.log(routeParams.value);
loading.value = false;
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
getInfo();
}
});
});
</script>
<style scoped lang="scss">
/* 全局样式 */
:root {
--primary: #409eff;
--primary-light: #66b1ff;
--primary-dark: #3a8ee6;
--success: #67c23a;
--warning: #e6a23c;
--danger: #f56c6c;
--info: #909399;
}
/* 表单样式优化 */
.el-form-item {
.el-form-item__label {
color: #606266;
font-weight: 500;
}
.el-input__inner,
.el-select .el-input__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
.el-textarea__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
}
/* 按钮样式优化 */
.el-button {
border-radius: 4px;
transition: all 0.2s;
&.is-primary {
background-color: var(--primary);
border-color: var(--primary);
&:hover {
background-color: var(--primary-light);
border-color: var(--primary-light);
}
&:active {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
}
&.is-text {
color: var(--primary);
&:hover {
color: var(--primary-light);
background-color: rgba(64, 158, 255, 0.05);
}
}
}
/* 卡片样式优化 */
.el-card {
transition: all 0.3s ease;
&:hover {
/* transform: translateY(-2px); */
}
}
/* 对话框样式优化 */
.el-dialog {
.el-dialog__header {
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
padding: 15px 20px;
}
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-dialog__footer {
padding: 15px 20px;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@ -0,0 +1,220 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="分包内容" prop="subContent">
<el-input v-model="queryParams.subContent" placeholder="请输入分包内容" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery" v-hasPermi="['design:subcontract:add']">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['design:subcontract:add']">新增</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="subcontractList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" type="index" width="50" />
<el-table-column label="分包内容" align="center" prop="subContent" />
<el-table-column label="创建时间" align="center" prop="createTime" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['design:subcontract:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['design:subcontract:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改设计分包对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="subcontractFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="分包内容" prop="subContent">
<el-input v-model="form.subContent" type="textarea" placeholder="请输入分包内容" clearable></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Subcontract" lang="ts">
import { listSubcontract, getSubcontract, delSubcontract, addSubcontract, updateSubcontract } from '@/api/design/subcontract';
import { SubcontractVO, SubcontractQuery, SubcontractForm } from '@/api/design/subcontract/types';
import { useUserStoreHook } from '@/store/modules/user';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const subcontractList = ref<SubcontractVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const subcontractFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: SubcontractForm = {
id: undefined,
projectId: currentProject.value?.id,
subContent: undefined
};
const data = reactive<PageData<SubcontractForm, SubcontractQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
subContent: undefined,
params: {}
},
rules: {
id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
projectId: [{ required: true, message: '项目id不能为空', trigger: 'blur' }],
subContent: [{ required: true, message: '分包内容不能为空', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询设计分包列表 */
const getList = async () => {
loading.value = true;
const res = await listSubcontract(queryParams.value);
subcontractList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
subcontractFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: SubcontractVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加设计分包';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: SubcontractVO) => {
reset();
const _id = row?.id || ids.value[0];
const res = await getSubcontract(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改设计分包';
};
/** 提交按钮 */
const submitForm = () => {
subcontractFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateSubcontract(form.value).finally(() => (buttonLoading.value = false));
} else {
await addSubcontract(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: SubcontractVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除设计分包编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
await delSubcontract(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'design/subcontract/export',
{
...queryParams.value
},
`subcontract_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
queryParams.value.projectId = nid;
getList();
}
);
onUnmounted(() => {
listeningProject();
});
</script>

View File

@ -0,0 +1,137 @@
<template>
<div class="book_file">
<!-- 添加或修改公司对话框 -->
<el-dialog v-model="isShowDialog" width="80vw" custom-class="book_file_loading" :close-on-click-modal="false" :destroy-on-close="true">
<template #header>
<div>查看资料文件</div>
</template>
<el-form ref="queryRef" :inline="true" label-width="100px">
<el-row>
<el-col :span="8" class="colBlock">
<el-form-item label="文件名称" prop="fileName">
<el-input v-model="formData.fileName" placeholder="请输入文件名称搜索" clearable @keyup.enter.native="getDataFileQuery" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item>
<el-button type="primary" @click="getDataFileQuery">
<el-icon><Search /></el-icon>搜索
</el-button>
<el-button @click="resetQuery" type="danger">
<el-icon><Refresh /></el-icon>清空
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="content">
<div class="left_box" :style="treeList.length ? 'width: 70%' : 'width: 100%'">
<el-table v-loading="loading" :data="tableData" border height="63vh" :empty-text="emptyText">
<el-table-column label="序号" align="center" type="index" width="80px" />
<el-table-column label="文件名称" align="center" prop="fileName" min-width="100px" />
<el-table-column label="文件类型" align="center" prop="fileSuffix" width="100px" />
<el-table-column label="文件路径" align="center" min-width="100px">
<template #default="scope">
<span>{{ filterfilenPath(scope.row.filePath) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding" width="250px" fixed="right">
<template #default="scope">
<el-button type="success" link @click="onExport(scope.row)">
<el-icon><Download /></el-icon>下载
</el-button>
<el-button type="primary" link @click="onBook(scope.row)">
<el-icon><View /></el-icon>查看
</el-button>
</template>
</el-table-column>
</el-table>
<pagination :total="total" v-model:page="formData.pageNum" v-model:limit="formData.pageSize" @pagination="getDataFileQuery" />
</div>
</div>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { listKnowledgeDocument } from '@/api/design/technicalStandard';
import { useUserStoreHook } from '@/store/modules/user';
const emit = defineEmits(['onExport', 'onExportView', 'onBook']);
const stores = useUserStoreHook();
const loading = ref(false);
const tableData = ref<any[]>([]);
const isShowDialog = ref(false);
const formData = reactive({
fileName: '',
projectId: stores.selectedProject.id,
pageNum: 1,
pageSize: 10
});
const total = ref(0);
const emptyText = ref('暂无数据');
const treeList = ref<any[]>([]);
const openDialog = () => {
isShowDialog.value = true;
getDataFileQuery();
emptyText.value = '请输入文件名称进行搜索!';
resetForm();
};
const resetForm = () => {
tableData.value = [];
formData.fileName = '';
treeList.value = [];
emptyText.value = '暂无数据';
};
const getDataFileQuery = () => {
loading.value = true;
emptyText.value = '数据加载中……';
listKnowledgeDocument(formData).then((res: any) => {
loading.value = false;
tableData.value = [];
if (res.code == 200 && res.rows?.length) {
tableData.value = res.rows;
total.value = res.total;
} else {
emptyText.value = '没有查询到数据,请重新输入搜索';
}
});
};
const onBook = (row: any) => {
emit('onBook', row);
};
const onExport = (row: any) => {
emit('onExportView', row);
};
const resetQuery = () => {
tableData.value = [];
formData.fileName = '';
loading.value = false;
emptyText.value = '暂无数据';
};
const filterfilenPath = (val: string): string => {
return val.replace(/^.*?知识库\//, '知识库/');
};
defineExpose({ openDialog });
</script>
<style scoped lang="scss">
.book_file {
.content {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
}
}
</style>

View File

@ -0,0 +1,190 @@
<template>
<div class="document_detail" id="document_detail">
<div class="move_pop" id="detial_pop">
<!-- <span>{{ title }}</span> -->
<div class="box">
<img v-if="type == 2" src="../icon/suo.png" @click="onFull(1)" />
<img v-else src="../icon/full.png" @click="onFull(2)" />
<span class="close" @click="onClose"></span>
</div>
</div>
<div class="box_app" id="box_app"></div>
</div>
</template>
<script lang="ts">
import { setMove } from '@/utils/moveDiv';
declare const CXO_API: any;
export default defineComponent({
name: 'index',
setup(props, { emit }) {
const { proxy } = <any>getCurrentInstance();
const state = reactive({
title: '',
type: 2
});
onMounted(() => {
setMove('detial_pop', 'document_detail');
});
// 打开弹窗
const openDialog = (obj) => {
state.title = obj.fileName;
init(obj);
};
const onError = function (event) {
//举例,强制保存后,判断文档内容是否保存成功
if (event.data) {
if (event.data.errorCode == 'forcesave') {
var desc = event.data.errorDescription;
desc = JSON.parse(desc);
if (desc.error == 0) {
//保存成功
} else {
//保存失败或异常
}
} else if (event.data.errorCode == 'setallcellvalue') {
var desc = event.data.errorDescription;
desc = JSON.parse(desc);
if (desc.error == 0) {
//填充成功
} else if (desc.error == -1) {
//当前文档正处于协同编辑中
} else {
//填充异常
}
} else if (event.data.errorCode == 'clearsheet') {
var desc = event.data.errorDescription;
desc = JSON.parse(desc);
if (desc.error == 0) {
//清除成功
} else if (desc.error == -1) {
//当前文档正处于协同编辑中
} else {
//清除异常
}
}
}
};
const onDocumentReady = function () {
// console.log('文档加载完成');
};
const init = (obj) => {
let documentKey = obj.id.toString() + new Date().getTime();
console.log('🚀 ~ init ~ url:', obj.fileUrl);
let type = obj.fileSuffix;
if (obj.fileSuffix.includes('.')) {
type = obj.fileSuffix.substring(1);
}
let documentType = 'word'; // docx doc
if (type == 'xlsx' || type == 'xls') {
documentType = 'cell'; //电子表格
} else if (type == 'ppt' || type == 'pptx') {
documentType = 'slide'; //演示文档文件
}
new CXO_API.CXEditor('box_app', {
document: {
fileType: type,
key: documentKey,
title: obj.fileName,
url: obj.fileUrl
},
documentType,
editorConfig: {
mode: 'view',
callbackUrl: ''
},
height: '100%',
events: {
onDocumentReady: onDocumentReady,
onError: onError
},
zoom: -1
});
};
const onClose = () => {
emit('onClose', false);
};
const onFull = (type) => {
// 全屏
let document_detail = document.getElementById('document_detail');
state.type = type;
if (type == 2) {
document_detail.style.width = '100%';
document_detail.style.height = '100%';
} else {
document_detail.style.width = '1200px';
document_detail.style.height = '80vh';
}
};
return {
proxy,
openDialog,
onClose,
onFull,
...toRefs(state)
};
}
});
</script>
<style lang="scss" scoped>
.document_detail {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 999999;
width: 100%;
height: 100%;
border: 1px solid #9f9f9f;
.box_app {
// width: 1300px !important;
// height: 80vh !important;
background-color: #f1f1f1;
}
.move_pop {
width: 100%;
// position: absolute;
// top: 0;
// right: 0%;
height: 24px;
// background: linear-gradient(#2a5095, #213f7b, #111e48);
background-color: #f4f5f6;
display: grid;
place-items: center;
> span {
color: #000000;
font-weight: bold;
font-size: 20px;
letter-spacing: 2px;
}
.box {
display: flex;
width: 60px;
position: absolute;
top: 0;
right: 10px;
// height: 100%;
align-items: center;
img {
width: 22px;
margin-top: 6px;
cursor: pointer;
}
.close {
position: absolute;
right: 2px;
/* top: -8px; */
color: #8d8d8d;
width: 24px;
height: 24px;
font-size: 20px;
//border: 2px solid #0ff;
border-radius: 50%;
display: grid;
place-items: center;
cursor: pointer;
}
}
}
}
</style>

View File

@ -0,0 +1,203 @@
<template>
<div class="document_detail_eidt" id="document_detail_eidt">
<div class="move_pop" id="detial_edit">
<!-- <span>{{ title }}</span> -->
<div class="box">
<img v-if="type == 2" src="../icon/full.png" @click="onFull(1)" />
<img v-else src="../icon/suo.png" @click="onFull(2)" />
<span class="close" @click="onClose"></span>
</div>
</div>
<div class="box_app" id="box_app_edit"></div>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, onMounted, ref, defineComponent, watch, getCurrentInstance } from 'vue';
import { setMove } from '@/utils/moveDiv';
import { useUserStoreHook } from '@/store/modules/user';
// Ensure CXO_API is available globally or import it if it's a module
// Example for global usage (e.g., included via script tag in index.html):
declare const CXO_API: any;
export default defineComponent({
name: 'index',
setup(props, { emit }) {
const stores = useUserStoreHook();
const { proxy } = <any>getCurrentInstance();
const state = reactive({
title: '',
projectId: stores.selectedProject.id,
type: 2,
postUrl: ''
});
onMounted(() => {
setMove('detial_edit', 'document_detail_eidt');
});
// 打开弹窗
const openDialog = (obj, url) => {
state.postUrl = url;
state.title = obj.name;
init(obj);
};
const onError = function (event) {
console.log('编辑器错误: code ' + event.data.errorCode + ', 描述' + event.data.errorDescription);
//举例,强制保存后,判断文档内容是否保存成功
if (event.data) {
if (event.data.errorCode == 'forcesave') {
var desc = event.data.errorDescription;
desc = JSON.parse(desc);
if (desc.error == 0) {
//保存成功
} else {
//保存失败或异常
}
} else if (event.data.errorCode == 'setallcellvalue') {
var desc = event.data.errorDescription;
desc = JSON.parse(desc);
if (desc.error == 0) {
//填充成功
} else if (desc.error == -1) {
//当前文档正处于协同编辑中
} else {
//填充异常
}
} else if (event.data.errorCode == 'clearsheet') {
var desc = event.data.errorDescription;
desc = JSON.parse(desc);
if (desc.error == 0) {
//清除成功
} else if (desc.error == -1) {
//当前文档正处于协同编辑中
} else {
//清除异常
}
}
}
};
const onDocumentReady = function () {
console.log('文档加载完成');
};
const init = (obj) => {
let documentKey = obj.id.toString() + new Date().getTime();
let baseURL = import.meta.env.VITE_APP_BASE_API;
let type = obj.fileSuffix;
if (obj.fileSuffix.includes('.')) {
type = obj.fileSuffix.substring(1);
}
let documentType = 'word'; // docx doc
if (type == 'xlsx' || type == 'xls') {
documentType = 'cell'; //电子表格
} else if (type == 'ppt' || type == 'pptx') {
documentType = 'slide'; //演示文档文件
}
console.log(baseURL + state.postUrl + '?path=' + obj.filePath + '&id=' + obj.id);
new CXO_API.CXEditor('box_app_edit', {
document: {
fileType: obj.fileSuffix,
key: documentKey,
title: obj.fileName,
url: obj.fileUrl
},
documentType,
token: stores.token,
editorConfig: {
callbackUrl: baseURL + state.postUrl + obj.id + '?path=' + obj.filePath
},
events: {
onDocumentReady: onDocumentReady,
onError: onError
}
});
};
const onClose = () => {
emit('onClose', false);
};
const onFull = (type) => {
// 全屏
let document_detail = document.getElementById('document_detail_eidt');
state.type = type;
if (type == 2) {
// 弹框放大
document_detail.style.width = '100%';
document_detail.style.height = '100%';
} else {
// 弹框缩小
document_detail.style.width = '1200px';
document_detail.style.height = '80vh';
}
};
return {
proxy,
onFull,
openDialog,
onClose,
...toRefs(state)
};
}
});
</script>
<style lang="scss" scoped>
.document_detail_eidt {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 999999;
width: 100%;
height: 100%;
border: 1px solid #9f9f9f;
.box_app {
width: 1200px !important;
height: 80vh !important;
background-color: #f1f1f1;
margin-top: 100px;
}
.move_pop {
width: 100%;
// position: absolute;
// top: 0;
// right: 0%;
height: 24px;
// background: linear-gradient(#2a5095, #213f7b, #111e48);
background-color: #f4f5f6;
display: grid;
place-items: center;
> span {
color: #000000;
font-weight: bold;
font-size: 20px;
letter-spacing: 2px;
}
.box {
display: flex;
width: 60px;
position: absolute;
top: 0;
right: 10px;
// height: 100%;
align-items: center;
img {
width: 22px;
margin-top: 6px;
cursor: pointer;
}
.close {
position: absolute;
right: 2px;
/* top: -8px; */
color: #8d8d8d;
width: 24px;
height: 24px;
font-size: 20px;
//border: 2px solid #0ff;
border-radius: 50%;
display: grid;
place-items: center;
cursor: pointer;
}
}
}
}
</style>

View File

@ -0,0 +1,149 @@
<template>
<div class="system-document-container">
<el-card shadow="hover">
<div class="system-document-search mb-5">
<el-form :model="param" ref="queryRef" :inline="true" label-width="100px">
<el-row>
<el-col :span="2">
<el-button type="success" :disabled="multiple" @click="onRecyclingStation(null, true)">
<el-icon><RefreshRight /></el-icon>批量恢复
</el-button>
</el-col>
<el-col :span="2">
<el-button type="danger" :disabled="multiple" @click="onRecyclingStation(null, false)">
<el-icon><DeleteFilled /></el-icon>批量删除
</el-button>
</el-col>
</el-row>
</el-form>
</div>
<el-table v-loading="loading" :data="tableData" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" type="index" min-width="30px" />
<el-table-column label="文件名称" align="center" prop="fileName" min-width="100px" />
<el-table-column label="文件路径" align="center" min-width="100px">
<template #default="scope">
<span>{{ filterfilenPath(scope.row.filePath) }}</span>
</template>
</el-table-column>
<el-table-column label="删除时间" align="center" prop="createTime" min-width="100px" />
<el-table-column label="操作" align="center" class-name="small-padding" min-width="80px" fixed="right">
<template #default="scope">
<div>
<el-button type="success" link @click="onRecyclingStation(scope.row, true)">
<el-icon><RefreshRight /></el-icon>恢复
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="param.pageNum" v-model:limit="param.pageSize" @pagination="getDocumentDataList" />
</el-card>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, getCurrentInstance, computed } from 'vue';
import { ElMessageBox, ElMessage, ElLoading } from 'element-plus';
import { documentDataAllList, templateRecycleBin, dataRecyclingStation } from '@/api/design/technicalStandard';
import { useUserStoreHook } from '@/store/modules/user';
const userStore = useUserStoreHook();
const currentProject = computed(() => userStore.selectedProject);
const loading = ref(false);
const queryRef = ref();
const multiple = ref(true);
const ids = ref<string>('');
const tableData = ref<any[]>([]);
const param = reactive({
type: 2,
projectId: currentProject.value?.id,
pageNum: 1,
pageSize: 10
});
const total = ref(0);
const value = ref('2');
const getDocumentDataList = () => {
loading.value = true;
tableData.value = [];
value.value = '2';
documentDataAllList(param).then((res: any) => {
tableData.value = res.rows ?? [];
total.value = res.total;
loading.value = false;
});
};
const handleSelectionChange = (selection: any[]) => {
ids.value = selection.map((item) => item.id).join(',');
multiple.value = !selection.length;
};
const onRecyclingStation = (row: any, flag: boolean) => {
let type = 2;
let selectedIds: string = '';
let title = '删除';
let msg = '你确定要删除所选文件或文件夹?';
if (row) {
selectedIds = row.id;
} else {
selectedIds = ids.value;
}
if (flag) {
type = 1;
title = '恢复';
msg = '你确定要恢复所选文件或文件夹?';
}
ElMessageBox.confirm(msg, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
const loadingInstance = ElLoading.service({
lock: true,
text: `正在${title}中……`,
background: 'rgba(0, 0, 0, 0.7)'
});
if (flag) {
dataRecyclingStation(selectedIds).then((res) => {
loadingInstance.close();
if (res.code == 200) {
getDocumentDataList();
ElMessage.success('操作成功');
}
});
} else {
templateRecycleBin(selectedIds).then((res) => {
loadingInstance.close();
if (res.code == 200) {
getDocumentDataList();
ElMessage.success('操作成功');
}
});
}
})
.catch(() => {});
};
const filterfilenPath = (val: string): string => {
return val.replace(/^.*?知识库\//, '知识库/');
};
defineExpose({
getDocumentDataList
});
</script>
<style lang="scss" scoped>
.colBlock {
display: block;
}
.colNone {
display: none;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,603 @@
<template>
<div>
<el-tabs v-model="activeName" class="demo-tabs p5" @tab-click="handleCheckMian">
<el-tab-pane label="资料" name="first">
<div class="profile_engin">
<div class="box_info">
<div class="tree_left1" id="tree_left1">
<div class="file_upload check_select">
<div class="box_btn">
<file-upload
v-model="state.paramsQuery.file"
:limit="100"
:uploadUrl="uploadUrl"
:params="uploadParams"
:on-upload-success="uploadFile"
:fileType="[]"
>
<el-button type="primary" style="float: left" :disabled="!state.parentPid">
<el-icon size="small"><Plus /></el-icon>上传文件
</el-button>
</file-upload>
</div>
<el-button type="primary" :disabled="!state.parentPid" @click="onExport"
><el-icon><Download /></el-icon>下载</el-button
>
<el-button type="primary" @click="onBook"
><el-icon><View /></el-icon>查看全项目文件</el-button
>
</div>
<div class="file_upload check_select">
<el-input class="input_left" v-model="filterText" size="small" placeholder="请输入文件名称" />
</div>
<el-tree
ref="treeRef"
highlight-current
:default-expand-all="state.checked"
:filter-node-method="filterFolder"
:data="state.treeList"
node-key="id"
accordion
:expand-on-click-node="false"
@node-click="handleNodeClick"
:current-node-key="state.selectedNodeId"
>
<template #default="{ node, data }">
<span class="custom-tree-node">
<el-icon color="#f1a81a"><FolderOpened /></el-icon>
<span>{{ node.label }}</span>
</span>
</template>
</el-tree>
<div class="resize-handle resize-handle-right right"></div>
</div>
<div class="list_right" id="list_right1">
<div>
<el-form :model="state.paramsQuery" ref="queryRef" :inline="true" label-width="100px">
<el-row>
<el-col :span="7" class="colBlock">
<el-form-item label="文件名称" prop="fileName">
<el-input
v-model="state.paramsQuery.fileName"
placeholder="请输入文件名称"
clearable
@keyup.enter.native="getdocumentDataList"
/>
</el-form-item>
</el-col>
<el-col :span="6" class="m-l10">
<el-form-item>
<el-button type="primary" @click="searchInfo"
><el-icon><Search /></el-icon>搜索</el-button
>
<el-button @click="resetQuery"
><el-icon><Refresh /></el-icon>重置</el-button
>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<el-table v-loading="state.loading" :data="state.infoList" height="67vh" border>
<el-table-column label="序号" align="center" type="index" min-width="50px" />
<el-table-column label="文件名称" align="center" prop="fileName"></el-table-column>
<el-table-column label="文件类型" align="center" prop="fileSuffix" width="100px" />
<el-table-column label="流程状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="wf_business_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="上传时间" align="center" prop="createTime"> </el-table-column>
<el-table-column label="操作" align="center" width="300">
<template #default="scope">
<el-button type="primary" link @click="handleApproval(scope.row)"
><el-icon><Plus /></el-icon>审批</el-button
>
<el-button type="primary" link @click="handleViewApproval(scope.row)"
><el-icon><View /></el-icon>流程查看</el-button
>
<el-button type="primary" link @click="handleView(scope.row)" v-if="acceptType.includes(scope.row.fileSuffix)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button type="primary" v-if="state.wordType.includes(scope.row.fileSuffix)" link @click="updataView(scope.row)"
><el-icon><EditPen /></el-icon>修改文件</el-button
>
<el-button type="primary" link @click="onExportView(scope.row)"
><el-icon><Download /></el-icon>下载</el-button
>
<el-button type="success" link @click="updateName(scope.row)"
><el-icon><EditPen /></el-icon>修改名称</el-button
>
<el-button type="danger" link @click="handleDelete(scope.row)"
><el-icon><DeleteFilled /></el-icon>删除</el-button
>
</template>
</el-table-column>
</el-table>
<pagination
:total="state.total"
v-model:page="state.paramsQuery.pageNum"
v-model:limit="state.paramsQuery.pageSize"
@pagination="getdocumentDataList"
/>
</div>
</div>
<documentsDeailsVue ref="documentDetailRef" v-if="state.showDocumentDetail" @onClose="onClose"></documentsDeailsVue>
<documentsEdit ref="documentDataEditRef" v-if="state.showdocumentDataEdit" @onClose="onCloseEdit"></documentsEdit>
<bookFile ref="bookFileRef" @onExportView="onExportView" @onBook="handleView" @onExport="onExport"></bookFile>
<el-dialog draggable title="上传文件" v-model="uploadFileder" width="30%">
<file-upload v-model="state.paramsQuery.file"></file-upload>
<template #footer>
<span>
<el-button @click="uploadFileder = false">取消</el-button>
<el-button type="primary" @click="subMitUpload">确定</el-button>
</span>
</template>
</el-dialog>
</div>
<el-image-viewer
ref="imageRef"
style="width: 100%; height: 100%"
:url-list="[imgUrl]"
v-if="imgUrl"
show-progress
fit="cover"
@close="imgUrl = ''"
/>
</el-tab-pane>
<el-tab-pane label="回收站" name="second">
<RecyclingStation ref="recylingRef"></RecyclingStation>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup name="KnowledgeDocument" lang="ts">
import {
listKnowledgeDocument,
delKnowledgeDocument,
addKnowledgeDocument,
getUniFolderDownloadList,
treeStructureData,
documentDataEdit
} from '@/api/design/technicalStandard';
import documentsEdit from './component/documentsEdit.vue';
import documentsDeailsVue from './component/documentsDeails.vue';
import RecyclingStation from './component/recyclingStation.vue';
import { useUserStoreHook } from '@/store/modules/user';
import bookFile from './component/bookFile.vue';
const activeName = ref('first');
const { proxy } = getCurrentInstance() as any;
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
const uploadUrl = computed(() => {
return `/design/technicalStandard/file`;
});
const uploadParams = computed(() => {
return {
pid: state.paramsQuery.folderId,
projectId: state.projectId
};
});
const imgUrl = ref<string>('');
const filterText = ref('');
const treeRef = ref();
const documentDetailRef = ref();
const documentDataEditRef = ref();
const uploadFileder = ref(false);
const imageRef = ref();
const recylingRef = ref();
const bookFileRef = ref();
const state = reactive({
treeList: [] as any,
arrayList: [] as any,
infoMap: new Map(),
infoList: [] as any,
total: 0,
paramsQuery: {
folderId: '',
fileName: '',
pageNum: 1,
pageSize: 20,
projectId: currentProject.value?.id,
file: null,
pid: null
},
loading: false,
checked: true,
showDocumentDetail: false,
showdocumentDataEdit: false,
showUploadFileder: false,
parentRow: null,
parentPid: null,
parentName: '',
selectedNodeId: null,
projectId: currentProject.value?.id || '',
imgType: ['jpg', 'png', 'jpeg', 'gif', 'svg'],
wordType: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'],
browserViewableType: ['html', 'htm', 'txt', 'log', 'md', 'json', 'xml', 'css', 'js'],
draggableCheck: true
});
const acceptType = computed(() => [...state.imgType, ...state.wordType, ...state.browserViewableType]);
watch(filterText, (val: any) => {
treeRef.value!.filter(val);
});
// 上传文件
const uploadFile = (files: any[]) => {
proxy.$modal.success('上传成功');
state.paramsQuery.file = null;
getdocumentDataList();
// uploadFileder.value = true;
};
// 提交上传文件
const subMitUpload = () => {
if (!state.paramsQuery.file) {
ElMessage.warning('请选择文件!');
return;
}
addKnowledgeDocument(state.paramsQuery, { projectId: state.projectId, pid: state.parentPid }).then((res: any) => {
if (res.code == 200) {
ElMessage.success('上传成功');
uploadFileder.value = false;
state.paramsQuery.file = null; //清空文件
getdocumentDataList();
} else {
ElMessage.error(res.message);
}
});
};
const searchInfo = () => {
// 搜索
getdocumentDataList();
};
const resetQuery = () => {
// 重置
state.paramsQuery.fileName = '';
getdocumentDataList();
};
// 获取树形结构文件夹目录
const gettreeStructureData = () => {
state.parentPid = null;
activeName.value = 'first';
const loading = ElLoading.service({
lock: true,
text: '正在加载中……',
background: 'rgba(0, 0, 0, 0.7)',
target: '.tree_left1'
});
treeStructureData(state.projectId).then((res: any) => {
loading.close();
if (res.code == 200 && res.data && res.data.length) {
state.selectedNodeId = '';
state.treeList = res.data;
state.paramsQuery.folderId = res.data[0].id;
getdocumentDataList();
// setInfo(res.data);
}
});
};
// 处理数据
const setInfo = (arr) => {
arr.forEach((element) => {
state.arrayList.push(element);
state.infoMap.set(element.folderId, element.id);
if (element.treeStructureDataRes && element.treeStructureDataRes.length) {
setInfo(element.treeStructureDataRes);
}
});
};
// 选择目录文件
const handleNodeClick = (row) => {
state.parentRow = row;
state.parentPid = row.parentId;
console.log(row);
state.parentName = row.label;
state.paramsQuery.folderId = row.id;
getdocumentDataList();
if (row.id === state.selectedNodeId) {
// 如果当前节点已经选中,则取消选中
state.selectedNodeId = null;
state.parentPid = null; //关闭父级选择的id
state.parentRow = null; //获取父级对象
state.parentName = ''; //获取父级对应的名称
state.paramsQuery.folderId = ''; //
} else {
// 否则选中当前节点 重新赋值
state.selectedNodeId = row.id;
}
};
// 获取文档列表数据
const getdocumentDataList = () => {
if (!state.paramsQuery.folderId) {
return;
}
state.loading = true;
listKnowledgeDocument(state.paramsQuery).then((res: any) => {
state.loading = false;
if (res.code == 200) {
state.infoList = res.rows;
state.total = res.total;
}
});
};
// 查询tree树形结构数据
const filterFolder = (value: string, data: any) => {
if (!value) return true;
return data.name.includes(value);
};
const handleDelete = (row) => {
// 删除文档
let msg = '你确定要删除所选文件?';
delFile(msg, row, () => {
getdocumentDataList();
});
};
//切换tab
const handleCheckMian = (tab, event) => {
activeName.value = tab.name;
if (activeName.value === 'first') {
gettreeStructureData();
} else {
// 回收站
recylingRef.value.getDocumentDataList();
}
};
const onExportView = (row) => {
console.log(row);
proxy.$download.direct(row.fileUrl, row.originalName);
};
const updateName = (row) => {
// 修改名称
editName(row, '请输入文件名称', 1);
};
const handleView = (row) => {
if (state.imgType.includes(row.fileSuffix)) {
imgUrl.value = row.fileUrl;
return;
} else if (state.wordType.includes(row.fileSuffix)) {
state.showDocumentDetail = true;
nextTick(() => {
documentDetailRef.value.openDialog(row);
});
} else {
window.open(row.fileUrl);
}
};
// 关闭在线编辑弹框
const onClose = () => {
state.showDocumentDetail = false;
};
// 关闭修改的在线文档弹框
const onCloseEdit = () => {
state.showdocumentDataEdit = false;
};
const updataView = (row) => {
// 修改文档
state.showdocumentDataEdit = true;
nextTick(() => {
documentDataEditRef.value.openDialog(row, '/design/technicalStandard/changxie/callback/');
});
};
// 删除文件及文件夹
const delFile = (msg, data, cb) => {
ElMessageBox.confirm(msg, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
delKnowledgeDocument([data.id]).then((res: any) => {
if (res.code == 200) {
ElMessage.success('删除成功');
cb();
}
});
})
.catch(() => {});
};
const editName = (data, title, type) => {
ElMessageBox.prompt(title, '温馨提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputErrorMessage: title,
inputValue: data.fileName
})
.then(({ value }) => {
documentDataEdit({ id: data.id, fileName: value }).then((res: any) => {
if (res.code == 200) {
ElMessage({
type: 'success',
message: '修改成功'
});
// 数据重新刷新
if (type == 2) {
gettreeStructureData();
} else {
getdocumentDataList();
}
} else {
ElMessage({
type: 'error',
message: res.message
});
}
});
})
.catch(() => {});
};
const onExport = () => {
getUniFolderDownloadList(state.paramsQuery.folderId).then((res: any) => {
if (res.code == 200) {
console.log(state.paramsQuery.fileName);
proxy.$download.downloadFilesAsZip(res.data, { urlKey: 'fileUrl', nameKey: 'originalName', zipName: state.parentName + '.zip' });
}
});
};
// 查看所有资料
const onBook = () => {
bookFileRef.value.openDialog();
};
onMounted(() => {
gettreeStructureData();
});
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
state.projectId = nid;
state.paramsQuery.projectId = nid;
if (activeName.value === 'first') {
gettreeStructureData();
} else {
// 回收站
recylingRef.value.getDocumentDataList();
}
}
);
onUnmounted(() => {
listeningProject();
});
const handleApproval = (row) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/technicalStandard/indexEdit`,
query: {
id: row.id,
type: 'update'
}
});
};
const handleViewApproval = (row) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/technicalStandard/indexEdit`,
query: {
id: row.id,
type: 'view'
}
});
};
</script>
<style lang="scss" scoped>
.profile_engin {
height: 80vh;
.set-tool {
display: none;
}
.el-tree-node__content:hover,
.el-tree-node__content:active {
.set-tool {
display: inline-block;
}
}
.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
background-color: #354e67 !important;
color: #fff;
}
.box_info {
display: flex;
justify-content: space-between;
}
.pagination-container {
padding: 10px 0 !important;
}
> div {
height: 100%;
width: 100%;
}
.tree_left1 {
width: 20%;
background-color: #fff;
border: 1px solid #dddddd;
border-radius: 6px;
padding: 6px 0px;
position: relative;
min-width: 26%;
border-right: 6px solid;
border-right-color: rgba(204, 230, 255, 0);
.resize-handle-right {
top: 0;
width: 6px;
height: 100%;
right: -10px;
cursor: ew-resize;
position: absolute;
z-index: 999;
}
.check_select {
display: flex;
align-items: center;
width: 100%;
// justify-content: space-between;
padding: 4px;
border-bottom: 1px solid #f1f1f1;
.box_btn {
margin: 0 10px 0 20px;
position: relative;
> span {
padding: 4px 10px;
background: #67c23a;
color: #fff;
border-radius: 2px;
}
.btn {
position: absolute;
left: 20%;
display: none;
top: -2px;
width: 220px;
.el-button {
float: left;
}
}
}
.box_btn:hover,
.box_btn:active {
cursor: pointer;
.btn {
display: block;
}
}
}
.file_upload {
margin: 2px 0;
}
.input_left {
padding: 6px;
box-sizing: border-box;
// border-bottom: 1px solid #cbcbcb;
}
}
.list_right {
width: 79.5%;
background: white;
border: 1px solid #ededed;
padding: 10px;
box-sizing: border-box;
}
.el-tree {
height: calc(80vh - 160px);
width: 100%;
overflow: auto !important;
}
// .el-tree-node__children {
// overflow: visible !important;
// }
}
</style>

View File

@ -0,0 +1,356 @@
<template>
<div class="p-4 bg-gray-50">
<div class="max-w-4xl mx-auto">
<!-- 顶部按钮区域 -->
<el-card class="mb-4 rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.id"
:status="form.status"
:pageType="routeParams.type"
/>
</el-card>
<!-- 表单区域 -->
<el-card class="rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md overflow-hidden">
<div class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-800">设计原则</h3>
</div>
<div class="p-6">
<el-form
ref="leaveFormRef"
v-loading="loading"
:disabled="routeParams.type === 'view' || form.status == 'waiting'"
:model="form"
:rules="rules"
label-width="100px"
class="space-y-4"
>
<div class="grid grid-cols-1 gap-4">
<el-row>
<el-col :span="12">
<el-form-item label="文件名称" prop="formNo">
<el-input disabled v-model="form.fileName" placeholder="请输入文件名称" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="文件" prop="formNo">
<div style="display: flex">
<span style="color: rgb(50, 142, 248)">{{ form.originalName }}</span>
<!-- <el-button type="primary" link @click="handleView(scope.row)"
><el-icon><View /></el-icon>查看</el-button
> -->
</div>
</el-form-item>
</el-col>
</el-row>
</div>
</el-form>
</div>
</el-card>
<!-- 提交组件 -->
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<approvalRecord ref="approvalRecordRef"></approvalRecord>
<!-- 流程选择对话框 -->
<el-dialog
draggable
v-model="dialogVisible.visible"
:title="dialogVisible.title"
:before-close="handleClose"
width="500"
class="rounded-lg shadow-lg"
>
<div class="p-4">
<p class="text-gray-600 mb-4">请选择要启动的流程</p>
<el-select v-model="flowCode" placeholder="请选择流程" style="width: 100%">
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<template #footer>
<div class="dialog-footer p-4 border-t border-gray-100 flex justify-end space-x-3">
<el-button @click="handleClose" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
>取消</el-button
>
<el-button type="primary" @click="submitFlow()" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
>确认</el-button
>
</div>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup name="Leave" lang="ts">
import { LeaveForm, LeaveQuery, LeaveVO } from '@/api/workflow/leave/types';
import { startWorkFlow } from '@/api/workflow/task';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { useUserStoreHook } from '@/store/modules/user';
const { design_change_reason_type } = toRefs<any>(proxy?.useDict('design_change_reason_type'));
import { getKnowledgeDocument } from '@/api/design/technicalStandard';
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const buttonLoading = ref(false);
const loading = ref(true);
//路由参数
const routeParams = ref<Record<string, any>>({});
const flowCode = ref<string>('');
const status = ref<string>('');
const dialogVisible = reactive<DialogOption>({
visible: false,
title: '流程定义'
});
//提交组件
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
//审批记录组件
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
//按钮组件
const flowCodeOptions = [
{
value: currentProject.value?.id + '_principletechnical',
label: '设计原则审批'
},
{
value: currentProject.value?.id + '_requirementstechnica',
label: '业主需求清单审批'
}
];
const leaveFormRef = ref<ElFormInstance>();
const dialog = reactive({
visible: false,
title: '',
isEdit: false
});
const submitFormData = ref<StartProcessBo>({
businessId: '',
flowCode: '',
variables: {}
});
const taskVariables = ref<Record<string, any>>({});
const initFormData = {
id: undefined,
fileName: undefined,
fileUrl: undefined,
status: undefined,
originalName: undefined
};
const data = reactive({
form: { ...initFormData },
rules: {}
});
const handleClose = () => {
dialogVisible.visible = false;
flowCode.value = '';
buttonLoading.value = false;
};
const { form, rules } = toRefs(data);
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
leaveFormRef.value?.resetFields();
};
/** 获取详情 */
const getInfo = () => {
loading.value = true;
buttonLoading.value = false;
nextTick(async () => {
const res = await getKnowledgeDocument(routeParams.value.id);
Object.assign(form.value, res.data);
loading.value = false;
buttonLoading.value = false;
});
};
/** 提交按钮 */
const submitForm = (status1: string) => {
status.value = status1;
submit(status.value, form.value);
};
const submitFlow = async () => {
handleStartWorkFlow(form.value);
dialogVisible.visible = false;
};
//提交申请
const handleStartWorkFlow = async (data: LeaveForm) => {
try {
submitFormData.value.flowCode = flowCode.value;
submitFormData.value.businessId = data.id;
//流程变量
taskVariables.value = {
// leave4/5 使用的流程变量
userList: ['1', '3', '4']
};
submitFormData.value.variables = taskVariables.value;
const resp = await startWorkFlow(submitFormData.value);
if (submitVerifyRef.value) {
buttonLoading.value = false;
submitVerifyRef.value.openDialog(resp.data.taskId);
}
} finally {
buttonLoading.value = false;
}
};
//审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value.init(form.value.id);
};
//提交回调
const submitCallback = async () => {
await proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
};
//审批
const approvalVerifyOpen = async () => {
submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
// 图纸上传成功之后 开始提交
const submit = async (status, data) => {
form.value = data;
if (status === 'draft') {
buttonLoading.value = false;
proxy?.$modal.msgSuccess('暂存成功');
proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
} else {
if ((form.value.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
flowCode.value = flowCodeOptions[0].value;
dialogVisible.visible = true;
return;
}
//说明启动过先随意穿个参数
if (flowCode.value === '' || flowCode.value === null) {
flowCode.value = 'xx';
}
await handleStartWorkFlow(data);
}
};
onMounted(() => {
nextTick(async () => {
routeParams.value = proxy.$route.query;
reset();
loading.value = false;
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
getInfo();
}
});
});
</script>
<style scoped lang="scss">
/* 全局样式 */
:root {
--primary: #409eff;
--primary-light: #66b1ff;
--primary-dark: #3a8ee6;
--success: #67c23a;
--warning: #e6a23c;
--danger: #f56c6c;
--info: #909399;
}
/* 表单样式优化 */
.el-form-item {
.el-form-item__label {
color: #606266;
font-weight: 500;
}
.el-input__inner,
.el-select .el-input__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
.el-textarea__inner {
border-radius: 4px;
transition:
border-color 0.2s,
box-shadow 0.2s;
&:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
}
/* 按钮样式优化 */
.el-button {
border-radius: 4px;
transition: all 0.2s;
&.is-primary {
background-color: var(--primary);
border-color: var(--primary);
&:hover {
background-color: var(--primary-light);
border-color: var(--primary-light);
}
&:active {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
}
&.is-text {
color: var(--primary);
&:hover {
color: var(--primary-light);
background-color: rgba(64, 158, 255, 0.05);
}
}
}
/* 卡片样式优化 */
.el-card {
transition: all 0.3s ease;
&:hover {
/* transform: translateY(-2px); */
}
}
/* 对话框样式优化 */
.el-dialog {
.el-dialog__header {
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
padding: 15px 20px;
}
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-dialog__footer {
padding: 15px 20px;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<el-table :data="data" style="width: 100%" border>
<!-- 文件名称列 -->
<el-table-column prop="fileName" label="文件" align="center">
<template #default="scope">
<el-link
:key="scope.row.fileId"
:href="scope.row.fileUrl"
target="_blank"
:type="scope.row.status == '1' ? 'primary' : 'info'"
:underline="false"
>
{{ scope.row.fileName }}
</el-link>
</template>
</el-table-column>
<!-- 版本号列 -->
<el-table-column prop="size" label="版本号" align="center" width="120">
<template #default="scope">
<span>{{ scope.row.version }}</span>
</template>
</el-table-column>
<!-- 审核状态列 -->
<el-table-column label="审核状态" align="center" prop="auditStatus">
<template #default="scope">
<dict-tag :options="wfBusinessStatus" :value="scope.row.status" />
</template>
</el-table-column>
<!-- 操作列 - 通过slot接收不同标签页的操作按钮 -->
<el-table-column label="操作" width="170" align="center">
<template #default="scope">
<slot name="operation" :row="scope.row"></slot>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
// 获取子组件传入的prop
// 定义文件数据类型
interface FileItem {
fileId: string | number;
fileName: string;
fileUrl: string;
version: string;
status: string;
[key: string]: any;
}
// 定义审核状态选项类型
interface StatusOption {
label: string;
value: string;
[key: string]: any;
}
// 定义组件Props
defineProps<{
// 表格数据
data: FileItem[];
// 审核状态选项
wfBusinessStatus: StatusOption[];
}>();
</script>
<style scoped>
/* 表格相关样式可以在这里定义 */
::v-deep .el-table {
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,253 @@
<template>
<div class="volume-catalog-container">
<div class="history-selector">
<span>选择历史退回记录</span>
<el-select v-model="hisId" placeholder="请选择" style="width: 240px" @change="handleShowInfo">
<el-option v-for="item in hisList" :key="item.id" :label="item.createTime" :value="item.id" />
</el-select>
</div>
<div class="volumeCatalog_box" style="margin-top: 20px">
<!-- <span style="color: #0d9df5">查看excel文件</span> -->
<div class="table-content" id="table-content" style="margin-top: 20px">
<el-row class="mb20" style="display: flex; justify-content: center">
<h2>设计验证表</h2>
</el-row>
<el-row class="mb10" style="display: flex; justify-content: space-between">
<div class="head-text">
<span>编号</span>
<span>{{ examineForm.num }}</span>
</div>
<div class="head-text"></div>
</el-row>
<table style="width: 100%" border="1" cellspacing="1">
<thead>
<tr>
<th colspan="2">工程名称</th>
<td class="th-bg" colspan="10">{{ examineForm.projectName }}</td>
</tr>
</thead>
<tbody>
<tr>
<th colspan="2">子项名称</th>
<td class="th-bg" colspan="6">{{ examineForm.subprojectName }}</td>
<th colspan="2">设计阶段</th>
<td class="th-bg" colspan="2">{{ examineForm.stage }}</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2">专业</th>
<td class="th-bg" colspan="2">{{ examineForm.professional }}</td>
<th colspan="2">卷册</th>
<td class="th-bg" colspan="2">{{ examineForm.volume }}</td>
<th colspan="2">设计人</th>
<td class="th-bg" colspan="2">{{ examineForm.designer }}</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2">验证内容</th>
<td class="th-bg" colspan="10">{{ examineForm.verificationContent }}</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="6">验证意见</th>
<th class="th-bg" colspan="6">执行意见</th>
</tr>
</tbody>
<tbody>
<tr>
<td colspan="6">
<div style="height: 400px; padding: 8px; box-sizing: border-box">
{{ examineForm.verificationOpinion }}
</div>
</td>
<td class="th-bg" colspan="6">
<div style="height: 400px; padding: 8px; box-sizing: border-box">
{{ examineForm.executionOpinion }}
</div>
</td>
</tr>
</tbody>
<tbody>
<tr>
<td colspan="6">
<div style="display: flex; align-items: center; justify-content: space-between; padding: 8px">
<span>校审 {{ examineForm.proofreading }}</span>
<span>{{ dateFormat(examineForm.proofreadingDate) }}</span>
</div>
</td>
<td rowspan="2" colspan="6" class="th-bg">
<div style="padding: 8px">执行人 {{ examineForm.executor }}</div>
<div style="display: flex; align-items: center; justify-content: flex-end; padding: 8px">
{{ dateFormat(examineForm.executorDate) }}
</div>
</td>
</tr>
<tr>
<td class="th-bg" colspan="6">
<div style="display: flex; align-items: center; justify-content: space-between; padding: 8px">
<span>审核 {{ examineForm.audit }}</span>
<span>{{ dateFormat(examineForm.auditDate) }}</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup name="VolumeCatalog" lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ElSelect, ElOption, ElRow } from 'element-plus';
import { drawingreviewReceiptsDetail, drawingreviewReceiptsList } from '@/api/design/drawingreview';
// 定义历史记录项类型
interface HistoryItem {
id: string | number;
createTime: string;
}
// 定义表单数据类型
interface ExamineForm {
num: string;
projectName: string;
subprojectName: string;
stage: string;
professional: string;
volume: string;
designer: string;
verificationContent: string;
verificationOpinion: string;
executionOpinion: string;
proofreading: string;
proofreadingDate: string | Date | null;
executor: string;
executorDate: string | Date | null;
audit: string;
auditDate: string | Date | null;
}
// 响应式变量
const hisId = ref<string | number>('');
const hisList = ref<HistoryItem[]>([]);
const examineForm = reactive<ExamineForm>({
num: '',
projectName: '',
subprojectName: '',
stage: '',
professional: '',
volume: '',
designer: '',
verificationContent: '',
verificationOpinion: '',
executionOpinion: '',
proofreading: '',
proofreadingDate: null,
executor: '',
executorDate: null,
audit: '',
auditDate: null
});
// 处理历史记录选择变化
const handleShowInfo = async (value: string | number) => {
getDetails(value);
};
// 日期格式化方法
const dateFormat = (date: string | Date | null): string => {
if (!date) return '';
// 处理字符串类型的日期
if (typeof date === 'string') {
date = new Date(date);
}
// 检查日期是否有效
if (isNaN(date.getTime())) {
return '';
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
// 获取详情
const getList = async (drawingreviewId) => {
let res = await drawingreviewReceiptsList({ drawingreviewId });
hisList.value = res.rows;
if (hisList.value.length > 0) {
hisId.value = hisList.value[0].id;
getDetails(hisId.value);
}
};
const getDetails = async (id) => {
let res = await drawingreviewReceiptsDetail(id);
Object.assign(examineForm, res.data);
};
// 抛出
defineExpose({ getList });
</script>
<style scoped lang="scss">
.volume-catalog-container {
padding: 20px;
box-sizing: border-box;
table {
border-collapse: collapse; //合并为一个单一的边框
border-color: rgba(199, 199, 199, 1); //边框颜色按实际自定义即可
}
thead {
tr {
th {
background-color: rgba(247, 247, 247, 1); //设置表格标题背景色
height: 35px; //设置单元格最小高度
text-align: center;
letter-spacing: 5px;
padding: 15px;
}
td {
text-align: left;
height: 35px; //设置单元格最小高度
padding: 15px;
}
.th-bg {
background-color: rgba(247, 247, 247, 1);
}
}
}
tbody {
tr {
td {
text-align: left;
height: 40px; //设置单元格最小高度
padding: 15px;
}
th {
height: 35px; //设置单元格最小高度
text-align: center;
letter-spacing: 5px;
padding: 15px;
}
}
}
.table-content {
box-shadow: 0px 0px 10px #ddd;
padding: 20px;
position: relative;
}
}
</style>

View File

@ -0,0 +1,622 @@
<template>
<div class="p-2 volumeCatalog">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="卷册号" prop="volumeNumber">
<el-input v-model="queryParams.volumeNumber" placeholder="请输入卷册号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="资料名称" prop="documentName">
<el-input v-model="queryParams.documentName" placeholder="请输入资料名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery" v-hasPermi="['design:volumeCatalog:query']">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery" v-hasPermi="['design:volumeCatalog:query']">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['design:volumeCatalog:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<file-upload
v-model="form.file"
isImportInfo
:isShowTip="false"
uploadUrl="/design/volumeCatalog/importData"
:limit="1"
:fileType="['xlsx', 'xls']"
:data="{
projectId: currentProject?.id
}"
:file-size="50"
:onUploadSuccess="handleUploadSuccess"
>
<el-button type="warning" plain icon="Upload">导入</el-button>
</file-upload>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="volumeCatalogList">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="子项名称" align="center" prop="designSubitem" />
<el-table-column label="设计状态" align="center" prop="designState">
<template #default="scope">
<dict-tag :options="design_state" :value="scope.row.designState" />
</template>
</el-table-column>
<el-table-column label="专业" align="center" prop="specialtyName"> </el-table-column>
<el-table-column label="设计人员" align="center" prop="principalName" />
<el-table-column label="卷册号" align="center" prop="volumeNumber" />
<el-table-column label="资料名称" align="center" prop="documentName" />
<el-table-column label="计划出图时间" align="center" prop="plannedCompletion" width="200" />
<el-table-column label="审核状态" align="center" prop="auditStatus">
<template #default="scope">
<dict-tag :options="wf_business_status" :value="scope.row.auditStatus" />
</template>
</el-table-column>
<el-table-column label="上传说明" align="center" prop="explainText">
<template #default="scope">
{{ scope.row.fileVoList[0]?.explainText }}
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="图纸文件" align="center" prop="remark" width="150">
<template #default="scope">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['design:volumeFile:query']">查看文件</el-button>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="200">
<template #default="scope">
<el-button
link
type="primary"
v-if="scope.row.auditStatus != 'finish' && scope.row.auditStatus != 'termination' && scope.row.auditStatus != 'waiting'"
icon="Edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['design:volumeFile:edit']"
>修改</el-button
>
<el-button
link
type="primary"
icon="Upload"
@click="handleUpload(scope.row)"
v-if="scope.row.auditStatus == 'draft' || scope.row.auditStatus == 'back'"
v-hasPermi="['design:volumeFile:add']"
>上传图纸</el-button
>
<!-- <el-button
link
type="primary"
icon="edit"
@click="handleAudit(scope.row)"
v-if="scope.row.auditStatus == 'draft' || scope.row.auditStatus == 'back'"
>审核</el-button
>
<el-button link type="primary" icon="View" v-if="scope.row.auditStatus != 'draft'" @click="handleAuditView(scope.row)"
>查看流程</el-button
>
<el-button
type="warning"
link
icon="View"
v-if="scope.row.auditType == 'back' || scope.row.auditStatus == 'termination'"
@click="handleViewHistory(scope.row)"
>查看单据</el-button
> -->
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改卷册目录对话框 -->
<el-dialog draggable :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="volumeCatalogFormRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="子项" prop="designSubitem">
<el-input v-model="form.designSubitem" placeholder="请输入设计子项" />
</el-form-item>
<el-form-item label="专业" prop="specialty">
<el-select v-model="form.specialty" placeholder="请选择专业">
<el-option :value="item.value" v-for="item in des_user_major" :key="item.value" :label="item.label" />
</el-select>
</el-form-item>
<el-form-item label="设计人员" prop="principal">
<el-select v-model="form.principal" placeholder="请选择设计人员" class="transition-all duration-300 border-gray-300">
<el-option v-for="item in userAppList" :key="item.userId" :label="item.userName" :value="item.userId" />
</el-select>
</el-form-item>
<!-- <el-form-item label="设计状态" prop="designState">
<el-select v-model="form.designState" placeholder="请选择设计状态">
<el-option :value="item.value" v-for="item in design_state" :key="item.value" :label="item.label" />
</el-select>
</el-form-item> -->
<el-form-item label="计划出图时间" prop="plannedCompletion">
<el-date-picker v-model="form.plannedCompletion" type="date" value-format="YYYY-MM-DD" placeholder="请选择计划出图时间" />
</el-form-item>
<el-form-item label="卷册号" prop="volumeNumber">
<el-input v-model="form.volumeNumber" placeholder="请输入卷册号" />
</el-form-item>
<el-form-item label="资料名称" prop="documentName">
<el-input v-model="form.documentName" placeholder="请输入资料名称" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog draggable title="上传图纸文件" v-model="uploadVisible" width="500px" append-to-body>
<el-form :model="uploadForm" label-width="80px" :inline="false">
<el-form-item label="蓝图" prop="type">
<el-select v-model="uploadForm.type" placeholder="请选择图纸类型"
><el-option label="过程图纸" value="1" /><el-option label="蓝图" value="3"
/></el-select>
</el-form-item>
<el-form-item v-if="uploadForm.type == '3'" label="蓝图" prop="fileIds">
<file-upload :fileType="['pdf']" :isShowTip="false" :fileSize="100" v-model="uploadForm.fileIds"></file-upload>
</el-form-item>
<el-form-item v-if="uploadForm.type == '1'" label="过程图纸" prop="cancellationIds">
<file-upload :fileType="['pdf']" :isShowTip="false" :fileSize="100" v-model="uploadForm.cancellationIds"></file-upload>
</el-form-item>
</el-form>
<span style="font-size: 12px; color: #999999">注意:请上传pdf格式文件</span>
<div style="display: flex; justify-content: flex-end">
<el-button type="primary" @click="onSubmit" :loading="buttonLoading">确定</el-button>
<el-button @click="uploadVisible = false">取消</el-button>
</div>
</el-dialog>
<!-- 查看文件列表 -->
<el-dialog draggable title="图纸列表" v-model="viewVisible" width="45%">
<el-tabs type="border-card" v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane label="蓝图" name="first"
><TableContent :data="fileList" :wf-business-status="wf_business_status">
<template #operation="{ row }">
<el-button link type="primary" icon="edit" @click="handleAudit(row)" v-if="row.status == 'draft' || row.status == 'back'"
>审核</el-button
>
<el-button link type="primary" icon="View" v-if="row.status != 'draft'" @click="handleAuditView(row)">查看流程</el-button>
<el-button type="danger" link icon="Download" @click="handleDownload(row)"> 下载 </el-button>
</template>
</TableContent></el-tab-pane
>
<el-tab-pane label="过程图纸 " name="second"
><TableContent :data="fileList" :wf-business-status="wf_business_status">
<template #operation="{ row }">
<el-button link type="primary" icon="edit" @click="handleAudit(row)" v-if="row.status == 'draft' || row.status == 'back'"
>审核</el-button
>
<el-button link type="primary" icon="View" v-if="row.status != 'draft'" @click="handleAuditView(row)">查看流程</el-button>
<el-button type="danger" link icon="Download" @click="handleDownload(row)"> 下载 </el-button>
</template>
</TableContent></el-tab-pane
>
<el-tab-pane label="变更" name="third"
><TableContent :data="fileList" :wf-business-status="wf_business_status">
<template #operation="{ row }">
<el-button type="danger" link icon="Download" @click="handleDownload(row)"> 下载 </el-button>
</template>
</TableContent></el-tab-pane
>
<el-tab-pane label="作废 " name="fourth"
><TableContent :data="fileList" :wf-business-status="wf_business_status">
<template #operation="{ row }">
<el-button type="danger" link icon="Download" @click="handleDownload(row)"> 下载1 </el-button>
</template>
</TableContent></el-tab-pane
>
</el-tabs>
<template #footer>
<span>
<el-button type="danger" @click="viewVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
<el-dialog draggable title="单据" v-model="dialogHistory" width="800px">
<histroy ref="histroyRef"></histroy>
</el-dialog>
</div>
</template>
<script setup name="VolumeCatalog" lang="ts">
import {
listVolumeCatalog,
getVolumeCatalog,
delVolumeCatalog,
addVolumeCatalog,
updateVolumeCatalog,
uploadVolumeFile,
getVolumeCatafileList,
lookViewerFile
} from '@/api/design/volumeCatalog';
import { VolumeCatalogVO } from '@/api/design/volumeCatalog/types';
import { useUserStoreHook } from '@/store/modules/user';
import histroy from './comm/histroy.vue';
import TableContent from './comm/tableContent.vue';
const fileList = ref([]);
import { designUserList } from '@/api/design/appointment';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { design_state, wf_business_status, des_user_major } = toRefs(proxy?.useDict('design_state', 'wf_business_status', 'des_user_major'));
import { drawingreviewReceiptsDetail, drawingreviewReceiptsList } from '@/api/design/drawingreview';
const volumeCatalogList = ref<VolumeCatalogVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const histroyRef = ref<InstanceType<typeof histroy>>();
const TableContentRef = ref<InstanceType<typeof TableContentRef>>();
const single = ref(true);
const multiple = ref(true);
const activeName = ref('first');
const total = ref(0);
const dialogHistory = ref(false);
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const queryFormRef = ref<ElFormInstance>();
const volumeCatalogFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const uploadForm = reactive({
userIds: [],
volumeCatalogId: undefined,
fileIds: undefined,
cancellationIds: undefined,
explainText: '',
fileList: [],
type: '1',
cancellationIds: [] // 用于存储已作废的文件ID
});
const examineForm = ref({
audit: '',
auditDate: '',
auditId: '',
designer: '',
executionOpinion: '',
executor: '',
executorDate: '',
executorId: '',
id: '1',
num: '',
professional: '',
projectId: '',
projectName: '',
proofreading: '',
proofreadingDate: '',
proofreadingId: '',
stage: '',
subprojectId: '',
subprojectName: '',
verificationContent: '',
verificationOpinion: '',
volume: ''
});
const userList = ref([]);
const userAppList = ref([]); //人事任命的用户
const initFormData: any = {
design: undefined,
projectId: currentProject.value?.id || '',
designSubitemId: undefined,
volumeNumber: undefined,
documentName: undefined,
designState: '2',
remark: undefined
};
const data = reactive({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
designSubitemId: undefined,
volumeNumber: undefined,
documentName: undefined,
params: {}
},
rules: {
design: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
projectId: [{ required: true, message: '项目ID不能为空', trigger: 'blur' }],
volumeNumber: [{ required: true, message: '卷册号不能为空', trigger: 'blur' }],
documentName: [{ required: true, message: '资料名称不能为空', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询卷册目录列表 */
const getList = async () => {
loading.value = true;
try {
const res = await listVolumeCatalog(queryParams.value);
volumeCatalogList.value = res.rows;
total.value = res.total;
} finally {
loading.value = false;
}
};
const getUserAppList = async () => {
const res = await designUserList({ projectId: currentProject.value?.id });
if (res.code === 200) {
console.log(res.rows);
res.rows.forEach((item: any) => {
if (item.userType == 2) {
//设计人员
userAppList.value.push(item);
}
});
}
};
const handleViewHistory = async (row) => {
// 查看历史流程记录
dialogHistory.value = true;
// 查看历史流程记录
nextTick(() => {
histroyRef.value?.getList(row.design);
});
};
const handleShowInfo = (val) => {
getDetails(val);
};
const getDetails = async (id) => {
let res = await drawingreviewReceiptsDetail(id);
examineForm.value = res.data;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
volumeCatalogFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: VolumeCatalogVO[]) => {
ids.value = selection.map((item) => item.design);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加设计出图计划';
};
const handleView = (row?: any) => {
fileList.value = row.fileVoList;
viewVisible.value = true;
};
/** 上传文件按钮操作 */
const uploadVisible = ref(false);
const viewVisible = ref(false);
const handleUpload = async (row?: any) => {
resetUploadForm();
uploadForm.volumeCatalogId = row.design;
userList.value = row.noViewerList;
const res = await getVolumeCatafileList(row.design);
uploadForm.fileList = res.data.filter((item) => item.status == '1') || [];
uploadVisible.value = true;
};
/** 重置上传表单 */
const resetUploadForm = () => {
uploadForm.userIds = [];
uploadForm.volumeCatalogId = undefined;
uploadForm.fileIds = undefined;
uploadForm.cancellationIds = undefined;
uploadForm.explainText = '';
uploadForm.fileList = [];
uploadForm.type = '1';
uploadForm.cancellationIds = [];
};
/** 提交按钮 */
const submitForm = () => {
volumeCatalogFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.design) {
await updateVolumeCatalog(form.value).finally(() => (buttonLoading.value = false));
} else {
await addVolumeCatalog(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
const handleDownload = (row: any) => {
proxy?.$download.oss(row.fileId);
};
/** 上传文件提交 */
const onSubmit = async () => {
buttonLoading.value = true;
let cancellationIds = [];
let fileIds = [];
if (uploadForm.cancellationIds && uploadForm.cancellationIds.length > 0) {
cancellationIds = uploadForm.cancellationIds.split(',');
}
if (uploadForm.fileIds && uploadForm.fileIds.length > 0) {
fileIds = uploadForm.fileIds.split(',');
}
let obj = {
volumeCatalogId: uploadForm.volumeCatalogId,
fileIds: fileIds,
cancellationIds: cancellationIds,
type: uploadForm.type
};
try {
await uploadVolumeFile(obj);
proxy?.$modal.msgSuccess('文件上传成功');
uploadVisible.value = false;
await getList();
} catch (error) {
console.error('上传文件失败:', error);
} finally {
buttonLoading.value = false;
}
};
/** 删除按钮操作 */
const handleDelete = async (row?: VolumeCatalogVO) => {
const _ids = row?.design || ids.value;
await proxy?.$modal.confirm('是否确认删除卷册目录编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
await delVolumeCatalog(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
const handleUploadSuccess = async (flieList: any, res: any) => {
proxy?.$modal.msgSuccess('文件上传成功');
getList();
};
/** 审核按钮操作 */
const handleAudit = async (row) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/drawingreview/indexEdit`,
query: {
id: row.id,
type: 'update'
}
});
};
/** 查看按钮操作 */
const handleAuditView = async (row) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/drawingreview/indexEdit`,
query: {
id: row.id,
type: 'view'
}
});
};
/** 修改按钮操作 */
const handleUpdate = async (row?: VolumeCatalogVO) => {
reset();
const _id = row?.design || ids.value[0];
const res = await getVolumeCatalog(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改设计出图计划';
};
// 过程图纸
const handleClick = () => {
//
};
const handleAuditInfo = (row) => {
// 审核图纸
};
onMounted(() => {
getUserAppList();
getList();
});
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
queryParams.value.projectId = nid;
form.value.projectId = nid;
getUserAppList();
getList();
}
);
onUnmounted(() => {
listeningProject();
});
</script>
<style lang="scss">
.volumeCatalog_box {
/* .upload-demo {
width: 100% !important;
} */
table {
border-collapse: collapse; //合并为一个单一的边框
border-color: rgba(199, 199, 199, 1); //边框颜色按实际自定义即可
}
thead {
tr {
th {
background-color: rgba(247, 247, 247, 1); //设置表格标题背景色
height: 35px; //设置单元格最小高度
text-align: center;
letter-spacing: 5px;
padding: 15px;
}
td {
text-align: left;
height: 35px; //设置单元格最小高度
padding: 15px;
}
.th-bg {
background-color: rgba(247, 247, 247, 1);
}
}
}
tbody {
tr {
td {
text-align: left;
height: 40px; //设置单元格最小高度
padding: 15px;
}
th {
height: 35px; //设置单元格最小高度
text-align: center;
letter-spacing: 5px;
padding: 15px;
}
}
}
.table-content {
box-shadow: 0px 0px 10px #ddd;
padding: 20px;
position: relative;
}
}
</style>

View File

@ -0,0 +1,365 @@
<template>
<div class="air_advancedSet uav_box">
<div class="header">
<div class="logo">
<img class="title" src="../../images/airsetup.png" alt="" />
<div class="switch" @click="save" title="返回航线设置"></div>
</div>
</div>
<div class="content">
<!-- 拍照设置 -->
<div class="photograph">
<div class="content_header">拍照设置</div>
<div class="btns flex space_between">
<el-checkbox-group v-model="checkboxGroup1" :min="1" class="flex space_between">
<el-checkbox-button v-for="city in cities" :label="city.label" :key="city.value">{{ city.label }}</el-checkbox-button>
</el-checkbox-group>
</div>
<div class="flex space_between">
<div>智能低光</div>
<el-switch
v-model="value"
active-color="rgba(0, 255, 255, 1)"
inactive-color="rgba(0, 255, 255, 0.2)"
:active-value="true"
:inactive-value="false"
>
</el-switch>
</div>
</div>
<!-- 爬升模式 -->
<div class="climb">
<el-radio-group v-model="radio1" class="flex space_between">
<el-radio-button v-for="item in climbs" :label="item"></el-radio-button>
</el-radio-group>
<div class="flex space_between">
<div class="left_img">
<svg-icon icon-class="climb" style="width: 240px; height: 160px; vertical-align: middle"></svg-icon>
</div>
<div class="right_value">
<plusResduce :column="true" unit="m" :max="10000" :min="10"></plusResduce>
</div>
</div>
</div>
<!-- 航线高度模式 -->
<div class="routeAltitude">
<div class="content_header">拍照设置</div>
<el-radio-group v-model="radio2" class="flex space_between">
<el-radio-button v-for="item in routeAltitude" :label="item"></el-radio-button>
</el-radio-group>
<div class="flex space_between">
<div class="left_img">
<svg-icon icon-class="height1" style="width: 240px; height: 160px; vertical-align: middle"></svg-icon>
</div>
<div class="right_value">
<plusResduce :column="true" unit="m"></plusResduce>
</div>
</div>
</div>
<!-- 全局航线速度 -->
<div class="routeSpeed">
<div class="content_header">全局航线速度</div>
<myProgress progressType="num" :max="15" :min="1"></myProgress>
</div>
<!-- 高级设置 -->
<div class="setting">
<el-collapse v-model="activeNames" @change="handleChange">
<el-collapse-item name="1">
<template #title> 高级设置 </template>
<el-divider></el-divider>
<myProgress progressType="num" :max="15" :min="1"></myProgress>
<!-- 航点类型 -->
<div class="subtitle">
<div class="f14 fw500">航点类型</div>
<el-select v-model="value2" placeholder="请选择">
<el-option v-for="item in waypointType" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</div>
<!-- 飞行器偏航角模式 -->
<div class="subtitle">
<div class="f14 fw500">飞行器偏航角模式</div>
<el-select v-model="value3" placeholder="请选择">
<el-option v-for="item in yawAngleMode" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</div>
<!-- 完成动作 -->
<div class="subtitle">
<div class="f14 fw500">完成动作</div>
<el-select v-model="value4" placeholder="请选择">
<el-option v-for="item in completeAction" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</div>
</template>
<script>
import plusResduce from '../component/plusReduce/index.vue';
import myProgress from '../component/progress/index.vue';
import { waypointType, yawAngleMode, completeAction } from '../../utils/options';
export default {
name: 'air_advancedSet',
components: { plusResduce, myProgress },
data() {
return {
value: false,
cities: [
{
label: '广角照片',
value: 'wide'
},
{
label: '变焦照片',
value: 'zoom'
},
{
label: '红外照片',
value: 'ir'
}
],
checkboxGroup1: ['广角照片', '变焦照片', '红外照片'],
climbs: ['垂直爬升', '倾斜爬升'],
radio1: '垂直爬升',
routeAltitude: ['绝对高度', '相对起飞点高度', '相对地形高度'],
radio2: '绝对高度',
activeNames: [],
waypointType: waypointType,
yawAngleMode: yawAngleMode,
completeAction: completeAction,
value2: '2', //航点类型
value3: '1', //偏航角模式
value4: '1' //完成动作
};
},
mounted() {},
methods: {
handleChange(val) {
// console.log(val);
},
// 保存
save() {
this.$emit('save');
}
}
};
</script>
<style lang="scss">
.air_advancedSet {
left: 20px;
user-select: none;
.header {
margin-bottom: 10px;
.logo {
position: relative;
text-align: center;
.title {
width: 380px;
height: 32px;
}
}
.switch {
position: absolute;
top: 5px;
right: 10px;
width: 16px;
height: 16px;
z-index: 20;
background: url('../../images/switch.png');
background-size: cover;
cursor: pointer;
}
.switch:hover {
background: url('../../images/switchh.png');
background-size: cover;
}
}
.content {
height: calc(100% - 36px);
color: #fff;
overflow: auto;
padding-right: 5px;
.content_header {
font-family: 'alimamashuheiti';
font-size: 14px;
font-weight: 700;
margin-bottom: 10px;
}
.photograph {
height: 120px;
padding: 15px 15px 0 15px;
background-color: rgba(0, 0, 0, 0.5);
}
.btns {
margin: 10px 0;
.el-checkbox-group {
width: 100%;
}
.el-checkbox-button:last-child .el-checkbox-button__inner,
.el-checkbox-button:first-child .el-checkbox-button__inner {
border-radius: 5px;
box-shadow: none;
}
.el-checkbox-button.is-checked .el-checkbox-button__inner {
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 1);
color: rgba(0, 255, 255, 1);
}
.el-checkbox-button__inner {
background: rgba(0, 255, 255, 0.2);
color: rgba(0, 255, 255, 0.8);
border: 1px solid rgba(0, 255, 255, 0.5);
margin-right: 8px;
border-radius: 5px;
padding: 8px 15px;
box-shadow: none;
}
}
.climb,
.routeAltitude,
.routeSpeed {
margin-top: 15px;
padding: 15px 15px 0 15px;
background-color: rgba(0, 0, 0, 0.5);
.el-radio-group {
width: 100%;
}
.el-radio-button {
flex: 1;
}
.el-radio-button__inner {
padding: 8px 15px;
width: 100%;
background: rgba(0, 0, 0, 0.5);
border-color: rgba(0, 0, 0, 0);
color: #fff;
}
.el-radio-button__orig-radio:checked + .el-radio-button__inner {
box-shadow: none;
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 1);
}
.el-radio-button:first-child .el-radio-button__inner,
.el-radio-button:last-child .el-radio-button__inner {
border-radius: 0;
}
.left_img {
padding: 15px 0;
}
}
.routeSpeed {
padding-bottom: 5px;
}
.setting {
margin-top: 15px;
padding: 10px;
background-color: rgba(0, 0, 0, 0.5);
position: relative;
z-index: 10;
.el-select {
width: 100%;
margin: 10px 0;
}
.el-input__inner {
background-color: rgba(0, 0, 0, 0.5);
border-color: rgba(0, 255, 255, 0.5);
color: #fff;
}
.el-select .el-input__inner:focus {
border-color: rgba(0, 255, 255, 0.5);
}
// .el-select-dropdown__item.hover,
// .el-select-dropdown__item:hover,
.el-divider--horizontal {
margin: 10px 0;
}
.el-divider {
background: rgba(204, 204, 204, 0.2);
}
.el-collapse-item__header {
background-color: transparent !important;
font-family: 'alimamashuheiti';
font-size: 14px;
}
.el-collapse-item__content {
color: #fff;
padding-bottom: 0;
}
.el-collapse-item__wrap {
border-bottom: none;
}
}
}
/* 修改垂直滚动条 */
.content::-webkit-scrollbar {
width: 8px;
/* 修改宽度 */
}
/* 修改滚动条轨道背景色 */
.content::-webkit-scrollbar-track {
background-color: rgba(0, 255, 255, 0.1);
border-radius: 10px;
}
/* 修改滚动条滑块颜色 */
.content::-webkit-scrollbar-thumb {
background-color: rgba(0, 255, 255, 0.3);
border-radius: 10px;
}
}
.el-select-dropdown {
background-color: rgba(0, 0, 0, 1);
border-color: rgba(0, 255, 255, 0.5);
}
.el-select-dropdown__item.hover,
.el-select-dropdown__item:hover {
background-color: rgba(0, 255, 255, 0.5);
}
.el-select-dropdown__item {
color: #fff;
}
.el-select-dropdown__item.selected {
color: rgba(0, 255, 255, 1);
}
</style>

View File

@ -0,0 +1,750 @@
import { waypoint } from "@/api/air";
export default {
data() {
return {
leftShow: false,
pzDisabled: false,
keywordShow: false,
setupShow: false,
selectId: null,
inputShow: false,
selectObj: {
label: "",
params: {},
type: "",
}, // 选中的动作
selectIndexF: null,
selectIndexS: null,
checkboxGroup1: ["visable", "ir"],
checkboxGroup2: ["wide", "ir", "zoom"],
startPoint: null,
pohoto: [
{
label: "可见光",
value: "visable",
},
{
label: "红外照片",
value: "ir",
},
],
pohoto1: [
{
label: "广角照片",
value: "wide",
},
{
label: "红外照片",
value: "ir",
},
{
label: "变焦照片",
value: "zoom",
},
],
dropdownList: [
{
label: "新增航点(前)",
value: 1,
type: "before",
},
{
label: "新增航点(后)",
value: 2,
type: "after",
},
{
label: "删除",
value: 3,
},
],
controls: [
{
url: require("../../../images/control_start.png"),
label: "开始录像",
value: "startRecord",
svg: "start_record",
params: {
fileSuffix: "123456",
payloadPositionIndex: 0,
useGlobalPayloadLensIndex: 1, //是否使用全局 1为使用全局 0不使用全局
payloadLensIndex: "visable,ir",
},
},
{
url: require("../../../images/control_suspend.png"),
label: "停止录像",
value: "stopRecord",
svg: "stop_record",
params: {
payloadPositionIndex: 0,
},
},
// {
// url: require("../../../images/control_Isochronous.png"),
// label: "开始等时间隔拍照",
// value: "takePhoto",
// svg: "interval_time",
// params: {
// payloadPositionIndex: 0,
// useGlobalPayloadLensIndex: 1,
// payloadLensIndex: "visable,ir",
// },
// actionTrigger: {
// actionTriggerType: "multipleTiming",
// actionTriggerParam: 10,
// },
// },
// {
// url: require("../../../images/control_equidistant.png"),
// label: "开始等距间隔拍照",
// value: "takePhoto",
// svg: "interval_distance",
// params: {
// payloadPositionIndex: 0,
// useGlobalPayloadLensIndex: 1,
// payloadLensIndex: "visable,ir",
// },
// actionTrigger: {
// actionTriggerType: "multipleDistance",
// actionTriggerParam: 10,
// },
// },
// {
// url: require("../../../images/control_interval.png"),
// label: "结束间隔拍照",
// value: "",
// svg: "interval_stop",
// },
{
url: require("../../../images/control_hover.png"),
label: "悬停",
value: "hover",
svg: "hover",
params: {
hoverTime: 10,
},
},
{
url: require("../../../images/control_aircraft.png"),
label: "飞行器偏航角",
value: "rotateYaw",
svg: "drone_yaw",
params: {
aircraftHeading: 0,
aircraftPathMode: "counterClockwise",
},
},
// {
// url: require("../../../images/control_yaw.png"),
// label: "云台偏航角",
// value: "gimbalRotate",
// svg: "action_gimbal_pitch",
// params: {
// gimbalHeadingYawBase: "north",
// gimbalRotateMode: "absoluteAngle",
// gimbalPitchRotateEnable: 1,
// gimbalPitchRotateAngle: 0,
// gimbalRollRotateEnable: 0,
// gimbalRollRotateAngle: 0,
// gimbalYawRotateEnable: 0,
// gimbalYawRotateAngle: 0,
// gimbalRotateTimeEnable: 0,
// gimbalRotateTime: 0,
// payloadPositionIndex: 0,
// },
// },
{
url: require("../../../images/control_pitch.png"),
label: "云台俯仰角",
value: "gimbalRotate",
svg: "action_gimbal_pitch",
params: {
gimbalHeadingYawBase: "north",
gimbalRotateMode: "absoluteAngle",
gimbalPitchRotateEnable: 1,
gimbalPitchRotateAngle: 0, // 变化的值
gimbalRollRotateEnable: 0,
gimbalRollRotateAngle: 0,
gimbalYawRotateEnable: 0,
gimbalYawRotateAngle: 0,
gimbalRotateTimeEnable: 0,
gimbalRotateTime: 0,
payloadPositionIndex: 0,
},
},
{
url: require("../../../images/control_photograph.png"),
label: "拍照",
value: "takePhoto",
svg: "take_photo",
params: {
payloadPositionIndex: 0,
useGlobalPayloadLensIndex: 1,
payloadLensIndex: "wide,zoom,ir",
},
},
{
url: require("../../../images/control_zoom.png"),
label: "相机变焦",
value: "zoom",
svg: "camera_zoom",
params: {
focalLength: 24,
isUseFocalFactor: 0,
payloadPositionIndex: 0,
},
},
{
url: require("../../../images/control_panorama.png"),
label: "全景拍照",
value: "panoShot",
svg: "pano_shot",
params: {
payloadPositionIndex: 0,
useGlobalPayloadLensIndex: 0,
payloadLensIndex: "visable,ir",
panoShotSubMode: "panoShot_360",
},
},
],
points: [],
obj: {
type: 0, // 航线类型
imageFormat: "wide,zoom,ir,narrow_band,visable", // 拍照配置
estimateTime: "", // 预计执行时间 sdk
photoNum: 0, // 航点拍照数量 sdk
waylineLen: "", // 航线长度 sdk
flag: "",
// 0-91-1
droneInfo: {
// 无人机的型号及其子类型
droneEnumValue: "91",
droneSubEnumValue: "1",
},
// 3-2-0
payloadInfo: {
// 负载类型和子类型以及负载的位置
payloadEnumValue: "81",
payloadPositionIndex: "0",
payloadSubEnumValue: "0",
},
placemarkList: [], // 经纬度
globalHeight: 120, // 相对起飞点高度
autoFlightSpeed: 10, // 全局航线速度
globalShootHeight: 60,
// remark: undefined,
// imageFormat: '',
takeOffSecurityHeight: 100, //安全起飞高度
globalTransitionalSpeed: 10, //飞向首航点速度
takeOffRefPoint: {
lng: undefined,
lat: undefined,
alt: undefined,
},
},
networkState: {},
length: 0,
time: 0,
gensui: true,
inputDiv: false,
};
},
mounted() {
this.createTongBu();
// let points = JSON.parse(localStorage.getItem("routePoints"));
let pointsAndTakeOff = JSON.parse(this.routeLine.points);
console.log(
"pointsAndTakeOff",
this.routeLine,
this.routeLine.normalHeight
);
// console.log("points", points);
if (pointsAndTakeOff) {
this.points = pointsAndTakeOff.points;
console.log(".takeOffRefPoint", pointsAndTakeOff);
// return;
let takeOff = pointsAndTakeOff.takeOffRefPoint.split(",");
this.obj.takeOffRefPoint = {
lng: takeOff[1] - 0,
lat: takeOff[0] - 0,
alt: takeOff[2] - 0,
};
console.log("this.obj.takeOffRefPoint", this.obj.takeOffRefPoint);
if (!window.airLine) {
console.log("this.points", this.routeLine.normalHeight);
this.renderRouter(this.points);
}
if (!window.PointEntity) {
this.renderStart(this.obj.takeOffRefPoint);
}
} else {
this.drawStart();
}
},
computed: {
getPhotoNum() {
let photoNum = 0;
this.points.forEach((item) => {
if (item.actions.length > 0) {
item.actions.forEach((item1) => {
if (item1.type == "takePhoto" || item1.type == "panoShot") {
photoNum++;
}
});
}
});
return photoNum;
},
},
methods: {
// 创建地球
createTongBu() {
window.EarthTongbu = new YJ.YJEarth("tongbuEarth");
YJ.Global.setKeyboardEventActive(window.EarthTongbu, false);
const obj = {
compass: false, //罗盘
legend: false, //比例尺
info: false, //信息栏
frame: false, //刷新率
};
let viewer = window.EarthTongbu.viewer;
YJ.Global.CesiumContainer(window.EarthTongbu, obj);
viewer.terrainProvider = new Cesium.createWorldTerrain();
let imageryProvider = new Cesium.ArcGisMapServerImageryProvider({
url:
"https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer",
});
viewer.imageryLayers.addImageryProvider(imageryProvider);
viewer.scene.screenSpaceCameraController.enableRotate = false;
viewer.scene.screenSpaceCameraController.enableZoom = false;
viewer.scene.screenSpaceCameraController.enableTilt = false;
},
// 跟随航线
genliuhangxian() {
this.gensui = !this.gensui;
if (this.gensui) {
this.selectObj.params.useGlobalPayloadLensIndex = 1;
} else {
this.selectObj.params.useGlobalPayloadLensIndex = 0;
}
},
// genliuhangxian
genliuhangxian2() {
this.selectObj.params.useGlobalPayloadLensIndex = 1;
},
// 保存成功
saveSuucess() {
this.inputDiv = false;
this.$message.success("保存成功");
},
// 删除成功
deleteSuucess() {
this.$message.success("删除成功");
this.inputShow = false;
this.selectObj.params.fileSuffix = "";
},
changeGroup() {
this.selectObj.params.payloadLensIndex = this.checkboxGroup1.join();
},
changeGroup1() {
this.selectObj.params.payloadLensIndex = this.checkboxGroup2.join();
},
interval_distance(value) {
let svg = this.selectObj.svg;
if (svg == "interval_time" || svg == "interval_distance") {
this.selectObj.actionTrigger.actionTriggerParam = value;
}
if (svg == "hover") {
console.log("hoverTime", this.selectObj.params.hoverTime);
this.selectObj.params.hoverTime = value;
}
},
//
hpr(value) {
let svg = this.selectObj.svg;
if (svg == "drone_yaw") {
this.selectObj.params.aircraftHeading = value;
let h = value == 0 ? 0 : value;
window.airLine.frustum.updateFrustumHPR(
h,
window.airLine.frustum.pitch,
window.airLine.frustum.roll,
true,
"alone"
);
}
if (svg == "action_gimbal_pitch") {
function mapValue(x, a1, b1, a2, b2) {
// 线性映射公式
return a2 + ((x - a1) / (b1 - a1)) * (b2 - a2);
}
// 原始区间和目标区间
const range1 = [35, -90]; // 原始区间
const range2 = [60, 180]; // 目标区间
let mappedValue = mapValue(
value,
range1[0],
range1[1],
range2[0],
range2[1]
);
this.selectObj.params.gimbalPitchRotateAngle = value;
let p = mappedValue == 0 ? 90 : mappedValue;
// return;
let hh = Cesium.Math.toDegrees(window.airLine.frustum.hpr.heading);
window.airLine.frustum.updateFrustumHPR(hh, p, 0, true, "alone");
}
if (svg == "camera_zoom") {
this.selectObj.params.focalLength = value * 24;
}
},
closeSetup() {
this.setupShow = false;
this.selectIndexS = null;
},
beforeHandleDropMenu(dropId, airLine) {
return {
dropId: dropId,
airLine: airLine,
};
},
handleDropMenu(command) {
const { dropId, airLine } = command;
if (dropId == 1 || dropId == 2) {
let draw = new YJ.Draw.DrawPoint(window.Earth1);
draw.start((err, params) => {
if (params != undefined) {
let position = {
...params,
actions: [],
};
if (dropId == 1) {
if (window.airLine) {
this.points.splice(this.selectId, 0, position);
window.airLine.addPoint(this.points);
}
}
if (dropId == 2) {
if (window.airLine) {
if (this.selectId == this.points.length - 1) {
this.points.push(position);
} else {
this.points.splice(this.selectId + 1, 0, position);
}
window.airLine.addPoint(this.points);
}
}
}
});
}
if (dropId == 3) {
const index = this.points.findIndex((obj) => obj.lng == airLine.lng);
this.points = this.points.filter((obj) => obj.lng !== airLine.lng);
if (window.airLine) {
window.airLine.delPosition(index, this);
}
}
},
// 新增航点
addPoint() {
let draw = new YJ.Draw.DrawPoint(window.Earth1);
draw.start((err, params) => {
if (params != undefined) {
let position = {
...params,
actions: [],
};
this.points.push(position);
window.airLine.addPoint(this.points);
this.save(() => {}, false);
}
});
},
// 选中
select(index) {
this.selectId = index;
let item = this.points[index];
let obj = item.actions[0];
if (obj) {
this.openSet(index, 0, obj);
} else {
this.setupShow = false;
this.selectIndexS = null;
}
if (window.airLine) {
//判断动作组中有value为rotateYaw、gimbalRotate时就不执行window.airLine.updateFrustumPosition(index);
let flag = item.actions.some(
(obj) => obj.value == "rotateYaw" || obj.value == "gimbalRotate"
);
if (!flag) {
window.airLine.updateFrustumPosition(index);
}
}
},
add() {},
more() {
this.leftShow = !this.leftShow;
},
// 打开高级设置
senior() {
this.save(() => {
this.$emit("seniorSet");
});
},
// 返回
back() {
if (this.points.length > 0) {
this.save(() => {
if (airLine) {
window.airLine.remove();
window.airLine = null;
}
if (window.PointEntity) {
window.PointEntity.remove();
window.PointEntity = null;
}
this.$emit("back");
}, false);
} else {
this.$emit("back");
}
this.$changeComponentShow(".bottomTabs", true);
},
// 绘制航线
draw() {
let draw = new YJ.Draw.DrawPolyline(window.Earth1, {
tipText: "请选择航点(左键确定,右键结束)",
});
draw.start((err, positions) => {
if (positions.length == 0) {
return;
}
positions.forEach((el) => {
el.actions = [];
return el;
});
this.points = [...positions];
this.renderRouter(positions);
});
},
// 绘制起飞点
drawStart() {
let draw = new YJ.Draw.DrawTakeOff(window.Earth1, {
tipText: "选择参考起飞点(左键确定)",
});
draw.start((err, params, flag) => {
if (params != undefined) {
if (!flag) {
this.renderStart(params);
this.obj.takeOffRefPoint = window.PointEntity.options.positions;
} else {
this.obj.takeOffRefPoint = window.airportEntity.options.positions;
}
this.draw();
}
});
},
// 渲染参考起飞点
renderStart(positions) {
let option = {
positions,
label: {
text: "参考起飞点",
fontSize: 20,
},
billboard: {
image: "/static/sdk/img/start.png",
},
};
window.PointEntity = new YJ.Obj.BillboardObject(window.Earth1, option);
window.PointEntity.picking = false;
window.PointEntity.onClick = () => {
setTimeout(() => {
window.PointEntity.positionEditing = true;
}, 100);
};
},
// 渲染航线
renderRouter(positions) {
console.log("globalPointHeight", this.routeLine.globalPointHeight);
let airLine = new YJ.Obj.newAirLine(
{
positions,
image: "/static/img/定位.png",
saveFun: this.save,
selectFun: this.select,
normalHeight: this.routeLine.globalPointHeight,
airHeight: this.routeLine.height,
},
window.Earth1.viewer,
window.EarthTongbu.viewer
);
window.airLine = airLine;
window.airLine.flyTo();
this.length = airLine.countLength();
this.time = airLine.countTime();
// console.log("airLine", airLine);
},
// 添加动作到航点
onClick(item) {
console.log("item", item);
// 添加动作时,
// console.log("this.selectId, item", this.selectId, item);
if (this.selectId === null) {
this.$message.warning("请选择航点,再进行动作添加");
return;
}
let selectItem = this.points[this.selectId];
selectItem.actions.push({
label: item.label,
type: item.value,
params: item.params,
svg: item.svg,
actionTrigger: item.actionTrigger,
});
// 打开当前动作设置
// this.openSet(this.selectId, selectItem.actions.length - 1, item);
// console.log("this.points", this.points);
},
// 点击动作图标
openSet(indexF, indexS, item) {
this.selectIndexF = indexF;
this.selectIndexS = indexS;
this.selectObj = item;
console.log("this.selectObj", this.selectObj);
if (
this.selectObj.type == "takePhoto" ||
this.selectObj.type == "startRecord"
) {
this.checkboxGroup2 = this.selectObj.params.payloadLensIndex.split(",");
if (this.selectObj.params.fileSuffix) {
this.inputDiv = false;
this.inputShow = true;
} else {
this.inputShow = false;
this.inputDiv = true;
}
}
this.setupShow = true;
},
// 删除动作
delAction() {
let curAction = this.points[this.selectIndexF];
curAction.actions.splice(this.selectIndexS, 1);
this.setupShow = false;
// console.log(
// "curActioncurActioncurActioncurAction",
// curAction,
// this.selectIndexS
// );
},
// 在保存之前判断每个点中得动作组,如果前一个动作组中有开始录像,后面的动作组中有全景拍照的动作,就提示用户该航线因为全景拍照的动作处于录像过程中,无法执行,并返回当前有问题的航点的索引
saveBefore() {
let flag = true;
let indexf = null;
this.points.forEach((item, index) => {
if (item.actions.length > 0) {
let startRecord = item.actions.find(
(obj) => obj.type == "startRecord"
);
if (startRecord) {
let nextAction = this.points[index + 1];
if (nextAction) {
let panoShot = nextAction.actions.find(
(obj) => obj.type == "panoShot"
);
if (panoShot) {
flag = false;
indexf = index + 1;
}
}
}
}
});
return {
flag: !flag,
index: indexf,
};
},
// 保存操作
save(cb = null, flag = true) {
let before = this.saveBefore();
if (before.flag) {
this.$message.warning(
`${before.index +
1}#航点的全景拍照动作处于录像过程中,无法执行。请删除该动作后再保存`
);
return;
}
let positions = window.airLine.getNewPositions();
let newPoints = [];
this.points.forEach((item1, index1) => {
let obj = {
actions: item1.actions,
};
positions.forEach((item, index) => {
if (index == index1) {
obj.lng = item.lng;
obj.lat = item.lat;
obj.alt = item.alt - this.routeLine.globalPointHeight;
obj.longitude = item.lng;
obj.latitude = item.lat;
obj.altitude =
item.alt -
this.routeLine.height +
this.routeLine.globalPointHeight;
}
});
newPoints.push(obj);
});
this.points = newPoints;
this.obj.placemarkList = newPoints;
this.obj.id = this.routeLine.id;
if (typeof this.obj.takeOffRefPoint == "object") {
this.obj.takeOffRefPoint =
this.obj.takeOffRefPoint.lat +
"," +
this.obj.takeOffRefPoint.lng +
"," +
this.obj.takeOffRefPoint.alt;
} else {
this.obj.takeOffRefPoint = this.obj.takeOffRefPoint;
}
let obj = {
points: newPoints,
takeOffRefPoint: this.obj.takeOffRefPoint,
};
if (!obj.points[0].lng) {
this.obj.points = [];
this.takeOffRefPoint = "";
window.PointEntity.remove();
window.PointEntity = null;
}
this.obj.points = JSON.stringify(obj);
waypoint(this.obj).then((res) => {
if (flag) {
this.$message.success("操作成功");
}
if (typeof cb == "function") {
cb();
}
});
},
// 航线航点移动
towardsLeft() {},
up() {},
towardsRight() {},
left() {},
after() {},
right() {},
},
};

View File

@ -0,0 +1,627 @@
<template>
<div class="air_list uav_box">
<div class="header">
<div class="logo">
<img class="title" src="../../images/airlist.png" alt="" />
<el-tooltip class="item" effect="dark" content="导入航线" placement="top">
<div class="importPath" @click="importPath"></div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="添加" placement="top">
<div class="add" @click="add"></div>
</el-tooltip>
</div>
<div class="search">
<el-row>
<el-col :span="17">
<el-input placeholder="请输入内容" v-model="queryParam.fileName">
<i slot="suffix" class="el-input__icon el-icon-search" @click="search"></i>
</el-input>
</el-col>
<el-col :span="7">
<div class="date flex">
<span style="margin-right: 5px">时间排序</span>
<div class="flex arrow">
<i class="el-icon-caret-top" @click="order(0)"></i>
<i class="el-icon-caret-bottom" @click="order(1)"></i>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
<el-divider></el-divider>
<div class="content">
<div v-for="item in airList" :key="item.id" class="air_item">
<div class="top flex">
<el-tooltip effect="dark" :content="item.fileName" placement="top-start">
<div class="text">
{{ item.fileName }}
</div>
</el-tooltip>
<div class="flex" style="align-items: center">
<div v-if="item.isImport == 'false'" class="edit" @click="edit(item)"></div>
<el-dropdown @command="handleDropMenu">
<div class="more_icon"></div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="beforeHandleDropMenu(drop.value, item)" v-for="drop in dropdownList" :key="drop.value">{{
drop.label
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<div class="middle m10">
<img src="../../images/wurenji.png" alt="" />
<span>{{ item.deviceType }}</span>
</div>
<div class="bottom m10">
<span class="f14">更新时间</span>
<span class="f12">{{ parseTime(item.updatedAt, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</div>
</div>
</div>
<div class="pagination"></div>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="1"
:pager-count="5"
:page-sizes="[10, 20, 30, 40]"
:page-size="10"
layout="total, sizes, prev, pager, next"
:total="total"
>
</el-pagination>
<myDialog ref="createHX" class="createHX" :title="title" @close="close">
<el-divider></el-divider>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item label="航线名称" prop="value1" style="margin-top: 10px; margin-bottom: 20px">
<el-input placeholder="请输入名称" v-model.trim="form.value1" clearable></el-input>
</el-form-item>
<el-form-item v-if="globalPointHeightShow" label="全局航线高度" prop="value2" style="margin-top: 10px; margin-bottom: 20px">
<el-input placeholder="请输入全局航线高度(2~3000)" v-model.trim.number="form.value2" clearable></el-input>
</el-form-item>
</el-form>
<div class="btns">
<el-button size="small" @click="submit">确定</el-button>
<el-button size="small" @click="cancel">取消</el-button>
</div>
</myDialog>
<input type="file" id="fileInput" style="display: none" accept=".kmz" @change="uploadFile" />
</div>
</template>
<script>
import myDialog from '../component/dialog/index.vue';
import { listPaths, routerAdd, routerRename, routerCopy, delPaths, listInfo, returnImport } from '@/api/air';
import { useAirStore } from '@/store/modules/drone';
import { parseTime } from '@/utils';
export default {
name: 'airList',
components: {
myDialog
},
data() {
return {
value: '',
form: {
value1: '',
value2: ''
},
rules: {
value1: [{ required: true, message: '请输入名称', trigger: 'blur' }],
value2: [
{ required: true, message: '请输入全局航线高度', trigger: 'blur', type: 'number' },
{ min: 2, max: 3000, message: '请输入2~3000之间的数字', trigger: 'blur', type: 'number' }
]
},
airList: [],
curId: '',
title: '创建新航线',
parseTime: parseTime,
queryParam: {
pageNum: 1,
pageSize: 10,
fileName: '',
order: 1
},
dropdownList: [
{
label: '重命名',
value: 1
},
{
label: '复制',
value: 2
},
{
label: '下载',
value: 3
},
{
label: '删除',
value: 4
}
],
gateWay: useAirStore().gateWay,
info: {},
networkState: {},
total: 0,
height: 100,
globalPointHeightShow: true
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getAirRouteList();
this.getList();
},
deep: true
}
},
mounted() {
this.getAirRouteList();
this.getList();
this.$recvChanel('websocketBus', (data) => {
if (data.businessType == 'osd3') {
this.networkState = { ...data.data };
}
});
},
methods: {
handleSizeChange(val) {
this.queryParam.pageSize = val;
this.getAirRouteList();
},
handleCurrentChange(val) {
this.queryParam.pageNum = val;
this.getAirRouteList();
},
uploadFile(event) {
let files = event.target.files;
if (files && files[0]) {
const file = files[0];
const formData = new FormData();
formData.append('file', file);
formData.append('flag', this.gateWay.gateway);
returnImport(formData).then((res) => {
if (res.code == 200) {
this.$message.success('上传成功');
this.getAirRouteList();
}
});
}
},
// 导入航线
importPath() {
document.getElementById('fileInput').click();
},
// 判断是否有地形
getList() {
listInfo({
pageNum: 1,
pageSize: 10,
gateway: this.gateWay.gateway,
type: '飞机'
}).then((res) => {
this.info = res.rows[0];
// console.log("this.info", this.info);
});
},
// 搜索
search() {
this.getAirRouteList();
},
// 获取航线列表数据
getAirRouteList() {
this.queryParam.flag = this.gateWay.gateway;
listPaths(this.queryParam).then((res) => {
let list = res.rows || [];
this.airList = list;
this.total = res.total;
});
},
beforeHandleDropMenu(dropId, airLine) {
return {
dropId: dropId,
airLine: airLine
};
},
handleDropMenu(command) {
// console.log("command", command);
const { dropId, airLine } = command;
if (dropId == 1) {
this.title = '重命名';
this.form.value1 = airLine.fileName;
this.curId = airLine.id;
this.globalPointHeightShow = false;
this.$refs.createHX.open();
}
if (dropId == 2) {
routerCopy(airLine.id).then((res) => {
if (res.code == 200) {
this.$message.success('复制成功');
this.getAirRouteList();
}
});
}
if (dropId == 3) {
if (airLine.fileUrl) {
let link = document.createElement('a');
link.style.display = 'none';
link.href = airLine.fileUrl;
// decodeURIComponent
link.setAttribute('download', airLine.fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link); //下载完成移除元素
} else {
this.$message.warning('请编辑后再下载');
}
}
if (dropId == 4) {
this.$confirm('此操作将永久删除航线数据,是否继续?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
delPaths(airLine.id).then(() => {
this.$message.success('删除成功');
if (window.airLine) {
window.airLine.remove();
window.airLine = null;
}
// 若当前项之前为选中状态清除
this.getAirRouteList();
});
})
.catch(() => {});
}
},
// 创建航线
add() {
this.title = '创建新航线';
this.globalPointHeightShow = true;
this.$refs.createHX.open();
},
// 升降序
order(order) {
this.queryParam.order = order;
this.getAirRouteList();
},
// 确认
submit() {
this.$refs['form'].validate((valid) => {
if (valid) {
if (this.title == '创建新航线') {
routerAdd({
filename: this.form.value1,
remark: this.info.remark,
flag: this.gateWay.gateway,
globalPointHeight: this.form.value2
}).then((res) => {
// console.log(res);
if (res.code == 200) {
this.$message.success('创建成功');
this.getAirRouteList();
}
});
}
if (this.title == '重命名') {
routerRename({
id: this.curId,
newFileName: this.form.value1
}).then((res) => {
if (res.code == 200) {
this.$message.success('重命名成功');
this.getAirRouteList();
}
});
}
this.form.value1 = '';
this.$refs.createHX.close();
} else {
console.log('error submit!!');
return false;
}
});
},
close() {
this.form.value1 = '';
this.form.value2 = '';
// this.$refs.createHX.close();
},
// 取消
cancel() {
this.form.value1 = '';
this.form.value2 = '';
this.$refs.createHX.close();
},
hasTerrain() {
return window.Earth1.viewer.terrainProvider.availability;
},
// 编辑
edit(item) {
let hasTerrain = window.Earth1.viewer.terrainProvider.availability;
if (!hasTerrain) {
this.$message.warning('请先添加地形文件(.pnk,再进行操作');
return;
}
this.$emit('routeEdit', item, this.networkState);
}
}
};
</script>
<style lang="scss">
.air_list {
left: 20px;
.el-pager li {
min-width: 18px;
}
.el-pager li.btn-quicknext,
.el-pager li.btn-quickprev {
color: #fff;
}
.header {
.logo {
position: relative;
text-align: center;
.title {
width: 380px;
height: 32px;
}
.add {
position: absolute;
top: 5px;
right: 10px;
width: 16px;
height: 16px;
z-index: 20;
background: url('../../images/add.png');
background-size: cover;
cursor: pointer;
}
.add:hover {
background: url('../../images/addh.png');
background-size: cover;
}
.importPath {
position: absolute;
top: 5px;
right: 40px;
width: 16px;
height: 16px;
z-index: 20;
background: url('../../images/importpath.png');
background-size: cover;
cursor: pointer;
}
.importPath:hover {
background: url('../../images/importpathh.png');
background-size: cover;
}
}
.search {
margin-top: 16px;
.el-input__inner {
background-color: rgba(0, 0, 0, 0.5);
height: 32px;
line-height: 32px;
border-color: rgba(0, 255, 255, 0.5);
}
.el-input__prefix,
.el-input__suffix {
top: -2px;
}
.date {
height: 32px;
color: #fff;
align-items: center;
justify-content: center;
}
.arrow {
flex-direction: column;
font-size: 12px;
i {
cursor: pointer;
}
}
}
}
.el-divider--horizontal {
margin: 10px 0;
}
.el-divider {
background: rgba(204, 204, 204, 0.2);
}
.content {
height: calc(100% - 130px);
color: #fff;
overflow: auto;
padding-right: 5px;
.air_item {
background: rgba(0, 0, 0, 0.5);
height: 113px;
border-radius: 2px;
padding: 16px;
margin-bottom: 10px;
.top {
justify-content: space-between;
.text {
font-family: 'alimamashuheiti';
width: 200px;
overflow: hidden;
white-space: nowrap;
/* 确保文本在一行内显示允许设置overflow属性 */
text-overflow: ellipsis;
/* 使用省略符号表示文本溢出 */
}
}
.middle {
img {
width: 14px;
height: 14px;
}
}
.more_icon {
height: 20px;
width: 20px;
margin: 0 6px 0 10px;
cursor: pointer;
background: url('../../images/more1.png');
background-size: cover;
}
.more_icon:hover {
background: url('../../images/more2.png');
background-size: cover;
}
.edit {
width: 18px;
height: 18px;
cursor: pointer;
background: url('../../images/edit.png');
background-size: cover;
}
.edit:hover {
background: url('../../images/edith.png');
background-size: cover;
}
.middle,
.bottom {
font-family: sans-serif;
color: rgba(230, 247, 255, 1);
}
}
}
.pagination {
.el-pager li {
width: 20px;
}
}
/* 修改垂直滚动条 */
.content::-webkit-scrollbar {
width: 8px;
/* 修改宽度 */
}
/* 修改滚动条轨道背景色 */
.content::-webkit-scrollbar-track {
background-color: rgba(0, 255, 255, 0.1);
border-radius: 10px;
}
/* 修改滚动条滑块颜色 */
.content::-webkit-scrollbar-thumb {
background-color: rgba(0, 255, 255, 0.3);
border-radius: 10px;
}
.createHX {
.el-input__inner {
background-color: rgba(0, 0, 0, 0.5);
height: 32px;
border-color: rgba(0, 255, 255, 0.5);
width: 100%;
color: #fff;
}
}
.btns {
text-align: center;
.el-button {
background: rgba(0, 255, 255, 0.2);
color: #fff;
border-color: rgba(0, 255, 255, 0.5);
}
.el-button:focus,
.el-button:hover {
border-color: rgba(0, 255, 255, 1);
}
}
}
.el-dropdown-menu {
background: rgba(0, 0, 0, 1);
border: 1px solid rgba(0, 255, 255, 0.5);
color: #fff;
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: rgba(0, 255, 255, 1);
background-color: transparent;
}
.el-dropdown-menu__item {
color: #fff;
}
}
.el-popper .popper__arrow,
.el-popper .popper__arrow::after {
display: none;
}
.el-message-box {
background: linear-gradient(180deg, rgba(0, 255, 255, 0.2) 0%, rgba(0, 255, 255, 0) 100%), rgba(0, 0, 0, 0.3);
border: none;
.el-message-box__content,
.el-message-box__title {
color: #fff;
}
.el-button {
border-radius: 4px;
background: rgba(0, 255, 255, 0.2);
color: #fff;
border: 1px solid rgba(0, 255, 255, 0.5);
}
.el-button:hover {
border-color: rgba(0, 255, 255, 1);
}
}
.el-form-item__label {
color: #fff;
}
</style>

View File

@ -0,0 +1,881 @@
<template>
<div class="airPointSetup uav_box">
<div class="header">
<div class="logo">
<img class="title" src="../../images/airsetup.png" alt="" />
<div class="item">
<div class="back com" @click="back" title="返回"></div>
</div>
<div class="item">
<div class="save com" @click="save" title="保存"></div>
</div>
<div class="item">
<div class="switch com" @click="senior" title="高级设置"></div>
</div>
<!-- <el-tooltip class="item" effect="dark" content="返回" placement="top">
</el-tooltip>
<el-tooltip class="item" effect="dark" content="保存" placement="top">
</el-tooltip>
<el-tooltip class="item" effect="dark" content="高级设置" placement="top">
</el-tooltip> -->
</div>
</div>
<div class="title">
<img src="../../images/title.png" alt="" />
<span>航点列表</span>
</div>
<el-divider></el-divider>
<!-- 航线信息 -->
<div class="info">
<el-row>
<el-col :span="10">
<div class="info_item">
<div>航线长度</div>
<div>{{ length }} m</div>
</div>
</el-col>
<el-col :span="7">
<div class="info_item">
<div>航点</div>
<div>{{ points.length }}</div>
</div>
</el-col>
<el-col :span="7">
<div class="info_item">
<div>照片</div>
<div>{{ getPhotoNum }}</div>
</div>
</el-col>
</el-row>
</div>
<el-divider></el-divider>
<!-- 航点列表 -->
<div class="content">
<div v-for="(item, index) in points" :key="index" @click="select(index, item)" class="point_item"
:class="{ active: selectId == index }">
<div class="left">
<img src="../../images/point.png" alt="" />
<span style="margin: 0 5px;">{{ index + 1 }}</span>
</div>
<div v-if="item.actions.length > 0" class="middle">
<div v-for="(item1, index1) in item.actions" :key="index1" @click.stop="openSet(index, index1, item1)"
style="display: inline-block;margin: 0 3px;cursor: pointer;">
<svg-icon :icon-class="item1.svg" style="width: 16px;height: 16px;" :style="selectIndexS == index1 && selectIndexF == index
? 'fill:rgba(0, 255, 255, 1)'
: 'fill:#fff'
"></svg-icon>
</div>
</div>
<div class="right">
<el-dropdown @command="handleDropMenu" trigger="click">
<div class="more_icon"></div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="beforeHandleDropMenu(drop.value, item)" v-for="drop in dropdownList"
:key="drop.value">{{ drop.label }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<!-- 操作仓 -->
<div v-show="keywordShow" class="keyword flex">
<div class="left mr20">
<div class="top">
<div class="flex space_between">
<div class="img_box">
<img src="../../images/left.png" alt="" />
</div>
<div class="img_box">
<img style="width: 16px;height: 20px;" src="../../images/top.png" alt="" />
</div>
<div class="img_box">
<img src="../../images/right.png" alt="" />
</div>
</div>
<div class="flex space_between ">
<div class="item_bottom" @mousedown="towardsLeft">Q</div>
<div class="item_bottom" @mousedown="up">W</div>
<div class="item_bottom" @mousedown="towardsRight">E</div>
</div>
</div>
<div class="bottom">
<div class="flex space_between ">
<div class="img_box">
<img src="../../images/left_f.png" alt="" />
</div>
<div class="img_box">
<img src="../../images/after_f.png" alt="" />
</div>
<div class="img_box">
<img src="../../images/right_f.png" alt="" />
</div>
</div>
<div class="flex space_between ">
<div class="item_bottom" @mousedown="left">A</div>
<div class="item_bottom" @mousedown="after">S</div>
<div class="item_bottom" @mousedown="right">D</div>
</div>
</div>
</div>
<div class="middle mr20">
<div class="middle_item">
<div class="titel">经度</div>
<div class="value">106.25467890 °</div>
</div>
<div class="middle_item">
<div class="titel">维度</div>
<div class="value">36.25467890 °</div>
</div>
<div class="middle_item">
<div class="titel">高度</div>
<div class="value">90 m</div>
</div>
</div>
<div class="right flex">
<img src="../../images/top_r.png" alt="" />
<div class="item_right">C</div>
<div class="alt flex space_between">
<div class="f30">96.3</div>
<div class="f14 text_algin_right">
<div>ALT</div>
<div>m</div>
</div>
</div>
<div class="f12 asl text_algin_right">581 ASL</div>
<img src="../../images/bottom_r.png" alt="" />
<div class="item_right">Z</div>
</div>
</div>
<!-- 动作 -->
<div class="control">
<div class="right">
<div v-if="points.length > 0" class="control_item" @click="addPoint">
<span>新增航点</span>
<img src="../../images/control_add.png" alt="" />
</div>
<div class="control_item" @click="more">
<span>更多</span>
<img src="../../images/control_more.png" alt="" />
</div>
</div>
<div v-show="leftShow" class="left">
<div class="control_item" v-for="item in controls" :key="item.label" @click="onClick(item)">
<span>{{ item.label }}</span>
<img :src="item.url" alt="" />
</div>
</div>
</div>
<!-- 动作设置 -->
<div v-show="setupShow" class="setup">
<div class="setup_header flex space_between">
<div class="setup_header_left">
<svg-icon icon-class="camera" style="width: 20px;height: 16px;vertical-align: middle;"></svg-icon>
<span class="f14 header_title">{{ selectObj.label }}</span>
</div>
<div class="setup_header_right flex ai_center">
<svg-icon icon-class="left" style="width: 18px;height: 10px;"></svg-icon>
{{ selectIndexF + 1 }}-{{ selectIndexS + 1 }}
<svg-icon icon-class="right" style="width: 18px;height: 10px;color: red;"></svg-icon>
<div class="del" @click="delAction"></div>
</div>
<div class="close" @click="closeSetup"></div>
</div>
<el-divider></el-divider>
<!-- 开始录像 -->
<div v-if="selectObj.svg == 'start_record'" class="setup_content">
<div class="flex space_between">
<div>DJI_YYYYMMDDhhmm_XXX_</div>
<div v-if="!selectObj.params.fileSuffix" class="edit" @click="inputShow = true, inputDiv = true"></div>
</div>
<div v-if="inputShow" class="flex wuyongBox">
<el-input v-if="inputDiv" v-model="selectObj.params.fileSuffix"></el-input>
<div style="flex: 1;" v-if="!inputDiv">{{ selectObj.params.fileSuffix }}</div>
<i class="el-icon-edit wuyong-icon" v-if="!inputDiv" @click="inputDiv = true"></i>
<i class="el-icon-check wuyong-icon" v-if="inputDiv" style="color: green;" @click="saveSuucess"></i>
<i class="el-icon-close wuyong-icon" v-if="inputDiv" style="color: red;" @click="deleteSuucess"></i>
</div>
<div class="flex mt15 space_between">
<div class="btns">
<el-checkbox-group :disabled="!selectObj.params.useGlobalPayloadLensIndex" v-model="checkboxGroup1" :min="1"
:max="2" @change="changeGroup">
<el-checkbox-button v-for="item in pohoto" :label="item.value" :key="item.value">{{ item.label
}}</el-checkbox-button>
</el-checkbox-group>
</div>
<el-button size="small" :class="{ bugen: selectObj.params.useGlobalPayloadLensIndex == 1 }"
@click="genliuhangxian">
<svg-icon icon-class="fly" style="width: 18px;height: 10px;"></svg-icon>跟随航线</el-button>
</div>
</div>
<!-- 停止录像 -->
<!-- 开始等时隔拍照 -->
<div v-if="selectObj.svg == 'interval_time'" class="setup_content">
<div class="flex space_between">
<div>间隔时间</div>
</div>
<div>
<plusReduce :defValue="selectObj.actionTrigger.actionTriggerParam" @value="interval_distance" :max="30"
:min="1">
</plusReduce>
</div>
<div class="flex mt15 space_between">
<div>
<el-button size="small" :disabled="pzDisabled">可见光</el-button>
<el-button size="small" :disabled="pzDisabled">红外照片</el-button>
</div>
<el-button size="small" :class="{ bugen: selectObj.params.useGlobalPayloadLensIndex == 1 }">
<svg-icon icon-class="fly" style="width: 18px;height: 10px;"></svg-icon>跟随航线</el-button>
</div>
</div>
<!-- 拍照 -->
<div v-if="selectObj.svg == 'take_photo'" class="setup_content">
<div class="flex space_between">
<div>DJI_YYYYMMDDhhmm_XXX_</div>
<div v-if="!selectObj.params.fileSuffix" class="edit" @click="inputShow = true, inputDiv = true"></div>
</div>
<div v-if="inputShow" class="flex wuyongBox">
<el-input v-if="inputDiv" v-model="selectObj.params.fileSuffix"></el-input>
<div style="flex: 1;" v-if="!inputDiv">{{ selectObj.params.fileSuffix }}</div>
<i class="el-icon-edit wuyong-icon" v-if="!inputDiv" @click="inputDiv = true"></i>
<i class="el-icon-check wuyong-icon" v-if="inputDiv" style="color: green;" @click="saveSuucess"></i>
<i class="el-icon-close wuyong-icon" v-if="inputDiv" style="color: red;" @click="deleteSuucess"></i>
</div>
<div class="flex mt15 space_between">
<div class="btnss">
<el-checkbox-group :disabled="!selectObj.params.useGlobalPayloadLensIndex" v-model="checkboxGroup2" :min="1"
@change="changeGroup1">
<el-checkbox-button v-for="item in pohoto1" :label="item.value" :key="item.value">{{ item.label
}}</el-checkbox-button>
</el-checkbox-group>
<!-- <el-button size="small" :disabled="pzDisabled">可见光</el-button>
<el-button size="small" :disabled="pzDisabled">红外照片</el-button> -->
</div>
<el-button size="small" :class="{ bugen: selectObj.params.useGlobalPayloadLensIndex == 1 }"
@click="genliuhangxian">
<svg-icon icon-class="fly" style="width: 18px;height: 10px;"></svg-icon>跟随航线</el-button>
</div>
</div>
<!-- 开始等距隔拍照 -->
<div v-if="selectObj.svg == 'interval_distance'" class="setup_content">
<div class="flex space_between">
<div>间隔时间</div>
</div>
<div>
<plusReduce @value="interval_distance" :defValue="selectObj.actionTrigger.actionTriggerParam" unit="m"
:max="100" :min="1"></plusReduce>
</div>
<div class="flex mt15 space_between">
<div>
<el-button size="small" :disabled="pzDisabled">可见光</el-button>
<el-button size="small" :disabled="pzDisabled">红外照片</el-button>
</div>
<el-button size="small" @click="pzDisabled = !pzDisabled">
<svg-icon icon-class="fly" style="width: 18px;height: 10px;"></svg-icon>跟随航线</el-button>
</div>
</div>
<!-- 悬停 -->
<div v-if="selectObj.svg == 'hover'" class="setup_content">
<div class="flex space_between">
<div>间隔时间</div>
</div>
<div>
<plusReduce @value="interval_distance" :defValue="Number(selectObj.params.hoverTime)" :max="1800" :min="1">
</plusReduce>
</div>
</div>
<!-- 飞行器偏航角 -->
<div v-if="selectObj.svg == 'drone_yaw'" class="setup_content">
<div class="flex space_between">
<div>飞行器偏航角</div>
<div class="lan">{{ selectObj.params.aircraftHeading }}°</div>
</div>
<div>
<myProgress :defValue="Number(selectObj.params.aircraftHeading)" @value="hpr" :sliderMax="180"
:sliderMin="-180">
</myProgress>
</div>
<div class="flex mt15 space_between"></div>
</div>
<!-- 云台俯仰角 -->
<div v-if="selectObj.svg == 'action_gimbal_pitch'" class="setup_content">
<div class="flex space_between">
<div>云台俯仰角</div>
<div class="lan">{{ selectObj.params.gimbalPitchRotateAngle }}°</div>
</div>
<div>
<myProgress :defValue="Number(selectObj.params.gimbalPitchRotateAngle)" @value="hpr" :sliderMax="30"
:sliderMin="-90"></myProgress>
</div>
<div class="flex mt15 space_between"></div>
</div>
<!-- 变焦 camera_zoom -->
<div v-if="selectObj.svg == 'camera_zoom'" class="setup_content">
<div class="flex space_between">
<div>变焦</div>
<div class="lan">
{{ Number(selectObj.params.focalLength) / 24 }}
<span style="font-size: 24px;color: #fff;">X</span>
</div>
</div>
<div>
<myProgress :defValue="Number(selectObj.params.focalLength) / 24" @value="hpr" :sliderMax="56" :sliderMin="1"
unit="X"></myProgress>
</div>
<div class="flex mt15 space_between"></div>
</div>
</div>
<div id="tongbuEarth"></div>
</div>
</template>
<script>
import plusReduce from "../component/plusReduce/index.vue";
import myProgress from "../component/progress/index.vue";
import mixin from "./js/pointSetup";
export default {
name: "airSetup",
components: { plusReduce, myProgress },
props: {
routeLine: {
type: Object,
default: () => { },
},
},
mixins: [mixin],
};
</script>
<style lang="scss">
.airPointSetup {
color: #fff;
left: 20px;
.header {
.logo {
position: relative;
text-align: center;
.title {
width: 380px;
height: 32px;
}
.com {
position: absolute;
top: 5px;
width: 16px;
height: 16px;
z-index: 20;
cursor: pointer;
}
.back {
right: 70px;
background: url("../../images/back.png");
background-size: cover;
}
.back:hover {
background: url("../../images/back_h.png");
background-size: cover;
}
.save {
right: 40px;
background: url("../../images/save.png");
background-size: cover;
}
.save:hover {
background: url("../../images/saveh.png");
background-size: cover;
}
.switch {
right: 10px;
background: url("../../images/switch.png");
background-size: cover;
}
.switch:hover {
background: url("../../images/switchh.png");
background-size: cover;
}
}
.search {
.el-input__inner {
background-color: rgba(0, 0, 0, 0.5);
height: 32px;
border-color: rgba(0, 255, 255, 0.5);
}
.date {
height: 32px;
color: #fff;
align-items: center;
justify-content: center;
}
.arrow {
flex-direction: column;
font-size: 12px;
}
}
}
.title {
height: 24px;
color: #fff;
display: flex;
align-items: center;
margin-top: 8px;
img {
width: 20px;
height: 16px;
margin-right: 10px;
}
}
.info {
.info_item {
div {
text-align: center;
line-height: 24px;
}
div:last-child {
font-family: "ddin";
}
}
}
.content {
height: calc(100% - 154px);
color: #fff;
overflow: auto;
padding-right: 5px;
.point_item {
display: flex;
justify-content: space-between;
align-items: center;
min-height: 40px;
// margin-bottom: 10px;
border-bottom: 1px solid rgba(204, 204, 204, 0.2);
background: transparent;
.left {
min-width: 50px;
margin-left: 15px;
img {
width: 12px;
height: 16px;
}
}
.middle {
flex: 1;
align-items: center;
img {
width: 12px;
height: 16px;
margin: 0 5px;
}
}
.right {
.more_icon {
height: 20px;
width: 20px;
margin: 0 6px 0 10px;
cursor: pointer;
background: url("../../images/more1.png");
background-size: cover;
}
.more_icon:hover {
background: url("../../images/more2.png");
background-size: cover;
}
}
}
.active {
background: linear-gradient(180deg,
rgba(0, 255, 255, 0) 0%,
rgba(0, 255, 255, 0.5) 100%);
}
.point_item:hover {
background: linear-gradient(180deg,
rgba(0, 255, 255, 0) 0%,
rgba(0, 255, 255, 0.5) 100%);
}
}
/* 修改垂直滚动条 */
.content::-webkit-scrollbar {
width: 8px;
/* 修改宽度 */
}
/* 修改滚动条轨道背景色 */
.content::-webkit-scrollbar-track {
background-color: rgba(0, 255, 255, 0.1);
border-radius: 10px;
}
/* 修改滚动条滑块颜色 */
.content::-webkit-scrollbar-thumb {
background-color: rgba(0, 255, 255, 0.3);
border-radius: 10px;
}
.el-divider--horizontal {
margin: 10px 0;
}
.el-divider {
background: rgba(204, 204, 204, 0.2);
}
.text_align_center {
text-align: center;
}
.keyword {
position: fixed;
left: 50%;
bottom: 6%;
transform: translate(-50%, -8%);
.left {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.top,
.bottom {
width: 157px;
height: 80px;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 8px;
padding: 10px;
.img_box {
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
img {
width: 20px;
height: 18px;
}
}
.item_bottom {
width: 30px;
height: 30px;
line-height: 30px;
border-radius: 2px;
background: rgba(60, 60, 60, 1);
text-align: center;
}
}
.bottom {
.img_box:nth-child(1) {
img {
width: 12px;
height: 18px;
}
}
.img_box:nth-child(2) {
img {
width: 18px;
height: 12px;
}
}
.img_box:nth-child(3) {
img {
width: 12px;
height: 18px;
}
}
}
.middle {
min-width: 145px;
border-radius: 8px;
padding: 16px;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
justify-content: space-between;
.middle_item {
.title {
font-size: 16px;
font-weight: 400;
color: rgba(230, 247, 255, 1);
}
.value {
font-family: "ddin";
font-size: 18px;
font-weight: 700;
color: rgba(255, 255, 255, 1);
}
}
}
.right {
min-width: 120px;
border-radius: 8px;
padding: 10px 16px;
background-color: rgba(0, 0, 0, 0.5);
flex-direction: column;
align-items: center;
justify-content: space-between;
img {
width: 16px;
height: 20px;
}
.item_right {
width: 30px;
height: 30px;
line-height: 30px;
border-radius: 2px;
background: rgba(60, 60, 60, 1);
text-align: center;
}
.alt,
.asl {
font-family: "ddin";
}
.alt {
color: rgba(0, 255, 255, 1);
}
.asl {
width: 100%;
}
}
.middle,
.right {
height: 200px;
}
}
.control {
position: fixed;
right: 2%;
top: 35%;
// background-color: transparent;
.control_item {
text-align: right;
margin-bottom: 20px;
span {
font-size: 14px;
font-weight: 700;
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000,
1px 1px 0 #000;
/* 文字阴影,模拟边框效果 */
}
img {
width: 30px;
height: 30px;
vertical-align: middle;
}
}
.left {
position: absolute;
right: 120px;
top: 0;
min-width: 200px;
}
}
// 设置
.setup {
position: fixed;
top: 10%;
right: 5%;
min-width: 400px;
min-height: 100px;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 8px;
border: 1px solid rgba(0, 255, 255, 0.5);
padding: 30px 12px 12px 16px;
.close {
width: 15px;
height: 15px;
position: absolute;
top: 8px;
right: 12px;
cursor: pointer;
background: url("../../images/close1.png");
background-size: cover;
}
.setup_header_left {
.header_title {
font-weight: 500;
color: rgba(0, 255, 255, 1);
}
}
.setup_header_right {
.del {
width: 14px;
height: 14px;
background: url("../../images/del.png");
background-size: cover;
cursor: pointer;
margin: 0 0 0 8px;
}
.del:hover {
background: url("../../images/delh.png");
background-size: cover;
}
}
.setup_content {
margin: 10px 0;
.wuyongBox {
align-items: center;
margin-top: 10px;
.wuyong-icon {
font-weight: bold;
margin: 0 5px;
cursor: pointer;
}
}
.bugen {
// background: rgba(0, 255, 255, 0.2) !important;
// color: rgba(0, 255, 255, 0.8) !important;
// border: 1px solid rgba(0, 255, 255, 0.5) !important;
background: rgb(255 255 255 / 20%) !important;
color: rgb(255 255 255 / 80%) !important;
border: 1px solid rgb(107 107 107 / 50%) !important;
}
.el-checkbox-group {
width: 100%;
}
.el-checkbox-button:last-child .el-checkbox-button__inner,
.el-checkbox-button:first-child .el-checkbox-button__inner {
border-radius: 5px;
box-shadow: none;
}
.el-checkbox-button.is-checked .el-checkbox-button__inner {
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 1);
color: rgba(0, 255, 255, 1);
}
.el-checkbox-button__inner {
// background: rgba(0, 255, 255, 0.2);
// color: rgba(0, 255, 255, 0.8);
// border: 1px solid rgba(0, 255, 255, 0.5);
background: rgb(255 255 255 / 20%);
color: rgb(255 255 255 / 80%);
border: 1px solid rgb(107 107 107 / 50%);
margin-right: 8px;
border-radius: 5px;
padding: 8px 15px;
box-shadow: none;
}
.el-input__inner {
background-color: transparent;
border-color: aqua;
height: 30px;
line-height: 30px;
color: #fff;
}
.edit {
width: 14px;
height: 14px;
background: url("../../images/edit.png");
background-size: cover;
cursor: pointer;
}
.el-button.is-disabled,
.el-button.is-disabled:focus,
.el-button.is-disabled:hover {
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 0.5);
color: rgba(255, 255, 255, 0.8);
}
.el-button {
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 1);
color: rgba(0, 255, 255, 1);
}
.lan {
color: rgba(0, 255, 255, 1);
font-size: 30px;
font-family: "ddin";
}
}
}
}
.el-dropdown-menu {
background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(0, 255, 255, 0.5);
color: #fff;
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: rgba(0, 255, 255, 1);
background-color: transparent;
}
}
.el-popper .popper__arrow,
.el-popper .popper__arrow::after {
display: none;
}
#tongbuEarth {
width: 300px;
height: 200px;
position: fixed;
right: 0;
bottom: 0;
background: aqua;
}
</style>

View File

@ -0,0 +1,139 @@
<template>
<div class="bottom_tabs flex ai_center center">
<div v-for="item in tabs" class="item" :class="{ active: item.id == selectId }" @click="select(item)">
<div class="text" :class="{ active: item.id == selectId }">
{{ item.label }}
</div>
</div>
</div>
</template>
<script>
import $modal from '@/plugins/modal';
export default {
data() {
return {
tabs: [
{
label: '无人机',
id: 1
},
{
label: '航线库',
id: 2
},
{
label: '计划库',
id: 3
},
{
label: '媒体库',
id: 4
}
],
selectId: 1
};
},
created() {},
mounted() {},
methods: {
select(item) {
let airGateway = JSON.parse(localStorage.getItem('airGateway'));
if (!airGateway) {
$modal.msgWarning('请先选择无人机');
return;
}
this.selectId = item.id;
this.$emit('select', item);
}
}
};
</script>
<style lang="scss" scoped>
.bottom_tabs {
width: 50%;
height: 80px;
position: absolute;
z-index: 10;
left: 25%;
bottom: 2%;
// background: rgba(0, 0, 0, 0.5);
// border: 1px solid rgba(0, 255, 255, 0.5);
.item {
position: relative;
width: 136px;
height: 40px;
line-height: 45px;
background: url('../../images/tab.png');
background-size: cover;
font-family: 'alimamashuheiti';
text-align: center;
color: aliceblue;
cursor: pointer;
.text {
background: linear-gradient(180deg, #ffffff, #efffff, #00ffff);
/* 标准语法 */
-webkit-background-clip: text;
/* Chrome, Safari */
background-clip: text;
-webkit-text-fill-color: transparent;
/* Chrome, Safari */
color: transparent;
/* 其他浏览器 */
}
.text:hover,
.active {
background: linear-gradient(180deg, #ffffff, #fff8eb, #ffa600);
/* 标准语法 */
-webkit-background-clip: text;
/* Chrome, Safari */
background-clip: text;
-webkit-text-fill-color: transparent;
/* Chrome, Safari */
color: transparent;
/* 其他浏览器 */
}
}
.item:hover,
.active {
background: url('../../images/tab_h.png');
background-size: cover;
}
.item::after {
// content: " 🔗";
position: absolute;
left: 50%;
bottom: -16px;
/* 初始设置为不可见 */
opacity: 0;
transition: opacity 0.3s ease;
transform: translate(-50%);
content: '';
display: block;
width: 22px;
/* 设置图片宽度 */
height: 16px;
/* 设置图片高度 */
background-image: url('../../images/hover.png');
/* 设置图片路径 */
background-size: cover;
/* 背景图片覆盖整个元素 */
background-position: center;
/* 背景图片居中 */
}
.item:hover::after {
opacity: 1;
}
.active::after {
opacity: 1;
}
}
</style>

View File

@ -0,0 +1,114 @@
<template>
<div class="dialog" id="my_dialog" v-if="dialogShow">
<div class="header" id="my_dialog_header">
<span class="title">{{ title }}</span>
<div class="close" @click="close">
<div class="sector"></div>
<img src="../../../images/close.png" alt="" />
</div>
</div>
<slot></slot>
</div>
</template>
<script>
import { setMove } from '@/utils/moveDiv.ts';
export default {
name: 'dialog',
props: {
width: {
type: String,
default: ''
},
title: {
type: String,
default: '默认'
}
},
data() {
return {
dialogShow: false
};
},
mounted() {},
methods: {
close() {
this.$emit('close', false);
this.dialogShow = false;
},
open() {
this.dialogShow = true;
this.$nextTick(() => {
setMove('my_dialog_header', 'my_dialog');
});
}
}
};
</script>
<style lang="scss" scoped>
.dialog::before {
content: '';
width: 100px;
height: 6px;
background: aqua;
position: absolute;
top: -6px;
left: -2px;
clip-path: polygon(0% 0%, 90% 0%, 100% 100%, 0% 100%);
}
.dialog {
position: fixed;
top: 50%;
left: 50%;
z-index: 100;
width: 515px;
transform: translate(-50%, -50%);
padding: 10px;
background: linear-gradient(180deg, rgba(0, 255, 255, 0.2) 0%, rgba(0, 255, 255, 0) 100%), rgba(0, 0, 0, 0.6);
border: 1.5px solid rgba(0, 255, 255, 1);
color: #fff;
.header {
height: 40px;
padding: 10px;
.title {
font-family: 'alimamashuheiti';
font-size: 18px;
font-weight: 700;
text-shadow: 0px 0px 9px rgba(20, 118, 255, 1);
}
.close {
cursor: pointer;
}
.sector {
position: absolute;
top: -30px;
right: -30px;
width: 0;
height: 0;
border: 30px solid transparent;
/* 边框宽度和颜色可以调整 */
border-bottom-color: rgba(0, 255, 255, 0.5);
/* 底边的颜色 */
border-radius: 50%;
/* 将边框变为圆形 */
transform: rotate(45deg);
/* 旋转45度可根据需要调整角度 */
}
img {
position: absolute;
top: 5px;
right: 5px;
width: 14px;
height: 14px;
}
}
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<div class="plusReduce flex" :class="{ flex_column: column }">
<div class="left flex" :class="{ flex_column: column }" @click="calculate">
<span :class="{ disabled: active }">-100</span>
<span :class="{ disabled: active }">-10</span>
<span v-show="!column" :class="{ disabled: active }">-1</span>
</div>
<div class="middle">{{ value }} {{ unit }}</div>
<div class="right flex" :class="{ flex_column: column }" @click="calculate">
<span v-show="!column">+1</span>
<span>+10</span>
<span>+100</span>
</div>
</div>
</template>
<script>
export default {
name: "plusReduce",
props: {
defValue: {
type: Number || String,
default: 0,
},
unit: {
type: String,
default: "s",
},
max: {
type: Number,
default: 10,
},
min: {
type: Number,
default: 1,
},
column: {
type: Boolean,
default: false,
},
},
data() {
return {
active: false,
value: 1,
};
},
watch: {
value(newVal) {
if (newVal == this.min) {
this.active = true;
} else {
this.active = false;
}
},
defValue(newVal) {
this.value = newVal;
},
},
mounted() {
this.value = this.defValue;
console.log('this.value', this.value);
},
methods: {
calculate(e) {
console.log(e.target.innerHTML);
if (this.value >= 0 && e.target.innerHTML.length < 10) {
this.value += Number(e.target.innerText);
if (this.value <= this.min) {
this.value = this.min;
}
if (this.value >= this.max) {
this.value = this.max;
}
}
this.$emit("value", this.value);
},
},
};
</script>
<style lang="scss" scoped>
.plusReduce {
align-items: center;
margin: 10px 0;
span {
display: inline-block;
padding: 6px 7px;
background: rgba(60, 60, 60, 1);
border-radius: 3px;
margin: 5px;
cursor: pointer;
}
.disabled {
background: rgba(60, 60, 60, 0.5);
color: rgba(255, 255, 255, 0.5);
cursor: not-allowed;
pointer-events: none;
}
// :not(.disabled)
span:hover {
background: rgba(0, 255, 255, 0.5);
}
.middle {
flex: 1;
text-align: center;
}
}
.flex_column {
flex-direction: column;
}
.flex {
display: flex;
}
</style>

View File

@ -0,0 +1,143 @@
<template>
<div class="plusReduce flex">
<div class="left" @click="calculate(-1)">
<svg-icon icon-class="jian" style="width: 20px;height: 20px;cursor: pointer;"></svg-icon>
</div>
<div class="middle">
<el-slider v-if="progressType == 'progress'" style="width: 90%;" v-model="value" :max="sliderMax" :min="sliderMin"
:show-tooltip="false" @input="change"></el-slider>
<div v-if="progressType == 'num'">
<span class="num">{{ value2 }}</span>
m/s
</div>
</div>
<div class="right" @click="calculate(1)">
<svg-icon icon-class="jia" style="width: 20px;height: 20px;cursor: pointer;"></svg-icon>
</div>
</div>
</template>
<script>
export default {
name: "plusReduce",
props: {
defValue: {
type: Number,
default: 0,
},
unit: {
type: String,
default: "s",
},
sliderMax: {
type: Number,
default: 0,
},
sliderMin: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 10,
},
min: {
type: Number,
default: 1,
},
progressType: {
type: String,
default: "progress",
},
},
data() {
return {
active: false,
value: 1,
value2: 10,
};
},
watch: {
value(newVal) {
if (newVal == this.min) {
this.active = true;
} else {
this.active = false;
}
},
defValue(newVal) {
this.value = newVal;
this.value2 = newVal;
},
},
mounted() {
this.value = this.defValue;
this.value2 = this.defValue;
},
methods: {
calculate(num) {
if (this.progressType == "progress") {
if (this.value >= this.sliderMin || this.value < this.sliderMax) {
this.value += Number(num);
if (this.value < this.sliderMin) {
this.value = this.sliderMin;
}
if (this.value > this.sliderMax) {
this.value = this.sliderMax;
}
}
this.$emit("value", this.value);
} else {
if (this.value2 >= this.min || this.value2 < this.max) {
this.value2 += num;
if (this.value2 < this.min) {
this.value2 = this.min;
}
if (this.value2 > this.max) {
this.value2 = this.max;
}
this.$emit("value", this.value2);
}
}
},
change(value) {
this.$emit("value", value);
},
},
};
</script>
<style lang="scss">
.plusReduce {
align-items: center;
margin: 10px 0;
.middle {
flex: 1;
display: flex;
justify-content: center;
.el-slider__bar {
background-color: rgba(0, 255, 255, 1);
}
.el-slider__button {
border: none;
}
.num {
font-family: "ddin";
font-size: 30px;
font-weight: 700;
color: rgba(0, 255, 255, 1);
}
}
.disabled {
background: rgba(60, 60, 60, 0.5);
color: rgba(255, 255, 255, 0.5);
cursor: not-allowed;
pointer-events: none;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@ -0,0 +1,37 @@
<template>
<div class="flyerimg">
<img style="width: 100%;height: 100%;" src="./1.jpg" alt="" />
</div>
</template>
<script>
export default {
name: 'flyer',
data() {
return {
};
},
mounted() {
},
methods: {
},
};
</script>
<style>
.flyerimg {
position: absolute;
left: 50%;
top: 50%;
z-index: 20;
transform: translate(-50%, -50%);
width: 80%;
height: 75%;
color: #fff;
}
</style>

View File

@ -0,0 +1,446 @@
<template>
<div class="airport-information-wrapper">
<ModuleItem :title="'飞行器信息详情'" :subTitle="'Aircraft Information Details'">
<div class="airport-information-content">
<div class="airport-info-wrapper">
<div style="display: flex">
<div class="airport-left">
<img src="../../../../images/home/aircraft.png" alt="" />
</div>
<div class="airport-center">
<div>设备型号 {{ fxqinfo?.deviceType || '----' }}</div>
<div>
设备项目呼号 {{ fxqinfo?.businessName || '----' }}
<img @click="onview" src="../../../../images/home/local-point.png" alt="" />
</div>
</div>
</div>
<!--
<div class="airport-right">
{{ specialHandling("drone_in_dock")
}}{{ specialHandling("device_online_status") }}
</div> -->
</div>
<div class="airport-list-wrapper">
<div v-for="item in airportsList" :key="item.name" class="airport-list-item">
<div class="airport-list-top">
<img v-if="item.img" :src="item.img" alt="" />
<div>{{ item.data }}{{ item.unit }}</div>
</div>
<div class="airport-list-bottom">
<div>{{ item.name }}</div>
</div>
</div>
</div>
</div>
</ModuleItem>
</div>
</template>
<script>
import ModuleItem from '../ModuleItem/index.vue';
import { listInfo } from '@/api/air';
import { debounce } from '@/utils/index';
import satellite from '../../../../images/home/satellite.png';
import paizhao from '../../../../images/home/paizhao.png';
import pzsyl from '../../../../images/home/pzsyl.png';
import { useAirStore } from '@/store/modules/drone';
export default {
name: 'AirportInformationDetails',
components: { ModuleItem },
props: {},
data() {
return {
airportsList: [
{
name: 'RTK',
data: 0,
unit: '',
key: 'rtk_number',
img: satellite
},
{
name: '拍照状态',
data: '空闲',
unit: '',
key: 'photo_state',
img: paizhao
},
{
name: '拍照剩余数量',
data: '0',
unit: '张',
key: 'remain_photo_num',
img: pzsyl
},
{
name: '电池剩余总量',
data: '0',
unit: '%',
key: 'capacity_percent',
img: null
},
{
name: '剩余飞行时间',
data: '00:00:00',
key: 'remain_flight_time',
img: null
},
{
name: '返航所需电量',
data: '0',
unit: '%',
key: 'return_home_power',
img: null
},
{
name: '内存总容量',
data: '0',
unit: 'GB',
key: 'total',
img: null
},
{
name: '内存已使用容量',
data: '0',
unit: 'GB',
key: 'used',
img: null
},
{
name: '飞行高度',
data: '0',
unit: 'm',
key: 'elevation',
img: null
}
],
osd4: null,
osd4Map: new Map(),
dataMap: new Map(),
dataAll: {},
info: {},
fxqinfo: {},
gateWay: useAirStore().gateWay,
flag: true,
timer: null,
time: ''
};
},
// computed: {
// // 映射 Vuex 中的状态到当前组件的计算属性
// ...mapState({
// gateWay: (state) => state.gateWay,
// }),
// },
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getList();
},
deep: true
}
},
methods: {
// 机场位置
onview: debounce(function () {
this.$sendChanel('onViewUAV');
}, 500),
getList() {
listInfo({
pageNum: 1,
pageSize: 10,
gateway: this.gateWay.gateway
}).then((res) => {
// console.log("list-------", res);
// 暂时写死
this.info = res.rows[0];
this.fxqinfo = res.rows[1];
});
},
dataTreat(info) {
// console.log(456);
if (info.businessType !== 'osd4') {
this[info.businessType] = info.data;
Object.assign(this.dataAll, info.data);
// this.osd3.
} else {
this.osd4 = info.data;
// console.log("this.dataAll--------", this.dataAll);
}
function flattenObjectToMap(obj, map = new Map()) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
flattenObjectToMap(value, map);
} else {
map.set(key, value);
}
}
}
return map;
}
this.osd4Map = flattenObjectToMap(this.osd4, this.osd4Map);
this.dataMap = flattenObjectToMap(this.dataAll, this.dataMap);
// console.log("this.dataAll--------", this.dataAll);
// 需要特殊处理的字段
const spcial = ['remain_flight_time', 'photo_state', 'remain_photo_num', 'total', 'used'];
// 特殊处理
this.airportsList.forEach((item) => {
if (!spcial.includes(item.key)) {
item.data = this.osd4Map.get(item.key) || 0;
} else {
item.data = this.specialHandling(item.key);
}
});
},
specialHandling(key) {
function kbToGb(kb) {
return kb / (1024 * 1024);
}
switch (key) {
case 'remain_flight_time':
const val = this.osd4Map.get(key);
if (!this.osd4Map.get(key)) {
return 0 + '时' + 0 + '分' + 0 + '秒';
}
//返回分钟
let h = parseInt(val / 60 / 60);
let miu = parseInt(h % 60);
let s = val % 60;
return h + '时' + miu + '分' + s + '秒';
case 'photo_state':
if (!this.osd4Map.get('cameras')) {
return '空闲';
}
const photoState = this.osd4Map.get('cameras')[0][key] == 0 ? '空闲' : '拍照中';
return photoState ? photoState : '空闲';
case 'remain_photo_num':
if (!this.osd4Map.get('cameras')) {
return 0;
}
const remain_photo_num = this.osd4Map.get('cameras')[0][key];
return remain_photo_num ? remain_photo_num : 0;
case 'total':
if (!this.osd4Map.get(key)) {
return 0;
}
let b = kbToGb(this.osd4Map.get(key));
const total = b.toFixed(2);
return total ? total : 0;
case 'used':
if (!this.osd4Map.get(key)) {
return 0;
}
let a = kbToGb(this.osd4Map.get(key));
const used = a.toFixed(2);
return used ? used : 0;
case 'device_online_status':
return String(this.dataMap.get(key)) === '1' ? '开机' : '关机';
case 'drone_in_dock':
return String(this.dataMap.get(key)) === '1' ? '舱内' : '舱外';
default:
break;
}
},
restet() {
// console.log(123);
this.airportsList = [
{
name: 'RTK',
data: 0,
unit: '',
key: 'rtk_number',
img: satellite
},
{
name: '拍照状态',
data: '空闲',
unit: '',
key: 'photo_state',
img: paizhao
},
{
name: '拍照剩余数量',
data: '0',
unit: '张',
key: 'remain_photo_num',
img: pzsyl
},
{
name: '电池剩余总量',
data: '0',
unit: '%',
key: 'capacity_percent',
img: null
},
{
name: '剩余飞行时间',
data: '00:00:00',
key: 'remain_flight_time',
img: null
},
{
name: '返航所需电量',
data: '0',
unit: '%',
key: 'return_home_power',
img: null
},
{
name: '内存总容量',
data: '0',
unit: 'GB',
key: 'total',
img: null
},
{
name: '内存已使用容量',
data: '0',
unit: 'GB',
key: 'used',
img: null
},
{
name: '飞行高度',
data: '0',
unit: 'm',
key: 'elevation',
img: null
}
];
},
onReset() {
let that = this;
that.timer = setInterval(() => {
const currentTimestamp = Date.now(); // 获取当前时间戳
const elapsed = currentTimestamp - that.time; // 计算差值
if (elapsed > 2000) {
that.restet(); // 如果大于2000毫秒则执行方法
}
}, 2000);
}
},
// 定时器
created() {
this.$recvChanel('liveBus', (bool) => {
setTimeout(() => {
this.restet();
}, 2000);
});
this.$recvChanel('airInfoBus', (bool) => {
this.flag = bool;
});
this.$recvChanel('websocketBus', (airdata) => {
// if (airdata.businessType == 'osd3') {
// this.dataTreat(airdata);
// }
if (airdata.businessType == 'osd4' && this.flag) {
this.dataTreat(airdata);
this.time = new Date().getTime();
}
if (airdata.businessType == 'osd4' && !this.flag) {
this.restet();
}
});
},
mounted() {
if (this.gateWay) {
this.getList();
}
this.onReset();
}
};
</script>
<style lang="scss" scoped>
.airport-information-wrapper {
width: 100%;
position: relative;
.airport-information-content {
height: 100%;
width: 100%;
// background-color: blueviolet;
.airport-info-wrapper {
padding: 0 2%;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid rgba(0, 255, 255, 0.2);
background-color: rgba(0, 0, 0, 0.5);
margin: 5px 0;
.airport-left {
transform: translateY(5px);
margin-right: 10px;
}
.airport-center {
font-size: 12px;
color: rgba(207, 255, 255, 1);
display: flex;
flex-direction: column;
justify-content: center;
> div:nth-of-type(2) {
display: flex;
align-items: center;
margin-top: 5px;
img {
margin-left: 5px;
cursor: pointer;
}
}
}
.airport-right {
display: flex;
align-items: center;
color: rgba(241, 108, 85, 1);
}
}
.airport-list-wrapper {
display: grid;
grid-template-columns: repeat(3, 1fr);
/* 创建3列每列等宽 */
gap: 15px;
/* 设置列和行之间的间距 */
padding: 10px 0;
.airport-list-item {
font-size: 14px;
.airport-list-top {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 4px;
img {
width: 14px;
height: 14px;
margin-right: 4px;
}
}
.airport-list-bottom {
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
}
</style>

View File

@ -0,0 +1,487 @@
<template>
<div class="airport-information-wrapper">
<ModuleItem :title="'无人机机场信息详情'" :subTitle="'Airport Information Details'">
<div class="airport-information-content">
<div class="airport-info-wrapper">
<div style="display: flex">
<div class="airport-left">
<img src="../../../../images/home/cabin.png" alt="" />
</div>
<div class="airport-center">
<!-- {{ gateway.deviceType }} -->
<div>机舱型号{{ info.deviceType || '----' }}</div>
<div>
<!-- {{ gateway.businessName }} -->
机舱项目呼号{{ info.businessName || '----' }}
<!-- 防止重复点击,使用once修饰符 -->
<img @click="onview" src="../../../../images/home/local-point.png" alt="" />
</div>
</div>
</div>
<div class="airport-right">
{{ specialHandling('flighttask_step_code') }}
</div>
</div>
<div class="airport-list-wrapper">
<div v-for="item in airportsList" :key="item.name" class="airport-list-item">
<div class="airport-list-top">
<img v-if="item.img" :src="item.img" alt="" />
<div>{{ item.data }}{{ item.unit }}</div>
</div>
<div class="airport-list-bottom">
<div>{{ item.name }}</div>
</div>
</div>
</div>
</div>
</ModuleItem>
</div>
</template>
<script>
import ModuleItem from '../ModuleItem/index.vue';
import { dayjs } from 'element-plus';
import { listInfo } from '@/api/air';
import { debounce } from '@/utils/index';
import satellite from '../../../../images/home/satellite.png';
import calibration from '../../../../images/home/calibration.png';
import recharge from '../../../../images/home/recharge.png';
import file from '../../../../images/home/file.png';
import uav from '../../../../images/home/uav.png';
import uavLocal from '../../../../images/home/uav-local.png';
import { useAirStore } from '@/store/modules/drone';
export default {
name: 'AirportInformationDetails',
components: { ModuleItem },
data() {
return {
airportsList: [
{
name: '机场搜星',
key: 'gps_number',
data: 0,
unit: '',
img: satellite
},
{
name: '标定状态',
data: '已标定',
key: 'is_calibration',
unit: '',
img: calibration
},
{
name: '机场存储',
data: '0/0',
unit: 'GB',
key: 'storage',
img: null
},
{
name: '以太网网络',
data: '0',
unit: 'KB/s',
key: 'rate',
img: null
},
{
name: '风速',
data: '0',
unit: 'm/s',
key: 'wind_speed',
img: null
},
{
name: '降雨量',
data: '0',
unit: 'mm',
key: 'rainfall',
img: null
},
{
name: '充电状态',
data: '空闲',
unit: '',
key: 'state',
img: recharge
},
{
name: '待上传数',
data: '0',
unit: '',
img: file,
key: 'remain_upload'
},
{
name: '舱内温度',
data: '0',
unit: '℃',
key: 'temperature',
img: null
},
{
name: '舱外温度',
data: '0',
unit: '℃',
key: 'environment_temperature',
img: null
},
{
name: '舱内湿度',
data: '0',
unit: '%RH',
key: 'humidity',
img: null
},
{
name: '运行时长',
data: '0天0时0分',
unit: '',
key: 'acc_time',
img: null
},
{
name: '飞行器状态',
data: '关机',
unit: '',
key: 'device_online_status',
img: uav
},
{
name: '飞行器位置',
data: '舱内',
unit: '',
key: 'drone_in_dock',
img: uavLocal
}
],
osd1: null,
osd2: null,
osd3: null,
dataMap: new Map(),
dataAll: {},
info: {},
gateway: useAirStore().gateWay
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateway = newValue;
this.getList();
},
deep: true
}
},
methods: {
dataTreat(info) {
if (['osd1', 'osd2', 'osd3'].includes(info.businessType)) {
this[info.businessType] = info.data;
Object.assign(this.dataAll, info.data);
}
// console.log("osd1", this.osd1);
// console.log("osd2", this.osd2);
// console.log("osd3", this.osd3);
// console.log("数据", this.dataAll);
function flattenObjectToMap(obj, map = new Map()) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
flattenObjectToMap(value, map);
} else {
map.set(key, value);
}
}
}
return map;
}
this.dataMap = flattenObjectToMap(this.dataAll, this.dataMap);
// console.log("dataMap", this.dataMap);
// 需要特殊处理的字段
const spcial = ['is_calibration', 'storage', 'rate', 'state', 'acc_time', 'device_online_status', 'drone_in_dock'];
// 特殊处理
this.airportsList.forEach((item) => {
if (!spcial.includes(item.key)) {
item.data = this.dataMap.get(item.key);
} else {
item.data = this.specialHandling(item.key);
}
});
},
specialHandling(key) {
switch (key) {
case 'flighttask_step_code':
const flighttaskObj = {
'0': '作业准备中',
'1': '飞行作业中',
'2': '作业后状态恢复',
'3': '自定义飞行区更新中',
'4': '地形障碍物更新中',
'5': '任务空闲',
'255': '飞行器异常',
'256': '未知状态'
};
return flighttaskObj[String(this.dataMap.get(key))];
case 'is_calibration':
return String(this.dataMap.get(key)) === '1' ? '已标定' : '未标定';
case 'storage':
function kbToGb(kb) {
return kb / (1024 * 1024);
}
let a = kbToGb(this.dataMap.get('used')) || 0;
let b = kbToGb(this.dataMap.get('total')) || 0;
const used = a.toFixed(2);
const total = b.toFixed(2);
return `${used}/${total}`;
case 'rate':
let c = this.dataMap.get(key) || 0;
return c.toFixed(2);
case 'state':
return String(this.dataMap.get(key)) === '1' ? '充电中' : '空闲';
case 'acc_time':
function formatTime(seconds) {
const days = Math.floor(seconds / (3600 * 24));
const hours = Math.floor((seconds % (3600 * 24)) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${days}${hours}${minutes}`;
}
const timeFormatter = formatTime(dayjs(this.dataMap.get(key)));
return timeFormatter;
case 'device_online_status':
return String(this.dataMap.get(key)) === '1' ? '开机' : '关机';
case 'drone_in_dock':
return String(this.dataMap.get(key)) === '1' ? '舱内' : '舱外';
default:
break;
}
},
// 获取设备列表
getList() {
listInfo({
pageNum: 1,
pageSize: 10,
gateway: this.gateway.gateway
}).then((res) => {
this.number = res.rows.length;
this.info = res.rows[0];
console.log('this.infothis.infothis.infothis.info', this.info);
});
},
// 机场位置
// 使用防抖处理点击事件,避免短时间内重复触发
onview: debounce(function () {
this.$sendChanel('onView');
}, 500), // 500ms内只执行一次
restet() {
this.airportsList = [
{
name: '机场搜星',
key: 'gps_number',
data: 0,
unit: '',
img: satellite
},
{
name: '标定状态',
data: '已标定',
key: 'is_calibration',
unit: '',
img: calibration
},
{
name: '机场存储',
data: '0/0',
unit: 'GB',
key: 'storage',
img: null
},
{
name: '以太网网络',
data: '0',
unit: 'KB/s',
key: 'rate',
img: null
},
{
name: '风速',
data: '0',
unit: 'm/s',
key: 'wind_speed',
img: null
},
{
name: '降雨量',
data: '0',
unit: 'mm',
key: 'rainfall',
img: null
},
{
name: '充电状态',
data: '空闲',
unit: '',
key: 'state',
img: recharge
},
{
name: '待上传数',
data: '0',
unit: '',
img: file,
key: 'remain_upload'
},
{
name: '舱内温度',
data: '0',
unit: '℃',
key: 'temperature',
img: null
},
{
name: '舱外温度',
data: '0',
unit: '℃',
key: 'environment_temperature',
img: null
},
{
name: '舱内湿度',
data: '0',
unit: '%RH',
key: 'humidity',
img: null
},
{
name: '运行时长',
data: '0天0时0分',
unit: '',
key: 'acc_time',
img: null
},
{
name: '飞行器状态',
data: '关机',
unit: '',
key: 'device_online_status',
img: uav
},
{
name: '飞行器位置',
data: '舱内',
unit: '',
key: 'drone_in_dock',
img: uavLocal
}
];
}
},
mounted() {
console.log(useAirStore().gateWay);
if (this.gateway) {
this.getList();
}
this.$recvChanel('liveBus', (bool) => {
this.restet();
});
this.$recvChanel('websocketBus', (data) => {
this.dataTreat(data);
});
}
};
</script>
<style lang="scss" scoped>
.airport-information-wrapper {
width: 100%;
height: 36%;
position: relative;
margin-bottom: 15px;
.airport-information-content {
height: 100%;
width: 100%;
// background-color: blueviolet;
.airport-info-wrapper {
padding: 0 2%;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid rgba(0, 255, 255, 0.2);
background-color: rgba(0, 0, 0, 0.5);
margin: 5px 0;
.airport-left {
margin-right: 10px;
}
.airport-center {
// transform: translateX(-40px);
font-size: 12px;
color: rgba(207, 255, 255, 1);
display: flex;
flex-direction: column;
// justify-content: space-between;
justify-content: center;
> div:nth-of-type(2) {
display: flex;
align-items: center;
margin-top: 5px;
img {
margin-left: 5px;
cursor: pointer;
}
}
}
.airport-right {
display: flex;
align-items: center;
color: rgba(27, 248, 195, 1);
}
}
.airport-list-wrapper {
display: grid;
grid-template-columns: repeat(4, 1fr);
/* 创建4列每列等宽 */
// gap: 6px; /* 设置列和行之间的间距 */
grid-gap: 10px 6px;
.airport-list-item {
font-size: 14px;
.airport-list-top {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 4px;
img {
width: 14px;
height: 14px;
margin-right: 4px;
}
div:first-child {
font-family: 'ddin';
}
}
.airport-list-bottom {
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<div class="airport-debug-list-item" :style="{ width: cusWidth ? cusWidth : '100%' }">
<div class="debug-top" :style="{ minHeight: showBottom ? '70%' : '100%' }">
<div class="debug-l">
<div class="debug-l-img">
<img :src="imgUrl" alt="" />
</div>
<div class="debug-l-text">
<div>{{ name }}</div>
<el-tooltip :content="status" placement="bottom">
<div class="debug-l-status">{{ status }}</div>
</el-tooltip>
</div>
</div>
<div class="debug-r">
<slot name="btn"></slot>
</div>
</div>
<div v-if="showBottom" class="debug-bottom">
<div class="debug-bottom-wrapper">
<slot name="progress"></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: "DebugItem",
props: {
imgUrl: {
type: String,
},
status: {
type: String,
},
name: {
type: String,
},
cusWidth: {
type: String,
default: `100%`,
},
showBottom: {
type: Boolean,
default: false,
},
},
data() {
return {};
},
methods: {},
created() { },
mounted() { },
};
</script>
<style lang="scss" scoped>
.airport-debug-list-item {
font-size: 14px;
border: 1px solid rgba(0, 255, 255, 0.2);
background-color: rgba(0, 0, 0, 0.5);
// height: 10vh;
padding: 20px 0;
width: 100%;
.debug-top {
padding: 0 3%;
display: flex;
justify-content: space-around;
align-items: center;
width: 100%;
min-height: 70%;
.debug-l {
display: flex;
align-items: center;
width: 70%;
.debug-l-img {
width: 24px;
height: 24px;
margin-right: 10px;
img {
width: 24px;
height: 24px;
}
}
.debug-l-text {
>div {
margin: 4px 0;
}
.debug-l-status {
width: 2.8vw;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.debug-r {
// width: 20%;
}
}
.debug-bottom {
padding: 0 2%;
transform: translateY(10px);
width: 100%;
display: flex;
align-items: center;
.debug-bottom-wrapper {
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,811 @@
<template>
<div class="airport-debug-wrapper">
<ModuleItem :title="'机场远程调试'" :subTitle="'Airport Remote Debugging'">
<template #titleOption>
<el-switch v-model="debugSwitch" active-color="#00FFFFFF" inactive-color="#CCCCCC33" @change="changeSwitch"> </el-switch>
</template>
<div class="airport-debug-content">
<div class="airport-debug-list-wrapper">
<div class="debug-item-box">
<DebugItem :name="jcxt.name" :imgUrl="jcxt.img" :status="jcxt.data" :showBottom="true">
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(jcxt.key)" size="small">
{{ jcxt.btnStatus }}
</el-button>
</div>
</template>
<template #progress>
<el-progress :percentage="jcxt.progress" :show-text="false" :color="'#00FFFFFF'" :define-back-color="'#CCCCCC33'"></el-progress>
</template>
</DebugItem>
</div>
<div class="debug-item-box">
<DebugItem :name="cg.name" :imgUrl="cg.img" :status="cg.data" :showBottom="true">
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(cg.key)" size="small">
{{ specialHandling(cg.key) !== '打开' ? '打开' : '关闭' }}
</el-button>
</div>
</template>
<template #progress>
<el-progress :percentage="cg.progress" :show-text="false" :color="'#00FFFFFF'" style="background-color: #000"></el-progress>
</template>
</DebugItem>
</div>
<div v-if="number == 2" class="debug-item-box">
<DebugItem :name="tg.name" :imgUrl="tg.img" :status="tg.data" :showBottom="true">
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(tg.key)" size="small">
{{ specialHandling(tg.key) !== '打开' ? '打开' : '关闭' }}
</el-button>
</div>
</template>
<template #progress>
<el-progress :percentage="tg.progress" :show-text="false" :color="'#00FFFFFF'" :define-back-color="'#CCCCCC33'"></el-progress>
</template>
</DebugItem>
</div>
<div class="debug-item-box">
<DebugItem :name="jyms.name" :imgUrl="jyms.img" :status="jyms.data">
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(jyms.key)" size="small">
{{ jyms.btnStatus }}
</el-button>
</div>
</template>
</DebugItem>
</div>
<div class="debug-item-box">
<DebugItem :name="jccc.name" :imgUrl="jccc.img" :status="jccc.data">
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(jccc.key)" size="small">
{{ jccc.btnStatus }}
</el-button>
</div>
</template>
</DebugItem>
</div>
<div class="debug-item-box">
<DebugItem :name="zqtc.name" :imgUrl="zqtc.img" :status="zqtc.data">
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(zqtc.key)" size="small">
{{ dataMap.get(zqtc.key) == 0 ? '开启' : '关闭' }}
</el-button>
</div>
</template>
</DebugItem>
</div>
<div class="debug-item-box">
<!-- 我需要页面刚进来时status显示开机或者关机而不是开机中或者关机中这个状态来自dataTreat -->
<DebugItem
:name="fxq.name"
:imgUrl="fxq.img"
:status="
fxqLoading ? (fxqMethod == 'drone_open' ? '开机中' : '关机中') : fxqMethod ? (fxqMethod == 'drone_open' ? '开机' : '关机') : fxq.data
"
:showBottom="true"
>
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(fxq.key)" size="small">
<span v-show="!fxqLoading">{{ fxq.data == '开机' ? '关机' : '开机' }}</span>
<img v-show="fxqLoading" class="myLoading" src="../../../../images/loading.png" alt="" />
</el-button>
</div>
</template>
</DebugItem>
</div>
<div class="debug-item-box">
<DebugItem :name="kt.name" :imgUrl="kt.img" :status="kt.data" class="custom-width">
<template #btn>
<div class="debug-btn-groups">
<el-button v-for="item in kt.btns" :key="item.id" @click="handleBtnClick(item.id, true)" size="mini">
{{ item.name }}
</el-button>
</div>
</template>
</DebugItem>
</div>
<div class="debug-item-box">
<DebugItem :name="cd.name" :imgUrl="cd.img" :status="cd.data">
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(cd.key)" size="small">
{{ dataAll.drone_charge_state.state == 0 ? '开启' : '关闭' }}
</el-button>
</div>
</template>
<template #progress>
<el-progress :percentage="cd.progress" :show-text="false" :color="'#00FFFFFF'" :define-back-color="'#CCCCCC33'"></el-progress>
</template>
</DebugItem>
</div>
</div>
</div>
</ModuleItem>
</div>
</template>
<script>
import { useAirStore } from '@/store/modules/drone';
import ModuleItem from '../ModuleItem/index.vue';
import DebugItem from './DebugItem.vue';
import { remoteDebug, listInfo, propertySet, sdrWorkmodeSwitch, airConditionerModeSwitch } from '@/api/air';
import debugJcxt from '../../../../images/home/debug-jcxt.png';
import debugCg from '../../../../images/home/debug-cg.png';
import debugTg from '../../../../images/home/debug-tg.png';
import debugJyms from '../../../../images/home/debug-jyms.png';
import debugJccc from '../../../../images/home/debug-jccc.png';
import debugZqtc from '../../../../images/home/debug-zqtc.png';
import debugFxq from '../../../../images/home/debug-fxq.png';
import debugKt from '../../../../images/home/debug-kt.png';
export default {
name: 'AirportRemoteDebugging',
components: { ModuleItem, DebugItem },
data() {
return {
debugSwitch: false,
// 机场系统
jcxt: {
name: '机场系统',
data: '工作中',
img: debugJcxt,
key: 'flighttask_step_code',
btnStatus: '重启',
progress: 0,
progressMethod: ['device_reboot']
},
cg: {
name: '舱盖',
data: '关',
img: debugCg,
key: 'cover_state',
btnStatus: '开启',
progress: 0,
progressMethod: ['cover_open', 'cover_close']
},
tg: {
name: '推杆',
data: '闭合',
img: debugTg,
key: 'putter_state',
btnStatus: '关闭',
progress: 0,
progressMethod: ['putter_open', 'putter_close']
},
jyms: {
name: '静音模式',
data: '未开启',
img: debugJyms,
key: 'silent_mode',
btnStatus: '开启'
},
jccc: {
name: '机场存储',
data: '0/0 GB',
img: debugJccc,
key: 'storage',
btnStatus: '格式化'
},
zqtc: {
name: '增强图传',
data: '未开启',
img: debugZqtc,
key: 'link_workmode',
btnStatus: '开启'
},
fxq: {
name: '飞行器',
data: '关机',
img: debugFxq,
key: 'device_online_status',
progress: 0,
progressMethod: ['drone_close'],
btnStatus: '开启'
},
kt: {
name: '空调',
data: '空闲中',
img: debugKt,
key: 'air_conditioner_state',
btns: [
{ name: '空闲', id: '0' },
{ name: '制冷', id: '1' },
{ name: '制热', id: '2' },
{ name: '除湿', id: '3' }
]
},
cd: {
name: '充电',
data: '未充电',
img: debugTg,
key: 'drone_charge_state',
btnStatus: '开启',
progress: 0,
progressMethod: ['charge_open', 'charge_close']
},
osd1: null,
osd2: null,
osd3: null,
osd4: null,
dataMap: new Map(),
dataAll: {
drone_charge_state: {
state: 0
}
},
info: {},
gateWay: useAirStore().gateWay,
number: 0,
fxqLoading: false,
fxqMethod: ''
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getDeviceNumber();
if (newValue) {
// 重置飞行器状态相关数据
this.fxqLoading = false;
this.fxqMethod = '';
this.fxq.data = '关机'; // 设置默认状态为关机
// 清空数据映射
this.dataMap = new Map();
this.dataAll = {
drone_charge_state: {
state: 0
}
};
}
},
deep: true
}
},
methods: {
// 获取设备列表
dataTreat(info) {
if (info.businessType !== 'osd4') {
if (['osd1', 'osd2', 'osd3'].includes(info.businessType)) {
this[info.businessType] = info.data;
Object.assign(this.dataAll, info.data);
} else {
// console.log("this[info.businessType]", info);
this.debuggingProgress(info.businessType, info.data);
}
} else {
this.osd4 = info.data;
}
//
if (this.dataMap.get('device_online_status') == '1' && this.fxqMethod == 'drone_open') {
this.fxqLoading = false;
this.fxqMethod = 'drone_open';
} else if (this.dataMap.get('device_online_status') == '0' && this.fxqMethod == 'drone_close') {
this.fxqLoading = false;
this.fxqMethod = 'drone_close';
if (window.airModel) {
window.airModel.remove();
window.airModel = null;
return;
}
} else if (this.dataMap.get('device_online_status') == '0') {
if (window.airModel) {
window.airModel.remove();
window.airModel = null;
return;
}
}
function flattenObjectToMap(obj, map = new Map()) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
flattenObjectToMap(value, map);
} else {
map.set(key, value);
}
}
}
return map;
}
this.dataMap = flattenObjectToMap(this.dataAll, this.dataMap);
this.jcxt.data = this.specialHandling(this.jcxt.key);
this.cg.data = this.specialHandling(this.cg.key);
this.jyms.data = this.specialHandling(this.jyms.key);
this.tg.data = this.specialHandling(this.tg.key);
this.jccc.data = this.specialHandling(this.jccc.key);
this.zqtc.data = this.specialHandling(this.zqtc.key);
this.fxq.data = this.specialHandling(this.fxq.key);
this.kt.data = this.specialHandling(this.kt.key);
this.cd.data = this.dataAll.drone_charge_state.state == 0 ? '未充电' : '充电中';
// console.log("this.dataMap", this.dataMap);
// console.log("this.jccc.data", this.dataMap.get(this.jccc.key));
if (this.dataMap.get('mode_code') && this.dataMap.get('mode_code') === 2) {
this.debugSwitch = true;
} else {
this.debugSwitch = false;
}
// console.log("mode_code", this.dataMap.get("mode_code"));
},
specialHandling(key) {
switch (key) {
case 'flighttask_step_code':
const flighttaskObj = {
'0': '作业准备中',
'1': '飞行作业中',
'2': '作业后状态恢复',
'3': '自定义飞行区更新中',
'4': '地形障碍物更新中',
'5': '任务空闲',
'255': '飞行器异常',
'256': '未知状态'
};
// console.log("this.dataMap.get(key)", this.dataMap.get(key));
return flighttaskObj[String(this.dataMap.get(key))];
case 'storage':
function kbToGb(kb) {
return kb / (1024 * 1024);
}
let a = kbToGb(this.dataMap.get('used')) || 0;
let b = kbToGb(this.dataMap.get('total')) || 0;
const used = a.toFixed(2);
const total = b.toFixed(2);
return `${used}/${total}GB`;
case 'cover_state':
const coverStateObj = {
'0': '关闭',
'1': '打开',
'2': '半开',
'3': '舱盖状态异常'
};
return coverStateObj[String(this.dataMap.get(key))];
case 'silent_mode':
const silentModeObj = {
'0': '非静音模式',
'1': '静音模式'
};
return silentModeObj[String(this.dataMap.get(key))];
case 'putter_state':
const putterStateObj = {
'0': '关闭',
'1': '打开',
'2': '半开',
'3': '推杆状态异常'
};
return putterStateObj[String(this.dataMap.get(key))];
case 'link_workmode':
const linkWorkmodeObj = {
'0': 'SDR 模式',
'1': '4G 融合模式'
};
return linkWorkmodeObj[String(this.dataMap.get(key))];
case 'device_online_status':
const deviceOnlineObj = {
'0': '关机',
'1': '开机'
};
return deviceOnlineObj[String(this.dataMap.get(key))];
case 'air_conditioner_state':
const airConditionerObj = {
'0': '空闲模式',
'1': '制冷模式',
'2': '制热模式',
'3': '除湿模式',
'4': '制冷退出模式',
'5': '制热退出模式',
'6': '除湿退出模式',
'7': '制冷准备模式',
'8': '制热准备模式',
'9': '除湿准备模式'
};
return airConditionerObj[String(this.dataMap.get(key))];
case 'drone_charge_state':
const chargeObj = {
'0': '空闲',
'1': '充电中'
};
return chargeObj[String(this.dataMap.get(key).state)];
default:
break;
}
},
handleBtnClick(key, isAirConditioner = false) {
// console.log("key", key);
switch (key) {
case 'flighttask_step_code':
this.airportRestart();
break;
case 'cover_state':
this.hatch();
break;
case 'putter_state':
this.putterStateChange();
break;
case 'silent_mode':
this.silentModeChange();
break;
case 'storage':
this.storageFormat();
break;
case 'link_workmode':
this.linkWorkmodeChange();
break;
case 'device_online_status':
this.AircraftBoot();
break;
case 'drone_charge_state':
this.chargeState();
break;
default:
// 空调
if (isAirConditioner) {
this.airConditioner(key);
}
break;
}
},
// 远程调试开关
changeSwitch(status) {
let air = localStorage.getItem('airGateway');
if (!air) {
this.$message.error('请先选择机场');
this.debugSwitch = false;
return;
}
this.debugSwitch = status;
const { gateway } = this.gateWay;
const params = { gateway, method: '' };
if (this.debugSwitch) {
params.method = 'debug_mode_open';
} else {
params.method = 'debug_mode_close';
}
remoteDebug(params).then((res) => {
this.$message.success(res.msg);
});
// console.log("this.debugSwitch", this.debugSwitch);
},
// 机场重启
airportRestart() {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const method = 'device_reboot';
const params = { gateway, method };
this.$sendChanel('liveBus', true);
remoteDebug(params).then((res) => {
this.$message.success(res.msg);
});
},
// 飞行器开机
AircraftBoot() {
if (!this.debugSwitch) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const method = Number(this.dataMap.get('device_online_status')) === 0 ? 'drone_open' : 'drone_close';
const params = { gateway, method };
this.fxqLoading = true;
this.fxqMethod = method;
if (method == 'drone_close') {
this.$sendChanel('liveBus', false);
}
this.$sendChanel('airInfoBus', method == 'drone_open' ? true : false);
remoteDebug(params).then((res) => {
this.$message.success(res.msg);
});
},
// 舱盖开关
hatch() {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const method = Number(this.dataMap.get('cover_state')) === 0 ? 'cover_open' : 'cover_close';
const params = { gateway, method };
remoteDebug(params).then((res) => {
this.$message.success(res.msg);
});
},
// 推杆开关
putterStateChange() {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const method = Number(this.dataMap.get('putter_state')) === 0 ? 'putter_open' : 'putter_close';
const params = { gateway, method };
remoteDebug(params).then((res) => {
this.$message.success(res.msg);
});
},
// 静音模式开关
silentModeChange() {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const status = Number(this.dataMap.get('silent_mode')) === 0 ? 1 : 0;
if (status === 0) {
this.jyms.btnStatus = '开启';
} else {
this.jyms.btnStatus = '关闭';
}
const params = { gateway, params: { silent_mode: status } };
propertySet(params).then((res) => {
this.$message.success(res.msg);
});
},
// 增强图传
linkWorkmodeChange() {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
// const method = "sdr_workmode_switch";
// const params = { gateway, method };
// remoteDebug(params).then((res) => {});
const action = this.dataMap.get('link_workmode') == 1 ? 0 : 1;
const params = { gateway, action };
sdrWorkmodeSwitch(params).then((res) => {
const { code, msg } = res;
if (code == 200) {
this.$message.success('操作成功');
} else {
this.$message.error(msg);
}
});
},
// 空调
airConditioner(key) {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const params = { gateway, action: key };
airConditionerModeSwitch(params).then((res) => {
// console.log("空调-----------", res);
this.$message.success(res.msg);
});
},
// 机场存储格式化
storageFormat() {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const params = { gateway, method: 'device_format' };
this.$confirm('此操作将清除机场存储数据, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
customClass: 'customMessageBox',
confirmButtonClass: 'customConfirm',
cancelButtonClass: 'customCancel'
}).then(() => {
remoteDebug(params).then((res) => {
this.$message.success(res.msg);
});
});
},
chargeState() {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const method = Number(this.dataAll.drone_charge_state.state) === 0 ? 'charge_open' : 'charge_close';
const params = { gateway, method };
remoteDebug(params).then((res) => {
this.$message.success(res.msg);
});
},
// 调试进度
debuggingProgress(key, data) {
if (this.cg.progressMethod.includes(key)) {
this.cg.progress = data.output.progress.percent;
} else if (this.jcxt.progressMethod.includes(key)) {
this.jcxt.progress = data.output.progress.percent;
} else if (this.fxq.progressMethod.includes(key)) {
this.fxq.progress = data.output.progress.percent;
} else if (this.tg.progressMethod.includes(key)) {
this.tg.progress = data.output.progress.percent;
} else if (this.cd.progressMethod.includes(key)) {
this.cd.progress = data.output.progress.percent;
}
},
// 获取数量
getDeviceNumber() {
const gateway = this.gateWay.gateway;
// 获取首位数字
const firstDigit = parseInt(gateway.charAt(0));
// 判断是4还是7
if (firstDigit === 4) {
// 是4返回4
this.number = 2;
} else {
this.number = 1;
}
}
},
created() {
this.$recvChanel('websocketBus', (data) => {
// console.log("收到的数据222", data);
this.dataTreat(data);
});
if (this.gateWay) {
this.getDeviceNumber();
}
},
mounted() {}
};
</script>
<style lang="scss">
@keyframes identifier {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.airport-debug-wrapper {
width: 100%;
height: 32%;
margin-bottom: 15px;
position: relative;
.airport-debug-content {
width: 100%;
height: 100%;
padding: 0 2%;
.airport-debug-list-wrapper {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(2, 1fr);
/* 创建2列每列等宽 */
gap: 12px;
/* 设置列和行之间的间距 */
overflow-y: auto;
.debug-item-box {
.myLoading {
width: 10px;
height: 10px;
animation: identifier 1s linear infinite;
}
.custom-width {
// width: 130% !important;
// display: flex;
// justify-content: space-around;
// background-color: aquamarine;
.debug-l {
width: 50%;
}
.debug-r {
width: 50%;
}
}
.debug-btn-group {
display: flex;
justify-content: flex-end;
.el-button--mini {
padding: 5px !important;
}
}
.debug-btn-groups {
display: grid;
grid-template-columns: repeat(2, 1fr);
/* 创建2列每列等宽 */
gap: 2px;
/* 设置列和行之间的间距 */
// padding: 2%;
.el-button--mini {
padding: 3px !important;
}
}
.el-button {
pointer-events: all;
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 0.5);
color: #fff;
margin: 0 !important;
}
}
}
}
}
.customMessageBox {
background: linear-gradient(180deg, rgba(0, 255, 255, 0.2) 0%, rgba(0, 255, 255, 0) 100%), rgba(0, 0, 0, 0.6);
border: 1.5px solid rgba(0, 255, 255, 1);
.el-message-box__message,
.el-message-box__title,
.el-message-box__close {
color: #fff;
&:hover {
color: #fff;
}
}
}
.customConfirm,
.customCancel {
border-radius: 4px !important;
background: rgba(0, 255, 255, 0.2) !important;
border: 1px solid rgba(0, 255, 255, 1) !important;
color: #fff;
&:hover {
color: #fff;
}
}
/* 整个滚动条 */
::-webkit-scrollbar {
width: 8px;
/* 滚动条的宽度 */
height: 8px;
/* 滚动条的高度(横向滚动条) */
}
/* 滑块部分 */
::-webkit-scrollbar-thumb {
background-color: rgba(0, 255, 255, 0.5);
/* 滑块的背景颜色 */
border-radius: 10px;
/* 滑块的圆角 */
border: none;
/* 滑块的边框 */
background-clip: content-box;
/* 背景裁剪 */
}
/* 滑块在悬停状态下的样式 */
::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 255, 255, 0.7);
/* 悬停时滑块的颜色 */
}
/* 滚动条轨道 */
::-webkit-scrollbar-track {
background-color: transparent;
/* 轨道的背景颜色 */
border-radius: 10px;
/* 轨道的圆角 */
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<div class="module-item-container">
<div class="module-item-title">
<div class="module-item-title-left">
<div class="module-item-title-left-main">{{ title }}</div>
<div class="module-item-title-left-sub">{{ subTitle }}</div>
</div>
<div class="module-item-title-right">
<slot name="titleOption"></slot>
</div>
</div>
<div class="module-item-content">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "ModuleItem",
props: ["title", "subTitle"],
computed: {},
data() {
return {};
},
created() { },
mounted() { },
};
</script>
<style lang="scss" scoped>
.module-item-container {
width: 100%;
height: 100%;
color: #fff;
.module-item-title {
height: 40px;
width: 100%;
margin-bottom: 15px;
background: url("../../../../images/home/module-item-title.png") no-repeat;
background-size: 100% 100%;
display: flex;
padding-left: 8%;
justify-content: space-between;
align-items: center;
.module-item-title-left {
display: flex;
align-items: flex-end;
&-main {
font-size: 18px;
font-weight: 700;
background: linear-gradient(180deg,
rgba(255, 255, 255, 1) 40%,
rgba(0, 255, 255, 1) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-right: 10px;
font-family: "alimamashuheiti";
}
&-sub {
font-size: 14px;
font-weight: 500;
}
}
.module-item-title-right {
// background-color: aqua;
}
}
.module-item-content {
width: 100%;
height: calc(100% - 50px);
}
}
</style>

View File

@ -0,0 +1,753 @@
<template>
<div class="cabin-live-wrapper">
<ModuleItem :title="'无人机机场监控直播'" :subTitle="'Monitor live streaming'">
<!-- <button @click="startRecording" :disabled="isRecording">开始录制</button>
<button @click="stopRecording" :disabled="!isRecording">停止录制</button>
<a ref="downloadLink" style="display: none;">下载视频</a> -->
<div
:id="'cabin-live-video_' + info.cameraIndex"
ref="monitorLiveVideo"
:class="[screen.currentStatus ? 'cabin-live-content-full' : 'cabin-live-content']"
>
<div class="cabin-live-title">
<div class="cabin-live-title-left">机舱SN:{{ info.sn || '----' }}</div>
<div class="cabin-live-title-right">
<!-- <el-tooltip :content="backlight.tips" placement="bottom"> -->
<div class="bgd cabin-live-title-right-wrapper">
<img :src="backlight.img" @click="changeBacklight" :title="backlight.tips" />
</div>
<!-- </el-tooltip> -->
<el-dropdown v-if="number == 1" size="mini" placement="bottom" trigger="click" :hide-on-click="false" @visible-change="visibleDropdown">
<!-- <el-tooltip :content="monitor.tips" placement="bottom"> -->
<div class="monitor cabin-live-title-right-wrapper">
<img :src="monitor.img" :title="monitor.tips" />
</div>
<!-- </el-tooltip> -->
<el-dropdown-menu #dropdown>
<el-dropdown-item v-for="item in monitor.list" :key="item.name" :class="{ active: monitor.current === item.index }">
<span @click="monitorCheck(item)">{{ item.name }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-dropdown size="mini" trigger="click" placement="bottom" :hide-on-click="false" :teleported="false">
<!-- <el-tooltip :content="set.tips"> -->
<div class="monitor cabin-live-title-right-wrapper">
<img :src="set.img" :title="set.tips" />
</div>
<!-- </el-tooltip> -->
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item in set.list" :key="item.name" :class="{ active: set.current === item.index }">
<span @click="setCheck(item)">{{ item.name }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- <el-tooltip :content="reset.tips" placement="bottom"> -->
<div class="bgd cabin-live-title-right-wrapper">
<img :src="reset.img" @click="handleReset" :title="reset.tips" />
</div>
<!-- </el-tooltip> -->
<!-- <el-tooltip :content="screen.tips" placement="bottom"> -->
<div class="bgd cabin-live-title-right-wrapper">
<img :src="screen.img" @click="makeFullScreen(`cabin-live-video_${info.cameraIndex}`)" :title="screen.tips" />
</div>
<!-- </el-tooltip> -->
<!-- <el-tooltip :content="switchs.tips" placement="bottom"> -->
<div class="bgd cabin-live-title-right-wrapper">
<img :src="switchs.img" @click="handleOpen" :title="switchs.tips" />
</div>
<!-- </el-tooltip> -->
</div>
</div>
<div class="cabin-live-video">
<!-- <video ref="sourceVideo" v-show="switchs.currentStatus && !switchs.loading" class="cabin-live-video-item"
style="position: absolute;width: 100%;height: 100%;object-fit: fill;"
:id="'video-webrtc_' + info.cameraIndex" autoplay muted width="100%" height="100%"></video> -->
<div v-show="switchs.currentStatus && !switchs.loading" class="cabin-live-video-item">
<webrtc :ref="'video-webrtc_' + info.cameraIndex"></webrtc>
</div>
<div v-show="!switchs.currentStatus && !switchs.loading" class="live-stop">
<img src="../../../../images/home/live-stop.png" alt="" />
<span>机舱已经停止直播</span>
</div>
<div v-show="switchs.loading" class="live-loading">
<img src="../../../../images/home/loading.png" alt="" />
<span>加载中请稍侯</span>
</div>
</div>
</div>
</ModuleItem>
</div>
</template>
<script>
import ModuleItem from '../ModuleItem/index.vue';
import { listInfo, liveStart, liveStop, remoteDebug, liveChangeQuality, cameraChange } from '@/api/air';
import webrtc from '@/components/webrtc';
import { useAirStore } from '@/store/modules/drone';
import { getHost, getRtmpPort, getRtcPort } from '@/utils/auth';
import uavBgd from '../../../../images/home/uav-bgd.png';
import uavMonitor from '../../../../images/home/uav-monitor.png';
import uavSet from '../../../../images/home/uav-set.png';
import uavReset from '../../../../images/home/uav-reset.png';
import uavScreen from '../../../../images/home/uav-screen.png';
import uavClose from '../../../../images/home/uav-close.png';
import uavBgdOpen from '../../../../images/home/uav-bgd-open.png';
import uavScreenEsc from '../../../../images/home/uav-screen-esc.png';
import _ from 'lodash';
export default {
name: 'MonitorLiveStreaming',
components: { ModuleItem, webrtc },
data() {
return {
videoUrl: '',
flvPlayer: null,
info: {},
gateWay: useAirStore().gateWay,
time1: null,
time2: null,
//背光灯
backlight: {
light: false, // 是否开启
img: uavBgd,
tips: '打开背光灯',
key: 'supplement_light_open'
},
// 摄像头
monitor: {
img: uavMonitor,
list: [
{ name: '舱内', active: true, index: 0 },
{ name: '舱外', active: false, index: 1 }
],
tips: '摄像头',
key: 'supplement_light_open',
current: null
},
// 设置
set: {
img: uavSet,
list: [
{ name: '自适应', active: true, index: 0 },
{ name: '流畅', active: false, index: 1 },
{ name: '标清', active: false, index: 2 },
{ name: '高清', active: false, index: 3 },
{ name: '超清', active: false, index: 4 }
],
tips: '设置',
current: null
},
// 重置
reset: {
img: uavReset,
tips: '刷新',
key: 'device_reboot'
},
// 全屏切换
screen: {
img: uavScreen,
tips: '全屏切换',
currentStatus: false
},
//开关
switchs: {
img: uavClose,
tips: '开启直播',
currentStatus: false,
loading: false
},
number: 0,
isRecording: false,
mediaRecorder: null,
recordedChunks: [],
canvas: null,
canvasStream: null,
drawInterval: null
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getList();
this.getDeviceNumber();
},
deep: true
}
},
methods: {
// 背光灯
changeBacklight() {
this.backlight.light = !this.backlight.light;
if (this.backlight.light) {
this.backlight.img = uavBgdOpen;
this.backlight.key = 'supplement_light_open';
this.backlight.tips = '关闭背光灯';
this.backlight.msg = '已为你开启背光灯';
} else {
this.backlight.img = uavBgd;
this.backlight.key = 'supplement_light_close';
this.backlight.tips = '打开背光灯';
this.backlight.msg = '已为关闭背光灯';
}
const { gateway } = this.info;
let method = this.backlight.key;
const params = { gateway, method };
remoteDebug(params)
.then((res) => {
this.$message.success(this.backlight.msg);
})
.finally(() => {
this.handleFullscreenChange();
});
},
// 摄像头
monitorCheck(data) {
console.log('data', data);
this.monitor.current = data.index;
this.monitor.list.forEach((item) => {
if (this.monitor.current === item.index) {
item.active = true;
} else {
item.active = false;
}
});
const { gateway } = this.info;
const params = {
gateway,
cameraIndex: this.gateWay.cameraIndex,
cameraPosition: data.index,
sn: this.gateWay.sn,
videoIndex: this.gateWay.videoIndex
};
cameraChange(params)
.then((res) => {
this.$message.success(res.data.msg);
})
.finally(() => {
this.handleFullscreenChange();
});
console.log('this.monitor', this.monitor.list);
},
// // 开始录制
// startRecording() {
// this.isRecording = true;
// this.recordedChunks = [];
// // 获取 video 元素
// const sourceVideo = this.$refs.sourceVideo;
// console.log('sourceVideo', sourceVideo);
// // 创建 canvas 并设置大小
// this.canvas = document.createElement("canvas");
// this.canvas.width = sourceVideo.videoWidth;
// this.canvas.height = sourceVideo.videoHeight;
// const ctx = this.canvas.getContext("2d");
// // 定时将 video 内容绘制到 canvas
// const drawToCanvas = () => {
// if (!sourceVideo.paused && !sourceVideo.ended) {
// ctx.drawImage(sourceVideo, 0, 0, this.canvas.width, this.canvas.height);
// this.drawInterval = requestAnimationFrame(drawToCanvas);
// }
// };
// drawToCanvas();
// // 获取 canvas 的 MediaStream 并创建 MediaRecorder
// this.canvasStream = this.canvas.captureStream();
// this.mediaRecorder = new MediaRecorder(this.canvasStream, { mimeType: "video/webm" });
// // 处理录制的数据块
// this.mediaRecorder.ondataavailable = (event) => {
// if (event.data.size > 0) {
// this.recordedChunks.push(event.data);
// }
// };
// // 录制完成后生成下载链接
// this.mediaRecorder.onstop = () => {
// const blob = new Blob(this.recordedChunks, { type: "video/webm" });
// console.log('blobblobblobblob', blob);
// const url = URL.createObjectURL(blob);
// const downloadLink = document.createElement("a");
// downloadLink.href = url;
// downloadLink.download = String(new Date().getTime()) + ".webm";
// downloadLink.click();
// };
// // 开始录制
// this.mediaRecorder.start();
// },
// stopRecording() {
// this.isRecording = false;
// // 停止录制和绘制
// if (this.mediaRecorder) {
// this.mediaRecorder.stop();
// }
// if (this.drawInterval) {
// cancelAnimationFrame(this.drawInterval);
// }
// },
// 设置
setCheck(data) {
console.log('data', data);
this.set.current = data.index;
const { gateway, sn, cameraIndex, videoIndex } = this.info;
const params = {
gateway,
sn,
cameraIndex,
videoIndex,
videoQuality: this.set.current
};
liveChangeQuality(params)
.then((res) => {
this.$message.success(res.data.msg);
this.set.list.forEach((item) => {
if (this.set.current === item.index) {
item.active = true;
} else {
item.active = false;
}
});
})
.finally(() => {
this.handleFullscreenChange();
});
},
// 刷新
handleReset() {
if (this.switchs.currentStatus) {
this.switchs.loading = true;
this.debouncehandleReset();
}
},
debouncehandleReset: _.debounce(async function () {
{
if (this.switchs.currentStatus) {
console.log(123123);
await this.stopLive();
const res = await this.startLive();
this.$message.success(res.msg);
this.switchs.currentStatus = true;
this.switchs.loading = false;
this.handleFullscreenChange();
}
}
}, 600),
// 电源
handleOpen() {
this.switchs.loading = true;
// 关闭就不显示加载动画
this.switchs.currentStatus && (this.switchs.loading = false);
this.debounceHandleOpen();
},
// 加个防抖不准频繁请求解决频繁请求关不了的问题
debounceHandleOpen: _.debounce(function () {
if (this.switchs.currentStatus) {
this.stopLive()
.then((res) => {
this.switchs.currentStatus = false;
this.switchs.tips = '开启直播';
this.$message.success(res.msg);
this.switchs.loading = false;
})
.catch((err) => {
console.log('err', err);
this.switchs.loading = false;
})
.finally(() => {
this.switchs.loading = false;
this.handleFullscreenChange();
});
} else {
this.startLive()
.then((res) => {
console.log('res', res);
this.switchs.currentStatus = true;
this.switchs.tips = '关闭直播';
this.$message.success(res.msg);
this.switchs.loading = false;
})
.catch((err) => {
console.log('err', err);
this.switchs.loading = false;
})
.finally(() => {
console.log('finally');
this.switchs.loading = false;
this.handleFullscreenChange();
});
}
}, 600),
makeFullScreen(id) {
const videoElement = document.getElementById(id);
console.log('videoElement', videoElement);
if (!this.screen.currentStatus) {
this.screen.img = uavScreenEsc;
this.screen.currentStatus = true;
if (videoElement.requestFullscreen) {
videoElement.requestFullscreen();
} else if (videoElement.mozRequestFullScreen) {
// Firefox
videoElement.mozRequestFullScreen();
} else if (videoElement.webkitRequestFullscreen) {
// Chrome, Safari and Opera
videoElement.webkitRequestFullscreen();
} else if (videoElement.msRequestFullscreen) {
// IE/Edge
videoElement.msRequestFullscreen();
}
} else {
this.screen.img = uavScreen;
if (document.fullscreenElement) {
document
.exitFullscreen()
.then(() => {
this.screen.currentStatus = false;
clearInterval(this.time1);
clearInterval(this.time2);
})
.catch((err) => {
console.error(`Failed to exit full screen: ${err}`);
});
}
}
},
// 获取设备列表
getList() {
listInfo({
pageNum: 1,
pageSize: 10,
gateway: this.gateWay.gateway,
type: '机场'
}).then((res) => {
console.log('list-------', res);
this.info = res.rows[0];
});
},
// 开启直播
startLive() {
return new Promise((resovle, reject) => {
if (this.info) {
const { cameraIndex, gateway, sn, videoIndex } = this.info;
console.log(this.info);
liveStart({
cameraIndex,
definition: 0,
gateway,
sn,
videoIndex,
host: getHost(),
rtmpPort: getRtmpPort(),
rtcPort: getRtcPort()
})
.then((res) => {
// this.playFlv(
// this.videoUrl,
// "video-webrtc_" + this.info.cameraIndex,
// this.info.cameraIndex
// );
// webrtc
if (res.code == 200) {
this.videoUrl = res.data.url;
this.$refs['video-webrtc_' + cameraIndex].startPlay(res.data.url);
resovle(res);
}
resovle(res);
this.handleFullscreenChange();
})
.catch((err) => {
reject(err);
this.handleFullscreenChange();
});
}
});
},
// 关闭直播
stopLive() {
return new Promise((resovle, reject) => {
if (this.info) {
const { cameraIndex, gateway, sn, videoIndex } = this.info;
liveStop({
cameraIndex,
definition: 0,
gateway,
sn,
videoIndex
})
.then((res) => {
console.log('res-------', res);
this.flvDestroy();
resovle(res);
this.handleFullscreenChange();
})
.catch((err) => {
reject(err);
this.handleFullscreenChange();
});
}
});
},
// 播放flv视频
playFlv(flv, videoId) {
if (flvjs.isSupported()) {
var videoElement = document.getElementById(videoId);
this.flvPlayer = flvjs.createPlayer({
type: 'flv',
isLive: true,
hasAudio: false,
url: flv,
enableStashBuffer: true, //
enableWorker: true,
autoCleanupSourceBuffer: true //自动清除缓存
});
this.$nextTick(() => {
this.flvPlayer.attachMediaElement(videoElement);
this.flvPlayer.load();
this.flvPlayer.play();
});
}
},
// 关闭flv
flvDestroy() {
if (this.flvPlayer) {
try {
this.flvPlayer.pause(); // 暂停播放数据流
this.flvPlayer.unload(); // 取消数据流加载
this.flvPlayer.destroy(); // 销毁播放实例
// flvPlayer.off("error"); // 移除错误处理程序
this.flvPlayer.detachMediaElement(); // 将播放实例从节点中取出
this.flvPlayer = null;
} catch (error) {}
}
},
// 判断全屏添加消息提示
handleFullscreenChange() {
console.log('全屏---------');
// 检查是否处于全屏模式
if (document.fullscreenElement) {
console.log('进入全屏模式');
// 执行你的代码
if (document.querySelector('.el-message')) {
this.$refs.monitorLiveVideo.appendChild(document.querySelectorAll('.el-message')[0]);
}
}
},
// 我需要通过获取this.gateWay.gateway(7CTDM4S00B459L)中的首位数字来判断当前设备的数量
getDeviceNumber() {
const gateway = this.gateWay.gateway;
// 获取首位数字
const firstDigit = parseInt(gateway.charAt(0));
// 判断是4还是7
if (firstDigit === 4) {
// 是4返回4
this.number = 2;
} else {
this.number = 1;
}
}
},
mounted() {
if (this.gateWay) {
this.getList();
this.getDeviceNumber();
}
this.$recvChanel('liveBus', (flag) => {
if (flag && this.switchs.currentStatus) {
this.switchs.currentStatus = false;
this.switchs.tips = '开启直播';
}
});
this.$nextTick(() => {
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
this.screen.currentStatus = false;
this.screen.img = uavScreen;
}
});
});
},
beforeDestroy() {
// 在组件销毁前移除事件监听
document.removeEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
this.screen.currentStatus = false;
this.screen.img = uavScreen;
}
});
}
};
</script>
<style lang="scss">
.cabin-live-wrapper {
width: 100%;
height: 30%;
margin-bottom: 15px;
position: relative;
.cabin-live-content {
width: 100%;
height: 100%;
padding: 10px;
background: url('../../../../images/home/UAV-bg.png') no-repeat;
background-size: 100% 100%;
.cabin-live-title {
height: 24px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
&-left {
font-size: 14px;
}
&-right {
display: flex;
align-items: center;
justify-content: center;
&-wrapper {
width: 16px;
height: 16px;
margin: 0 3px;
cursor: pointer;
img {
height: 100%;
width: 100%;
}
}
}
}
.cabin-live-video {
margin-top: 10px;
height: calc(100% - 34px);
width: 100%;
position: relative;
clip-path: polygon(0 0, 97% 0, 100% 7%, 100% 100%, 3% 100%, 0 93%);
border: 1px solid rgba(0, 255, 255, 1);
.cabin-live-video-item {
height: 100%;
width: 100%;
}
.live-stop {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #000;
flex-direction: column;
size: 12px;
}
.live-loading {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
size: 12px;
> img {
height: 50px;
height: 50px;
}
}
}
}
// 全屏
.cabin-live-content-full {
width: 100%;
height: 100%;
padding: 10px;
.cabin-live-title {
height: 24px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
top: 0;
left: 0;
&-left {
font-size: 14px;
}
&-right {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 0;
&-wrapper {
width: 16px;
height: 16px;
margin: 0 3px;
cursor: pointer;
img {
height: 100%;
width: 100%;
}
}
}
}
.cabin-live-video {
margin-top: 10px;
height: calc(100% - 34px);
width: 100%;
position: relative;
.cabin-live-video-item {
height: 100%;
width: 100%;
}
.live-stop {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #000;
flex-direction: column;
size: 12px;
}
.live-loading {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
size: 12px;
> img {
height: 50px;
height: 50px;
}
}
}
}
}
.active {
background: rgba(0, 255, 255, 0.2);
color: rgba(0, 255, 255, 1);
}
</style>

View File

@ -0,0 +1,784 @@
<template>
<div class="cabin-live-wrapper">
<ModuleItem :title="'无人机机场监控直播'" :subTitle="'Monitor live streaming'">
<div
:id="'cabin-live-video_' + info.cameraIndex"
ref="monitorLiveVideo"
:class="[screen.currentStatus ? 'cabin-live-content-full' : 'cabin-live-content']"
>
<div class="cabin-live-title">
<div class="cabin-live-title-left">机舱SN:{{ info.sn || '----' }}</div>
<div class="cabin-live-title-right">
<el-tooltip :content="backlight.tips" placement="bottom">
<div class="bgd cabin-live-title-right-wrapper">
<img :src="backlight.img" @click="changeBacklight" />
</div>
</el-tooltip>
<!-- -->
<el-dropdown v-if="number == 2" size="mini" trigger="click" :hide-on-click="false" @visible-change="visibleDropdown">
<el-tooltip :content="monitor.tips" placement="bottom">
<div class="monitor cabin-live-title-right-wrapper">
<img :src="monitor.img" />
</div>
</el-tooltip>
<el-dropdown-menu #dropdown>
<el-dropdown-item v-for="item in monitor.list" :key="item.name" :class="{ active: monitor.current === item.index }">
<span @click="monitorCheck(item)">{{ item.name }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-dropdown size="mini" trigger="click" :hide-on-click="false" @visible-change="visibleDropdown">
<el-tooltip :content="set.tips" placement="bottom">
<div class="monitor cabin-live-title-right-wrapper">
<img :src="set.img" />
</div>
</el-tooltip>
<el-dropdown-menu #dropdown>
<el-dropdown-item v-for="item in set.list" :key="item.name" :class="{ active: set.current === item.index }">
<span @click="setCheck(item)">{{ item.name }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- <div class="monitor cabin-live-title-right-wrapper" :ref="'screenFull'">
<el-tooltip :content="set.tips" placement="bottom">
<img :src="set.img" @click="showDropDown()" />
</el-tooltip>
<div v-show="set.show" class="monitor-live-dropdown-menu">
<span v-for="itm in set.list" :key="itm.value" class="dropdown-item"
:class="{ 'dropdown-active': itm.active }" @click="setCheck(itm)">
{{ itm.name }}
</span>
</div>
</div> -->
<el-tooltip :content="reset.tips" placement="bottom">
<div class="bgd cabin-live-title-right-wrapper">
<img :src="reset.img" @click="handleReset" />
</div>
</el-tooltip>
<el-tooltip :content="screen.tips" placement="bottom">
<div class="bgd cabin-live-title-right-wrapper">
<img :src="screen.img" @click="makeFullScreen(`cabin-live-video_${info.cameraIndex}`)" />
</div>
</el-tooltip>
<el-tooltip :content="switchs.tips" placement="bottom">
<div class="bgd cabin-live-title-right-wrapper">
<img :src="switchs.img" @click="handleOpen" />
</div>
</el-tooltip>
</div>
</div>
<div class="cabin-live-video">
<video
v-show="switchs.currentStatus && !switchs.loading"
class="cabin-live-video-item"
style="position: absolute; width: 100%; height: 100%; object-fit: fill"
:id="'video-webrtc_' + info.cameraIndex"
autoplay
muted
width="100%"
height="100%"
></video>
<div v-show="!switchs.currentStatus && !switchs.loading" class="live-stop">
<img :src="require('../../../../images/home/live-stop.png')" alt="" />
<span>机舱已经停止直播</span>
</div>
<div v-show="switchs.loading" class="live-loading">
<img :src="require('../../../../images/home/loading.png')" alt="" />
<span>加载中请稍侯</span>
</div>
</div>
</div>
</ModuleItem>
</div>
</template>
<script>
import ModuleItem from '../ModuleItem/index.vue';
import { listInfo, liveStart, liveStop, remoteDebug, liveChangeQuality, cameraChange } from '@/api/air';
import _ from 'lodash';
import { getHost, getRtmpPort, getRtcPort } from '@/utils/auth';
import { useAirStore } from '@/store/modules/drone';
export default {
name: 'MonitorLiveStreaming',
components: { ModuleItem },
data() {
return {
videoUrl: '',
flvPlayer: null,
info: {},
gateWay: useAirStore().gateWay,
time1: null,
time2: null,
number: 0,
//背光灯
backlight: {
light: false, // 是否开启
img: require('../../../../images/home/uav-bgd.png'),
tips: '打开补光灯',
key: 'supplement_light_open',
msg: '补光灯已开启'
},
// 摄像头
monitor: {
img: require('../../../../images/home/uav-monitor.png'),
list: [
{ name: '舱外', active: false, index: 1 },
{ name: '舱内', active: true, index: 0 }
],
tips: '摄像头',
key: 'supplement_light_open',
current: 0
},
// 设置
set: {
show: false,
img: require('../../../../images/home/uav-set.png'),
list: [
{ name: '自适应', active: true, index: 0 },
{ name: '流畅', active: false, index: 1 },
{ name: '标清', active: false, index: 2 },
{ name: '高清', active: false, index: 3 },
{ name: '超清', active: false, index: 4 }
],
tips: '设置',
current: 0
},
// 重置
reset: {
img: require('../../../../images/home/uav-reset.png'),
tips: '刷新',
key: 'device_reboot'
},
// 全屏切换
screen: {
img: require('../../../../images/home/uav-screen.png'),
tips: '全屏切换',
currentStatus: false
},
//开关
switchs: {
img: require('../../../../images/home/uav-close.png'),
tips: '开启直播',
currentStatus: false,
loading: false
}
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getList();
},
deep: true
}
},
methods: {
showDropDown() {
this.set.show = true;
},
// 关闭直播
stopLives() {
if (this.flvPlayer) {
this.stopLive();
}
},
// 背光灯
changeBacklight() {
this.backlight.light = !this.backlight.light;
if (this.backlight.light) {
this.backlight.img = require('../../../../images/home/uav-bgd-open.png');
this.backlight.key = 'supplement_light_open';
this.backlight.tips = '关闭补光灯';
this.backlight.msg = '补光灯已开启';
} else {
this.backlight.img = require('../../../../images/home/uav-bgd.png');
this.backlight.key = 'supplement_light_close';
this.backlight.tips = '打开补光灯';
this.backlight.msg = '补光灯已关闭';
}
const { gateway } = this.info;
let method = this.backlight.key;
const params = { gateway, method };
remoteDebug(params)
.then((res) => {
console.log(res);
console.log(this.backlight.msg);
this.$message.success(this.backlight.msg);
})
.finally(() => {
this.handleFullscreenChange();
});
},
// 摄像头
monitorCheck(data) {
// console.log("data", data);
this.monitor.current = data.index;
this.monitor.list.forEach((item) => {
if (this.monitor.current === item.index) {
item.active = true;
} else {
item.active = false;
}
});
const { gateway } = this.info;
const params = {
gateway,
cameraIndex: this.gateWay.cameraIndex,
cameraPosition: data.index,
sn: this.gateWay.sn,
videoIndex: this.gateWay.videoIndex
};
cameraChange(params)
.then((res) => {
this.$message.success(res.msg);
})
.finally(() => {
this.handleFullscreenChange();
});
// console.log("this.monitor", this.monitor.list);
},
// 设置
setCheck(data) {
// console.log("data", data);
this.set.show = false;
this.set.current = data.index;
const { gateway, sn, cameraIndex, videoIndex } = this.info;
const params = {
gateway,
sn,
cameraIndex,
videoIndex,
videoQuality: this.set.current
};
liveChangeQuality(params)
.then((res) => {
this.set.list.forEach((item) => {
if (this.set.current === item.index) {
item.active = true;
this.$message.success(res.msg);
} else {
item.active = false;
}
});
})
.finally(() => {
this.handleFullscreenChange();
});
},
// 刷新
handleReset() {
// if (this.switchs.currentStatus) {
this.switchs.loading = true;
this.debouncehandleReset();
// }
},
debouncehandleReset: _.debounce(async function () {
{
if (this.switchs.currentStatus) {
// await this.stopLive();
const res = await this.startLive();
this.$message.success(res.msg);
this.switchs.currentStatus = true;
this.switchs.loading = false;
this.handleFullscreenChange();
}
}
}, 600),
// 电源
handleOpen() {
this.switchs.loading = true;
// 关闭就不显示加载动画
this.switchs.currentStatus && (this.switchs.loading = false);
this.debounceHandleOpen();
},
// 加个防抖不准频繁请求解决频繁请求关不了的问题
debounceHandleOpen: _.debounce(function () {
if (this.switchs.currentStatus) {
this.stopLive()
.then((res) => {
this.switchs.currentStatus = false;
this.switchs.tips = '开启直播';
this.$message.success(res.msg);
this.switchs.loading = false;
})
.catch((err) => {
// console.log("err", err);
this.switchs.loading = false;
})
.finally(() => {
this.switchs.loading = false;
this.handleFullscreenChange();
});
} else {
this.startLive()
.then((res) => {
// console.log("res", res);
this.switchs.currentStatus = true;
this.switchs.tips = '关闭直播';
this.$message.success(res.msg);
this.switchs.loading = false;
})
.catch((err) => {
// console.log("err", err);
this.switchs.loading = false;
})
.finally(() => {
// console.log("finally");
this.switchs.loading = false;
this.handleFullscreenChange();
});
}
}, 600),
makeFullScreen(id) {
this.$nextTick(() => {
const videoElement = document.getElementById(id);
if (!this.screen.currentStatus) {
this.screen.img = require('../../../../images/home/uav-screen-esc.png');
this.screen.currentStatus = true;
if (videoElement.requestFullscreen) {
videoElement.requestFullscreen();
} else if (videoElement.mozRequestFullScreen) {
// Firefox
videoElement.mozRequestFullScreen();
} else if (videoElement.webkitRequestFullscreen) {
// Chrome, Safari and Opera
videoElement.webkitRequestFullscreen();
} else if (videoElement.msRequestFullscreen) {
// IE/Edge
videoElement.msRequestFullscreen();
}
} else {
this.screen.img = require('../../../../images/home/uav-screen.png');
if (document.fullscreenElement) {
document
.exitFullscreen()
.then(() => {
this.screen.currentStatus = false;
clearInterval(this.time1);
clearInterval(this.time2);
})
.catch((err) => {
console.error(`Failed to exit full screen: ${err}`);
});
}
}
});
},
// 用于解决全屏下下拉菜单不显示的问题
visibleDropdown() {
// this.time1 = setTimeout(() => {
// // console.log("有执行吗", this.$refs.monitorLiveVideo);
// // console.log("el-dropdown", document.querySelector(".el-dropdown-menu"));
// if (document.querySelector(".el-dropdown-menu")) {
// this.$refs.monitorLiveVideo.appendChild(
// document.querySelectorAll(".el-dropdown-menu")[0]
// );
// this.$refs.monitorLiveVideo.appendChild(
// document.querySelectorAll(".el-dropdown-menu")[1]
// );
// }
// }, 200);
},
// 获取设备列表
getList() {
listInfo({
pageNum: 1,
pageSize: 10,
gateway: this.gateWay.gateway,
type: '机场'
}).then((res) => {
// console.log("list-------", res);
this.info = res.rows[0];
});
},
// 获取数量
getNumber() {
listInfo({
pageNum: 1,
pageSize: 10,
gateway: this.gateWay.gateway
}).then((res) => {
// console.log("list-------", res);
this.number = res.rows.length;
});
},
// 开启直播
startLive() {
return new Promise((resovle, reject) => {
if (this.info) {
const { cameraIndex, gateway, sn, videoIndex } = this.info;
liveStart({
cameraIndex,
definition: 0,
gateway,
sn,
videoIndex,
host: getHost(),
rtmpPort: getRtmpPort(),
rtcPort: getRtcPort()
})
.then((res) => {
this.videoUrl = res.data.url;
this.playFlv(this.videoUrl, 'video-webrtc_' + this.info.cameraIndex, this.info.cameraIndex);
resovle(res);
this.handleFullscreenChange();
})
.catch((err) => {
reject(err);
this.handleFullscreenChange();
});
}
});
},
// 关闭直播
stopLive() {
return new Promise((resovle, reject) => {
if (this.info) {
const { cameraIndex, gateway, sn, videoIndex } = this.info;
liveStop({
cameraIndex,
definition: 0,
gateway,
sn,
videoIndex
})
.then((res) => {
// console.log("res-------", res);
this.flvDestroy();
resovle(res);
this.handleFullscreenChange();
})
.catch((err) => {
reject(err);
this.handleFullscreenChange();
});
}
});
},
// 播放flv视频
playFlv(flv, videoId) {
if (flvjs.isSupported()) {
var videoElement = document.getElementById(videoId);
this.flvPlayer = flvjs.createPlayer({
type: 'flv',
isLive: true,
hasAudio: false,
url: flv,
enableStashBuffer: true, //
enableWorker: true,
autoCleanupSourceBuffer: true //自动清除缓存
});
this.$nextTick(() => {
this.flvPlayer.attachMediaElement(videoElement);
this.flvPlayer.load();
this.flvPlayer.play();
});
}
},
// 关闭flv
flvDestroy() {
if (this.flvPlayer) {
try {
this.flvPlayer.pause(); // 暂停播放数据流
this.flvPlayer.unload(); // 取消数据流加载
this.flvPlayer.destroy(); // 销毁播放实例
// flvPlayer.off("error"); // 移除错误处理程序
this.flvPlayer.detachMediaElement(); // 将播放实例从节点中取出
this.flvPlayer = null;
} catch (error) {}
}
},
// 判断全屏添加消息提示
handleFullscreenChange() {
// console.log("全屏---------");
// 检查是否处于全屏模式
if (document.fullscreenElement) {
// console.log("进入全屏模式");
// 执行你的代码
if (document.querySelector('.el-message')) {
this.$refs.monitorLiveVideo.appendChild(document.querySelectorAll('.el-message')[0]);
}
}
},
handleClickOutside(event) {
const dropdown = this.$refs.screenFull;
// console.log("dropdown", dropdown);
setTimeout(() => {
if (!dropdown.contains(event.target)) {
this.set.show = false; // 点击外部区域时隐藏下拉框
}
}, 200);
}
},
mounted() {
if (this.gateWay) {
this.getList();
this.getNumber();
}
this.$recvChanel('liveBus', (flag) => {
if (flag && this.switchs.currentStatus) {
this.switchs.currentStatus = false;
this.switchs.tips = '开启直播';
}
});
// this.$recvChanel("flyerChange", () => {
// this.stopLives()
// });
// document.addEventListener("click", this.handleClickOutside);
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
this.screen.currentStatus = false;
this.screen.img = require('../../../../images/home/uav-screen.png');
}
});
},
beforeDestroy() {
// 在组件销毁前移除事件监听
document.removeEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
this.screen.currentStatus = false;
this.screen.img = require('../../../../images/home/uav-screen.png');
}
});
}
};
</script>
<style lang="scss">
.cabin-live-wrapper {
width: 100%;
height: 30%;
margin-bottom: 15px;
position: relative;
.cabin-live-content {
width: 100%;
height: 100%;
padding: 10px;
background: url('../../../../images/home/UAV-bg.png') no-repeat;
background-size: 100% 100%;
.cabin-live-title {
height: 24px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 10;
&-left {
font-size: 14px;
}
&-right {
display: flex;
align-items: center;
justify-content: center;
&-wrapper {
width: 16px;
height: 16px;
margin: 0 3px;
cursor: pointer;
// position: relative;
img {
height: 100%;
width: 100%;
}
}
.monitor-live-dropdown-menu {
display: flex;
flex-direction: column;
background: rgba(0, 0, 0, 0.5);
border: 1.5px solid rgba(0, 255, 255, 1);
backdrop-filter: blur(2px);
border-radius: 5px;
margin: 0;
width: 60px;
.dropdown-item {
padding: 5px;
display: inline-block;
cursor: pointer;
width: 100%;
text-align: center;
line-height: 24px;
font-size: 12px;
&:hover {
background: rgba(0, 255, 255, 0.2);
color: rgba(0, 255, 255, 1);
}
}
.dropdown-active {
background: rgba(0, 255, 255, 0.2);
color: rgba(0, 255, 255, 1);
}
}
}
}
.cabin-live-video {
margin-top: 10px;
height: calc(100% - 34px);
width: 100%;
position: relative;
// clip-path: polygon(0 0, 97% 0, 100% 7%, 100% 100%, 3% 100%, 0 93%);
border: 1px solid rgba(0, 255, 255, 1);
.cabin-live-video-item {
height: 100%;
width: 100%;
}
.live-stop {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #000;
flex-direction: column;
size: 12px;
}
.live-loading {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
size: 12px;
> img {
height: 50px;
height: 50px;
}
}
}
}
// 全屏
.cabin-live-content-full {
width: 100%;
height: 100%;
padding: 10px;
.cabin-live-title {
height: 24px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
top: 0;
left: 0;
&-left {
font-size: 14px;
}
&-right {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 0;
&-wrapper {
width: 16px;
height: 16px;
margin: 0 3px;
cursor: pointer;
position: relative;
img {
height: 100%;
width: 100%;
}
}
// .monitor-live-dropdown-menu {
// display: flex;
// flex-direction: column;
// background: rgba(0, 0, 0, 0.5);
// border: 1.5px solid rgba(0, 255, 255, 1);
// backdrop-filter: blur(2px);
// border-radius: 5px;
// margin: 0;
// width: 60px;
// .dropdown-item {
// padding: 5px;
// display: inline-block;
// cursor: pointer;
// width: 100%;
// text-align: center;
// line-height: 24px;
// font-size: 12px;
// &:hover {
// background: rgba(0, 255, 255, 0.2);
// color: rgba(0, 255, 255, 1);
// }
// }
// .dropdown-active {
// background: rgba(0, 255, 255, 0.2);
// color: rgba(0, 255, 255, 1);
// }
// }
}
}
.cabin-live-video {
margin-top: 10px;
height: calc(100% - 34px);
width: 100%;
position: relative;
.cabin-live-video-item {
height: 100%;
width: 100%;
}
.live-stop {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #000;
flex-direction: column;
size: 12px;
}
.live-loading {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
size: 12px;
> img {
height: 50px;
height: 50px;
}
}
}
}
}
.active {
background: rgba(0, 255, 255, 0.2);
color: rgba(0, 255, 255, 1);
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<div class="home_left uav_box">
<MonitorLiveStreaming></MonitorLiveStreaming>
<AirportInformationDetails></AirportInformationDetails>
<AirportRemoteDebugging></AirportRemoteDebugging>
<!-- <div v-for="(url, index) in urls" :key="index">
<webrtc :ref="`webrtc-${index}`"></webrtc>
<button @click="startPlay(url, index)">播放</button>
</div> -->
</div>
</template>
<script>
import MonitorLiveStreaming from './components/MonitorLiveStreaming/index.vue';
import AirportInformationDetails from './components/AirportInformationDetails/index.vue';
import AirportRemoteDebugging from './components/AirportRemoteDebugging/index.vue';
// import webrtc from "@/components/webrtc";
export default {
components: {
MonitorLiveStreaming,
AirportInformationDetails,
AirportRemoteDebugging
// webrtc,
},
data() {
return {
urls: [
'http://121.37.237.116:28453/rtc/v1/whep/?app=live&stream=4SEDL9C001X8GE4SEDL9C001X8GE165-0-7normal-0',
'http://121.37.237.116:28453/rtc/v1/whep/?app=live&stream=4SEDL9C001X8GE1581F5BMD238V00172JR39-0-7normal-0',
'http://121.37.237.116:28453/rtc/v1/whep/?app=live&stream=4SEDL9C001X8GE1581F5BMD238V00172JR53-0-0normal-0'
]
};
},
methods: {
startPlay(url, index) {
// console.log(this.$refs[`webrtc-${index}`][0]);
this.$refs[`webrtc-${index}`][0].startPlay(url);
}
},
created() {},
mounted() {}
};
</script>
<style lang="scss" scoped>
.home_left {
left: 20px;
top: 6%;
width: 450px;
height: 90%;
position: absolute;
z-index: 2;
}
</style>

View File

@ -0,0 +1,68 @@
<template>
<div class="home_right uav_box">
<!-- <el-button @click="chongdian">充电</el-button> -->
<AircraftMonitoringLiveBroadcast></AircraftMonitoringLiveBroadcast>
<AircraftInformationDetails></AircraftInformationDetails>
</div>
</template>
<script>
import AircraftMonitoringLiveBroadcast from './components/AircraftMonitoringLiveBroadcast/index.vue';
import AircraftInformationDetails from './components/AircraftInformationDetails/index.vue';
import { useAirStore } from '@/store/modules/drone';
// import { remoteDebug } from "@/api/air";
export default {
components: {
AircraftMonitoringLiveBroadcast,
AircraftInformationDetails
},
data() {
return {
gateWay: useAirStore().gateWay
};
},
watch: {
// // 监听 Vuex 中的状态变化
// "useAirStore().gateWay": {
// handler(newValue, oldVal) {
// this.gateWay = newValue;
// },
// deep: true,
// },
},
created() {},
mounted() {},
methods: {
// chongdian() {
// let obj = {
// gateway: this.gateWay.gateway,
// method: "charge_open",
// };
// // this.$store.commit("UP_debugValue", val);
// this.droneDebug(obj);
// },
// // 调试模式
// droneDebug(obj, val) {
// remoteDebug(obj).then((res) => {
// const { code, data, msg } = res;
// if (code == 200) {
// this.$message.success("操作成功");
// } else {
// this.$message.error(msg);
// this[val] = !this[val];
// }
// });
// },
}
};
</script>
<style lang="scss">
.home_right {
position: absolute;
right: 20px;
width: 450px;
height: 90%;
z-index: 2;
top: 6%;
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<div class="home-container" ref="monitorLiveVideo">
<homeLeft></homeLeft>
<homeRight></homeRight>
</div>
</template>
<script>
import homeLeft from './homeLeft.vue';
import homeRight from './homeRight.vue';
export default {
components: { homeLeft, homeRight },
data() {
return {};
},
methods: {},
created() {},
mounted() {}
};
</script>
<style lang="scss" scoped>
.home-container {
height: 100%;
width: 100%;
}
.uav_box {
// border-radius: 8px;
// background: rgba(0, 0, 0, 0.5);
// border: 1px solid rgba(0, 255, 255, 0.5);
// padding: 15px 10px;
}
</style>

View File

@ -0,0 +1,266 @@
<template>
<div class="medium-container">
<div class="medium-wrapper">
<div class="medium-menu-container">
<div class="medium-title">媒体库</div>
<div class="medium-info">
<div class="medium-info-title">项目空间使用情况</div>
<div v-for="item in mediumInfoList" :key="item.name" class="medium-info-item">
<div class="medium-info-item-left">
<div class="medium-img-box">
<img :src="item.img" alt="" />
</div>
<div class="medium-num-box">
<div>{{ item.name }}</div>
<div>{{ item.count }} 文件</div>
</div>
</div>
<div class="medium-info-item-right">
{{ item.storage }}
</div>
</div>
<div v-for="item in aiInfoList" :key="item.name" class="medium-info-item">
<div class="medium-info-item-left">
<div class="medium-img-box">
<img :src="item.img" alt="" />
</div>
<div class="medium-num-box">
<div>{{ item.name }}</div>
<div>{{ item.count }} 文件</div>
</div>
</div>
<div class="medium-info-item-right">
{{ item.storage }}
</div>
</div>
</div>
<!-- <div class="medium-info medium-storage">
<div class="medium-info-item">
<div class="medium-info-item-left">
<div class="medium-img-box">
<img src="../../images/medium/medium-storage.png" alt="" />
</div>
<div class="medium-num-box">
剩余可用空间
</div>
</div>
<div class="medium-info-item-right">
50 G
</div>
</div>
</div> -->
</div>
<div class="medium-main-container">
<el-tabs class="myTABS" v-model="activeName">
<el-tab-pane label="无人机" name="first">
<MediumHome></MediumHome>
</el-tab-pane>
<el-tab-pane label="AI识别" name="second">
<mediumHomeAi></mediumHomeAi>
</el-tab-pane>
</el-tabs>
</div>
</div>
<Preview></Preview>
</div>
</template>
<script>
import MediumHome from './mediumHome.vue';
import mediumHomeAi from './mediumHomeAi.vue';
import { getInfo } from '@/api/fileMangement';
import { aiInfo } from '@/api/air';
import Preview from './preview.vue';
import { useAirStore } from '@/store/modules/drone';
import mediumImg from '../../images/medium/medium-img.png';
import mediumVideo from '../../images/medium/medium-video.png';
export default {
components: { MediumHome, Preview, mediumHomeAi },
data() {
return {
info: {},
gateway: {},
mediumHomeVisible: false,
previewVisible: false,
previewImage: '',
previewTitle: '',
activeName: 'first',
mediumInfoList: [
{
img: mediumImg,
name: '照片',
count: 0,
storage: '0'
},
{
img: mediumVideo,
name: '视频',
count: 0,
storage: '0'
}
],
aiInfoList: [
{
img: mediumVideo,
name: 'AI',
count: 0,
storage: '0'
}
],
gateWay: useAirStore().gateWay
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getFileInfo();
},
deep: true
}
},
methods: {
handleClick(tab, event) {
console.log(tab, event);
},
// 文件信息概况
async getFileInfo() {
const { gateway } = this.gateWay;
const res = await getInfo({ gateway });
// console.log("文件信息概况", res);
this.mediumInfoList[0].count = res.data.totalImageCount;
this.mediumInfoList[0].storage = res.data.totalImageSize;
this.mediumInfoList[1].count = res.data.totalVideoCount;
this.mediumInfoList[1].storage = res.data.totalVideoSize;
},
// ai信息
async getAiInfo() {
const { gateway } = this.gateWay;
const res = await aiInfo({ gateway });
this.aiInfoList[0].count = res.data.totalImageCount;
this.aiInfoList[0].storage = res.data.totalImageSize;
}
},
created() {},
mounted() {
this.getFileInfo();
this.getAiInfo();
this.$recvChanel('mediumDel', (bool) => {
this.getFileInfo();
this.getAiInfo();
});
}
};
</script>
<style lang="scss">
.medium-container {
// height: 100%;
// width: 100%;
color: #fff;
// background-color: rgb(224, 137, 22);
// position: absolute;
display: flex;
justify-content: center;
align-items: center;
.medium-wrapper {
position: absolute;
top: 15%;
margin: auto;
width: 80%;
height: 75%;
background-color: antiquewhite;
display: flex;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(2px);
z-index: 2;
.medium-menu-container {
width: 20%;
height: 100%;
.medium-title {
font-size: 18px;
border-bottom: 1px solid rgba(204, 204, 204, 0.2);
line-height: 60px;
padding-left: 20px;
}
.medium-info {
padding: 0 20px;
border-bottom: 1px solid rgba(204, 204, 204, 0.2);
.medium-info-title {
line-height: 60px;
font-size: 16px;
font-weight: 400;
color: rgba(255, 255, 255, 1);
}
.medium-info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.medium-info-item-left {
display: flex;
.medium-img-box {
width: 40px;
height: 40px;
border-radius: 8px;
background: rgba(0, 0, 0, 1);
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
margin-right: 10px;
img {
width: 24px;
height: 20px;
}
}
.medium-num-box {
font-size: 12px;
display: flex;
flex-direction: column;
justify-content: space-around;
}
}
.medium-info-item-right {
height: 40px;
display: flex;
align-items: center;
font-size: 14px;
}
}
}
.medium-storage {
border-bottom: none;
padding-top: 20px;
img {
width: 20px !important;
height: 20px;
}
}
}
.medium-main-container {
width: 80%;
height: 100%;
background-color: #f6f8fa;
overflow: hidden;
.el-tabs__item {
padding-left: 20px !important;
}
}
}
}
</style>

View File

@ -0,0 +1,623 @@
<template>
<div class="medium-home-container">
<div class="medium-home-main">
<div class="medium-home-main-top">
<div class="medium-breadcrumb">
<div v-for="item in paths" :key="item" class="breadcrumb-item" @click="changePath(item)">
{{ item || '全部文件' }}
</div>
</div>
</div>
<div class="medium-home-top-btn">
<div>
<el-button size="small" @click="handleDelete(false)" type="danger"> 删除 </el-button>
<el-button size="small" @click="handleDownload(false)" type="success"> 下载 </el-button>
</div>
<div class="medium-check">
<div class="check-item">已选/全部</div>
<div class="check-item">{{ selectNum }}/{{ fileList.length }}</div>
</div>
</div>
<div class="medium-home-main-table">
<el-table
ref="multipleTable"
:data="fileList"
tooltip-effect="dark"
:default-sort="{ prop: 'creatTime' }"
stripe
size="small"
height="600"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
>
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="fileName" label="文件名称">
<template #default="scope">
<div
class="file-name-wrapper"
style="display: flex; align-items: center; cursor: pointer"
@click="handleSelectFile(scope.row.path, scope.row)"
>
<img :src="scope.row.img" alt="" style="width: 16px; height: 16px; transform: translateY(-1px); margin-right: 2px" />
<div class="file-name">{{ scope.row.name ? scope.row.name : scope.row.fileName }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="size" label="大小" width="80" show-overflow-tooltip> </el-table-column>
<el-table-column prop="creatTime" label="创建时间" sortable="custom">
<template #default="scope">{{ scope.row.creatTime }}</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="120">
<template #default="scope">
<div class="opration-btns">
<el-tooltip v-if="scope.row.size !== 'N/A'" class="item" content="下载" effect="dark">
<span class="opration-btn" style="display: inline-block; cursor: pointer" @click="handleDownload(scope.row)">
<img src="../../images/medium/down-load.png" srcset="" />
</span>
</el-tooltip>
<el-tooltip v-if="scope.row.size !== 'N/A'" class="item" content="预览" effect="dark">
<span class="opration-btn" style="display: inline-block; cursor: pointer" @click="handlePreview(scope.row)">
<img src="../../images/medium/prview-icon.png" srcset="" />
</span>
</el-tooltip>
<el-tooltip class="item" content="删除" effect="dark">
<span class="opration-btn" style="display: inline-block; cursor: pointer" @click="handleDelete(scope.row)">
<img src="../../images/medium/delete.png" srcset="" />
</span>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="shade" v-if="loading">
<el-progress type="circle" :percentage="percentage" :format="percentageFun"></el-progress>
<!-- <div
style="white-space: pre-line;text-align: center;color: aqua;color: aqua; display: flex; flex-direction: column;position: relative;">
<img src="../../images/loadingdow.gif" alt="">
<span style="display: inline-block;position: absolute;left: 50%;top:50%;transform: translate(-50%, -50%);">{{
this.text }}</span>
</div> -->
</div>
</div>
</template>
<script>
import axios from 'axios';
import JSZip from 'jszip';
import { listInfo } from '@/api/air';
import { getFiles, fileDownload, deleteFile, downloadBatch, getUrlList } from '@/api/fileMangement';
import Preview from './preview.vue';
import { useAirStore } from '@/store/modules/drone';
import videoIcon from '../../images/medium/video-icon.png';
import filesIcon from '../../images/medium/files-icon.png';
export default {
components: { Preview },
data() {
return {
percentage: 0,
info: {},
paths: [''],
fileList: [],
totle: 0,
selectNum: 0,
length: 0,
sn: '',
currentPath: '',
searchName: '', // 搜索关键字
dateTime: [new Date(), new Date()], // 初始时间范围为今日至今日
typeOptions: [
{
value: '选项1',
label: '选项1'
},
{
value: '选项2',
label: '选项2'
}
],
typeValue: '',
loadOptions: [
{
value: '选项1',
label: '选项1'
},
{
value: '选项2',
label: '选项2'
}
],
loadValue: '',
selectList: [],
loading: false,
gateWay: useAirStore().gateWay,
text: ''
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getFileList(this.gateWay.gateway);
},
deep: true
}
},
methods: {
percentageFun(percentage) {
let text = '';
if (percentage == 99) {
text = `${percentage}%\n 正在打包中`;
} else {
text = `${percentage}%\n 下载中`;
}
return text;
},
handleSortChange({ column, prop, order }) {
this.fileList.sort((a, b) => {
if (a.excludeSort || b.excludeSort) {
// 如果有数据被标记为排除排序,则直接返回原始顺序
return 0;
}
if (order === 'ascending') {
return a[prop] > b[prop] ? 1 : -1;
} else if (order === 'descending') {
return a[prop] < b[prop] ? 1 : -1;
}
return 0;
});
},
handleSelectionChange(datas) {
// console.log("datas: ", datas);
this.selectList = datas;
this.selectNum = datas.length;
},
// 获取文件列表
async getFileList(gateway, path = '') {
this.fileList = [];
const list = await getFiles({ gateway, itemPath: path });
// console.log("文件列表1-------", list);
this.paths = [
'',
...Object.keys(list)[0]
.split('/')
.filter((path) => path !== '')
.slice(0, -1)
];
this.fileList = Object.keys(list).map((item) => {
const pathList = item.split('/').filter((path) => path !== '');
let img = filesIcon;
let type = 'file';
// 文件缩略图
if (list[item].size !== 'N/A') {
type = item.split('.')[1];
if (item.split('.')[1] !== 'mp4') {
img = list[item].download_url;
} else {
img = videoIcon;
}
}
return {
name: list[item].missionName,
fileName: pathList[pathList.length - 1],
size: list[item].size,
creatTime: list[item].creation_time,
path: item,
img,
type,
url: list[item].download_url,
fileLength: list[item].length,
excludeSort: pathList[pathList.length - 1] == '视频录制' ? true : false
};
});
console.log(this.fileList);
this.totle = this.fileList.length;
this.length = this.fileList.fileLength;
},
// 文件夹点击进入文件夹
handleSelectFile(path, data) {
console.log('path11111111111', path);
if (data && data.size !== 'N/A') {
// console.log("文件------------");
} else {
this.currentPath = path;
const { gateway } = this.gateWay;
this.getFileList(gateway, path);
}
},
// 路径变化
changePath(path) {
if (path) {
// console.log("currentPath", this.currentPath);
this.currentPath = this.currentPath.split(path)[0] + path;
// console.log("path", path);
// 跳转到该路径
this.handleSelectFile(this.currentPath);
} else {
this.handleSelectFile('');
}
},
// 删除
handleDelete(row) {
// console.log("删除---------", row);
let params = { itemPaths: [row.path], gateway: this.gateWay.gateway };
if (!row) {
if (this.selectList.length === 0) {
this.$message.warning('请先选择要删除的文件');
this.loading = false;
return;
}
const pathList = this.selectList.map((item) => item.path);
params = { itemPaths: pathList, gateway: this.gateWay.gateway };
}
this.$confirm('此操作将删除该文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
customClass: 'customMessageBox',
confirmButtonClass: 'customConfirm',
cancelButtonClass: 'customCancel'
}).then(() => {
deleteFile(params).then((res) => {
this.$message.success(res.msg);
this.$sendChanel('mediumDel');
// 过滤掉删除的文件
this.fileList = this.fileList.filter((file) => {
return !params.itemPaths.includes(file.path);
});
if (this.fileList.length === 0) {
// 如果列表为空跳转到根路径
this.handleSelectFile('');
}
});
});
},
// 下载
handleDownload(row) {
this.loading = true;
// console.log("下载---------", row);
let params = { itemPath: row.path, gateway: this.gateWay.gateway };
if (!row) {
if (this.selectList.length === 0) {
this.$message.warning('请先选择要下载的文件');
this.loading = false;
return;
}
let pathList = [];
// 定义是否是文件夹
let isFile = false;
// console.log("this.selectList", this.selectList);
this.selectList.forEach((item) => {
if (item.type === 'file') {
isFile = true;
}
});
if (isFile) {
const { gateway } = this.gateWay;
const promises = this.selectList.map((item) => {
const params = { gateway, itemPath: item.path };
return getUrlList(params).then((res) => {
console.log('res', res);
pathList.push(...res); // 将结果合并到 pathList
});
});
// 等待所有异步操作完成后,再执行下载操作
Promise.all(promises)
.then(() => {
// console.log("pathList1", pathList);
// 确保 pathList 有值后再调用下载方法
console.log('pathList', pathList);
this.downloadAndZipFiles(pathList);
})
.catch((error) => {
console.error('获取文件路径列表时出错:', error);
});
} else {
pathList = this.selectList.map((item) => item.url);
// console.log("pathList2", pathList);
// 并行下载文件并压缩为 zip
this.downloadAndZipFiles(pathList);
}
} else {
this.downloadAndZipFiles([row.url], row);
}
},
async downloadAndZipFiles(fileUrls, file) {
try {
const zip = new JSZip();
const fileSizeMap = new Map();
let fileTotal = 0;
if (file) {
fileTotal = file.fileLength;
} else {
this.selectList.forEach((item) => {
fileTotal += item.fileLength;
});
}
// console.log("file-----", fileTotal);
// 并行下载所有文件
const downloadPromises = fileUrls.map((url) =>
axios({
url: url,
method: 'get',
responseType: 'blob',
onDownloadProgress: (progressEvent) => {
const { loaded, total } = progressEvent;
fileSizeMap.set(url, loaded);
const totalFileSize = [...fileSizeMap.values()].reduce((acc, value) => acc + value, 0);
const progress = Math.round((totalFileSize / fileTotal) * 100); // 计算下载进度百分比
// console.log("progressEvent", progressEvent);
// console.log("overallProgress", progress);
this.percentage = progress;
if (progress == 100) {
this.percentage = progress * 0.99;
}
// if (progress == 100) {
// this.text = `${99}%\n 正在打包中`;
// } else {
// this.text = `${progress}%\n 下载中`
// }
}
}).then((response) => {
// console.log("Response:", response); // 在这里打印 response 对象
// console.log("url:", url); // 在这里打印 response 对象
const regex = /\/(DJI_[^\/]+\/DJI_[^\/]+\.[^\/?]+)(?=\?)/;
// 提取匹配的部分
const match = url.match(regex);
let extractedString = 'undefined';
if (match) {
extractedString = match[1]; // 捕获的字符串
}
// 打印文件名和文件数据
const fileName = file ? file.fileName : extractedString; // 获取文件名
const fileData = response.data; // 保存文件数据
// 获取文件大小:使用 Blob 的 size 属性
const fileSize = fileData.size; // 单位为字节bytes
// console.log("File size:", fileSize);
// 返回一个对象,继续后续操作
return { fileName, fileData };
})
);
// 等待所有文件下载完成
const downloadedFiles = await Promise.all(downloadPromises);
if (file) {
this.percentage = 100;
this.selectList = [];
// console.log("Downloading", downloadedFiles);
const url = window.URL.createObjectURL(downloadedFiles[0].fileData);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', downloadedFiles[0].fileName); // 设置下载的文件名
document.body.appendChild(link);
link.click();
link.remove();
this.loading = false;
this.percentage = 0; // 下载完进度百分比清零
return;
}
// 将每个文件添加到 zip 中
downloadedFiles.forEach(({ fileName, fileData }) => {
// console.log("File Name:", fileName);
zip.file(fileName, fileData); // 添加文件到 zip
});
// 生成 zip 文件并触发下载
const zipBlob = await zip.generateAsync({ type: 'blob' });
const url = window.URL.createObjectURL(zipBlob);
const link = document.createElement('a');
link.href = url;
const temp = new Date().getTime();
link.setAttribute('download', `文件${temp}.zip`); // 设置下载的文件名
document.body.appendChild(link);
link.click();
link.remove();
this.loading = false;
this.percentage = 0; // 下载完进度百分比清零
} catch (error) {
// console.error("Error downloading or zipping files:", error);
this.loading = false;
this.percentage = 0; // 下载完进度百分比清零
}
},
// 预览
handlePreview(row) {
const list = this.fileList;
// console.log("预览---------", row);
// console.log("list---------", list);
const index = list.findIndex((item) => item === row);
const data = { list, detail: row, index };
this.$sendChanel('previewBus', data);
}
},
created() {},
mounted() {
this.getFileList(this.gateWay.gateway);
}
};
</script>
<style lang="scss">
.medium-home-container {
height: 100%;
width: 100%;
position: relative;
background-color: antiquewhite;
.medium-home-search-wrapper {
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
> .el-date-editor {
width: 38%;
}
> .el-select {
width: 18%;
}
> .el-input {
width: 18%;
}
> .el-button {
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
img {
width: 20px;
height: 20px;
}
}
}
.medium-home-main {
height: calc(100%);
background: #fff;
.medium-home-main-top {
display: flex;
justify-content: space-between;
line-height: 40px;
padding: 0 16px;
font-size: 12px;
width: 100%;
.medium-breadcrumb {
display: flex;
.breadcrumb-item {
// margin-right: 20px;
color: #909399;
cursor: pointer;
&::after {
content: '/';
display: inline-block;
margin: 0 3px;
}
}
.breadcrumb-item:last-child {
color: #000;
&::after {
content: '';
display: inline-block;
}
}
}
.file-name-wrapper {
display: flex;
.file-img-wrapper {
height: 20px !important;
width: 20px !important;
img {
width: 100%;
height: 100%;
}
}
}
.el-table {
.el-checkbox {
margin-left: 8px !important;
}
}
}
.medium-home-top-btn {
width: 100%;
height: 40px;
padding: 0 16px;
display: flex;
align-items: center;
font-size: 12px;
justify-content: space-between;
.medium-check {
display: flex;
align-items: center;
color: #000;
}
}
.medium-home-main-table {
overflow-y: auto;
height: calc(100% - 80px);
width: 100%;
}
.el-checkbox__input {
margin-left: 8px !important;
}
}
.shade {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 10;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
.el-progress__text {
color: aqua !important;
white-space: pre-line;
line-height: 20px;
}
}
.el-picker-panel {
background: rgba(255, 255, 255, 1) !important;
color: #606266 !important;
filter: drop-shadow(2px 4px 6px black);
}
.opration-btns {
display: flex;
justify-content: start;
align-items: center;
// background-color: #606266;
.opration-btn {
width: 16px;
height: 16px;
display: flex;
justify-content: center;
align-items: center;
margin-left: 10px;
cursor: pointer;
img {
width: 12px;
height: 12px;
}
&:hover {
// background: #e6e6e6;
}
}
}
}
</style>

View File

@ -0,0 +1,611 @@
<template>
<div class="medium-home-container">
<div class="medium-home-main">
<div class="medium-home-main-top">
<div class="medium-breadcrumb">
<div v-for="item in paths" :key="item" class="breadcrumb-item" @click="changePath(item)">
{{ item || '全部文件' }}
</div>
</div>
</div>
<div class="medium-home-top-btn">
<div>
<el-button size="small" @click="handleDelete(false)" type="danger"> 删除 </el-button>
<el-button size="small" @click="handleDownload(false)" type="success"> 下载 </el-button>
</div>
<div class="medium-check">
<div class="check-item">已选/全部</div>
<div class="check-item">{{ selectNum }}/{{ totle }}</div>
</div>
</div>
<div class="medium-home-main-table">
<el-table
ref="multipleTable"
:data="fileList"
tooltip-effect="dark"
stripe
size="small"
height="600"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="fileName" label="文件名称">
<template #default="scope">
<div
class="file-name-wrapper"
style="display: flex; align-items: center; cursor: pointer"
@click="handleSelectFile(scope.row.path, scope.row)"
>
<img :src="scope.row.img" alt="" style="width: 16px; height: 16px; transform: translateY(-1px); margin-right: 2px" />
<div class="file-name">{{ scope.row.name ? scope.row.name : scope.row.fileName }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="size" label="大小" width="80" show-overflow-tooltip> </el-table-column>
<el-table-column prop="creatTime" label="创建时间">
<template #default="scope">{{ scope.row.creatTime }}</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="120">
<template #default="scope">
<div class="opration-btns">
<el-tooltip v-if="scope.row.size !== 'N/A'" class="item" content="下载" effect="dark">
<!-- v-if="scope.row.size !== 'N/A'" -->
<span class="opration-btn" style="display: inline-block; cursor: pointer" @click="handleDownload(scope.row)">
<img src="../../images/medium/down-load.png" srcset="" />
</span>
</el-tooltip>
<el-tooltip v-if="scope.row.size !== 'N/A'" class="item" content="预览" effect="dark">
<span class="opration-btn" style="display: inline-block; cursor: pointer" @click="handlePreview(scope.row)">
<img src="../../images/medium/prview-icon.png" srcset="" />
</span>
</el-tooltip>
<el-tooltip class="item" content="删除" effect="dark">
<span class="opration-btn" style="display: inline-block; cursor: pointer" @click="handleDelete(scope.row)">
<img src="../../images/medium/delete.png" srcset="" />
</span>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="shade" v-if="loading">
<el-progress type="circle" :percentage="percentage"></el-progress>
<!-- <div
style="white-space: pre-line;text-align: center;color: aqua;color: aqua; display: flex; flex-direction: column;">
<img src="../../images/loadingdow.gif" alt="">
<span style="display: inline-block;position: absolute;left: 50%;top:50%;transform: translate(-50%, -50%);">{{
this.text }}</span>
</div> -->
</div>
</div>
</template>
<script>
import axios from 'axios';
import JSZip from 'jszip';
import { getAiFiles, deleteAiFile, getAiUrlList } from '@/api/fileMangement';
import Preview from './preview.vue';
import { useAirStore } from '@/store/modules/drone';
import filesIcon from '../../images/medium/files-icon.png';
import videoIcon from '../../images/medium/video-icon.png';
export default {
components: { Preview },
data() {
return {
percentage: 0,
info: {},
paths: [''],
fileList: [
// {
// fileName: item,
// size: list[item].size,
// creatTime: list[item].creation_time,
// path:''
// img: ''
// },
],
totle: 0,
selectNum: 0,
length: 0,
sn: '',
currentPath: '',
searchName: '', // 搜索关键字
dateTime: [new Date(), new Date()], // 初始时间范围为今日至今日
typeOptions: [
{
value: '选项1',
label: '选项1'
},
{
value: '选项2',
label: '选项2'
}
],
typeValue: '',
loadOptions: [
{
value: '选项1',
label: '选项1'
},
{
value: '选项2',
label: '选项2'
}
],
loadValue: '',
selectList: [],
loading: false,
gateWay: useAirStore().gateWay,
text: ''
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getFileList(this.gateWay.gateway);
},
deep: true
}
},
methods: {
handleSelectionChange(datas) {
// console.log("datas: ", datas);
this.selectList = datas;
this.selectNum = datas.length;
},
isEmptyObject(obj) {
return Object.keys(obj).length === 0;
},
// 获取文件列表
async getFileList(gateway, path = '') {
this.fileList = [];
const list = await getAiFiles({ gateway, itemPath: path });
// console.log("文件列表1-------", list);
if (Object.keys(list).length === 0) {
return;
}
this.paths = [
'',
...Object.keys(list)[0]
.split('/')
.filter((path) => path !== '')
.slice(0, -1)
];
this.fileList = Object.keys(list).map((item) => {
const pathList = item.split('/').filter((path) => path !== '');
let img = filesIcon;
let type = 'file';
// console.log("item----------", item);
// console.log("list----------", list[item]);
// 文件缩略图
if (list[item].size !== 'N/A') {
type = item.split('.')[1];
if (item.split('.')[1] !== 'mp4') {
img = list[item].download_url;
} else {
img = videoIcon;
}
}
return {
name: list[item].missionName,
fileName: pathList[pathList.length - 1],
size: list[item].size,
creatTime: list[item].creation_time,
path: item,
img,
type,
url: list[item].download_url,
fileLength: list[item].length
};
});
this.totle = this.fileList.length;
this.length = this.fileList.fileLength;
// console.log("文件列表2-------", this.fileList);
},
// 文件夹点击进入文件夹
handleSelectFile(path, data) {
console.log('path11111111111', path);
if (data && data.size !== 'N/A') {
// console.log("文件------------");
} else {
this.currentPath = path;
const { gateway } = this.gateWay;
this.getFileList(gateway, path);
}
},
// 路径变化
changePath(path) {
if (path) {
// console.log("currentPath", this.currentPath);
this.currentPath = this.currentPath.split(path)[0] + path;
// console.log("path", path);
// 跳转到该路径
this.handleSelectFile(this.currentPath);
} else {
this.handleSelectFile('');
}
},
// 删除
handleDelete(row) {
// console.log("删除---------", row);
let params = { itemPaths: [row.path], gateway: this.gateWay.gateway };
if (!row) {
if (this.selectList.length === 0) {
this.$message.warning('请先选择要删除的文件');
this.loading = false;
return;
}
const pathList = this.selectList.map((item) => item.path);
params = { itemPaths: pathList, gateway: this.gateWay.gateway };
}
this.$confirm('此操作将删除该文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
customClass: 'customMessageBox',
confirmButtonClass: 'customConfirm',
cancelButtonClass: 'customCancel'
}).then(() => {
deleteAiFile(params).then((res) => {
this.$message.success(res.msg);
this.$sendChanel('mediumDel');
// 过滤掉删除的文件
this.fileList = this.fileList.filter((file) => {
return !params.itemPaths.includes(file.path);
});
if (this.fileList.length === 0) {
// 如果列表为空跳转到根路径
this.handleSelectFile('');
}
});
});
},
// 下载
handleDownload(row) {
this.loading = true;
// console.log("下载---------", row);
let params = { itemPath: row.path, gateway: this.gateWay.gateway };
if (!row) {
if (this.selectList.length === 0) {
this.$message.warning('请先选择要下载的文件');
this.loading = false;
return;
}
let pathList = [];
// 定义是否是文件夹
let isFile = false;
// console.log("this.selectList", this.selectList);
this.selectList.forEach((item) => {
if (item.type === 'file') {
isFile = true;
}
});
if (isFile) {
const { gateway } = this.gateWay;
// this.selectList.forEach((item) => {
// const params = { gateway, itemPath: item.path };
// getUrlList(params).then((res) => {
// pathList.push(...res);
// });
// });
// console.log("pathList1", pathList);
// // 并行下载文件并压缩为 zip
// this.downloadAndZipFiles(pathList);
const promises = this.selectList.map((item) => {
const params = { gateway, itemPath: item.path };
return getAiUrlList(params).then((res) => {
pathList.push(...res); // 将结果合并到 pathList
});
});
// 等待所有异步操作完成后,再执行下载操作
Promise.all(promises)
.then(() => {
// console.log("pathList1", pathList);
// 确保 pathList 有值后再调用下载方法
this.downloadAndZipFiles(pathList);
})
.catch((error) => {
console.error('获取文件路径列表时出错:', error);
});
} else {
pathList = this.selectList.map((item) => item.url);
// console.log("pathList2", pathList);
// 并行下载文件并压缩为 zip
this.downloadAndZipFiles(pathList);
}
} else {
this.downloadAndZipFiles([row.url], row);
}
},
async downloadAndZipFiles(fileUrls, file) {
try {
const zip = new JSZip();
const fileSizeMap = new Map();
let fileTotal = 0;
if (file) {
fileTotal = file.fileLength;
} else {
this.selectList.forEach((item) => {
fileTotal += item.fileLength;
});
}
// console.log("file-----", fileTotal);l
// 并行下载所有文件
const downloadPromises = fileUrls.map((url) =>
axios({
url: url,
method: 'get',
responseType: 'blob',
onDownloadProgress: (progressEvent) => {
const { loaded, total } = progressEvent;
fileSizeMap.set(url, loaded);
const totalFileSize = [...fileSizeMap.values()].reduce((acc, value) => acc + value, 0);
const progress = Math.round((totalFileSize / fileTotal) * 100); // 计算下载进度百分比
// console.log("progressEvent", progressEvent);
// console.log("overallProgress", progress);
// this.percentage = progress; // 更新进度百分比
if (progress == 100) {
this.text = `${99}%\n 正在打包中`;
} else {
this.text = `${progress}%\n 下载中`;
}
}
}).then((response) => {
console.log('Response:', response); // 在这里打印 response 对象
console.log('url:', url); // 在这里打印 response 对象
console.log('flie:', file); // 在这里打印 response 对象
const regex = /\/(DJI_[^\/]+\/DJI_[^\/]+\.[^\/?]+)(?=\?)/;
// 提取匹配的部分
const match = url.match(regex);
let extractedString = response.data.size + '.jpg';
if (match) {
extractedString = match[1]; // 捕获的字符串
}
// 打印文件名和文件数据
const fileName = file ? file.fileName : extractedString; // 获取文件名
const fileData = response.data; // 保存文件数据
// 获取文件大小:使用 Blob 的 size 属性
const fileSize = fileData.size; // 单位为字节bytes
// console.log("File size:", fileSize);
// 返回一个对象,继续后续操作
return { fileName, fileData };
})
);
// 等待所有文件下载完成
const downloadedFiles = await Promise.all(downloadPromises);
if (file) {
// console.log("Downloading", downloadedFiles);
const url = window.URL.createObjectURL(downloadedFiles[0].fileData);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', downloadedFiles[0].fileName); // 设置下载的文件名
document.body.appendChild(link);
link.click();
link.remove();
this.loading = false;
this.percentage = 0; // 下载完进度百分比清零
return;
}
// 将每个文件添加到 zip 中
downloadedFiles.forEach(({ fileName, fileData }) => {
// console.log("File Name:", fileName);
zip.file(fileName, fileData); // 添加文件到 zip
});
// 生成 zip 文件并触发下载
const zipBlob = await zip.generateAsync({ type: 'blob' });
const url = window.URL.createObjectURL(zipBlob);
const link = document.createElement('a');
link.href = url;
const temp = new Date().getTime();
link.setAttribute('download', `文件${temp}.zip`); // 设置下载的文件名
document.body.appendChild(link);
link.click();
link.remove();
this.loading = false;
this.percentage = 0; // 下载完进度百分比清零
} catch (error) {
// console.error("Error downloading or zipping files:", error);
this.loading = false;
this.percentage = 0; // 下载完进度百分比清零
}
},
// 预览
handlePreview(row) {
const list = this.fileList;
// console.log("预览---------", row);
// console.log("list---------", list);
const index = list.findIndex((item) => item === row);
const data = { list, detail: row, index };
this.$sendChanel('previewBus', data);
}
},
created() {},
mounted() {
this.getFileList(this.gateWay.gateway);
}
};
</script>
<style lang="scss">
.medium-home-container {
height: 100%;
width: 100%;
position: relative;
background-color: antiquewhite;
.medium-home-search-wrapper {
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
> .el-date-editor {
width: 38%;
}
> .el-select {
width: 18%;
}
> .el-input {
width: 18%;
}
> .el-button {
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
img {
width: 20px;
height: 20px;
}
}
}
.medium-home-main {
height: calc(100%);
background: #fff;
.medium-home-main-top {
display: flex;
justify-content: space-between;
line-height: 40px;
padding: 0 16px;
font-size: 12px;
width: 100%;
.medium-breadcrumb {
display: flex;
.breadcrumb-item {
// margin-right: 20px;
color: #909399;
cursor: pointer;
&::after {
content: '/';
display: inline-block;
margin: 0 3px;
}
}
.breadcrumb-item:last-child {
color: #000;
&::after {
content: '';
display: inline-block;
}
}
}
.file-name-wrapper {
display: flex;
.file-img-wrapper {
height: 20px !important;
width: 20px !important;
img {
width: 100%;
height: 100%;
}
}
}
.el-table {
.el-checkbox {
margin-left: 8px !important;
}
}
}
.medium-home-top-btn {
width: 100%;
height: 40px;
padding: 0 16px;
display: flex;
align-items: center;
font-size: 12px;
justify-content: space-between;
.medium-check {
display: flex;
align-items: center;
color: #000;
}
}
.medium-home-main-table {
overflow-y: auto;
height: calc(100% - 80px);
width: 100%;
}
.el-checkbox__input {
margin-left: 8px !important;
}
}
.shade {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 10;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
.el-progress__text {
color: aqua !important;
white-space: pre-line;
line-height: 20px;
}
}
.el-picker-panel {
background: rgba(255, 255, 255, 1) !important;
color: #606266 !important;
filter: drop-shadow(2px 4px 6px black);
}
.opration-btns {
display: flex;
justify-content: start;
align-items: center;
// background-color: #606266;
.opration-btn {
width: 16px;
height: 16px;
display: flex;
justify-content: center;
align-items: center;
margin-left: 10px;
cursor: pointer;
img {
width: 12px;
height: 12px;
}
&:hover {
// background: #e6e6e6;
}
}
}
}
</style>

View File

@ -0,0 +1,271 @@
<template>
<div v-if="show" class="preview-container">
<div class="preview-top">
<div class="preview-t-l">
<div class="preview-item-wrapper">
<img v-if="detail.type !== 'mp4'" :src="detail.url" alt="" class="preview-item" />
<video v-else :src="detail.url" controls crossorigin="anonymous" class="preview-item"></video>
<div class="arrows arrow-l" @click="check(current - 1)">
<img src="../../images/medium/arrow.png" />
</div>
<div class="arrows arrow-r" @click="check(current + 1)">
<img src="../../images/medium/arrow.png" />
</div>
</div>
</div>
<div class="preview-t-r">
<div class="preview-close" @click="show = false">
<img src="../../images/medium/close.png" />
</div>
<div class="preview-t-infos">
<div class="preview-t-info">
<span class="preview-t-info-text label">文件名</span>
<span class="preview-t-info-text">{{ detail.fileName }}</span>
</div>
<div class="preview-t-info">
<span class="preview-t-info-text label">文件大小</span>
<span class="preview-t-info-text">{{ detail.size }}</span>
</div>
<div class="preview-t-info">
<span class="preview-t-info-text label">创建时间</span>
<span class="preview-t-info-text">{{ detail.creatTime }}</span>
</div>
</div>
</div>
</div>
<div class="preview-bottom">
<div class="preview-list-wrapper scroll-container">
<div v-for="(item, index) in list" :key="item.url" class="preview-list-item" :class="{ active: current === index }" @click="check(index)">
<img :src="item.img" alt="" srcset="" />
</div>
</div>
</div>
</div>
</template>
<script>
import mediumVideo from '../../images/medium/medium-video.png';
import mediumImg from '../../images/medium/medium-img.png';
export default {
components: {},
data() {
return {
show: false,
list: [
{
fileName: '测试1',
size: '3M',
creatTime: '2024',
path: 'ua/',
img: mediumVideo,
type: 'mp4',
url: 'https://vdept3.bdstatic.com/mda-qh0g1z9fvz28wwe9/cae_h264/1722574521989748719/mda-qh0g1z9fvz28wwe9.mp4?v_from_s=hkapp-haokan-hbf&auth_key=1726726486-0-0-bdef95510c480bcaa1f3c311508a59de&bcevod_channel=searchbox_feed&pd=1&cr=0&cd=0&pt=3&logid=0886315216&vid=10817113278196649776&klogid=0886315216&abtest='
},
{
fileName: '测试2',
size: '3M',
creatTime: '2024',
path: 'ua/',
img: mediumImg,
url: '//lf-web-assets.juejin.cn/obj/juejin-web/xitu_juejin_web/e08da34488b114bd4c665ba2fa520a31.svg',
type: 'img'
}
],
detail: {
fileName: '测试1',
size: '3M',
creatTime: '2024',
path: 'ua/',
img: mediumVideo,
type: 'mp4',
url: 'https://vdept3.bdstatic.com/mda-qh0g1z9fvz28wwe9/cae_h264/1722574521989748719/mda-qh0g1z9fvz28wwe9.mp4?v_from_s=hkapp-haokan-hbf&auth_key=1726726486-0-0-bdef95510c480bcaa1f3c311508a59de&bcevod_channel=searchbox_feed&pd=1&cr=0&cd=0&pt=3&logid=0886315216&vid=10817113278196649776&klogid=0886315216&abtest='
},
current: 0
};
},
methods: {
check(idx) {
// const index = this.list.findIndex((item) => item.url === this.detail.url);
// console.log("index", index);
// console.log("list", this.list);
if (idx < 0) {
this.detail = this.list[this.list.length - 1];
this.current = this.list.length - 1;
} else if (idx > this.list.length - 1) {
this.detail = this.list[0];
this.current = 0;
} else {
this.detail = this.list[idx];
this.current = idx;
}
}
},
created() {
this.$recvChanel('previewBus', (data) => {
// console.log("收到的数据222", data);
this.list = data.list;
this.detail = data.detail;
this.current = data.index;
this.show = true;
});
},
mounted() {
// this.detail = this.list[0];
// const container = document.querySelector(".scroll-container");
// container.addEventListener("wheel", (event) => {
// event.preventDefault(); // 阻止默认纵向滚动
// // 横向滚动父容器中的内容
// container.scrollLeft += event.deltaY;
// });
}
};
</script>
<style lang="scss">
.preview-container {
height: 100vh;
width: 100vw;
position: fixed;
// top: -20%;
// left: -12%;
top: 0;
left: 0;
z-index: 111;
background-color: rgba($color: #000000, $alpha: 0.5);
.preview-top {
width: 100%;
height: 85%;
display: flex;
background-color: rgba(240, 248, 255, 0.281);
.preview-t-l {
width: 80%;
height: 100%;
background-color: #000000;
.preview-item-wrapper {
width: 100%;
height: 100%;
position: relative;
.arrows {
position: absolute;
top: 50%;
width: 50px;
height: 50px;
background-color: rgba(0, 0, 0, 0.4);
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
img {
width: 30px;
height: 30px;
}
}
.arrow-l {
left: 40px;
}
.arrow-r {
right: 40px;
transform: rotate(180deg);
}
.preview-item {
width: 100%;
height: 100%;
}
}
}
.preview-t-r {
width: 20%;
height: 100%;
background-color: #1c1c1c;
position: relative;
.preview-close {
position: absolute;
top: 10px;
right: 10px;
> img {
height: 20px;
width: 20px;
cursor: pointer;
}
}
.preview-t-infos {
height: 100%;
width: 100%;
padding: 15% 0 0 5%;
font-size: 12px;
.preview-t-info {
width: 100%;
display: flex;
// &:nth-child(1) {
// width: 25%;
// text-align: justify;
// background-color: aqua;
// }
.preview-t-info-text {
margin-right: 15px;
margin-bottom: 15px;
}
.label {
width: 18%;
text-align: justify;
}
}
}
}
}
.preview-bottom {
width: 100%;
height: 15%;
background-color: #000000;
display: flex;
align-items: center;
justify-content: center;
margin: auto;
.preview-list-wrapper {
height: 80px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
overflow-x: auto;
.preview-list-item {
display: inline-block;
height: 60px;
width: 100px;
border: 2px solid rgb(255, 255, 255);
margin: 0 5px;
cursor: pointer;
img {
height: 100%;
width: 100%;
}
}
.active {
border: 2px solid rgb(0, 140, 255);
}
}
}
}
</style>

View File

@ -0,0 +1,130 @@
<template>
<div class="flyHistroy">
<span class="flyHistroy_name" @click="back">返回</span>
<span class="flyHistroy_name">{{ data.missionName }}</span>
<div class="flyHistroy_bottom">
<div class="bottom_item">
<div class="title">飞行器</div>
<div class="content">M3TD-庆福广场</div>
</div>
<div class="bottom_item">
<div class="title">任务开始时间</div>
<div class="content">{{ data.createdAt }}</div>
</div>
<div class="bottom_item">
<div class="title">任务结束时间</div>
<div class="content">
{{ parseTime(data.lastTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</div>
</div>
<div class="bottom_item">
<div class="title">轨迹最后更新时间</div>
<div class="content">
{{ parseTime(data.lastTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</div>
</div>
<div class="bottom_item">
<div class="title">实际飞行距离</div>
<div class="content">{{ length }}m</div>
</div>
<div class="bottom_item">
<div class="title">实际飞行时长</div>
<div class="content">{{ time }}</div>
</div>
</div>
</div>
</template>
<script>
import { parseTime } from '@/utils/index.ts';
export default {
name: 'flyHistroy',
props: {
data: {
type: Object,
default: () => {}
}
},
data() {
return {
points: [],
length: '',
time: ''
};
},
// 监听data
// watch: {
// data: function handle(newVal, oldVal) {
// let pointss = JSON.parse(newVal);
// this.renderRouter(pointss.points);
// }
// },
mounted() {
let pointss = JSON.parse(this.data.points);
this.renderRouter(pointss.points);
},
methods: {
parseTime(val) {
return parseTime(val);
},
renderRouter(positions) {
let airLine = new YJ.Obj.newAirLine(
{
positions,
frustumShow: false,
keyboard: false
},
window.Earth1.viewer
);
window.airLine = airLine;
this.length = airLine.countLength();
this.time = airLine.countTime();
window.airLine.flyTo();
// console.log("airLine", airLine);
},
// 返回计划库
back() {
this.$emit('back');
}
}
};
</script>
<style lang="scss">
.flyHistroy {
position: absolute;
top: 8%;
left: 20px;
z-index: 20;
color: #fff;
&_name {
border-radius: 8px;
padding: 8px 16px;
background: linear-gradient(180deg, rgba(0, 255, 255, 0.2) 0%, rgba(0, 255, 255, 0) 100%), rgba(0, 0, 0, 0.6);
backdrop-filter: blur(2px);
}
&_bottom {
position: fixed;
bottom: 10%;
left: 30px;
height: 80px;
width: 60%;
display: flex;
justify-content: space-between;
.title {
font-size: 16px;
margin-bottom: 10px;
font-weight: 400;
}
.content {
font-size: 18px;
color: rgba(0, 255, 255, 1);
font-weight: 600;
}
}
}
</style>

View File

@ -0,0 +1,837 @@
<template>
<div class="planLank flex">
<div class="planLank_left">
<div class="header flex space_between ai_center">
<span class="text">计划库</span>
<div class="add" @click="add">
<img src="../../images/add_plan.png" alt="" />
<span class="f16">新建</span>
</div>
</div>
<div class="content">
<div class="title">
<span>{{ gateway.deviceType }}</span>
<span>{{ gateway.businessName }}</span>
</div>
<div class="card">
<div class="card_top flex">
<div class="card_top_left flex ai_center">
<div>
<img src="../../images/time.png" alt="" />
<span>待执行</span>
</div>
<div>
{{ listObj.timer ? listObj.timer : '' }}
</div>
</div>
<div class="card_top_right flex">
<span>{{ listObj.missionName ? listObj.missionName : '暂无' }}</span>
</div>
</div>
<div class="card_bottom flex">
<!-- <div class="card_bottom_top">
<span>{{ gateway.deviceType }}</span>
<span>{{ gateway.businessName }}</span>
</div> -->
<div class="card_bottom_bottom">
<div class="flex ai_center mr10 mb10">
<img src="../../images/eq.png" alt="" />
<span class="status">设备{{ filterModeCode(mode.flighttask_step_code) }}</span>
</div>
<div class="flex ai_center mb10">
<img src="../../images/air.png" alt="" />
<span
>{{ filterDroneInDock(networkState.drone_in_dock) }}
{{ filterDeviceOnlineStatus(networkState.sub_device.device_online_status) }}</span
>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="planLank_right">
<div class="header_form">
<el-form :inline="true" size="small" class="demo-form-inline">
<el-form-item>
<el-date-picker
v-model="time"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetimerange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="changeTime"
>
</el-date-picker>
</el-form-item>
<el-form-item>
<el-select v-model="tableForm.type" @change="getQuestRecordList" placeholder="所有类型">
<el-option v-for="item in options" :label="item.label" :value="item.value"></el-option>
</el-select>
<template #label></template>
</el-form-item>
<el-form-item>
<el-select v-model="tableForm.state" @change="getQuestRecordList" placeholder="全部状态">
<el-option v-for="item in states" :label="item" :value="item"></el-option>
</el-select>
<template #label></template>
</el-form-item>
<el-form-item>
<el-input v-model.trim="tableForm.name" @change="getQuestRecordList" clearable placeholder="按航线或者计划名称搜索">
<i style="cursor: pointer" slot="suffix" class="el-input__icon el-icon-search" @click="search"></i>
</el-input>
</el-form-item>
<el-form-item>
<el-button @click="refresh" icon="el-icon-refresh-right"></el-button>
</el-form-item>
</el-form>
</div>
<div class="table">
<el-table :data="tableData.data" style="width: 100%">
<el-table-column prop="createdAt" label="创建时间" width="180" align="center"> </el-table-column>
<el-table-column prop="state" label="执行状态" width="180" align="center"> </el-table-column>
<el-table-column prop="type" label="类型" align="center">
<template slot-scope="scope">
<span>{{ scope.row.type == 0 ? '立即' : '定时' }}</span>
</template>
</el-table-column>
<el-table-column prop="missionName" label="计划名称" align="center"> </el-table-column>
<el-table-column prop="fileName" label="航线名称" align="center"> </el-table-column>
<!-- <el-table-column prop="realPhotoNum" label="实际上传" align="center">
</el-table-column> -->
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<div>
<el-tooltip v-if="['执行中', '暂停'].includes(scope.row.state)" class="item" content="航线暂停" effect="dark">
<span style="display: inline-block; cursor: pointer" @click="handlerClick(1, scope.row)">
<img src="../../images/zanting.png" alt="" />
</span>
</el-tooltip>
<el-tooltip v-if="['执行中', '暂停'].includes(scope.row.state)" class="item" content="航线恢复" effect="dark">
<span style="display: inline-block; cursor: pointer" @click="handlerClick(2, scope.row)">
<img src="../../images/huifu.png" alt="" />
</span>
</el-tooltip>
<el-tooltip v-if="['执行中', '暂停'].includes(scope.row.state)" class="item" content="一键返航" effect="dark">
<span style="display: inline-block; cursor: pointer" @click="handlerClick(3, scope.row)">
<img src="../../images/fanhang.png" alt="" />
</span>
</el-tooltip>
<el-tooltip v-if="['执行成功'].includes(scope.row.state)" class="item" content="飞行记录" effect="dark">
<span style="display: inline-block; cursor: pointer" @click="handlerClick(4, scope.row)">
<img src="../../images/flyh.png" alt="" />
</span>
</el-tooltip>
<el-tooltip class="item" content="删除" effect="dark">
<span style="display: inline-block; cursor: pointer" @click="handlerClick(5, scope.row)">
<img src="../../images/shanchu.png" alt="" />
</span>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
<div class="progress">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="tableForm.pageNum"
:page-sizes="[10, 20, 30, 40]"
:page-size="tableForm.pageSize"
layout="prev, pager, next,sizes"
:total="tableData.total"
>
</el-pagination>
</div>
</div>
<myDialog ref="createPL" class="createPL" :title="title">
<div class="form">
<el-form :model="form" :rules="rules" ref="ruleForm" label-width="110px" class="demo-ruleForm">
<el-form-item label="计划名称" prop="missionName">
<el-input v-model="form.missionName" placeholder="请输入计划名称" clearable></el-input>
</el-form-item>
<el-form-item label="执行航线" prop="fileId">
<el-select style="width: 100%" v-model="form.fileId" filterable placeholder="请选择活动区域">
<el-option v-for="item in airList" :label="item.fileName" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="任务精度" prop="waylinePrecisionType">
<el-radio-group style="width: 100%" v-model="form.waylinePrecisionType">
<el-radio-button v-for="dict in waylinePrecisionTypeOptions" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="任务策略" prop="type">
<el-radio-group style="width: 100%" v-model="form.type">
<el-radio-button :label="0">立即</el-radio-button>
<el-radio-button :label="1">单次定时</el-radio-button>
<!-- <el-radio-button :label="2">重复定时</el-radio-button> -->
</el-radio-group>
</el-form-item>
<el-form-item label="返航高度模式" prop="returnAltitudeMode">
<el-radio-group style="width: 50%" v-model="form.returnAltitudeMode">
<el-radio-button v-for="dict in rthModeOptions" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.type == 1" label="执行时间" prop="timer">
<el-date-picker value-format="yyyy-MM-dd HH:mm:ss" v-model="form.timer" type="datetime" placeholder="选择日期时间"> </el-date-picker>
</el-form-item>
<el-form-item v-if="form.type == 2" label="执行时间" prop="execTime">
<el-date-picker v-model="form.execTime" type="datetime" placeholder="选择日期时间"> </el-date-picker>
</el-form-item>
<el-form-item label="航线飞行中失联" prop="outOfControlAction">
<el-radio-group style="width: 100%" v-model="form.outOfControlAction">
<el-radio-button v-for="dict in exitWaylineOptions" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio-button>
</el-radio-group>
</el-form-item>
<div>
<div>返航高度(相对机场返航高度)</div>
<div style="display: flex; justify-content: center">
<plusReduce :defValue="form.returnAltitude" @value="height" style="width: 80%" :max="1500" :min="20" unit="m"> </plusReduce>
</div>
</div>
<el-form-item label="失联动作" prop="flightControlAction">
<!-- 失控动作类型 -->
<el-radio-group style="width: 100%" v-model="form.flightControlAction">
<el-radio-button v-for="dict in outOfControlOptions" :label="dict.value">{{ dict.label }}</el-radio-button>
</el-radio-group>
</el-form-item>
</el-form>
</div>
<div class="btns">
<el-button size="small" @click="submit">确定</el-button>
<el-button size="small" @click="cancel">取消</el-button>
</div>
</myDialog>
</div>
</template>
<script>
import myDialog from '../component/dialog/index.vue';
import plusReduce from '../component/plusReduce/index.vue';
import {
listPaths,
taskList,
taskAdd,
getLatest,
flightTaskPauseNew,
flightTaskRecoveryNew,
flightTaskUndoNew,
delMissionsNew,
returnHomeNew
} from '@/api/air';
import { getLocal, parseTime } from '@/utils/';
import { exitWaylineOptions, waylinePrecisionTypeOptions, outOfControlOptions, rthModeOptions } from '../../utils/options.js';
import { useAirStore } from '@/store/modules/drone';
export default {
name: 'planLank',
components: {
myDialog,
plusReduce
},
data() {
return {
parseTime: parseTime,
rthModeOptions: rthModeOptions,
outOfControlOptions: outOfControlOptions,
exitWaylineOptions: exitWaylineOptions,
waylinePrecisionTypeOptions: waylinePrecisionTypeOptions,
rules: {
missionName: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
fileId: [{ required: true, message: '请选择任务航线', trigger: 'blur' }],
timer: [{ required: true, message: '请选择时间', trigger: 'blur' }]
},
formInline: {
region: '',
value: '',
user: ''
},
options: [
{
label: '全部',
value: ''
},
{
label: '立即',
value: 0
},
{
label: '定时',
value: 1
}
],
time: [],
states: ['全部', '取消或终止', '失败', '执行中', '定时待执行', '执行成功', '部分完成', '暂停', '拒绝', '已下发', '超时'],
form: {
missionName: '',
// executionTime: "",
timer: null,
type: 0,
fileId: '',
returnAltitude: 100,
returnAltitudeMode: 0,
outOfControlAction: 1,
flightControlAction: 0,
waylinePrecisionType: 1,
gateway: '',
timeStamp: ''
},
value1: '',
value2: '',
title: '新建计划',
queryParam: {
pageNum: 1,
pageSize: 10000000,
fileName: '',
order: 1,
gateway: ''
},
tableForm: {
pageNum: 1,
pageSize: 20,
gateway: ''
},
airList: [],
networkState: {
mode_code: 0,
drone_in_dock: 1,
sub_device: {
device_online_status: 0
}
},
mode: {},
gateway: useAirStore().gateWay,
tableData: {
data: [],
total: 0
},
time: [],
listObj: {}
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateway = newValue;
console.log('newValue', newValue);
this.getQuestRecordList();
this.getNewestTimer();
},
deep: true
}
},
mounted() {
this.allApi();
this.$recvChanel('websocketBus', (data) => {
if (data.businessType == 'osd3') {
this.networkState = { ...data.data };
// console.log(this.networkState);
}
if (data.businessType == 'osd2') {
this.mode = { ...data.data };
}
});
this.form.gateway = this.gateway.gateway;
this.queryParam.flag = this.gateway.gateway;
},
methods: {
allApi() {
this.getAirRouteList();
this.getQuestRecordList();
this.getNewestTimer();
},
search() {
if (this.time.length > 0) {
this.tableForm.startTime = this.time[0];
this.tableForm.endTime = this.time[1];
}
this.getQuestRecordList();
},
changeTime(value) {
this.tableForm.startTime = value[0];
this.tableForm.endTime = value[1];
this.getQuestRecordList();
},
handlerClick(num, item) {
console.log(num, item);
if (num == 1) {
this.pauseAirLine(item);
}
if (num == 2) {
this.recoverAirLine(item);
}
if (num == 3) {
this.onBackHome(item);
}
if (num == 4) {
this.$emit('openHistroy', item);
}
if (num == 5) {
this.onDelQuest(item);
}
},
// delMissions
// 航线暂停
pauseAirLine(row) {
let obj = { gateway: this.gateway.gateway, taskId: row.id };
flightTaskPauseNew(obj).then((res) => {
this.$message.success('暂停成功');
this.getQuestRecordList();
});
},
// 航线恢复
recoverAirLine(row) {
let obj = { gateway: this.gateway.gateway, taskId: row.id };
flightTaskRecoveryNew(obj).then((res) => {
this.$message.success('恢复成功');
this.getQuestRecordList();
});
},
// 一键航航
onBackHome(row) {
let obj = { gateway: this.gateway.gateway, taskId: row.id };
returnHomeNew(obj).then((res) => {
this.$message.success('返航成功');
this.getQuestRecordList();
});
},
// 删除 delMissions
onDelQuest(row) {
this.$confirm('确认删除该任务信息?', '温馨提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delMissionsNew(row.id).then((res) => {
if (res.code == 200) {
this.$message.success('删除成功');
this.getQuestRecordList();
this.getNewestTimer();
}
});
});
},
refresh() {
this.tableForm = {
pageNum: 1,
pageSize: 10,
gateway: this.gateway.gateway
};
this.getQuestRecordList();
},
// 获取任务历史记录数据
getQuestRecordList(flag = false) {
this.loading = true;
this.tableForm.gateway = this.gateway.gateway;
taskList(this.tableForm).then((res) => {
this.loading = false;
if (res.code == 200) {
console.log(res.rows);
let list = res.rows || [];
this.tableData.data = list;
this.tableData.total = res.total;
if (flag) {
this.$sendChanel('TaskIssuedSuccessfully', list[0].flightId);
}
}
});
},
// 最新的定时任务
getNewestTimer() {
let obj = { gateway: this.gateway.gateway };
getLatest(obj).then((res) => {
if (res.code == 200) {
this.listObj = res.data || {};
}
});
},
getAirRouteList() {
listPaths(this.queryParam).then((res) => {
let list = res.rows || [];
this.airList = list;
});
},
filterDeviceOnlineStatus(val) {
let obj = { '0': '关机', '1': '开机' };
return obj[val];
},
filterDroneInDock(val) {
let obj = { '0': '舱外', '1': '舱内' };
return obj[val];
},
filterModeCode(val) {
let obj = {
'0': '作业准备中',
'1': '飞行作业中',
'2': '作业后状态恢复',
'3': '自定义飞行区更新中',
'4': '地形障碍物更新中',
'5': '任务空闲',
'255': '飞行器异常',
'256': '未知状态'
};
return obj[val];
},
height(value) {
this.form.height = value;
},
add() {
this.$refs.createPL.open();
},
submit() {
// console.log("this.form", this.form);
this.form.timeStamp = Date.now();
const { mode_code } = this.networkState;
if (mode_code == 2) {
this.$message.warning('请退出调试模式');
return;
}
// 判断returnAltitude是否有值
if (this.form.returnAltitude == '') {
return this.$message.warning('请设置返航高度');
}
// 判断this.mode.flighttask_step_code是否为2
if (this.mode.flighttask_step_code !== 5) {
let text = this.filterModeCode(this.mode.flighttask_step_code);
this.$message.warning(text);
return;
}
this.$refs['ruleForm'].validate((valid) => {
if (valid) {
taskAdd(this.form).then((res) => {
// console.log(res);
if (res.code == 200) {
this.$message.success('新建成功');
this.getQuestRecordList(true);
this.getNewestTimer();
this.cancel();
this.reset();
}
});
} else {
return false;
}
});
},
cancel() {
this.$refs.createPL.close();
this.reset();
},
handleSizeChange(val) {
this.tableForm.pageSize = val;
this.getQuestRecordList();
},
handleCurrentChange(val) {
this.tableForm.pageNum = val;
this.getQuestRecordList();
},
reset() {
this.form = {
missionName: '',
timer: null,
type: 0,
fileId: '',
returnAltitude: 100,
returnAltitudeMode: 0,
outOfControlAction: 1,
flightControlAction: 0,
waylinePrecisionType: 0,
gateway: this.gateway.gateway,
timeStamp: ''
};
}
}
};
</script>
<style lang="scss">
.planLank {
position: absolute;
left: 50%;
top: 50%;
z-index: 20;
transform: translate(-50%, -50%);
width: 80%;
height: 75%;
color: #fff;
&_left {
width: 320px;
background-color: rgba(0, 0, 0, 0.6);
.header {
box-sizing: border-box;
height: 60px;
padding: 0 20px;
border-bottom: 1px solid rgba(204, 204, 204, 0.2);
.text {
font-size: 18px;
font-family: 'alimamashuheiti';
}
.add {
cursor: pointer;
img {
width: 13px;
height: 12px;
}
}
}
.content {
padding: 0 20px;
.title {
margin: 20px 0;
font-weight: 400;
}
.card {
border: 1px solid rgb(0, 255, 255, 0.5);
border-radius: 4px;
&_top {
border-bottom: 1px solid rgba(204, 204, 204, 0.2);
margin-bottom: 15px;
&_left {
flex-direction: column;
justify-content: space-evenly;
align-items: center;
font-size: 12px;
height: 44px;
background-color: rgba(0, 255, 255, 0.5);
padding: 0 5px;
img {
width: 12px;
height: 12px;
}
}
&_right {
padding: 0 20px;
align-items: center;
font-family: 'alimamashuheiti';
}
}
&_bottom {
flex-direction: column;
padding: 0 15px;
&_top {
margin-bottom: 15px;
}
&_bottom {
color: #1bf8c3;
}
img {
width: 16px;
height: 16px;
margin-right: 5px;
}
}
}
}
}
&_right {
flex: 1;
padding: 10px;
background-color: #f7f9fa;
overflow: hidden;
.table {
height: 650px;
overflow-y: auto;
background-color: #fff;
padding: 10px;
width: 100%;
.el-form-item--mini.el-form-item,
.el-form-item--small.el-form-item {
margin-bottom: 10px;
}
.el-table__header-wrapper table,
.el-table__body-wrapper table {
width: 100% !important;
}
.el-table__body,
.el-table__footer,
.el-table__header {
table-layout: auto;
}
.el-table__empty-block,
.el-table__body {
width: 100% !important;
}
.el-pagination .btn-next,
.el-pagination .btn-prev {
background: center center no-repeat #fff !important;
background-size: 16px !important;
cursor: pointer;
margin: 0;
color: #303133;
}
.el-pagination .el-pagination__total,
.el-pagination .el-pagination__jump,
.el-pagination .number {
color: #000 !important;
}
.el-pagination .el-pager li.active {
color: #409eff !important;
}
img {
width: 14px;
height: 14px;
vertical-align: middle;
}
}
.progress {
padding-top: 5px;
text-align: right;
background-color: #fff;
.el-pagination .el-pagination__total,
.el-pagination .el-pagination__jump,
.el-pagination .number {
color: #606266 !important;
}
.el-pagination .el-pager li.active {
color: #267aff !important;
}
}
}
.createPL {
.el-input__inner {
background-color: rgba(0, 0, 0, 0.5);
border-color: rgba(0, 255, 255, 0.5);
width: 100%;
color: #fff;
}
.el-form-item__label {
color: #fff;
}
.el-row {
line-height: 40px;
margin: 10px 0;
}
.el-radio-group {
display: flex;
.el-radio-button {
flex: 1;
.el-radio-button__inner {
width: 100%;
background: rgba(0, 0, 0, 0.5);
color: #fff;
border-color: rgba(0, 255, 255, 0.5);
-webkit-box-shadow: -1px 0 0 0 rgba(0, 255, 255, 0.5);
box-shadow: -1px 0 0 0 rgba(0, 255, 255, 0.5);
}
.el-radio-button__orig-radio:checked + .el-radio-button__inner {
color: rgba(0, 255, 255, 1);
border-color: rgba(0, 255, 255, 1);
}
}
}
}
.btns {
text-align: center;
.el-button {
background: rgba(0, 255, 255, 0.2);
color: #fff;
border-color: rgba(0, 255, 255, 0.5);
}
.el-button:focus,
.el-button:hover {
border-color: rgba(0, 255, 255, 1);
}
}
}
.el-picker-panel {
background: #fff !important;
color: #606266 !important;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
filter: none;
.el-picker-panel__footer .el-button--default {
color: #606266 !important;
}
.el-button {
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
background: #fff !important;
color: #606266 !important;
font-size: 14px;
border-radius: 4px;
}
.el-button.is-plain:focus,
.el-button.is-plain:hover {
background: #fff !important;
border-color: #409eff !important;
color: #409eff !important;
}
.el-button.is-disabled,
.el-button.is-disabled:focus,
.el-button.is-disabled:hover {
color: #c0c4cc !important;
cursor: not-allowed;
background-image: none;
background-color: #fff !important;
border-color: #ebeef5 !important;
}
}
.el-message-box {
background: linear-gradient(180deg, rgba(0, 255, 255, 0.2) 0%, rgba(0, 255, 255, 0) 100%), rgba(0, 0, 0, 0.3);
border: none;
.el-message-box__content,
.el-message-box__title {
color: #fff;
}
.el-button {
border-radius: 4px;
background: rgba(0, 255, 255, 0.2);
color: #fff;
border: 1px solid rgba(0, 255, 255, 0.5);
}
.el-button:hover {
border-color: rgba(0, 255, 255, 1);
}
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<div class="uavselect">
<el-select v-model="value" @change="onChange" placeholder="请选择" size="mini">
<el-option v-for="item in options" :key="item.gateway" :label="item.businessName || item.gateway" :value="item.gateway"> </el-option>
</el-select>
</div>
</template>
<script>
import { listInfo } from '@/api/air';
import { setLocal, getLocal } from '@/utils';
import { gatewaysRemove, gatewaysAdd } from '@/api/air';
import { useAirStore } from '@/store/modules/drone';
export default {
data() {
return {
options: [],
value: '无人机',
parma: {
pageNum: 1,
pageSize: 100,
type: '机场'
}
};
},
watch: {
value: {
handler(newV, oldV) {
if (newV) {
this.$emit('showAirListAndAttr');
}
}
}
},
mounted() {
this.getDrone();
this.value = JSON.parse(getLocal('airGateway')).gateway;
},
methods: {
// 保存sn gateway
onChange(value) {
// console.log("valuevalue", value);
let obj = this.options.filter((item) => item.gateway == value)[0];
setLocal('airGateway', JSON.stringify(obj));
useAirStore().SET_GATEWAY(obj);
this.$emit('showAirListAndAttr');
},
// 获取机场列表
getDrone() {
listInfo(this.parma).then((res) => {
if (res.code == 200) {
let list = res.rows || [];
this.options = list;
}
});
}
}
};
</script>
<style lang="scss">
.uavselect {
position: fixed;
top: 5%;
left: 1%;
z-index: 1999;
.el-input__inner {
background-color: transparent;
border-color: rgba(0, 255, 255, 0.5);
color: #fff;
}
}
</style>

View File

@ -0,0 +1,26 @@
import { request } from "@/utils/requset2";
//进入指令飞行控制模式
export function drcModeEnter(data) {
return request({
url: "/dj/cmdFly/drcModeEnter",
method: "post",
data,
});
}
//有参数的指令飞行
export function hasDataFlight(data) {
return request({
url: "/dj/cmdFly/hasDataFlight",
method: "post",
data,
});
}
//无参数的指令飞行
export function noDataFlight(data) {
return request({
url: "/dj/cmdFly/noDataFlight",
method: "post",
data,
});
}

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="15.52001953125" viewBox="0 0 16 15.52001953125" fill="none">
<path d="M15.9466 14.3674L11.9464 9.45645C11.8964 9.40634 11.8464 9.35623 11.7964 9.35623C11.7464 9.35623 11.6964 9.30611 11.6964 9.30611L10.4964 9.40634C10.5464 9.10566 10.5464 8.75488 10.4464 8.4041C10.2464 7.85286 9.89637 7.45197 9.39637 7.20141L10.1464 0.386176C10.1964 -0.014721 9.64637 -0.165056 9.49637 0.23584L7.19632 6.14906C7.14632 6.19917 7.14632 6.24928 7.14632 6.3495C7.14632 6.39962 7.14632 6.44973 7.19632 6.44973L7.69633 7.20141C6.84632 7.6023 6.34631 8.50432 6.54631 9.40634L0.196191 12.2126C-0.153816 12.3629 -0.00381303 12.9142 0.396195 12.8641L6.69631 11.8618C6.74632 11.8618 6.79632 11.8117 6.84632 11.8117C6.89632 11.8117 6.89632 11.7616 6.94632 11.7115L7.44633 10.7092C7.94634 11.0099 8.54635 11.1101 9.14636 10.9097C9.34636 10.8596 9.49637 10.7594 9.64637 10.6591L15.3965 14.8685C15.7465 15.1191 16.1466 14.7182 15.9466 14.3674ZM9.44637 9.45645C9.29636 9.70701 9.04636 9.95757 8.74635 10.0077L8.54635 10.0077C8.29634 10.0077 8.09634 9.90746 7.89634 9.80723C7.64633 9.60678 7.54633 9.35623 7.54633 9.00544C7.54633 8.55443 7.89634 8.15354 8.29634 8.05331C8.39635 8.0032 8.44635 8.0032 8.54635 8.0032C8.84635 8.0032 9.09636 8.15354 9.29636 8.35398C9.44637 8.50432 9.54637 8.75488 9.54637 9.00544C9.54637 9.20589 9.49637 9.35623 9.44637 9.45645ZM13.4964 8.50432C13.4964 9.10566 13.3964 9.70701 13.1964 10.2582L14.2465 11.5612C14.6965 10.609 14.9465 9.60678 14.9465 8.50432C14.9465 5.4976 13.0964 2.992 10.4464 1.93965L10.3464 3.49312C12.1964 4.39514 13.4964 6.29939 13.4964 8.50432ZM2.89624 10.5088C2.64624 9.85734 2.49623 9.20589 2.49623 8.50432C2.49623 7.6023 2.74624 6.70029 3.09625 5.94861L1.69622 5.34726C1.24621 6.29939 0.996206 7.40186 0.996206 8.50432C0.996206 9.40634 1.19621 10.3084 1.49622 11.1101L2.89624 10.5088ZM7.89634 2.992L8.49635 1.48864L8.04634 1.48864C6.84632 1.48864 5.7463 1.78931 4.79628 2.29043L4.84628 3.99424C5.6963 3.3929 6.74632 3.04211 7.89634 2.992ZM7.99634 14.0166C6.64631 14.0166 5.44629 13.5155 4.49627 12.7137L2.64624 13.0144C3.94626 14.5178 5.8463 15.52 7.99634 15.52C9.84637 15.52 11.5464 14.7683 12.7964 13.5656L11.5464 12.6636C10.5964 13.5155 9.34636 14.0166 7.99634 14.0166ZM4.19627 1.93965L0.996206 4.49536L4.49627 5.99872L4.19627 1.93965Z" fill="#FFFFFF" >
</path>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M12.16 11.52L12.568 11.112L12.568 15.432C12.568 15.7481 12.8279 16 13.144 16C13.4601 16 13.712 15.7481 13.712 15.432L13.712 11.112L14.128 11.52C14.3512 11.7432 14.7128 11.7432 14.936 11.52C15.0485 11.4075 15.104 11.2664 15.104 11.12C15.104 10.9736 15.0485 10.8227 14.936 10.712L13.544 9.328L13.528 9.304L13.52 9.296L13.504 9.288L13.488 9.28L13.48 9.272L13.464 9.264L13.456 9.256L13.44 9.248L13.432 9.24L13.416 9.232L13.408 9.224L13.392 9.216L13.384 9.216L13.368 9.208L13.36 9.2L13.344 9.2L13.328 9.192L13.32 9.192L13.304 9.184L13.296 9.184L13.28 9.176L13.264 9.176L13.248 9.168L13.24 9.168L13.224 9.168L13.216 9.168L13.192 9.16L13.184 9.16L13.168 9.16L13.112 9.16L13.096 9.16L13.08 9.16L13.064 9.168L13.048 9.168L13.032 9.168L13.024 9.168L13.008 9.176L13 9.176L12.984 9.184L12.968 9.184L12.952 9.192L12.944 9.192L12.928 9.2L12.92 9.2L12.904 9.208L12.888 9.216L12.88 9.216L12.864 9.224L12.856 9.232L12.84 9.24L12.832 9.248L12.816 9.256L12.808 9.264L12.792 9.272L12.784 9.28L12.768 9.288L12.76 9.296L12.752 9.304L12.728 9.328L11.344 10.712C11.1208 10.9352 11.1208 11.2968 11.344 11.52C11.5744 11.7414 11.9368 11.7432 12.16 11.52ZM10.304 3.432C10.304 3.11593 10.0521 2.856 9.736 2.856L2.888 2.856C2.57193 2.856 2.32 3.11593 2.32 3.432C2.32 3.74807 2.57193 4 2.888 4L9.736 4C10.0503 4 10.304 3.74807 10.304 3.432ZM10.304 6.864C10.304 6.54793 10.0521 6.288 9.736 6.288L2.888 6.288C2.57193 6.288 2.32 6.54793 2.32 6.864C2.32 7.18007 2.57193 7.432 2.888 7.432L9.736 7.432C10.0503 7.432 10.304 7.18007 10.304 6.864ZM2.888 9.72C2.57193 9.72 2.32 9.97193 2.32 10.288C2.32 10.6041 2.57193 10.864 2.888 10.864L6.312 10.864C6.62807 10.864 6.88 10.6041 6.88 10.288C6.88 9.97193 6.62807 9.72 6.312 9.72L2.888 9.72ZM8.584 13.16L2.32 13.16C2.00571 13.16 1.64 12.9081 1.64 12.592L1.64 2.176C1.64 1.85993 1.89371 1.6 2.208 1.6L10.4 1.6C10.7143 1.6 10.968 1.85993 10.968 2.176L10.968 7.432C10.968 7.74807 11.6857 8 12 8C12.3143 8 12.6 7.74807 12.6 7.432L12.6 1.144C12.6 0.511857 12.0864 0 11.456 0L1.144 0C0.513643 0 0 0.513643 0 1.144L0 13.72C0 14.3521 0.513643 14.864 1.144 14.864L8.584 14.864C8.89829 14.864 9.136 14.3321 9.136 14.016C9.136 13.7017 8.90007 13.16 8.584 13.16ZM10.856 13.44C10.5399 13.44 10.288 13.6999 10.288 14.016L10.288 15.152C10.288 15.4681 10.5399 15.728 10.856 15.728C11.1721 15.728 11.432 15.4681 11.432 15.152L11.432 14.016C11.432 13.6999 11.1721 13.44 10.856 13.44ZM9.712 10.856C10.0281 10.856 10.28 10.5961 10.28 10.28L10.28 9.712C10.28 9.39593 10.0281 9.144 9.712 9.144C9.39593 9.144 9.136 9.39593 9.136 9.712L9.136 10.28C9.136 10.5961 9.39771 10.856 9.712 10.856ZM14.856 13.16L14.856 14.304C14.856 14.6201 15.1159 14.872 15.432 14.872C15.7481 14.872 16 14.6201 16 14.304L16 13.16C16 12.8439 15.7481 12.584 15.432 12.584C15.1159 12.584 14.856 12.8439 14.856 13.16Z" fill="#FFFFFF" >
</path>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="15.919921875" height="16" viewBox="0 0 15.919921875 16" fill="none">
<path d="M15.1112 6.9457C14.7718 6.9457 14.4865 6.71375 14.4053 6.39967L14.4042 6.39988C14.363 6.23191 14.3153 6.06583 14.2609 5.90166C14.2065 5.73748 14.1458 5.57572 14.0786 5.41639C13.7466 4.6337 13.2723 3.93128 12.6703 3.32739C12.0666 2.7235 11.3643 2.24916 10.5817 1.91892C10.3134 1.80506 10.0378 1.71088 9.75837 1.63459C9.39315 1.59066 9.11 1.27987 9.11 0.902734C9.11 0.495495 9.44008 0.165363 9.84724 0.165363C9.95397 0.165363 10.0553 0.188238 10.1469 0.229059C12.9246 0.987503 15.1006 3.19988 15.8076 5.99827C15.8185 6.03353 15.8268 6.06944 15.8324 6.10597C15.838 6.14248 15.8407 6.17922 15.8407 6.21617C15.8408 6.61908 15.5141 6.9457 15.1112 6.9457ZM0 7.97245C0 12.4058 3.59352 16 8.02615 16C11.9404 16 15.1997 13.1977 15.9083 9.49084L15.906 9.49002C15.915 9.43909 15.92 9.38673 15.92 9.33319C15.92 8.87028 15.5633 8.49098 15.11 8.45452L15.1103 8.44802L8.00791 7.99434L8.00791 0.962189C8.0103 0.935711 8.01172 0.908938 8.01172 0.881822C8.01172 0.394809 7.61697 0 7.13004 0C7.0789 5.78452e-05 7.02814 0.00452013 6.97776 0.0133868C3.04116 0.526739 0 3.89402 0 7.97245ZM1.97552 10.5285C1.63259 9.72027 1.45929 8.85913 1.45929 7.97245C1.45929 7.08577 1.63259 6.22828 1.9737 5.42006C2.30387 4.63919 2.77631 3.93678 3.37827 3.33289C3.98023 2.73083 4.6807 2.25647 5.46143 1.92442C5.81349 1.773 6.1783 1.65623 6.54861 1.57048L6.54861 8.45897L6.54961 8.45963L6.55082 8.6708C6.55075 8.6748 6.55052 8.67873 6.55052 8.68273C6.55052 9.08825 6.87784 9.4172 7.28264 9.41997L7.28252 9.42023L7.42858 9.42094L7.42858 9.42091L7.91486 9.45208L14.3175 9.86075C13.9892 10.9591 13.3762 11.957 12.5226 12.7598C11.3005 13.9074 9.70435 14.5405 8.02615 14.5405C7.13962 14.5405 6.27863 14.3671 5.47238 14.026C4.68983 13.6957 3.98753 13.2214 3.38374 12.6175C2.77995 12.0136 2.30569 11.3112 1.97552 10.5285Z" fill="#FFFFFF" >
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14.3203125" height="16" viewBox="0 0 14.3203125 16" fill="none">
<path d="M13.9947 3.10876L10.0045 0.103786C9.91533 0.0362046 9.80931 0 9.69847 0L0.402396 0.00241378C0.197584 0.00241378 0 0.144818 0 0.407905L0 15.3435C0 15.679 0.289147 16 0.657809 16L13.9634 16C14.1754 16 14.32 15.8962 14.32 15.6573L14.32 3.5046C14.3176 3.34289 14.1248 3.20531 13.9947 3.10876ZM10.1563 1.75713L10.1563 4.76211C10.1563 4.88037 10.0985 4.98416 9.96834 4.98416L3.6553 4.98416C3.48181 4.98416 3.45772 4.89244 3.45772 4.75487L3.45772 1.25268L9.44306 1.25268L10.1563 1.75713ZM13.1032 14.3298C13.1032 14.588 12.9466 14.7642 12.696 14.7642L1.5879 14.7642C1.39272 14.7666 1.26261 14.6242 1.26261 14.4142L1.26261 1.53266C1.26261 1.36129 1.42164 1.25509 1.63127 1.25509C1.81199 1.25509 2.0409 1.25509 2.29872 1.25509L2.30113 5.85066C2.30113 6.0172 2.40474 6.09443 2.56136 6.09443L10.9008 6.09202C11.0647 6.09443 11.1587 5.97134 11.1538 5.78549L11.1563 2.52225L13.1032 3.98009L13.1032 14.3298ZM2.66738 10.5307L7.21422 10.5307C7.37566 10.5307 7.48409 10.4269 7.48409 10.2797C7.48409 10.1542 7.48409 9.79454 7.48409 9.65938C7.48409 9.50008 7.31542 9.3987 7.16602 9.3987L2.63365 9.3987C2.4457 9.3987 2.35173 9.51697 2.35173 9.65938C2.35173 9.81385 2.35173 10.1831 2.35173 10.2724C2.35173 10.4173 2.46498 10.5307 2.66738 10.5307ZM10.8189 9.3987L8.52983 9.3987C8.33225 9.3987 8.19009 9.51214 8.19009 9.67144C8.19009 9.78488 8.19009 10.13 8.19009 10.2459C8.19009 10.4414 8.34189 10.5331 8.51779 10.5331L10.79 10.5331C10.9924 10.5331 11.1563 10.3907 11.1563 10.2145C11.1563 10.0649 11.1563 9.9297 11.1563 9.75833C11.1587 9.56524 11.0261 9.3987 10.8189 9.3987ZM5.28175 12.3964L2.6722 12.3964C2.47221 12.3964 2.34932 12.5002 2.34932 12.7175C2.34932 12.8623 2.34932 13.0119 2.34932 13.135C2.34932 13.3498 2.47943 13.4826 2.67943 13.4826L5.22392 13.4826C5.41428 13.4826 5.58776 13.3547 5.58776 13.1447C5.58776 12.983 5.58776 12.8647 5.58776 12.7512C5.58776 12.5533 5.42632 12.3964 5.28175 12.3964ZM10.7996 12.3964L6.57086 12.3964C6.35641 12.3964 6.22871 12.5533 6.22871 12.7368C6.22871 12.843 6.22871 13.0385 6.22871 13.1543C6.22871 13.304 6.36364 13.4826 6.57809 13.4826L10.7707 13.4826C10.9852 13.4826 11.1563 13.3305 11.1563 13.1109C11.1563 13.0119 11.1563 12.9009 11.1563 12.7512C11.1587 12.5485 11.031 12.3964 10.7996 12.3964ZM8.83344 4.23593C9.00934 4.23593 9.1515 4.0887 9.1515 3.96078L9.1515 2.23261C9.1515 2.05401 9.05271 1.95505 8.90332 1.95505C8.78043 1.95505 8.30334 1.95505 8.19732 1.95505C8.04792 1.95505 7.97564 2.06366 7.97564 2.2302L7.97564 3.96561C7.97564 4.12008 8.12744 4.23835 8.26237 4.23835C8.37803 4.23593 8.70332 4.23593 8.83344 4.23593Z" fill="#FFFFFF" >
</path>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="12.6400146484375" viewBox="0 0 16 12.6400146484375" fill="none">
<path d="M13.52 12.64L2.48 12.64C1.12 12.64 0 11.52 0 10.16L0 2.48C0 1.12001 1.12 0 2.48 0L13.52 0C14.88 0 16 1.12001 16 2.48L16 10.16C16 11.52 14.88 12.64 13.52 12.64ZM2.8 1.6C2.16 1.6 1.6 2.16001 1.6 2.8L1.68 9.92C1.68 10.56 2.24 11.12 2.88 11.12L13.2 11.12C13.84 11.12 14.4 10.56 14.4 9.92L14.32 2.8C14.32 2.16001 13.76 1.6 13.12 1.6L2.8 1.6ZM6.4 9.84C4.48 9.84 2.88 8.24 2.88 6.32C2.88 4.4 4.48 2.8 6.4 2.8C8.32 2.8 9.92 4.4 9.92 6.32C10 8.24 8.39999 9.84 6.4 9.84ZM6.4 4C5.11999 4 4.08 5.04 4.08 6.32C4.08 7.6 5.11999 8.64 6.4 8.64C7.67999 8.64 8.72 7.6 8.72 6.32C8.72 5.04 7.67999 4 6.4 4ZM13.04 3.6L11.76 3.6C11.52 3.6 11.28 3.36 11.28 3.12L11.28 2.96C11.28 2.72 11.52 2.48 11.76 2.48L13.04 2.48C13.28 2.48 13.52 2.72 13.52 2.96L13.52 3.12C13.52 3.36 13.28 3.6 13.04 3.6Z" fill="#FFFFFF" >
</path>
</svg>

After

Width:  |  Height:  |  Size: 981 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,199 @@
<template>
<div class="left_detail">
<div>
<el-tooltip class="item" effect="dark" content="搜星数量" placement="top-start">
<div>
<img src="./icons/satellite.svg" alt="" />
<span>{{ info.position_state.rtk_number }}RTK</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="电池剩余电量" placement="top-start">
<div>
<img src="./icons/battery.svg" alt="" />
<span>{{ info.battery.capacity_percent }}%</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="返航所需电量" placement="top-start">
<div>
<img src="./icons/quantity.svg" alt="" />
<span>{{ info.battery.return_home_power }}%</span>
</div>
</el-tooltip>
</div>
<div>
<el-tooltip class="item" effect="dark" content="内存总容量" placement="top-start">
<div>
<img src="./icons/memory.svg" alt="" />
<span>{{ (info.storage.total / (1024 * 1024)).toFixed(2) }}GB</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="内存已使用容量" placement="top-start">
<div>
<img src="./icons/big.svg" alt="" />
<span>{{ (info.storage.used / (1024 * 1024)).toFixed(2) }}GB</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="飞行高度" placement="top-start">
<div>
<img src="./icons/plane.svg" alt="" />
<span>{{ info.elevation.toFixed(2) }}m</span>
</div>
</el-tooltip>
</div>
<div>
<el-tooltip class="item" effect="dark" content="拍照状态" placement="top-start">
<div>
<img src="./icons/picture.svg" alt="" />
<span>{{
info.cameras && info.cameras[0]
? (info.cameras[0].photo_state == 0 ? "空闲" : "拍照中")
: "-"
}}</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="拍照剩余数量" placement="top-start">
<div>
<img src="./icons/Remaining.svg" alt="" />
<span>{{ info.cameras && info.cameras[0] ? info.cameras[0].remain_photo_num : "-" }}</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="剩余飞行时间" placement="top-start">
<div>
<img src="./icons/FlightTime.svg" alt="" />
<span>{{ filterTime(info.battery.remain_flight_time) }}</span>
</div>
</el-tooltip>
</div>
<div>
<el-tooltip class="item" effect="dark" content="垂直速度" placement="top-start">
<div>
<span>VS {{ info.vertical_speed.toFixed(2) }}</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="相对起飞点高度" placement="top-start">
<div>
<span>ALT {{ Number(info.elevation).toFixed(2) }}</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="海拔高度" placement="top-start">
<div>
<span>ASL {{ (Number(info.elevation) + Number(info.height)).toFixed(2) }}</span>
</div>
</el-tooltip>
</div>
<div>
<el-tooltip class="item" effect="dark" content="水平速度" placement="top-start">
<div>
<span>HS {{ info.horizontal_speed.toFixed(2) }}</span>
</div>
</el-tooltip>
<div>&nbsp;</div>
<div>&nbsp;</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
info: {
//飞行器数据信息
cameras: [{ remain_photo_num: 0, photo_state: 0 }],
horizontal_speed: 0,
position_state: { rtk_number: 0 },
vertical_speed: 0,
horizontal_speed: 0,
elevation: 0,
height: 0,
wind_direction: "0",
battery: {
capacity_percent: 0,
remain_flight_time: 0,
return_home_power: 0,
},
wind_speed: 0,
storage: { total: 0, used: 0 },
},
};
},
created() { },
mounted() {
this.$recvChanel("websocketBus", (res) => {
if (res.businessType == "osd4") {
this.info = res.data || {}
this.time = new Date().getTime()
}
});
this.onReset()
},
methods: {
restet() {
this.info = {
cameras: [{ remain_photo_num: 0, photo_state: 0 }],
horizontal_speed: 0,
position_state: { rtk_number: 0 },
vertical_speed: 0,
horizontal_speed: 0,
elevation: 0,
height: 0,
wind_direction: "0",
battery: {
capacity_percent: 0,
remain_flight_time: 0,
return_home_power: 0,
},
wind_speed: 0,
storage: { total: 0, used: 0 },
};
},
onReset() {
let that = this
that.timer = setInterval(() => {
const currentTimestamp = Date.now(); // 获取当前时间戳
const elapsed = currentTimestamp - that.time; // 计算差值
if (elapsed > 2000) {
that.restet();
}
}, 2000);
},
filterTime(val) {
//返回分钟
let miu = parseInt(val / 60);
let s = val % 60;
return miu + "分" + s + "秒";
},
},
};
</script>
<style lang="scss" scoped>
.left_detail {
display: flex;
align-items: center;
padding: 20px 30px 0 80px;
justify-content: space-between;
height: 100%;
>div {
display: flex;
flex-direction: column;
justify-content: space-around;
height: 100%;
font-size: 14px;
>div {
display: flex;
// height: 100%;
align-items: center;
color: #fff;
font-weight: 600;
>img {
width: 20px;
margin-right: 6px;
height: 20px;
}
}
}
}
</style>

View File

@ -0,0 +1,282 @@
<template>
<div class="drone_video_box" id="drone_video_box" @wheel="debouncedWheel" @dblclick="onDetailPost">
<div class="shutter" :id="'shutter' + type"></div>
<!-- <video :class="{ video_drone: true }" :id="'drone_video-webrtc_' + droneObj.cameraIndex" autoplay muted width="100%"
height="100%"></video> -->
<div :id="'drone_video-webrtc_' + droneObj.cameraIndex" :class="{ video_drone: true }" style="width: 100%; height: 100%">
<webrtc :ref="'drone_video-webrtc_' + droneObj.cameraIndex" width="100%" height="100%"></webrtc>
</div>
<myLoading ref="myLoadingRef"></myLoading>
</div>
</template>
<script>
// 加载中
import myLoading from '../myLoading/index.vue';
import { hasDataFlight } from '@/api/air';
import webrtc from '@/components/webrtc';
import { getDockAir } from '@/utils/auth';
export default {
components: {
myLoading,
webrtc
},
props: {
type: {
type: Number,
default: 1 //1、无人机 2、机场
},
typeLabel: {
type: String,
default: '广角' //1、无人机 2、机场
}
},
data() {
return {
droneObj: {},
videoShow: false,
flvPlayer: null,
camera_type: 'wide',
videoUrl: '',
aiUrl: ''
};
},
mounted() {
this.$recvChanel('camera_type', (type) => {
this.camera_type = type;
});
this.$recvChanel('video_width', (height) => {
this.getInfo(this.droneObj);
let div = document.getElementById('drone_video_box');
div.style.height = height + 'px';
let div2 = document.getElementById('drone_video-webrtc_' + this.droneObj.cameraIndex);
div2.style.height = height + 'px';
});
},
beforeDestroy() {
this.getVideoEnd(this.droneObj);
},
methods: {
debouncedWheel(event) {
// 滚轮滚动
// 如果是机场视频和广角类型 不需要滚动
if (!(this.type == 2 || this.typeLabel == '广角')) {
this.$emit('wheelScale', event);
}
},
// 拍照效果
setShutter() {
const shutter = document.getElementById('shutter' + this.type);
shutter.classList.add('active');
// 动画结束后移除类,以便可以重新触发动画
setTimeout(() => {
shutter.classList.remove('active');
}, 300); // 动画时长
},
onDetailPost(e) {
if (this.type == 2) {
// 机场视频不需要点击
return;
}
// 获取坐标
let div = document.getElementById('drone_video_box');
// let div = document.getElementById("drone_video-webrtc_" + this.droneObj.cameraIndex);
let rect = div.getBoundingClientRect();
let width = rect.width;
let height = rect.height;
let x, y;
if (width && height && e.offsetX && e.offsetY) {
x = Number(e.offsetX / width);
y = Number(e.offsetY / height);
let obj = {
gateway: this.droneObj.gateway,
method: 'camera_aim',
params: {
camera_type: this.camera_type,
locked: true,
payload_index: this.droneObj.cameraIndex,
x,
y
}
};
hasDataFlight(obj).then((res) => {
if (res.code == 200) {
this.$message.success('操作成功');
}
});
}
},
// 数据获取并播放
getInfo(data) {
this.$refs.myLoadingRef.statusLoading(true);
this.droneObj = data;
console.log('this.droneObj', this.droneObj);
this.$nextTick(() => {
this.getVideo(data);
});
},
// 视频结束
getVideoEnd(data, cb) {
if (this.flvPlayer) {
axios({
// url: process.env.DOCKAIR + "/dj/live/stop",
url: getDockAir() + '/dj/live/stop',
method: 'post',
data
}).then((res) => {
if (res.data.code !== 200) {
this.$message.error(res.data.msg);
} else {
this.videoShow = false;
try {
this.destroyVideo();
if (cb) cb();
} catch (error) {}
}
});
} else {
if (cb) cb();
}
},
// 销毁
destroyVideo() {
this.flvPlayer.pause(); // 暂停播放数据流
this.flvPlayer.unload(); // 取消数据流加载
this.flvPlayer.destroy(); // 销毁播放实例
this.flvPlayer.detachMediaElement(); // 将播放实例从节点中取出
this.flvPlayer = null;
},
// 开始视频调用
getVideo(data1) {
if (this.flvPlayer) {
this.destroyVideo();
}
axios({
// url: process.env.DOCKAIR + "/dj/live/start",
url: getDockAir() + '/dj/live/start',
method: 'post',
data: data1
}).then((res) => {
const { code, data, msg } = res.data;
if (code == 200) {
// this.playFlv(data.url, "drone_video-webrtc_" + data1.cameraIndex, data1.cameraIndex)
setTimeout(() => {
this.$refs['drone_video-webrtc_' + data1.cameraIndex].startPlay(data.url);
this.videoUrl = data.url;
this.aiUrl = data.aiUrl;
}, 1000);
this.$refs.myLoadingRef.statusLoading(false);
} else {
this.$refs.myLoadingRef.statusLoading(false);
this.$message.error(msg);
}
});
},
// 播放flv视频
playFlv(flv, videoId) {
if (flvjs.isSupported()) {
var videoElement = document.getElementById(videoId);
this.flvPlayer = flvjs.createPlayer({
type: 'flv',
isLive: true,
hasAudio: false,
url: flv,
enableStashBuffer: true,
enableWorker: true,
autoCleanupSourceBuffer: true //自动清除缓存
});
this.$nextTick(() => {
this.flvPlayer.attachMediaElement(videoElement);
this.flvPlayer.load();
this.flvPlayer.play();
this.$refs.myLoadingRef.statusLoading(false);
// this.handleDelay(this.flvPlayer);
});
this.flvPlayer.on('error', () => {
console.log('播放错误');
});
// videoElement.addEventListener("error", () => {
// console.log("播放错误");
// // 重新播放
// // flvPlayer.attachMediaElement(videoElement);
// // flvPlayer.load();
// // flvPlayer.play();
// });
}
},
// 重新播放加载
onPlay() {
if (this.flvPlayer) {
this.flvPlayer.load();
this.flvPlayer.play();
}
},
// 处理延迟
handleDelay(flvPlayer) {
let timer = setInterval(() => {
if (!flvPlayer) return;
if (flvPlayer.buffered.length) {
let end = flvPlayer.buffered.end(0); //获取当前buffered值
let diff = end - flvPlayer.currentTime; //获取buffered与currentTime的差值
if (diff >= 2) {
//如果差值大于等于0.5 手动跳帧 这里可根据自身需求来定
flvPlayer.currentTime = flvPlayer.buffered.end(0) - 1.5; //手动跳帧
}
}
}, 1000); //1000毫秒执行一次
return timer;
}
}
};
</script>
<style lang="scss" scoped>
.drone_video_box {
width: 960px;
height: 720px;
position: relative;
overflow: hidden;
.shutter {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
transform: translateY(-100%);
opacity: 0;
transition:
transform 0.3s ease,
opacity 0.3s ease;
}
.shutter.active {
transform: translateY(0);
opacity: 1;
}
@keyframes shutterAnimation {
0% {
transform: translateY(-100%);
opacity: 0;
}
50% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(-100%);
opacity: 0;
}
}
.video_drone {
// width: 100%;
background-color: rgba(0, 0, 0, 1);
// object-fit: fill;
// height: 100%;
}
}
</style>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

View File

@ -0,0 +1,187 @@
<template>
<div class="flight_setup">
<div class="left_in">
<div>任务剩余距离</div>
<div>~</div>
<div>任务剩余时长</div>
<div>~</div>
<div>飞行作业模式</div>
<!-- <div></div> -->
<!-- display: flex;justify-content: center; -->
<div v-if="taskId" style="" @click="stopLine">
<div class="hangxian">
<span>{{ showPath ? '暂停航线' : '恢复航线' }}</span>
</div>
</div>
</div>
<div class="tool_flight">
<div @click="onFightTool">
<img src="./icons/icon1.svg" alt="" />
<span>飞行设置</span>
</div>
<div @click="onHomewardVoyage">
<img src="./icons/icon2.svg" alt="" />
<span>{{ showCancel ? '返航' : '取消返航' }}</span>
</div>
<div @click="onScram">
<img src="./icons/icon3.svg" alt="" />
<span>急停</span>
</div>
</div>
</div>
</template>
<script>
import { droneControl, droneEmergencyStop, flightTaskPauseNew, flightTaskRecoveryNew, drcModeEnte } from '@/api/air';
import { useAirStore } from '@/store/modules/drone';
export default {
data() {
return {
showCancel: true,
height: 0,
gateway: useAirStore().gateWay,
showPath: true,
taskId: ''
};
},
mounted() {
this.$recvChanel('TaskIssuedSuccessfully', (taskId) => {
this.taskId = taskId;
});
this.$recvChanel('showCancel', (showCancel) => {
this.showCancel = !showCancel;
});
},
methods: {
onHomewardVoyage() {
if (this.showCancel) {
// 飞机返航
this.$emit('showTips', true);
} else {
// 取消返航
this.$emit('showTipss', true);
}
},
// 飞行设置
onFightTool() {
this.$emit('onTakeOff', true);
},
// 暂停航线
stopLine() {
if (this.showPath) {
this.showPath = false;
// 暂停航线
this.pauseAirLine();
} else {
this.showPath = true;
// 恢复航线
this.recoverAirLine();
}
},
// 急停
onScram() {
// 无参数接口调用
droneEmergencyStop({ gateway: this.gateway.gateway }).then((res) => {
if (res.code == 200) {
this.$message.success('操作成功');
}
});
},
// 航线暂停
pauseAirLine() {
let obj = { gateway: this.gateway.gateway, taskId: this.taskId };
flightTaskPauseNew(obj).then((res) => {
this.$message.success('暂停成功');
});
},
// 航线恢复
recoverAirLine() {
let obj = { gateway: this.gateway.gateway, taskId: this.taskId };
flightTaskRecoveryNew(obj).then((res) => {
this.$message.success('恢复成功');
});
}
}
};
</script>
<style lang="scss" scoped>
.flight_setup {
width: 100%;
height: 100%;
position: relative;
padding: 20px;
box-sizing: border-box;
display: flex;
> div {
width: 50%;
}
.left_in {
color: #fff;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
> div {
font-size: 14px;
}
.hangxian {
text-align: center;
border: 1px solid rgb(255, 255, 255, 0.8);
width: 60%;
padding: 6px 0;
color: rgb(255, 255, 255, 0.8);
border-radius: 5px;
}
.hangxian:hover {
color: #fff;
border-color: #fff;
}
}
.tool_flight {
> div {
cursor: pointer;
margin-left: 30px;
border-radius: 4px;
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 1);
padding: 8px 0px;
display: flex;
width: 150px;
color: rgba(0, 255, 255, 1);
place-items: center;
justify-content: center;
margin-bottom: 8px;
> img {
width: 18px;
margin-right: 10px;
}
}
& > div:last-child {
background: linear-gradient(180deg, rgba(241, 108, 85, 0.2) 0%, rgba(241, 108, 85, 0) 100%), rgba(0, 0, 0, 0.5);
border: 1px solid rgba(241, 108, 85, 1);
color: rgb(241, 108, 85);
}
}
}
.flight_setup::before {
content: '';
display: block;
position: absolute;
top: 0;
left: 50%;
width: 1px;
height: 100%;
background: url('./icons/line.png') no-repeat;
background-size: 100% 100%;
}
</style>

View File

@ -0,0 +1,203 @@
<template>
<div class="graduated_scale" id="graduated_scale" @wheel="debouncedWheel">
<span class="max">{{ max }}X</span>
<div v-for="(item, i) of labelList" :key="i">
<div class="text">{{ item.count }}X</div>
<div class="boxs">
<div
v-for="(items, index) of item.arr"
:key="index"
:style="'height:' + (1 / item.arr.length) * 100 + '%'"
@click="onSelect(items)"
>
<div :class="valueScale == items ? 'select' : ''"></div>
</div>
</div>
</div>
</div>
</template>
<script>
let timeout;
export default {
props: {
valueScale: {
type: Number,
default: 2,
},
typeLabel: {
type: String,
default: "变焦",
},
},
data() {
return {
list: [
//变焦
{ count: 2, arr: [2, 3, 4, 5, 6].reverse() },
{ count: 7, arr: [7, 8, 9, 10, 11, 12].reverse() },
{
count: 14,
arr: [
14,
16,
18,
20,
22,
24,
26,
28,
30,
32,
34,
38,
43,
46,
50,
53,
56,
].reverse(),
},
].reverse(),
// 红外
listInfrared: [
//变焦
{ count: 2, arr: [2, 3, 4, 5].reverse() },
{ count: 6, arr: [7, 8, 9, 10, 11, 12].reverse() },
{ count: 13, arr: [13, 14, 15, 16, 17, 18, 19, 20].reverse() },
].reverse(),
labelList: [], //当前应该显示的倍速
detailList: [],
max: 56, //获取最大值 默认
index: 0, //鼠标滚轮滚动时 改变下标 获取对应的值(倍数大小)
};
},
watch: {
typeLabel(newlog) {
this.setInfo(newlog);
},
},
mounted() {
this.setInfo(this.typeLabel);
},
methods: {
onSelect(item) {
this.index = this.detailList.findIndex((items) => {
return items == item;
});
this.$emit("getScale", item);
},
handleWheel(event) {
// 处理滚轮事件的逻辑
if (event.deltaY > 0) {
this.index--;
if (this.index <= 0) {
this.index = 0;
}
} else {
this.index++;
if (this.index >= this.detailList.length) {
this.index = this.detailList.length - 1;
}
}
},
// 300 毫秒的防抖时间
debouncedWheel(event) {
// 清除之前的定时器
clearTimeout(timeout);
// 设置一个新的定时器
timeout = setTimeout(() => {
this.$emit("getScale", this.detailList[this.index]);
}, 200); // 200毫秒后判断为停止
this.handleWheel(event);
},
// 数据处理
setInfo(label) {
this.detailList = [];
if (label == "变焦") {
this.labelList = this.list;
this.list.forEach((element) => {
this.detailList.push(...element.arr);
});
} else {
//红外
this.labelList = this.listInfrared;
this.listInfrared.forEach((element) => {
this.detailList.push(...element.arr);
});
}
this.detailList.sort((a, b) => a - b);
this.index = this.detailList.findIndex(
(item) => item === this.valueScale
);
this.max = this.detailList[this.detailList.length - 1];
},
},
};
</script>
<style lang="scss" scoped>
.graduated_scale {
width: 70px;
height: 180px;
background: rgba(0, 0, 0, 0.6);
position: absolute;
color: #fff;
bottom: 70px;
right: 20px;
padding: 10px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
.max {
position: absolute;
top: 6px;
left: 10px;
}
> div {
height: 33%;
color: #fff;
font-size: 14px;
display: flex;
> div {
height: 100%;
}
.text {
display: flex;
justify-content: flex-end;
flex-direction: column;
width: 32px;
}
.boxs {
display: flex;
flex-direction: column;
justify-content: space-around;
height: 100%;
> div {
width: 100%;
display: grid;
place-items: center;
> div {
position: relative;
background-color: #fff;
display: inline-block;
width: 10px;
height: 1px;
}
.select::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #fff;
}
}
}
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12.96 18.4869C13.2 18.2469 13.44 17.9931 13.44 17.5131C13.44 17.0331 13.2 16.8069 12.96 16.5669C12.72 16.3269 12.48 16.0869 12 16.0869C11.52 16.0869 11.28 16.3269 11.04 16.5669C10.8 16.8069 10.56 17.0331 10.56 17.5131C10.56 17.9931 10.8 18.2469 11.04 18.4869C11.28 18.7269 11.52 18.7269 12 18.9669C12.48 18.7269 12.72 18.7269 12.96 18.4869ZM12 24C8.64 24 5.76686 22.8069 3.60686 20.4069C1.20686 18.2469 0 15.36 0 12C0 8.64 1.20686 5.76686 3.60686 3.60686C5.76686 1.20686 8.64 0 12 0C15.36 0 18.2469 1.20686 20.4069 3.60686C22.8069 5.76686 24 8.64 24 12C24 15.36 22.8069 18.2469 20.4069 20.4069C18.2469 22.8069 15.36 24 12 24ZM10.56 6.95314L11.04 13.6869C11.04 13.9269 11.2869 14.16 11.2869 14.4C11.5269 14.4 11.76 14.6469 12 14.6469C12.24 14.6469 12.4869 14.4 12.7269 14.4C12.7269 14.16 12.96 13.9269 12.96 13.6869L13.44 6.95314C13.68 6.47314 13.4469 6 13.2069 5.76C12.9669 5.28 12.48 5.04686 12 5.04686C11.52 5.04686 11.0469 5.28 10.8069 5.76C10.5669 6 10.32 6.47314 10.56 6.95314Z" fill-rule="evenodd" fill="#FFA145" >
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,126 @@
<template>
<div class="tips" v-if="showTip">
<div class="header_tips">
<span class="title"> 提示 </span>
<img src="./icons/close.png" @click="onClose" alt="" srcset="" />
</div>
<div class="content">
<img src="./icons/warn.svg" alt="" srcset="" />
<span> {{ obj.text }}</span>
</div>
<div class="footer_bottom">
<div class="cl" @click="onSubmit"> </div>
<div @click="onClose"> </div>
</div>
</div>
</template>
<script>
import { returnHome, returnHomeCancel } from '@/api/air';
export default {
props: {
Drone: {
type: Object,
default: () => ({})
},
obj: {
type: Object,
default: () => ({})
}
},
data() {
return { showTip: false };
},
mounted() {},
methods: {
openTip() {
this.showTip = true;
},
onClose() {
this.showTip = false;
},
// 返航
cancelHome(obj) {
returnHomeCancel(obj).then((res) => {
this.onClose();
this.$message.success('取消成功');
});
},
subHome(obj) {
returnHome(obj).then((res) => {
this.onClose();
this.$message.success('返航成功');
});
},
onSubmit() {
let obj = { gateway: this.Drone.gateway };
if (this.obj.flag) {
this.subHome(obj);
} else {
this.cancelHome(obj);
}
this.$sendChanel('showCancel', this.obj.flag);
}
}
};
</script>
<style lang="scss" scoped>
.tips {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 500px;
opacity: 1;
background: linear-gradient(180deg, rgb(33 33 33 / 70%) 0%, rgb(9 156 156 / 55%) 100%), #0000;
padding: 20px 20px 0;
box-sizing: border-box;
.header_tips {
display: flex;
justify-content: space-between;
align-items: center;
.title {
font-size: 16px;
color: #fff;
}
img {
width: 12px;
cursor: pointer;
}
}
.content {
height: 60px;
display: flex;
align-items: center;
color: #fff;
font-size: 16px;
> img {
width: 18px;
margin-right: 10px;
}
}
.footer_bottom {
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: flex-end;
> div {
border-radius: 4px;
cursor: pointer;
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 1);
padding: 10px 16px;
margin-right: 16px;
color: rgba(0, 255, 255, 1);
}
}
}
</style>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,98 @@
<template>
<div class="right_detail">
<div class="left_info">
<div>
<span>智能规划最佳飞行路线</span>
<div>
<el-switch v-model="value" inactive-color="#ff4949" active-color="rgba(0, 255, 255, 1)"> </el-switch>
</div>
</div>
<div>
<span>飞行作业高(ALT)</span>
<div>
<inputNumber
:min="0"
:max="10"
uuit="m"
@updateInput="(v) => (queryParams.commander_flight_height += v)"
:valueNumber="queryParams.commander_flight_height"
@change="(b) => (queryParams.commander_flight_height = b)"
>
</inputNumber>
</div>
</div>
<div>
<span>返航高(ALT)</span>
<div>
<inputNumber
:min="0"
:max="10"
uuit="m"
@updateInput="(v) => (queryParams.rth_altitude += v)"
:valueNumber="queryParams.rth_altitude"
@change="(b) => (queryParams.rth_altitude = b)"
>
</inputNumber>
</div>
</div>
<div>
<span>失联动作</span>
<div class="lost_action">
<el-select v-model="queryParams.rc_lost_action" placeholder="请选择">
<el-option v-for="item in lostAction" :key="item.id" :label="item.label" :value="item.id"> </el-option>
</el-select>
</div>
</div>
<div>
<span>目标点高度(AGL)</span>
<div>
<inputNumber
:min="0"
:max="10"
uuit="m"
@updateInput="(v) => (queryParams.height += v)"
:valueNumber="queryParams.height"
@change="(b) => (queryParams.height = b)"
></inputNumber>
</div>
</div>
</div>
<div class="take_off" @click="onTakeOff">
<img src="./icons/fight.svg" alt="" srcset="" />
</div>
</div>
</template>
<script>
// 输入框
import inputNumber from '../inputNumber/index.vue';
import { lostAction } from '../../index.js';
import { useAirStore } from '@/store/modules/drone';
export default {
components: { inputNumber },
data() {
return {
value: true,
queryParams: useAirStore().queryParams,
// queryParams: {
// target_height: 100, //目标点高度
// rth_altitude: 100, //返航高度
// security_takeoff_height: 100, //安全起飞高度
// rc_lost_action: 2, //遥控器失控动作
// commander_flight_height: 100, //指点飞行高度
// },
value4: 1,
lostAction
};
},
methods: {
onTakeOff() {
// 一键起飞
this.$emit('onTakeOff', false);
}
}
};
</script>
<style lang="scss" scoped>
@import '../../style/comm.scss';
</style>

View File

@ -0,0 +1,68 @@
<template>
<div class="type_controls">
<div
v-for="(item, i) of controls"
:key="i"
:class="item.id == selet_id ? 'select' : ''"
v-show="item.id != selet_id"
@click="onControl(item)"
>
{{ item.label }}
</div>
</div>
</template>
<script>
export default {
props: {
value: {
type: Number,
default: 2,
},
},
data() {
return {
controls: [
{ id: 1, label: "变焦", value: 2, type: "zoom" }, //value:倍速
{ id: 2, label: "红外", value: 2, type: "ir" }, //value:倍速
{ id: 3, label: "广角", value: 0, type: "wide" },
],
selet_id: 3,
};
},
watch: {
value(val) {},
},
methods: {
onControl(item) {
this.selet_id = item.id;
this.$emit("updateType", item);
},
},
};
</script>
<style lang="scss" scoped>
.type_controls {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
> div {
width: 48px;
height: 28px;
opacity: 1;
border-radius: 4px;
background: rgba(0, 255, 255, 0.2);
display: grid;
place-items: center;
border: 1px solid rgba(255, 255, 255, 0.5);
color: #fff;
margin-bottom: 6px;
cursor: pointer;
}
.select {
color: rgba(0, 255, 255);
border: 1px solid rgba(0, 255, 255, 0.5);
}
}
</style>

View File

@ -0,0 +1,38 @@
<template>
<div class="type_value">
{{ typeLabel }}<span v-if="typeLabel != '广角'"> {{ value }}X</span>
</div>
</template>
<script>
export default {
props: {
typeLabel: {
type: String,
default: "广角",
},
value: {
type: Number,
default: 2,
},
},
data() {
return {};
},
mounted() {},
methods: {},
};
</script>
<style lang="scss" scoped>
.type_value {
position: absolute;
left: 50%;
top: 50px;
transform: translateX(-50%);
color: rgba(27, 248, 195, 1);
border: 1px solid rgba(27, 248, 195, 1);
font-size: 14px;
border-radius: 4px;
padding: 6px 20px;
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<div class="articulation" v-show="showArticulation">
<span v-for="(item, i) of articulationList" :key="i" :style="select_id == item.id ? 'color: aqua;' : ''"
@click="setArticulation(item.id)">{{ item.label }}</span>
</div>
</template>
<script>
export default {
props: {
videoObj: {
type: Object,
default: () => ({}),
},
type: {
type: String,
default: "AerodromeVideoRef", //AerodromeVideoRef:机场视频 DroneVideoRef无人机视频
},
},
data() {
return {
articulationList: [
{ id: 0, label: "自适应" },
{ id: 1, label: "流畅" },
{ id: 2, label: "标清" },
{ id: 3, label: "高清" },
{ id: 4, label: "超清" },
],
select_id: 0,
showArticulation: false,
};
},
methods: {
openCloseShow(flag) {
this.showArticulation = !this.showArticulation;
},
// 清晰度调整
setArticulation(definition) {
this.showArticulation = false;
// 多次点击
if (this.select_id == definition) return;
// console.log("调用");
this.select_id = definition;
this.videoObj.definition = definition;
this.$emit("updateDefinition", this.videoObj, this.type);
},
},
};
</script>
<style lang="scss" scoped>
.articulation {
position: absolute;
top: 24px;
width: 60px;
right: 20px;
background-color: #404a5dcc;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
padding: 6px;
z-index: 999;
>span {
margin: 5px 0;
cursor: pointer;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

View File

@ -0,0 +1,285 @@
<template>
<div class="dialog_control" id="my_dialog_control" v-if="dialogShow">
<div class="header" id="my_dialog_header_control">
<span class="title">飞行前检查</span>
<div class="close" @click="close">
<div class="sector"></div>
<img src="./icons/close.png" alt="" />
</div>
</div>
<div class="right_detail">
<div class="left_info left_box">
<div>
<span>安全起飞高度</span>
<div>
<div>
<inputNumber
:min="0"
:max="10"
uuit="m"
@updateInput="(v) => (queryParams.security_takeoff_height += v)"
:valueNumber="queryParams.security_takeoff_height"
@change="(b) => (queryParams.security_takeoff_height = b)"
></inputNumber>
</div>
</div>
</div>
<div>
<span>飞行作业高(ALT)</span>
<div>
<inputNumber
:min="0"
:max="10"
uuit="m"
@updateInput="(v) => (queryParams.commander_flight_height += v)"
:valueNumber="queryParams.commander_flight_height"
@change="(b) => (queryParams.commander_flight_height = b)"
></inputNumber>
</div>
</div>
<div>
<span>返航高(ALT)</span>
<div>
<inputNumber
:min="0"
:max="10"
uuit="m"
@updateInput="(v) => (queryParams.rth_altitude += v)"
:valueNumber="queryParams.rth_altitude"
@change="(b) => (queryParams.rth_altitude = b)"
>
</inputNumber>
</div>
</div>
<div>
<span>失联动作</span>
<div class="lost_action">
<el-select v-model="queryParams.rc_lost_action" placeholder="请选择">
<el-option
v-for="item in lostAction"
:key="item.id"
:label="item.label"
:value="item.id"
@change="(b) => (queryParams.rc_lost_action = b)"
>
</el-option>
</el-select>
</div>
</div>
<div>
<span>目标点高度(AGL)</span>
<div>
<inputNumber
:min="0"
:max="10"
uuit="m"
@updateInput="(v) => (queryParams.height += v)"
:valueNumber="queryParams.height"
@change="(b) => (queryParams.height = b)"
>
</inputNumber>
</div>
</div>
</div>
</div>
<div class="footer_bottom">
<div class="cl" @click="onClose">取消</div>
<div @click="onCarryOut" v-if="!flag">立即执行</div>
</div>
</div>
</template>
<script>
import { setMove } from '@/utils/moveDiv';
// 输入框
import inputNumber from '../inputNumber/index.vue';
import { lostAction } from '../../index.js';
import { hasDataFlight, noDataFlight } from '../../api/index';
import { useAirStore } from '@/store/modules/drone';
export default {
name: 'dialogControl',
components: {
inputNumber
},
props: {
Drone: {
type: Object,
default: () => ({})
}
},
data() {
return {
dialogShow: false,
lostAction,
height: 100,
queryParams: useAirStore().queryParams,
// queryParams: {
// target_height: 100, //目标点高度
// rth_altitude: 100, //返航高度
// commander_flight_height: 100, //指点飞行高度
// rc_lost_action: 2, //遥控器失控动作
// rth_mode: 1, //返航模式设置值
// commander_mode_lost_action: 1, //指点飞行失控动作
// commander_flight_mode: 1, //指点飞行模式设置值
// max_speed: 12, //一键起飞的飞行过程中能达到的最大速度
// security_takeoff_height: 100, //安全起飞高度
// target_latitude: 0, //目标点纬度
// target_longitude: 0, //目标点经度
// },
value: true,
networkState: {},
flag: false //是否可以执行
};
},
mounted() {
this.$recvChanel('websocketBus', (data) => {
if (data.businessType == 'osd3') {
this.networkState = { ...data.data };
}
});
},
methods: {
onClose() {
this.dialogShow = false;
},
// 执行飞行操作
onCarryOut() {
this.queryParams.target_latitude = this.networkState.latitude;
this.queryParams.target_longitude = this.networkState.longitude;
let obj = {
params: {
...this.queryParams,
target_height: this.networkState.height + this.queryParams.height
},
gateway: this.Drone.gateway,
method: 'takeoff_to_point'
};
console.log('objobj', this.height, obj);
// return;
// 关闭调试模式
// debug_mode_close
noDataFlight({
gateway: this.Drone.gateway,
method: 'debug_mode_close'
}).then((res) => {
if (res.code == 200) {
setTimeout(() => {
this.setHasDataFlight(obj);
}, 1000);
} else {
this.$message.error(res.msg);
}
});
},
openDialog(flag) {
this.flag = flag;
this.queryParams.target_height = 100;
this.dialogShow = true;
this.$nextTick(() => {
setMove('my_dialog_header_control', 'my_dialog_control');
});
},
close() {
this.dialogShow = false;
},
// 有参数接口调用
setHasDataFlight(data) {
hasDataFlight(data).then((res) => {
if (res.code == 200) {
this.close();
this.$message.success('操作成功');
this.$emit('onflight', true);
}
});
}
}
};
</script>
<style lang="scss">
@import '../../style/comm.scss';
.dialog_control::before {
content: '';
width: 100px;
height: 6px;
background: aqua;
position: absolute;
top: -6px;
left: -2px;
clip-path: polygon(0% 0%, 90% 0%, 100% 100%, 0% 100%);
}
.dialog_control {
position: fixed;
top: 50%;
left: 50%;
width: 480px;
transform: translate(-50%, -50%);
padding: 10px;
background: linear-gradient(180deg, rgba(0, 255, 255, 0.2) 0%, rgba(0, 255, 255, 0) 100%), rgba(0, 0, 0, 0.6);
border: 1.5px solid rgba(0, 255, 255, 1);
color: #fff;
.header {
height: 40px;
padding: 10px;
.title {
font-family: 'alimamashuheiti';
font-size: 18px;
font-weight: 700;
text-shadow: 0px 0px 9px rgba(20, 118, 255, 1);
}
.close {
cursor: pointer;
}
.sector {
position: absolute;
top: -30px;
right: -30px;
width: 0;
height: 0;
border: 30px solid transparent;
/* 边框宽度和颜色可以调整 */
border-bottom-color: rgba(0, 255, 255, 0.5);
/* 底边的颜色 */
border-radius: 50%;
/* 将边框变为圆形 */
transform: rotate(45deg);
/* 旋转45度可根据需要调整角度 */
}
img {
position: absolute;
top: 5px;
right: 5px;
width: 14px;
height: 14px;
}
}
.footer_bottom {
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: flex-end;
> div {
cursor: pointer;
border-radius: 4px;
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 1);
padding: 10px 16px;
margin-right: 16px;
color: rgba(0, 255, 255, 1);
}
.cl {
border: 1px solid rgba(0, 255, 255, 0.5);
}
}
}
</style>

View File

@ -0,0 +1,116 @@
<template>
<div class="input_number">
<el-input class="custom-number-input" type="number" v-model.trim.number="value" @change="change">
<div slot="suffix" class="button_right">
<div>
<span @click="onAdd(1)">
</span>
<span @click="onAdd(-1)">
</span>
</div>
<span>{{ uuit }}</span>
</div>
</el-input>
</div>
</template>
<script>
export default {
props: {
uuit: {
type: String,
default: "m",
},
valueNumber: {
type: Number,
default: 0,
},
min: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 10,
},
},
data() {
return {
value: 0,
};
},
watch: {
valueNumber: {
handler(newValue) {
this.value = newValue;
},
immediate: true,
},
},
methods: {
onAdd(valueNumber) {
this.value += valueNumber;
if (this.value < this.min) {
this.value = this.min;
}
if (this.value > this.max) {
this.value = this.max;
}
this.$emit("updateInput", valueNumber);
},
change(value) {
this.$emit("change", Number(value));
}
},
};
</script>
<style lang="scss">
.input_number {
.custom-number-input input::-webkit-inner-spin-button,
.custom-number-input input::-webkit-outer-spin-button {
appearance: none;
margin: 0;
}
.el-input__inner {
height: 30px;
background: #f0f8ff00;
color: white;
border: 1px solid rgba(0, 255, 255, 0.5);
width: 90px;
}
.el-input {
width: 90px;
}
.button_right {
display: flex;
color: #fff;
font-size: 14px;
align-items: center;
height: 30px;
padding-right: 3px;
>div {
font-size: 9px;
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
margin-right: 6px;
>span {
cursor: pointer;
}
&>span:hover {
color: #c7c7c7;
}
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More