feat: 剩余导入导出功能

This commit is contained in:
xiangbo 2025-08-20 16:32:32 +08:00
parent 73bd653e9d
commit c54cebe31e
3 changed files with 494 additions and 124 deletions

View File

@ -2,7 +2,7 @@
<template>
<el-dialog
v-model="dialogVisible"
title="新建赴校信息"
title="新建"
width="800px"
@close="onCancel"
align-center

View File

@ -2,11 +2,12 @@
<template>
<el-dialog
v-model="dialogVisible"
title="跟进赴校信息"
:title="isDetail ? '详情' : '跟进'"
width="800px"
@close="closeModal"
align-center
>
<!-- {{ isDetail }} -->
<div class="modal-header">
<div class="status-box">
<span>当前状态</span>
@ -14,102 +15,222 @@
</div>
<div class="action-box">
<el-button @click="onClickCancel">取消</el-button>
<el-button type="primary" @click="onClickSave">保存</el-button>
<el-button :disabled="isDetail" type="primary" @click="onClickSave"
>保存</el-button
>
</div>
</div>
<div
style="height: 80vh; overflow-y: auto; padding-right: 22px"
v-loading="editModalLoading"
>
<!-- <el-divider /> -->
<el-divider />
<el-descriptions title="基础信息" :column="2" border>
<el-descriptions-item label="学校">
{{ safeDetail.schoolName || safeDetail.school || "-" }}
</el-descriptions-item>
<el-descriptions-item label="年级">
{{ safeDetail.grade || safeDetail.gradeLevel || "-" }}
</el-descriptions-item>
<el-descriptions-item label="赴校人员">
{{
Array.isArray(safeDetail.schoolBusinessUser)
? safeDetail.schoolBusinessUser.join("")
: safeDetail.schoolBusinessUser || "-"
}}
</el-descriptions-item>
<el-descriptions-item label="赴校时间">
{{ safeDetail.startTime?.split("T")[0] }}
</el-descriptions-item>
</el-descriptions>
<el-descriptions title="基础信息" :column="2" border>
<el-descriptions-item label="学校">
{{ safeDetail.schoolName || safeDetail.school || "-" }}
</el-descriptions-item>
<el-descriptions-item label="年级">
{{ safeDetail.grade || safeDetail.gradeLevel || "-" }}
</el-descriptions-item>
<el-descriptions-item label="赴校人员">
{{
Array.isArray(safeDetail.schoolBusinessUser)
? safeDetail.schoolBusinessUser.join("")
: safeDetail.schoolBusinessUser || "-"
}}
</el-descriptions-item>
<el-descriptions-item label="赴校时间">
{{ safeDetail.startTime?.split("T")[0] }}
</el-descriptions-item>
</el-descriptions>
<el-divider />
<el-divider />
<el-descriptions title="基础工作" :column="1" border>
<el-descriptions-item label="座谈">
<el-tag
:type="safeDetail.isDiscussion ? 'success' : 'info'"
style="margin-right: 8px"
<el-descriptions title="基础工作" :column="1" border>
<el-descriptions-item label="座谈">
<el-tag
:type="safeDetail.isDiscussion ? 'success' : 'info'"
style="margin-right: 8px"
>
{{ safeDetail.isDiscussion ? "已开展" : "未开展" }}
</el-tag>
<span>{{ safeDetail.discussion || "-" }}</span>
</el-descriptions-item>
<el-descriptions-item label="班会">
<el-tag
:type="safeDetail.isClassMeeting ? 'success' : 'info'"
style="margin-right: 8px"
>
{{ safeDetail.isClassMeeting ? "已开展" : "未开展" }}
</el-tag>
<span>{{ safeDetail.classMeeting || "-" }}</span>
</el-descriptions-item>
</el-descriptions>
<el-divider />
<el-descriptions title="反馈问题" :column="1" border> </el-descriptions>
<div style="display: flex; gap: 40px; margin-bottom: 5px">
<span> 问题总数{{ safeDetail.feedbackQuestions?.length }} </span>
<span> 未解决问题{{ unresolvedCount }} </span>
</div>
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane
v-for="(i, idx) in sortData(safeDetail.feedbackQuestions || [])"
:key="idx"
:label="'问题' + (idx + 1) + (i.solution ? '(已解决)' : '(未解决)')"
:name="idx"
>
{{ safeDetail.isDiscussion ? "已开展" : "未开展" }}
</el-tag>
<span>{{ safeDetail.discussion || "-" }}</span>
</el-descriptions-item>
<el-descriptions-item label="班会">
<el-tag
:type="safeDetail.isClassMeeting ? 'success' : 'info'"
style="margin-right: 8px"
>
{{ safeDetail.isClassMeeting ? "已开展" : "未开展" }}
</el-tag>
<span>{{ safeDetail.classMeeting || "-" }}</span>
</el-descriptions-item>
</el-descriptions>
<el-divider />
<el-descriptions title="反馈问题" :column="1" border> </el-descriptions>
<div style="display: flex; gap: 40px; margin-bottom: 5px">
<span> 问题总数{{ safeDetail.feedbackQuestions?.length }} </span>
<span>
未解决问题{{ handleUnHandleQust(safeDetail.feedbackQuestions) }}
</span>
</div>
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane
v-for="(i, idx) in sortData(safeDetail.feedbackQuestions)"
:key="idx"
:label="'问题' + (idx + 1) + (i.solution ? '(已解决)' : '(未解决)')"
:name="idx"
>
<div style="font-size: 12px; margin-bottom: 4px">
<span>问题类型</span> <span>{{ queType[i.questionType] }}</span>
</div>
<div style="padding: 10px; background-color: #f3f3f3">
{{ i.question }}
</div>
<div v-if="i.solution">
<span> 解决情况 </span>
<div style="padding: 10px; background-color: #f3f3f3">
解决情况描述111111111111111
<div style="font-size: 12px; margin-bottom: 4px">
<span>问题类型</span> <span>{{ queType[i.questionType] }}</span>
</div>
</div>
<div
v-else
<div style="padding: 10px; background-color: #f3f3f3">
{{ i.question }}
</div>
<div v-if="i.solution" style="font-size: 12px; margin-top: 10px">
<span> 解决时间{{ i.endTime.split("T")[0] }} </span>
<div style="padding: 10px; background-color: #f3f3f3">
{{ i.solution }}
</div>
</div>
<el-button
type="text"
v-else
style="margin-top: 5px; font-size: 12px"
class="markTitle"
:disabled="isDetail"
@click="markTitle(i)"
>
标记已解决
</el-button>
</el-tab-pane>
</el-tabs>
<el-divider />
<!-- {{ safeDetail.feedbackQuestions }} -->
<!-- .............................................................. -->
<el-descriptions title="备注" :column="1" border> </el-descriptions>
<el-input
v-model="safeDetail.remark"
:rows="4"
type="textarea"
:disabled="isDetail"
/>
<el-divider />
<el-descriptions title="解决方案执行跟踪记录" :column="1" border>
</el-descriptions>
<span>需求+解决方案</span>
<el-input
v-model="solutionText"
:rows="4"
type="textarea"
:disabled="isDetail"
/>
<!-- 添加按钮区域 -->
<div style="margin-top: 5px; display: flex; gap: 20px">
<el-button
type="text"
style="margin-top: 5px; font-size: 12px"
class="markTitle"
@click="markTitle"
:disabled="isDetail"
@click="addRecord"
>
标记已解决
</div>
</el-tab-pane>
</el-tabs>
<el-divider />
添加执行记录
</el-button>
<el-button
style="margin-top: 5px; font-size: 12px"
class="markTitle"
type="text"
:disabled="isDetail"
@click="addFinish"
>
{{ finishRecord ? "修改完结情况" : "添加完结情况" }}
</el-button>
</div>
<!-- {{ safeDetail.feedbackQuestions }} -->
<!-- .............................................................. -->
<el-descriptions title="备注" :column="1" border> </el-descriptions>
<el-input
v-model="safeDetail.remark"
:rows="4"
type="textarea"
maxlength="1000"
:show-word-limit="true"
/>
<!-- 执行记录列表 -->
<div v-if="executionRecords.length > 0" style="margin-top: 15px">
<!-- <div style="font-weight: bold; margin-bottom: 8px">执行记录</div> -->
<div
v-for="(record, index) in executionRecords"
:key="index"
style="
margin-bottom: 10px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
"
>
<div style="font-weight: bold; color: #409eff">
执行记录{{ index + 1 }}{{ record.time.split("T")[0] }}
</div>
<div style="margin-top: 5px; white-space: pre-wrap">
{{ record.content }}
</div>
</div>
</div>
<!-- 完结情况 -->
<div v-if="finishRecord" style="margin-top: 15px">
<!-- <div style="font-weight: bold; margin-bottom: 8px">完结情况</div> -->
<div
style="
padding: 10px;
background-color: #f0f9ff;
border-radius: 4px;
border: 1px solid #b3d8ff;
"
>
<div style="font-weight: bold; color: #a69400">
完结情况{{ finishRecord.time.split("T")[0] }}
</div>
<div style="margin-top: 5px; white-space: pre-wrap">
{{ finishRecord.content }}
</div>
</div>
</div>
</div>
</el-dialog>
<!-- 操作弹窗 -->
<el-dialog
v-model="operationDialogVisible"
title="操作"
width="500px"
align-center
>
<el-form
ref="operationFormRef"
:model="operationForm"
:rules="operationRules"
label-width="80px"
>
<el-form-item label="操作时间" prop="operationTime">
<el-date-picker
v-model="operationForm.operationTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择操作时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item :label="operationContentLabel" prop="operationContent">
<el-input
v-model="operationForm.operationContent"
type="textarea"
:rows="4"
:placeholder="`请输入${operationContentLabel}`"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeOperationDialog">取消</el-button>
<el-button type="primary" @click="confirmOperation">确认</el-button>
</span>
</template>
</el-dialog>
</template>
@ -124,16 +245,91 @@ import {
} from "@/api/toschoolinfomanage";
import { setFips } from "crypto";
const activeName = ref(0);
const activeName = ref<any>(0);
const handleClick = (tab: TabsPaneContext, event: Event) => {
console.log(tab, event);
console.log(tab.props.name, event);
activeName.value = tab.props.name;
};
const props = defineProps<{ visible: boolean; detailData: any }>();
const props = defineProps<{
visible: boolean;
detailData: any;
editModalLoading: boolean;
isDetail: boolean;
}>();
const emit = defineEmits<{
(e: "update:visible", value: boolean): void;
(e: "handleReset"): void;
}>();
//
const operationDialogVisible = ref(false);
const operationType = ref(""); // markSolved, addRecord, addFinish
const operationForm = reactive({
operationTime: "",
operationContent: ""
});
const operationFormRef = ref<FormInstance>();
//
const executionRecords = ref<Array<{ time: string; content: string }>>([]);
const finishRecord = ref<{ time: string; content: string } | null>(null);
// detailData.solutionRecord
watch(
() => props.detailData,
val => {
const sr = (val as any)?.solutionRecord || {};
//
const recs = Array.isArray(sr?.record) ? sr.record : [];
executionRecords.value = recs.map((r: any) => ({
time: r?.executionTime || "",
content: r?.executionRecords || ""
}));
//
if (sr?.endRecordTime || sr?.endRecord) {
finishRecord.value = {
time: sr?.endRecordTime || "",
content: sr?.endRecord || ""
};
} else {
finishRecord.value = null;
}
},
{ immediate: true, deep: true }
);
//
const currentMarkedQuestion = ref<any | null>(null);
//
const operationContentLabel = computed(() => {
switch (operationType.value) {
case "markSolved":
return "解决情况";
case "addRecord":
return "执行记录";
case "addFinish":
return "完结情况";
default:
return "操作内容";
}
});
//
const operationRules: FormRules = {
operationTime: [
{ required: true, message: "请选择操作时间", trigger: "change" }
],
operationContent: [
{
required: true,
message: `请输入${operationContentLabel.value}`,
trigger: "blur"
}
]
};
const dialogVisible = computed({
get: () => props.visible,
set: v => emit("update:visible", v)
@ -141,6 +337,7 @@ const dialogVisible = computed({
const closeModal = () => {
emit("update:visible", false);
activeName.value = 0;
};
const queType = {
1: "学校领导班子",
@ -154,10 +351,7 @@ const queType = {
* @param data
*/
const handleUnHandleQust = (data: Array<any>) => {
return (data || []).map(i => {
// solution
return !i.solution;
}).length;
return (data || []).filter(i => !i?.solution).length;
};
const sortData = (data: Array<any>) => {
const categorizedData = [
@ -186,14 +380,157 @@ const statusText = computed(() =>
const statusType = computed(() =>
safeDetail.value?.solutionEnd ? "success" : "warning"
);
const markTitle = () => {
console.log("标记已解决");
const solutionText = computed({
get: () => safeDetail.value?.solutionRecord?.solution || "",
set: (value: string) => {
if (!safeDetail.value.solutionRecord) {
safeDetail.value.solutionRecord = {};
}
safeDetail.value.solutionRecord.solution = value;
}
});
// solution
const unresolvedCount = computed(() => {
const list = (safeDetail.value?.feedbackQuestions as any[]) || [];
return list.filter(item => !item?.solution).length;
});
const markTitle = (data: any) => {
console.log("标记已解决", data);
operationType.value = "markSolved";
currentMarkedQuestion.value = data;
//
operationForm.operationTime = data?.endTime || "";
operationForm.operationContent = data?.solution || "";
operationFormRef.value?.clearValidate();
operationDialogVisible.value = true;
};
const addRecord = () => {
console.log("添加执行记录");
operationType.value = "addRecord";
operationDialogVisible.value = true;
};
const addFinish = () => {
console.log("添加完结情况");
operationType.value = "addFinish";
const sr = (props.detailData as any)?.solutionRecord;
// 使 finishRecord
if (sr && (sr.endRecordTime || sr.endRecord)) {
operationForm.operationTime = sr.endRecordTime || "";
operationForm.operationContent = sr.endRecord || "";
} else if (finishRecord.value) {
operationForm.operationTime = finishRecord.value.time;
operationForm.operationContent = finishRecord.value.content;
} else {
//
operationForm.operationTime = "";
operationForm.operationContent = "";
}
operationDialogVisible.value = true;
};
//
const closeOperationDialog = () => {
operationDialogVisible.value = false;
//
operationForm.operationTime = "";
operationForm.operationContent = "";
//
operationFormRef.value?.clearValidate();
//
currentMarkedQuestion.value = null;
};
const confirmOperation = async () => {
if (!operationFormRef.value) return;
try {
await operationFormRef.value.validate();
const { operationTime, operationContent } = operationForm;
//
switch (operationType.value) {
case "addRecord":
//
executionRecords.value.push({
time: operationTime,
content: operationContent
});
// solutionRecord.record
if (!props.detailData.solutionRecord)
props.detailData.solutionRecord = {} as any;
if (!Array.isArray(props.detailData.solutionRecord.record))
props.detailData.solutionRecord.record = [];
props.detailData.solutionRecord.record.push({
executionTime: operationTime,
executionRecords: operationContent
});
break;
case "addFinish":
//
finishRecord.value = {
time: operationTime,
content: operationContent
};
// solutionRecord.endRecordTime / endRecord
if (!props.detailData.solutionRecord)
props.detailData.solutionRecord = {} as any;
props.detailData.solutionRecord.endRecordTime = operationTime;
props.detailData.solutionRecord.endRecord = operationContent;
break;
case "markSolved":
//
if (currentMarkedQuestion.value) {
currentMarkedQuestion.value.endTime = operationTime;
currentMarkedQuestion.value.solution = operationContent;
}
break;
}
console.log("确认操作", {
type: operationType.value,
time: operationTime,
content: operationContent
});
closeOperationDialog();
} catch (error) {
console.log("表单验证失败", error);
}
};
function onClickCancel() {
console.log("取消");
emit("update:visible", false);
//
executionRecords.value = [];
finishRecord.value = null;
currentMarkedQuestion.value = null;
operationForm.operationTime = "";
operationForm.operationContent = "";
activeName.value = 0;
operationFormRef.value?.clearValidate();
}
function onClickSave() {
console.log("保存");
console.log("保存", props.detailData);
let copyParams = JSON.parse(JSON.stringify(props.detailData));
delete copyParams.solutionEnd;
addOrEditApi(copyParams).then(res => {
if (res.code === 200) {
ElMessage.success("提交成功");
//
emit("update:visible", false);
//
executionRecords.value = [];
finishRecord.value = null;
currentMarkedQuestion.value = null;
operationForm.operationTime = "";
operationForm.operationContent = "";
activeName.value = 0;
operationFormRef.value?.clearValidate();
// handleReset
emit("handleReset");
}
});
}
</script>
@ -202,6 +539,9 @@ function onClickSave() {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 10px;
border-bottom: 1px solid #ccc;
margin-bottom: 10px;
}
.status-box {
display: flex;

View File

@ -38,7 +38,6 @@
v-model="query.people"
placeholder="请选择赴校人员"
clearable
multiple
filterable
style="width: 300px"
>
@ -125,7 +124,11 @@
</template>
</el-popconfirm>
<el-button size="small" type="primary" plain @click="onDetail(row)"
<el-button
size="small"
type="primary"
plain
@click="onDetailOrFollow(row, true)"
>详情</el-button
>
<el-button
@ -133,7 +136,7 @@
size="small"
type="success"
plain
@click="onFollow(row)"
@click="onDetailOrFollow(row, false)"
>跟进</el-button
>
</template>
@ -157,7 +160,13 @@
<!-- 新建 -->
<AddModal v-model:visible="isShowAddModal" @handleReset="handleReset" />
<!-- 跟进 -->
<EditModal v-model:visible="isShowEditModal" :detailData="detailData" />
<EditModal
v-model:visible="isShowEditModal"
:editModalLoading="editModalLoading"
:detailData="detailData"
:isDetail="isDetail"
@handleReset="handleReset"
/>
</template>
<!-- 赴校信息管理菜单 -->
<script setup lang="ts" name="Toschoolinfomanage">
@ -189,6 +198,7 @@ interface TableItem {
const schoolOptions = ref([]);
const peopleOptions = ref([]);
const isDetail = ref(false);
/**
* 获取学校下拉数据
*/
@ -208,12 +218,18 @@ const getSchoolDataFn = () => {
const getSchoolBusinessPeopleList = () => {
getSchoolBusinessPeopleListApi({}).then((res: any) => {
if (res.code == 200) {
peopleOptions.value = res.data || [];
peopleOptions.value = (res.data || []).map(i => {
return {
value: i.text,
text: i.text
};
});
}
});
};
const isShowAddModal = ref(false);
const isShowEditModal = ref(false);
const editModalLoading = ref(false);
const detailData = ref({});
onMounted(() => {
// addOrEdit();
@ -261,7 +277,7 @@ const addOrEdit = () => {
// }
],
isDiscussion: true,
discussion: "开展座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈",
discussion: "开展座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈",
isClassMeeting: true,
classMeeting: "班会情况班会情况班会情况班会情况班会情况班会情况班会情况"
});
@ -270,7 +286,7 @@ const addOrEdit = () => {
const query = reactive({
school: "" as string | undefined,
grade: "" as string | undefined,
people: [] as number[],
people: "" as string | undefined,
solutionEnd: undefined,
times: [] as string[]
});
@ -317,8 +333,8 @@ async function loadList() {
};
if (query.school) payload.schoolId = query.school;
if (query.grade) payload.grade = query.grade;
if (query.people && query.people.length > 0) {
payload.people = query.people.join(",");
if (query.people) {
payload.UserName = query.people;
}
if (typeof query.solutionEnd !== "undefined")
payload.solutionEnd = query.solutionEnd;
@ -359,7 +375,7 @@ function handleSearch() {
function handleReset() {
query.school = "";
query.grade = "";
query.people = [];
query.people = "";
query.solutionEnd = undefined;
query.times = [];
page.value = 1;
@ -387,27 +403,41 @@ function onDelete(row: TableItem) {
});
}
/**
* 获取详情
* 详情或者跟进
* @param row
*/
function onDetail(row: TableItem) {
getSchoolBusinessDetailApi(row.id).then(res => {
if (res.code === 200) {
console.log("详情信息", res.data);
}
});
}
function onFollow(row: TableItem) {
console.log(`跟进`);
function onDetailOrFollow(row: TableItem, disabled = false) {
isDetail.value = disabled;
isShowEditModal.value = true;
detailData;
getSchoolBusinessDetailApi(row.id).then(res => {
if (res.code === 200) {
detailData.value = res.data;
}
});
editModalLoading.value = true;
getSchoolBusinessDetailApi(row.id)
.then(res => {
if (res.code === 200 && res.data) {
detailData.value = res.data;
}
})
.finally(() => {
editModalLoading.value = false;
});
}
/**
* 跟进
* @param row
*/
// function onFollow(row: TableItem) {
// isShowEditModal.value = true;
// editModalLoading.value = true;
// console.log(``);
// getSchoolBusinessDetailApi(row.id)
// .then(res => {
// if (res.code === 200 && res.data) {
// detailData.value = res.data;
// }
// })
// .finally(() => {
// editModalLoading.value = false;
// });
// }
function handleAdd() {
console.log("新建");