feat: 跟进弹窗功能开发中

This commit is contained in:
xiangbo 2025-08-19 18:24:01 +08:00
parent 528dcef08c
commit 73bd653e9d
4 changed files with 556 additions and 113 deletions

View File

@ -47,3 +47,12 @@ export function deleteSchoolBusinessApi(data: Array<string | number>) {
data
});
}
/**
* @description
* @return {object}
*/
export function getSchoolBusinessPeopleListApi(data: object) {
return http.request<Res<any>>("post", `/Admin/QueryCombo`, {
data
});
}

View File

@ -4,6 +4,7 @@
v-model="dialogVisible"
title="新建赴校信息"
width="800px"
@close="onCancel"
align-center
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
@ -55,10 +56,21 @@
</el-col>
<el-col :span="12">
<el-form-item label="赴校人员" prop="baseInfo.people">
<el-input
<el-select
v-model="form.baseInfo.people"
placeholder="请输入人员姓名,逗号分隔"
placeholder="请选择赴校人员"
clearable
multiple
filterable
style="width: 100%"
>
<el-option
v-for="p in peopleOptions"
:key="p.value"
:label="p.text"
:value="p.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
@ -150,12 +162,20 @@
</template>
<script setup lang="ts" name="AddModal">
import { ref, reactive, computed } from "vue";
import { ref, reactive, computed, defineProps, defineEmits, watch } from "vue";
import type { FormInstance, FormRules } from "element-plus";
import { ElMessage } from "element-plus";
import { getSchoolData } from "@/api/userCenter";
import {
getSchoolBusinessPeopleListApi,
addOrEditApi
} from "@/api/toschoolinfomanage";
const props = defineProps<{ visible: boolean }>();
const emit = defineEmits<{ (e: "update:visible", value: boolean): void }>();
// const emit = defineEmits<{ (e: "update:visible", value: boolean): void }>();
const emit = defineEmits<{
(e: "update:visible", value: boolean): void;
(e: "handleReset"): void;
}>();
const dialogVisible = computed({
get: () => props.visible,
set: v => emit("update:visible", v)
@ -166,17 +186,40 @@ function uid() {
return Math.random().toString(36).slice(2) + Date.now().toString(36);
}
const schoolOptions = ref([
{ label: "第一中学", value: "第一中学" },
{ label: "第二中学", value: "第二中学" },
{ label: "实验中学", value: "实验中学" },
{ label: "外国语学校", value: "外国语学校" }
]);
const schoolOptions = ref([]);
const peopleOptions = ref([]);
const getSchoolDataFn = () => {
getSchoolData().then(res => {
if (res.code == 200) {
schoolOptions.value = res.data.map((i: any) => ({
label: i.text,
value: i.value
}));
}
});
};
/**
* 获取赴校人员下拉数据
*/
const getSchoolBusinessPeopleList = () => {
getSchoolBusinessPeopleListApi({}).then((res: any) => {
if (res.code == 200) {
peopleOptions.value = (res.data || []).map(i => ({
label: i.text,
value: i.text
}));
}
});
};
getSchoolDataFn();
getSchoolBusinessPeopleList();
const gradeOptions = [
{ label: "初一", value: "初一" },
{ label: "初二", value: "初二" },
{ label: "初三", value: "初三" },
{ label: "高一", value: "高一" },
{ label: "高二", value: "高二" },
{ label: "高三", value: "高三" },
{ label: "初三", value: "初三" }
{ label: "高三", value: "高三" }
];
type FeedbackKey =
@ -194,7 +237,7 @@ interface FormModel {
school?: string;
grade?: string;
date?: string;
people: string;
people: string[];
};
work: {
talk: boolean;
@ -210,7 +253,7 @@ const form = reactive<FormModel>({
school: undefined,
grade: undefined,
date: undefined,
people: ""
people: []
},
work: {
talk: false,
@ -226,7 +269,18 @@ const form = reactive<FormModel>({
others: []
}
});
watch(
() => form.work.talk,
val => {
!val ? (form.work.talkDetail = "") : "";
}
);
watch(
() => form.work.classMeeting,
val => {
!val ? (form.work.classMeetingDetail = "") : "";
}
);
const rules: FormRules = {
"baseInfo.school": [
{ required: true, message: "请选择学校", trigger: "change" }
@ -238,7 +292,13 @@ const rules: FormRules = {
{ required: true, message: "请选择赴校时间", trigger: "change" }
],
"baseInfo.people": [
{ required: true, message: "请输入赴校人员", trigger: "blur" }
{ required: true, message: "请选择赴校人员", trigger: "change" },
{
type: "array",
min: 1,
message: "请至少选择一名赴校人员",
trigger: "change"
}
]
};
@ -284,8 +344,86 @@ const submitting = ref(false);
function onCancel() {
dialogVisible.value = false;
formRef.value.resetFields();
}
/**
* 把表单数据处理成接口需要的结构
* @param data
*/
const handleFeedback = (data: any) => {
const processData = (items: any[], questionType: number) => {
return items.map((item, idx) => ({
question: item.text,
questionType,
sort: (idx + 1).toString()
}));
};
let handledData = [];
if (data.leaders.length > 0)
handledData.push(...processData(data.leaders, 1));
if (data.classroom.length > 0)
handledData.push(...processData(data.classroom, 10));
if (data.equipment.length > 0)
handledData.push(...processData(data.equipment, 15));
if (data.students.length > 0)
handledData.push(...processData(data.students, 20));
if (data.others.length > 0)
handledData.push(...processData(data.others, 999));
return handledData;
};
let editParams = {
id: 0,
schoolId: 0,
schoolName: "string",
grade: "string",
gradeYear: 0,
gradeLevel: "string",
//
schoolBusinessUser: ["string"],
//
startTime: "2025-08-19T07:20:29.292Z",
//
remark: "string",
//
feedbackQuestions: [
{
//
questionType: 1,
//
sort: "string",
//
question: "string",
//
solution: "string",
//
endTime: "2025-08-19T07:20:29.292Z"
}
],
//
solutionRecord: {
//
solution: "string",
//
endRecord: "string",
//
endRecordTime: "string",
record: [
{
//
executionRecords: "string",
//
executionTime: "2025-08-19T07:20:29.292Z"
}
]
},
//
discussion: "string",
//
classMeeting: "string"
};
async function onSubmit() {
if (!formRef.value) return;
await formRef.value.validate(valid => {
@ -293,13 +431,47 @@ async function onSubmit() {
//
if (!validateFeedbackNotEmpty()) return;
submitting.value = true;
setTimeout(() => {
submitting.value = false;
console.log("Submit payload:", JSON.parse(JSON.stringify(form)));
ElMessage.success("提交成功(已使用假数据)");
console.log("Submit payload:", form);
// enum FeedbackQuestionTypeEnum {
// = 1,
// = 10,
// = 15,
// = 20,
// = 999
// }
let reqParams = {
id: 0, //id0
schoolId: form.baseInfo.school,
schoolName: schoolOptions.value.find(i => i.value == form.baseInfo.school)
.label,
grade: form.baseInfo.grade,
//
gradeLevel: "",
schoolBusinessUser: form.baseInfo.people,
startTime: form.baseInfo.date,
isDiscussion: form.work.talk,
discussion: form.work.talkDetail,
isClassMeeting: form.work.classMeeting,
classMeeting: form.work.classMeetingDetail,
feedbackQuestions: handleFeedback(form.feedback)
};
// return;
console.log("提交数据", reqParams);
addOrEditApi(reqParams)
.then(res => {
if (res.code === 200) {
ElMessage.success("提交成功");
dialogVisible.value = false;
resetForm();
}, 600);
formRef.value.resetFields();
emit("handleReset");
}
})
.finally(() => {
submitting.value = false;
});
// }, 600);
});
}
@ -307,7 +479,7 @@ function resetForm() {
form.baseInfo.school = undefined;
form.baseInfo.grade = undefined;
form.baseInfo.date = undefined;
form.baseInfo.people = "";
form.baseInfo.people = [];
form.work.talk = false;
form.work.talkDetail = "";
form.work.classMeeting = false;

View File

@ -0,0 +1,224 @@
<!-- 跟进 -->
<template>
<el-dialog
v-model="dialogVisible"
title="跟进赴校信息"
width="800px"
@close="closeModal"
align-center
>
<div class="modal-header">
<div class="status-box">
<span>当前状态</span>
<el-tag :type="statusType">{{ statusText }}</el-tag>
</div>
<div class="action-box">
<el-button @click="onClickCancel">取消</el-button>
<el-button type="primary" @click="onClickSave">保存</el-button>
</div>
</div>
<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-divider />
<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>
未解决问题{{ 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>
</div>
<div
v-else
style="margin-top: 5px; font-size: 12px"
class="markTitle"
@click="markTitle"
>
标记已解决
</div>
</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"
maxlength="1000"
:show-word-limit="true"
/>
</el-dialog>
</template>
<script setup lang="ts" name="EditModal">
import { ref, reactive, computed, defineProps, defineEmits, watch } from "vue";
import type { FormInstance, FormRules, TabsPaneContext } from "element-plus";
import { ElMessage } from "element-plus";
import { getSchoolData } from "@/api/userCenter";
import {
getSchoolBusinessPeopleListApi,
addOrEditApi
} from "@/api/toschoolinfomanage";
import { setFips } from "crypto";
const activeName = ref(0);
const handleClick = (tab: TabsPaneContext, event: Event) => {
console.log(tab, event);
};
const props = defineProps<{ visible: boolean; detailData: any }>();
const emit = defineEmits<{
(e: "update:visible", value: boolean): void;
}>();
const dialogVisible = computed({
get: () => props.visible,
set: v => emit("update:visible", v)
});
const closeModal = () => {
emit("update:visible", false);
};
const queType = {
1: "学校领导班子",
10: "双师课堂",
15: "设备",
20: "学生",
999: "其他"
};
/**
* 获取未解决问题数量
* @param data
*/
const handleUnHandleQust = (data: Array<any>) => {
return (data || []).map(i => {
// solution
return !i.solution;
}).length;
};
const sortData = (data: Array<any>) => {
const categorizedData = [
...data
.filter(item => item.questionType === 1)
.sort((a, b) => a.sort.localeCompare(b.sort)),
...data
.filter(item => item.questionType === 10)
.sort((a, b) => a.sort.localeCompare(b.sort)),
...data
.filter(item => item.questionType === 15)
.sort((a, b) => a.sort.localeCompare(b.sort)),
...data
.filter(item => item.questionType === 20)
.sort((a, b) => a.sort.localeCompare(b.sort)),
...data
.filter(item => item.questionType === 999)
.sort((a, b) => a.sort.localeCompare(b.sort))
];
return categorizedData;
};
const safeDetail = computed(() => props.detailData || {});
const statusText = computed(() =>
safeDetail.value?.solutionEnd ? "已完结" : "跟进中"
);
const statusType = computed(() =>
safeDetail.value?.solutionEnd ? "success" : "warning"
);
const markTitle = () => {
console.log("标记已解决");
};
function onClickCancel() {
console.log("取消");
}
function onClickSave() {
console.log("保存");
}
</script>
<style scoped lang="scss">
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.status-box {
display: flex;
align-items: center;
gap: 8px;
}
.action-box {
display: flex;
align-items: center;
gap: 8px;
}
.markTitle {
color: #409eff;
}
.markTitle:hover {
text-decoration: underline;
cursor: pointer;
user-select: none;
}
</style>

View File

@ -34,22 +34,31 @@
</el-select>
</el-form-item>
<el-form-item label="赴校人员">
<el-input
<el-select
v-model="query.people"
placeholder="请输入人员"
placeholder="请选择赴校人员"
clearable
style="width: 180px"
multiple
filterable
style="width: 300px"
>
<el-option
v-for="p in peopleOptions"
:key="p.value"
:label="p.text"
:value="p.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select
v-model="query.status"
v-model="query.solutionEnd"
placeholder="请选择状态"
clearable
style="width: 140px"
>
<el-option label="已完结" :value="1" />
<el-option label="跟进中" :value="2" />
<el-option label="已完结" :value="true" />
<el-option label="跟进中" :value="false" />
</el-select>
</el-form-item>
<el-form-item label="赴校时间">
@ -93,21 +102,38 @@
/>
<el-table-column label="状态" min-width="110">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'warning'">
{{ row.status === 1 ? "已完结" : "跟进中" }}
<el-tag :type="row.solutionEnd ? 'success' : 'warning'">
{{ row.solutionEnd ? "已完结" : "跟进中" }}
</el-tag>
</template>
</el-table-column>
<!-- <el-table-column prop="lastTime" label="最后跟进时间" min-width="160" /> -->
<el-table-column label="操作" fixed="right" min-width="220">
<template #default="{ row }">
<el-button size="small" type="danger" plain @click="onDelete(row)"
<!-- <el-button size="small" type="danger" plain @click="onDelete(row)"
>删除</el-button
> -->
<el-popconfirm
confirm-button-text="确定"
cancel-button-text="取消"
icon-color="#626AEF"
title="确定删除吗?"
@confirm="onDelete(row)"
>
<template #reference>
<el-button type="danger" size="small">删除</el-button>
</template>
</el-popconfirm>
<el-button size="small" type="primary" plain @click="onDetail(row)"
>详情</el-button
>
<el-button size="small" type="success" plain @click="onFollow(row)"
<el-button
v-if="!row.solutionEnd"
size="small"
type="success"
plain
@click="onFollow(row)"
>跟进</el-button
>
</template>
@ -128,21 +154,26 @@
/>
</div>
</div>
<AddModal v-model:visible="isShowAddModal" />
<!-- 新建 -->
<AddModal v-model:visible="isShowAddModal" @handleReset="handleReset" />
<!-- 跟进 -->
<EditModal v-model:visible="isShowEditModal" :detailData="detailData" />
</template>
<!-- 赴校信息管理菜单 -->
<script setup lang="ts" name="Toschoolinfomanage">
import {
addOrEditApi,
getenumApi,
getPageListApi,
getSchoolBusinessDetailApi,
deleteSchoolBusinessApi
deleteSchoolBusinessApi,
getSchoolBusinessPeopleListApi
} from "@/api/toschoolinfomanage";
import { getSchoolData } from "@/api/userCenter";
import { ref, reactive, computed, onMounted } from "vue";
import dayjs from "dayjs";
import { ElMessage } from "element-plus";
import AddModal from "./addModal.vue";
import EditModal from "./editModal.vue";
import { message } from "@/utils/message";
interface TableItem {
id: number;
@ -152,99 +183,97 @@ interface TableItem {
times: string; // YYYY-MM-DD
feedbackTotals: number;
solveTotals: number;
status: 1 | 2; // 1: , 2:
solutionEnd: boolean; // true: , false:
lastTime: string; // YYYY-MM-DD
}
const schoolOptions = ref([
{ label: "第一中学", value: "第一中学" },
{ label: "第二中学", value: "第二中学" },
{ label: "实验中学", value: "实验中学" },
{ label: "外国语学校", value: "外国语学校" }
]);
const isShowAddModal = ref(false);
onMounted(() => {
getenumApi({ TextName: "Name", ValueName: "Id" }).then(res => {
if (res.code === 200) {
schoolOptions.value = res.data.map(i => ({
const schoolOptions = ref([]);
const peopleOptions = ref([]);
/**
* 获取学校下拉数据
*/
const getSchoolDataFn = () => {
getSchoolData().then(res => {
if (res.code == 200) {
schoolOptions.value = res.data.map((i: any) => ({
label: i.text,
value: i.value
}));
}
});
};
/**
* 获取赴校人员下拉数据
*/
const getSchoolBusinessPeopleList = () => {
getSchoolBusinessPeopleListApi({}).then((res: any) => {
if (res.code == 200) {
peopleOptions.value = res.data || [];
}
});
};
const isShowAddModal = ref(false);
const isShowEditModal = ref(false);
const detailData = ref({});
onMounted(() => {
// addOrEdit();
loadList();
getSchoolDataFn();
getSchoolBusinessPeopleList();
});
const gradeOptions = [
{ label: "初一", value: "初一" },
{ label: "初二", value: "初二" },
{ label: "初三", value: "初三" },
{ label: "高一", value: "高一" },
{ label: "高二", value: "高二" },
{ label: "高三", value: "高三" },
{ label: "初三", value: "初三" }
{ label: "高三", value: "高三" }
];
/**
* 新建赴校信息提交
*/
const addOrEdit = () => {
addOrEditApi(
addOrEditApi({
id: 0,
schoolId: 708490619039814,
schoolName: "重庆测试学校",
id: 0, //id0
schoolId: 10079,
schoolName: "系统测试学校",
grade: "初二",
schoolBusinessUser: ["向波4"],
startTime: "2025-08-20T07:28:38",
gradeLevel: "",
schoolBusinessUser: ["刘德华123"],
startTime: "2025-07-24T07:28:38",
// remark: "string",
feedbackQuestions: [
{
endTime: null,
question: "xb测试反馈问题1双师课堂",
questionType: 10,
solution: "xb测试反馈问题1建议",
sort: "1"
sort: "1111111111"
}
// {
// endTime: null,
// question: "xb2",
// questionType: 15,
// solution: "xb2",
// sort: "2"
// },
// {
// endTime: null,
// question: "xb2",
// questionType: 20,
// solution: "xb3",
// sort: "3"
// }
],
// solutionRecord: {
// solution: "string",
// endRecord: "string",
// record: [
// {
// executionRecords: "string",
// executionTime: "2025-08-18T08:31:52.716Z"
// }
// ]
// },
// solutionEnd: true,
isDiscussion: false,
isDiscussion: true,
discussion: "开展座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈",
isClassMeeting: false,
isClassMeeting: true,
classMeeting: "班会情况班会情况班会情况班会情况班会情况班会情况班会情况"
})
);
});
};
const query = reactive({
school: "" as string | undefined,
grade: "" as string | undefined,
people: "",
status: undefined as 1 | 2 | undefined,
people: [] as number[],
solutionEnd: undefined,
times: [] as string[]
});
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
@ -275,7 +304,7 @@ function mapApiItemToRow(item: any): TableItem {
times: start,
feedbackTotals: Number(item.feedbackCount) || 0,
solveTotals: Number(item.solveFeedbackCount) || 0,
status: item.solutionEnd ? 1 : 2,
solutionEnd: item.solutionEnd,
lastTime: last
};
}
@ -288,12 +317,14 @@ async function loadList() {
};
if (query.school) payload.schoolId = query.school;
if (query.grade) payload.grade = query.grade;
if (query.people) payload.people = query.people.trim();
if (typeof query.status !== "undefined")
payload.solutionEnd = query.status === 1;
if (query.people && query.people.length > 0) {
payload.people = query.people.join(",");
}
if (typeof query.solutionEnd !== "undefined")
payload.solutionEnd = query.solutionEnd;
if (Array.isArray(query.times) && query.times.length === 2) {
payload.startTimeBegin = query.times[0];
payload.startTimeEnd = query.times[1];
payload.startTime = query.times[0];
payload.endTime = query.times[1];
}
try {
const res = await getPageListApi(payload);
@ -328,8 +359,8 @@ function handleSearch() {
function handleReset() {
query.school = "";
query.grade = "";
query.people = "";
query.status = undefined;
query.people = [];
query.solutionEnd = undefined;
query.times = [];
page.value = 1;
loadList();
@ -369,6 +400,13 @@ function onDetail(row: TableItem) {
function onFollow(row: TableItem) {
console.log(`跟进`);
isShowEditModal.value = true;
detailData;
getSchoolBusinessDetailApi(row.id).then(res => {
if (res.code === 200) {
detailData.value = res.data;
}
});
}
function handleAdd() {