xiangbo #1

Merged
hy merged 9 commits from xiangbo into main 2025-08-21 09:21:36 +08:00
17 changed files with 1649 additions and 43 deletions

View File

@ -6,7 +6,9 @@ VITE_PUBLIC_PATH = /
# 开发环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数" # 开发环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash" VITE_ROUTER_HISTORY = "hash"
VITE_API_BASEURL = "http://192.168.2.33:5199/api"
# 接口地址 # 接口地址
VITE_API_BASEURL = "http://localhost:5199/api" # VITE_API_BASEURL = "http://localhost:5199/api"
#数据中心后台地址 #数据中心后台地址
VITE_API_USERCENTER_URL = "https://dca.w.23544.com:8843/api" VITE_API_USERCENTER_URL = "https://dca.w.23544.com:8843/api"

View File

@ -1,8 +0,0 @@
#!/bin/sh
# shellcheck source=./_/husky.sh
. "$(dirname "$0")/_/husky.sh"
PATH="/usr/local/bin:$PATH"
npx --no-install commitlint --edit "$1"

View File

@ -1,9 +0,0 @@
#!/bin/sh
command_exists () {
command -v "$1" >/dev/null 2>&1
}
# Workaround for Windows 10, Git Bash and Pnpm
if command_exists winpty && test -t 1; then
exec < /dev/tty
fi

View File

@ -1,10 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
[ -n "$CI" ] && exit 0
PATH="/usr/local/bin:$PATH"
# Perform lint check on files in the staging area through .lintstagedrc configuration
pnpm exec lint-staged

View File

@ -1,5 +1,5 @@
import { http } from "@/utils/http"; import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types"; // import type { Res } from "@/utils/http/types";
/** /**
* @description * @description

View File

@ -0,0 +1,58 @@
import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types";
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getenumApi(data) {
return http.request<Res<any>>("post", `/SchoolBusiness/QueryCombo`, {
data
});
}
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getPageListApi(data) {
return http.request<Res<any>>("post", `/SchoolBusiness/QueryPageList`, {
data
});
}
/**
* @description id:0()
* @return {object}
*/
export function addOrEditApi(data: any) {
return http.request<Res<any>>("post", `/SchoolBusiness/Edit`, {
data
});
}
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getSchoolBusinessDetailApi(id: string | number) {
return http.request<Res<any>>("get", `/SchoolBusiness/${id}`);
}
/**
* @description
* @return {object}
*/
export function deleteSchoolBusinessApi(data: Array<string | number>) {
return http.request<Res<any>>("post", `/SchoolBusiness/Del`, {
data
});
}
/**
* @description
* @return {object}
*/
export function getSchoolBusinessPeopleListApi(data: object) {
return http.request<Res<any>>("post", `/Admin/QueryCombo`, {
data
});
}

View File

@ -587,26 +587,28 @@ function fetchPagedData() {
.columnTemplate .btn { .columnTemplate .btn {
margin-left: 0 !important; margin-left: 0 !important;
} }
.columnTemplate { .columnTemplate {
display: flex; display: flex;
flex-direction: row;
flex-flow: row wrap;
gap: 5px; gap: 5px;
place-content: flex-start flex-start;
align-items: center;
-webkit-box-orient: horizontal; -webkit-box-orient: horizontal;
-webkit-box-direction: normal; -webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
align-content: flex-start;
} }
.sty .tab_tip { .sty .tab_tip {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.toolbar-container { .toolbar-container {
padding-bottom: 5px; padding-bottom: 5px;
} }
.maxWidth600px { .maxWidth600px {
max-width: 600px; max-width: 600px;
} }

View File

@ -13,7 +13,7 @@ import { stringify } from "qs";
import NProgress from "../progress"; import NProgress from "../progress";
import { getToken, formatToken } from "@/utils/auth"; import { getToken, formatToken } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from "@/store/modules/user";
import { string } from "vue-types"; // import { string } from "vue-types";
import router from "@/router"; import router from "@/router";
/**请求后端的地址 未配置则访问BaseURL */ /**请求后端的地址 未配置则访问BaseURL */

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import ahTable from "@/components/hTable/index.vue"; import ahTable from "@/components/hTable/index.vue";
import { ConditionalType, TableConfig } from "@/components/hTable/hTable"; import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref } from "vue"; import { onMounted, ref, defineOptions } from "vue";
import { fa } from "element-plus/es/locales.mjs"; import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable"; import { hTableAPI } from "@/api/hTable";
import { import {

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import ahTable from "@/components/hTable/index.vue"; import ahTable from "@/components/hTable/index.vue";
import { ConditionalType, TableConfig } from "@/components/hTable/hTable"; import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref } from "vue"; import { onMounted, ref,defineOptions } from "vue";
import { fa } from "element-plus/es/locales.mjs"; import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable"; import { hTableAPI } from "@/api/hTable";
const ControllerName = "AdminRole"; const ControllerName = "AdminRole";

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import ahTable from "@/components/hTable/index.vue"; import ahTable from "@/components/hTable/index.vue";
import { ConditionalType, TableConfig } from "@/components/hTable/hTable"; import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref } from "vue"; import { onMounted, ref, defineOptions } from "vue";
import { fa } from "element-plus/es/locales.mjs"; import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable"; import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum"; import { getenum } from "@/api/enum";

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import ahTable from "@/components/hTable/index.vue"; import ahTable from "@/components/hTable/index.vue";
import { ConditionalType, TableConfig } from "@/components/hTable/hTable"; import { ConditionalType, TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref } from "vue"; import { onMounted, ref, defineOptions } from "vue";
import { fa } from "element-plus/es/locales.mjs"; import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable"; import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum"; import { getenum } from "@/api/enum";

View File

@ -96,5 +96,7 @@ const tableData: TableConfig = {
</script> </script>
<template> <template>
<div><ahTable ref="table" :tableConfig="tableData" /></div> <div>
<ahTable ref="table" :tableConfig="tableData" />
</div>
</template> </template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts" name="Testxb">
import { ref } from "vue";
// defineOptions({
// name: "Testxb"
// });
let name = ref("nihao");
</script>
<template>
<div>测试菜单</div>
</template>

View File

@ -0,0 +1,526 @@
<!-- 新建赴校信息 -->
<template>
<el-dialog
v-model="dialogVisible"
title="新建"
width="800px"
@close="onCancel"
align-center
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-divider>基础信息</el-divider>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="学校" prop="baseInfo.school">
<el-select
v-model="form.baseInfo.school"
placeholder="请选择学校"
clearable
filterable
>
<el-option
v-for="s in schoolOptions"
:key="s.value"
:label="s.label"
:value="s.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="年级" prop="baseInfo.grade">
<el-select
v-model="form.baseInfo.grade"
placeholder="请选择年级"
clearable
>
<el-option
v-for="g in gradeOptions"
:key="g.value"
:label="g.label"
:value="g.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="赴校时间" prop="baseInfo.date">
<el-date-picker
v-model="form.baseInfo.date"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="赴校人员" prop="baseInfo.people">
<el-select
v-model="form.baseInfo.people"
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>
<el-divider>基础工作</el-divider>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="开展座谈" prop="work.talk">
<el-radio-group v-model="form.work.talk">
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="座谈情况">
<el-input
v-model="form.work.talkDetail"
:disabled="!form.work.talk"
placeholder="请输入座谈情况"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开展班会" prop="work.classMeeting">
<el-radio-group v-model="form.work.classMeeting">
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="班会情况">
<el-input
v-model="form.work.classMeetingDetail"
:disabled="!form.work.classMeeting"
placeholder="请输入班会情况"
/>
</el-form-item>
</el-col>
</el-row>
<el-divider>反馈问题</el-divider>
<div style="height: 370px; overflow-y: auto">
<div
v-for="group in feedbackGroups"
:key="group.key"
class="feedback-group"
>
<div class="feedback-header">
<span class="group-title">{{ group.name }}</span>
<el-button type="primary" link @click="addProblem(group.key)"
>添加问题</el-button
>
</div>
<div
v-if="form.feedback[group.key].length === 0"
class="feedback-empty"
>
暂无问题
</div>
<div
v-for="(item, idx) in form.feedback[group.key]"
:key="item.id"
class="feedback-item"
>
<el-input
v-model="item.text"
:placeholder="`请输入${group.name}问题描述`"
/>
<el-button type="danger" text @click="removeProblem(group.key, idx)"
>删除</el-button
>
</div>
<el-divider />
</div>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel"> </el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit"
> </el-button
>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="AddModal">
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;
(e: "handleReset"): void;
}>();
const dialogVisible = computed({
get: () => props.visible,
set: v => emit("update:visible", v)
});
const formRef = ref<FormInstance>();
function uid() {
return Math.random().toString(36).slice(2) + Date.now().toString(36);
}
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: "高三" }
];
type FeedbackKey =
| "leaders"
| "classroom"
| "equipment"
| "students"
| "others";
interface FeedbackItem {
id: string;
text: string;
}
interface FormModel {
baseInfo: {
school?: string;
grade?: string;
date?: string;
people: string[];
};
work: {
talk: boolean;
talkDetail: string;
classMeeting: boolean;
classMeetingDetail: string;
};
feedback: Record<FeedbackKey, FeedbackItem[]>;
}
const form = reactive<FormModel>({
baseInfo: {
school: undefined,
grade: undefined,
date: undefined,
people: []
},
work: {
talk: false,
talkDetail: "",
classMeeting: false,
classMeetingDetail: ""
},
feedback: {
leaders: [],
classroom: [],
equipment: [],
students: [],
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" }
],
"baseInfo.grade": [
{ required: true, message: "请选择年级", trigger: "change" }
],
"baseInfo.date": [
{ required: true, message: "请选择赴校时间", trigger: "change" }
],
"baseInfo.people": [
{ required: true, message: "请选择赴校人员", trigger: "change" },
{
type: "array",
min: 1,
message: "请至少选择一名赴校人员",
trigger: "change"
}
]
};
const feedbackGroups = [
{ key: "leaders", name: "学校领导班子" },
{ key: "classroom", name: "双师课堂" },
{ key: "equipment", name: "设备" },
{ key: "students", name: "学生" },
{ key: "others", name: "其他" }
] as { key: FeedbackKey; name: string }[];
function addProblem(key: FeedbackKey) {
form.feedback[key].push({ id: uid(), text: "" });
}
function removeProblem(key: FeedbackKey, index: number) {
form.feedback[key].splice(index, 1);
}
//
const feedbackGroupNameMap: Record<FeedbackKey, string> = {
leaders: "学校领导班子",
classroom: "双师课堂",
equipment: "设备",
students: "学生",
others: "其他"
};
function validateFeedbackNotEmpty() {
for (const g of feedbackGroups) {
const items = form.feedback[g.key] || [];
if (items.length === 0) continue; // 0
for (const it of items) {
if (!it.text || !it.text.trim()) {
ElMessage.error(`${feedbackGroupNameMap[g.key]} 的问题内容不能为空`);
return false;
}
}
}
return true;
}
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 => {
if (!valid) return;
//
if (!validateFeedbackNotEmpty()) return;
submitting.value = true;
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();
formRef.value.resetFields();
emit("handleReset");
}
})
.finally(() => {
submitting.value = false;
});
// }, 600);
});
}
function resetForm() {
form.baseInfo.school = undefined;
form.baseInfo.grade = undefined;
form.baseInfo.date = undefined;
form.baseInfo.people = [];
form.work.talk = false;
form.work.talkDetail = "";
form.work.classMeeting = false;
form.work.classMeetingDetail = "";
(Object.keys(form.feedback) as FeedbackKey[]).forEach(k => {
form.feedback[k] = [];
});
}
</script>
<style scoped lang="scss">
.feedback-group {
margin-bottom: 8px;
}
.feedback-header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 6px 0;
}
.group-title {
font-weight: 600;
}
.feedback-item {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 6px;
}
.feedback-empty {
margin: 4px 0 8px;
font-size: 13px;
color: #999;
}
:deep(.el-dialog__body) {
max-height: 70vh;
overflow-y: auto;
}
</style>

View File

@ -0,0 +1,564 @@
<!-- 跟进 -->
<template>
<el-dialog
v-model="dialogVisible"
:title="isDetail ? '详情' : '跟进'"
width="800px"
@close="closeModal"
align-center
>
<!-- {{ isDetail }} -->
<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 :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-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> 未解决问题{{ 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"
>
<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" 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"
:disabled="isDetail"
@click="addRecord"
>
添加执行记录
</el-button>
<el-button
style="margin-top: 5px; font-size: 12px"
class="markTitle"
type="text"
:disabled="isDetail"
@click="addFinish"
>
{{ finishRecord ? "修改完结情况" : "添加完结情况" }}
</el-button>
</div>
<!-- 执行记录列表 -->
<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>
<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<any>(0);
const handleClick = (tab: TabsPaneContext, event: Event) => {
console.log(tab.props.name, event);
activeName.value = tab.props.name;
};
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)
});
const closeModal = () => {
emit("update:visible", false);
activeName.value = 0;
};
const queType = {
1: "学校领导班子",
10: "双师课堂",
15: "设备",
20: "学生",
999: "其他"
};
/**
* 获取未解决问题数量
* @param data
*/
const handleUnHandleQust = (data: Array<any>) => {
return (data || []).filter(i => !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 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("保存", 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>
<style scoped lang="scss">
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 10px;
border-bottom: 1px solid #ccc;
margin-bottom: 10px;
}
.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

@ -0,0 +1,468 @@
<template>
<div style="padding: 20px">
<!-- 搜索区域 -->
<el-form :model="query" inline label-width="80px" class="search-form">
<el-form-item label="学校">
<el-select
v-model="query.school"
placeholder="请选择学校"
clearable
filterable
style="width: 180px"
>
<el-option
v-for="s in schoolOptions"
:key="s.value"
:label="s.label"
:value="s.value"
/>
</el-select>
</el-form-item>
<el-form-item label="年级">
<el-select
v-model="query.grade"
placeholder="请选择年级"
clearable
style="width: 140px"
>
<el-option
v-for="g in gradeOptions"
:key="g.value"
:label="g.label"
:value="g.value"
/>
</el-select>
</el-form-item>
<el-form-item label="赴校人员">
<el-select
v-model="query.people"
placeholder="请选择赴校人员"
clearable
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.solutionEnd"
placeholder="请选择状态"
clearable
style="width: 140px"
>
<el-option label="已完结" :value="true" />
<el-option label="跟进中" :value="false" />
</el-select>
</el-form-item>
<el-form-item label="赴校时间">
<el-date-picker
v-model="query.times"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
unlink-panels
style="width: 300px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作按钮区域 -->
<div style="margin-bottom: 10px">
<el-button type="primary" @click="handleAdd">新建</el-button>
<el-button type="success" @click="handleImport">批量导入</el-button>
<el-button type="info" @click="handleExport">导出</el-button>
<el-button type="info" @click="downLoadTpl">下载模版</el-button>
</div>
<!-- 表格区域 -->
<el-table :data="listData" border style="width: 100%">
<el-table-column prop="school" label="学校" min-width="140" />
<el-table-column prop="grade" label="年级" min-width="100" />
<el-table-column prop="people" label="赴校人员" min-width="120" />
<el-table-column prop="times" label="赴校时间" min-width="140" />
<el-table-column
prop="feedbackTotals"
label="反馈问题数量"
min-width="140"
/>
<el-table-column
prop="solveTotals"
label="解决问题数量"
min-width="140"
/>
<el-table-column label="状态" min-width="110">
<template #default="{ row }">
<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
> -->
<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="onDetailOrFollow(row, true)"
>详情</el-button
>
<el-button
v-if="!row.solutionEnd"
size="small"
type="success"
plain
@click="onDetailOrFollow(row, false)"
>跟进</el-button
>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pager">
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<!-- 新建 -->
<AddModal v-model:visible="isShowAddModal" @handleReset="handleReset" />
<!-- 跟进 -->
<EditModal
v-model:visible="isShowEditModal"
:editModalLoading="editModalLoading"
:detailData="detailData"
:isDetail="isDetail"
@handleReset="handleReset"
/>
</template>
<!-- 赴校信息管理菜单 -->
<script setup lang="ts" name="Toschoolinfomanage">
import {
addOrEditApi,
getPageListApi,
getSchoolBusinessDetailApi,
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;
school: string;
grade: string;
people: string;
times: string; // YYYY-MM-DD
feedbackTotals: number;
solveTotals: number;
solutionEnd: boolean; // true: , false:
lastTime: string; // YYYY-MM-DD
}
const schoolOptions = ref([]);
const peopleOptions = ref([]);
const isDetail = ref(false);
/**
* 获取学校下拉数据
*/
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 => {
return {
value: i.text,
text: i.text
};
});
}
});
};
const isShowAddModal = ref(false);
const isShowEditModal = ref(false);
const editModalLoading = ref(false);
const detailData = ref({});
onMounted(() => {
// addOrEdit();
loadList();
getSchoolDataFn();
getSchoolBusinessPeopleList();
});
const gradeOptions = [
{ label: "初一", value: "初一" },
{ label: "初二", value: "初二" },
{ label: "初三", value: "初三" },
{ label: "高一", value: "高一" },
{ label: "高二", value: "高二" },
{ label: "高三", value: "高三" }
];
/**
* 新建赴校信息提交
*/
const addOrEdit = () => {
addOrEditApi({
id: 0, //id0
schoolId: 10079,
schoolName: "系统测试学校",
grade: "初二",
gradeLevel: "",
schoolBusinessUser: ["刘德华123"],
startTime: "2025-07-24T07:28:38",
// remark: "string",
feedbackQuestions: [
{
question: "xb测试反馈问题1双师课堂",
questionType: 10,
sort: "1111111111"
}
// {
// question: "xb2",
// questionType: 15,
// sort: "2"
// },
// {
// question: "xb2",
// questionType: 20,
// sort: "3"
// }
],
isDiscussion: true,
discussion: "开展座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈座谈",
isClassMeeting: true,
classMeeting: "班会情况班会情况班会情况班会情况班会情况班会情况班会情况"
});
};
const query = reactive({
school: "" as string | undefined,
grade: "" as string | undefined,
people: "" as string | undefined,
solutionEnd: undefined,
times: [] as string[]
});
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
const listData = ref<TableItem[]>([]);
function mapApiItemToRow(item: any): TableItem {
const peopleArr = Array.isArray(item.schoolBusinessUser)
? item.schoolBusinessUser
: [];
const start = item.startTime
? dayjs(item.startTime).format("YYYY-MM-DD")
: "";
let last = start;
const rec = item.solutionRecord?.record || [];
if (Array.isArray(rec) && rec.length > 0) {
const maxTs = rec
.map((r: any) => r.executionTime)
.filter(Boolean)
.map((t: string) => dayjs(t).valueOf())
.reduce((a: number, b: number) => Math.max(a, b), 0);
if (maxTs) last = dayjs(maxTs).format("YYYY-MM-DD");
}
return {
id: item.id,
school: item.schoolName || "",
grade: item.grade || "",
people: peopleArr.join(""),
times: start,
feedbackTotals: Number(item.feedbackCount) || 0,
solveTotals: Number(item.solveFeedbackCount) || 0,
solutionEnd: item.solutionEnd,
lastTime: last
};
}
async function loadList() {
const payload: any = {
pageIndex: page.value,
pageSize: pageSize.value,
orderBy: "startTime"
};
if (query.school) payload.schoolId = query.school;
if (query.grade) payload.grade = query.grade;
if (query.people) {
payload.UserName = query.people;
}
if (typeof query.solutionEnd !== "undefined")
payload.solutionEnd = query.solutionEnd;
if (Array.isArray(query.times) && query.times.length === 2) {
payload.startTime = query.times[0];
payload.endTime = query.times[1];
}
try {
const res = await getPageListApi(payload);
if (res.code === 200) {
const rows = Array.isArray(res.data?.data) ? res.data.data : [];
listData.value = rows.map(mapApiItemToRow);
total.value = Number(res.data?.total) || rows.length;
} else {
ElMessage.error(res.message || "加载失败");
}
} catch (e) {
ElMessage.error("列表加载失败");
}
}
function rand(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomDate(start: string, end: string) {
const startTs = dayjs(start).valueOf();
const endTs = dayjs(end).valueOf();
const v = rand(startTs, endTs);
return dayjs(v).format("YYYY-MM-DD");
}
function handleSearch() {
page.value = 1;
loadList();
}
function handleReset() {
query.school = "";
query.grade = "";
query.people = "";
query.solutionEnd = undefined;
query.times = [];
page.value = 1;
loadList();
}
function handlePageChange(p: number) {
page.value = p;
loadList();
}
function handleSizeChange(s: number) {
pageSize.value = s;
page.value = 1;
loadList();
}
function onDelete(row: TableItem) {
console.log(`删除`, row);
deleteSchoolBusinessApi([row.id]).then(res => {
if (res.code === 200) {
message("删除成功", { type: "success" });
loadList();
}
});
}
/**
* 详情或者跟进
* @param row
*/
function onDetailOrFollow(row: TableItem, disabled = false) {
isDetail.value = disabled;
isShowEditModal.value = true;
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("新建");
isShowAddModal.value = true;
}
function handleImport() {
console.log("批量导入");
}
function handleExport() {
console.log("导出");
}
function downLoadTpl() {
console.log("下载模版");
}
</script>
<style lang="scss" scoped>
.search-form {
margin-bottom: 12px;
}
.pager {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
</style>