新增签到管理
This commit is contained in:
parent
48c2f73396
commit
bc28a17e01
@ -58,6 +58,7 @@
|
||||
"lodash": "4.17.21",
|
||||
"pinia": "2.0.23",
|
||||
"pinia-plugin-persist-uni": "1.2.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"uview-plus": "3.1.20",
|
||||
"vconsole": "3.15.1",
|
||||
"vue": "3.2.45",
|
||||
@ -72,6 +73,7 @@
|
||||
"@types/html5plus": "1.0.2",
|
||||
"@types/lodash": "4.14.191",
|
||||
"@types/node": "18.11.15",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/uni-app": "1.4.4",
|
||||
"sass": "1.56.0",
|
||||
"sass-loader": "10.1.1",
|
||||
|
||||
@ -64,6 +64,26 @@ export const jsdfindJsByPhoneApi = async (params: any) => {
|
||||
return await get("/api/js/findJsByPhone", params);
|
||||
};
|
||||
|
||||
// 根据年级/班级ID查询教师
|
||||
export const jsFindByNjIdOrBjIdApi = async (params: { njOrBjId: string }) => {
|
||||
return await get(`/api/js/findByNjIdOrBjId?njOrBjId=${params.njOrBjId}`);
|
||||
};
|
||||
|
||||
// 根据科目ID查询教师
|
||||
export const jsFindByKmIdApi = async (params: { kmId: string }) => {
|
||||
return await get(`/api/js/findByKmId?kmId=${params.kmId}`);
|
||||
};
|
||||
|
||||
// 根据班主任年级ID查询教师
|
||||
export const jsFindByBzrNjIdApi = async (params: { njId: string }) => {
|
||||
return await get(`/api/js/findByBzrNjId?njId=${params.njId}`);
|
||||
};
|
||||
|
||||
// 根据职务ID查询教师
|
||||
export const jsFindByZwIdApi = async (params: { zwId: string; zwType: string }) => {
|
||||
return await get(`/api/js/findByZwId?zwId=${params.zwId}&zwType=${params.zwType}`);
|
||||
};
|
||||
|
||||
// 选课列表
|
||||
export const jsdXkListApi = async (params: any) => {
|
||||
return await get("/mobile/js/xk/list", params);
|
||||
@ -208,3 +228,64 @@ export const drpkkbApi = async (params: any) => {
|
||||
export const getRzRjApi = async (params: any) => {
|
||||
return await get("/mobile/js/getRzRj", params);
|
||||
};
|
||||
|
||||
// 签到发布相关API
|
||||
// 获取签到发布列表
|
||||
export const qdzxFindPageApi = async (params: any) => {
|
||||
return await get("/api/qdzx/findPage", params);
|
||||
};
|
||||
|
||||
// 保存签到发布(新增/编辑)
|
||||
export const qdzxSaveApi = async (params: any) => {
|
||||
return await post("/api/qdzx/save", params);
|
||||
};
|
||||
|
||||
// 根据签到ID查询签到执行情况(教师列表)
|
||||
export const qdzxFindByQdParamsApi = async (params: { qdId: string }) => {
|
||||
return await get("/api/qdzx/findByQdParams", params);
|
||||
};
|
||||
|
||||
export const qdzxFindByQdAndJsApi = async (params: { qdId: string; jsId: string }) => {
|
||||
return await get("/api/qdzx/findByQdAndJs", params);
|
||||
};
|
||||
|
||||
// 保存签到消息推送
|
||||
export const xxtsSaveByQdzxParamsApi = async (params: { qdId: string }) => {
|
||||
return await post("/api/xxts/saveByQdzxParams", params);
|
||||
};
|
||||
|
||||
// 签到主表相关API
|
||||
// 获取签到列表
|
||||
export const qdFindPageApi = async (params: any) => {
|
||||
return await get("/api/qd/findPage", params);
|
||||
};
|
||||
|
||||
// 保存签到(新增/编辑)
|
||||
export const qdSaveApi = async (params: any) => {
|
||||
return await post("/api/qd/save", params);
|
||||
};
|
||||
|
||||
// 根据ID获取签到详情
|
||||
export const qdFindByIdApi = async (params: { id: string }) => {
|
||||
return await get(`/api/qd/findById?id=${params.id}`);
|
||||
};
|
||||
|
||||
// 签到执行相关API
|
||||
export const qdzxSignInApi = async (params: any) => {
|
||||
return await post("/api/qdzx/signIn", params);
|
||||
};
|
||||
|
||||
// 生成签到二维码
|
||||
export const generateQRCodeApi = async (params: { qdId: string }) => {
|
||||
return await post("/api/qdzx/generateQRCode", params);
|
||||
};
|
||||
|
||||
// 年级相关API
|
||||
export const findAllNj = async () => {
|
||||
return await get("/api/nj/findAllNj");
|
||||
};
|
||||
|
||||
// 职务相关API
|
||||
export const zwFindAllApi = async () => {
|
||||
return await get("/api/zw/findAll");
|
||||
};
|
||||
|
||||
@ -493,6 +493,50 @@
|
||||
"navigationBarTitleText": "学生请假审批",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/qd/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "签到发布"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/qd/publish",
|
||||
"style": {
|
||||
"navigationBarTitleText": "新增签到"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/qd/push-list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "推送清单"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/qd/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "签到详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/qd/selectTeachers",
|
||||
"style": {
|
||||
"navigationBarTitleText": "选择教师"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/qd/qr-code",
|
||||
"style": {
|
||||
"navigationBarTitleText": "签到二维码",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/qd/confirm",
|
||||
"style": {
|
||||
"navigationBarTitleText": "确认签到",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
|
||||
@ -232,6 +232,13 @@ const sections = reactive<Section[]>([
|
||||
show: true,
|
||||
path: "/pages/view/notice/index",
|
||||
},
|
||||
{
|
||||
id: "r10",
|
||||
icon: "draftfill",
|
||||
text: "签到发布",
|
||||
show: true,
|
||||
path: "/pages/view/routine/qd/index",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -295,7 +295,7 @@ const signatureStatusText = computed(() => {
|
||||
});
|
||||
|
||||
// 树形数据
|
||||
const treeData = ref<any[]>([]);
|
||||
const treeData = ref([]);
|
||||
const treeRef = ref();
|
||||
|
||||
// 学生显示相关
|
||||
@ -310,23 +310,19 @@ const displayNames = computed(() => {
|
||||
// 添加 ref 来引用封面上传组件
|
||||
const coverUploadRef = ref();
|
||||
|
||||
// 统一接口返回类型声明
|
||||
interface ApiResponse<T = any> {
|
||||
resultCode: number;
|
||||
result?: T;
|
||||
resultMsg?: string;
|
||||
data?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 加载树形数据
|
||||
const loadTreeData = async () => {
|
||||
try {
|
||||
const res = await findAllNjBjTree() as ApiResponse<any[]>;
|
||||
if (res && res.resultCode === 1 && res.result) {
|
||||
const res = await findAllNjBjTree();
|
||||
if (res.resultCode === 1 && res.result) {
|
||||
// 添加调试信息,查看原始数据
|
||||
console.log("=== 后端返回的原始树形数据 ===");
|
||||
console.log("res.result:", res.result);
|
||||
console.log("================================");
|
||||
|
||||
// 递归转换数据格式以适配 BasicTree 组件,同时保留 njmcId 字段
|
||||
const convertTreeData = (items: Array<{ key: string; title: string; njmcId?: string; children?: any[] }>): any[] => {
|
||||
return items.map((item) => ({
|
||||
const convertTreeData = (items: any[]): any[] => {
|
||||
return items.map((item: any) => ({
|
||||
key: item.key,
|
||||
title: item.title,
|
||||
njmcId: item.njmcId, // 保留 njmcId 字段
|
||||
@ -335,6 +331,11 @@ const loadTreeData = async () => {
|
||||
};
|
||||
|
||||
treeData.value = convertTreeData(res.result);
|
||||
|
||||
// 添加调试信息,查看转换后的数据
|
||||
console.log("=== 转换后的树形数据 ===");
|
||||
console.log("treeData.value:", treeData.value);
|
||||
console.log("==========================");
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({ title: "加载班级数据失败", icon: "error" });
|
||||
@ -408,7 +409,9 @@ const resetForm = () => {
|
||||
const loadJlData = async (jlId: string) => {
|
||||
try {
|
||||
uni.showLoading({ title: "加载数据中..." });
|
||||
const response = await jlFindByIdApi({ id: jlId }) as ApiResponse<any>;
|
||||
|
||||
// 调用获取接龙详情的接口
|
||||
const response = await jlFindByIdApi({ id: jlId });
|
||||
|
||||
if (response && response.resultCode === 1 && response.result) {
|
||||
const jlData = response.result;
|
||||
@ -426,10 +429,10 @@ const loadJlData = async (jlId: string) => {
|
||||
// 处理附件
|
||||
if (jlData.jlfj) {
|
||||
const attachmentUrls = jlData.jlfj.split(",");
|
||||
formData.attachments = attachmentUrls.map((url: string) => ({
|
||||
formData.attachments = attachmentUrls.map(url => ({
|
||||
name: url.split("/").pop() || "附件",
|
||||
type: "file",
|
||||
url: url as string
|
||||
url: url
|
||||
}));
|
||||
}
|
||||
|
||||
@ -596,7 +599,7 @@ const getAttachmentIcon = (type: string): string => {
|
||||
const previewAttachment = (attachment: Attachment) => {
|
||||
// 如果是图片类型,可以预览
|
||||
if (attachment.type === "image") {
|
||||
const fullUrl = imagUrl(attachment.url as string);
|
||||
const fullUrl = imagUrl(attachment.url);
|
||||
uni.previewImage({
|
||||
urls: [fullUrl],
|
||||
current: fullUrl,
|
||||
@ -617,11 +620,11 @@ const showClassTree = () => {
|
||||
};
|
||||
|
||||
// 树形选择确认
|
||||
const onTreeConfirm = async (selectedItems: Array<any>) => {
|
||||
const onTreeConfirm = async (selectedItems: any[]) => {
|
||||
|
||||
if (selectedItems.length > 0) {
|
||||
// 处理多选情况
|
||||
const classNames = selectedItems.map((item: any) => item.title);
|
||||
const classNames = selectedItems.map((item) => item.title);
|
||||
formData.targetClass = classNames.join(", ");
|
||||
formData.targetNames = [];
|
||||
formData.targetStudentIds = [];
|
||||
@ -638,7 +641,7 @@ const onTreeConfirm = async (selectedItems: Array<any>) => {
|
||||
const bjIds: string[] = [];
|
||||
const gradeNames: string[] = []; // 用于存储年级名称,用于错误提示
|
||||
|
||||
selectedItems.forEach((item: any) => {
|
||||
selectedItems.forEach((item) => {
|
||||
// 如果选择的是班级(有parents表示是班级)
|
||||
if (item.parents && item.parents.length > 0) {
|
||||
const parent = item.parents[0]; // 年级信息
|
||||
@ -649,13 +652,23 @@ const onTreeConfirm = async (selectedItems: Array<any>) => {
|
||||
|
||||
// 通过年级key从treeData中查找对应的njmcId
|
||||
let njmcId: string | undefined;
|
||||
for (const grade of treeData.value || []) {
|
||||
for (const grade of treeData.value) {
|
||||
if (grade.key === njId) {
|
||||
njmcId = grade.njmcId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加调试信息
|
||||
console.log("=== 年级信息调试 ===");
|
||||
console.log("item:", item);
|
||||
console.log("parent:", parent);
|
||||
console.log("njId:", njId);
|
||||
console.log("njmcId (从treeData查找):", njmcId);
|
||||
console.log("bjId:", bjId);
|
||||
console.log("gradeName:", gradeName);
|
||||
console.log("====================");
|
||||
|
||||
// 确保年级信息完整
|
||||
if (njId && bjId && gradeName) {
|
||||
njIds.push(njId);
|
||||
@ -689,6 +702,14 @@ const onTreeConfirm = async (selectedItems: Array<any>) => {
|
||||
const uniqueNjIds = [...new Set(njIds)];
|
||||
const uniqueNjmcIds = [...new Set(njmcIds)];
|
||||
|
||||
// 添加调试信息
|
||||
console.log("=== 最终设置调试 ===");
|
||||
console.log("njIds:", njIds);
|
||||
console.log("njmcIds:", njmcIds);
|
||||
console.log("uniqueNjIds:", uniqueNjIds);
|
||||
console.log("uniqueNjmcIds:", uniqueNjmcIds);
|
||||
console.log("====================");
|
||||
|
||||
// 保存选择的年级ID、年级名称ID和班级ID到formData中
|
||||
formData.targetNjIds = uniqueNjIds;
|
||||
formData.targetNjmcIds = uniqueNjmcIds;
|
||||
@ -700,7 +721,7 @@ const onTreeConfirm = async (selectedItems: Array<any>) => {
|
||||
bjId: bjIds.join(","),
|
||||
};
|
||||
|
||||
const response = await mobilejlstudentListApi(params) as ApiResponse<any[]>;
|
||||
const response = await mobilejlstudentListApi(params);
|
||||
|
||||
if (response && response.resultCode === 1 && response.result) {
|
||||
// 提取学生姓名,尝试多个可能的字段名
|
||||
@ -730,6 +751,7 @@ const onTreeConfirm = async (selectedItems: Array<any>) => {
|
||||
icon: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("获取学生列表失败:", error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: "获取学生列表失败", icon: "error" });
|
||||
|
||||
@ -844,6 +866,14 @@ const buildJlDto = (status: string) => {
|
||||
const user = userData.value;
|
||||
const js = jsData.value;
|
||||
|
||||
// 添加调试信息
|
||||
console.log("=== buildJlDto 调试信息 ===");
|
||||
console.log("formData.targetNjIds:", formData.targetNjIds);
|
||||
console.log("formData.targetNjmcIds:", formData.targetNjmcIds);
|
||||
console.log("formData.targetBjIds:", formData.targetBjIds);
|
||||
console.log("targetNjmcIds.join(','):", formData.targetNjmcIds.join(","));
|
||||
console.log("==========================");
|
||||
|
||||
return {
|
||||
id: formData.id || "", // 接龙ID,新增时为空
|
||||
jlmc: formData.title.trim(), // 接龙名称
|
||||
@ -878,13 +908,13 @@ const saveDraft = async () => {
|
||||
const jlDto = buildJlDto("B"); // B表示暂存
|
||||
|
||||
// 调用后端接口
|
||||
const response = await jlSaveApi(jlDto) as ApiResponse<any>;
|
||||
const response = await jlSaveApi(jlDto);
|
||||
|
||||
uni.hideLoading();
|
||||
|
||||
if (response && response.resultCode === 1) {
|
||||
// 获取接龙ID
|
||||
const jlId = response.result || (response as any).data || jlDto.id;
|
||||
const jlId = response.result || response.data || jlDto.id;
|
||||
|
||||
// 更新表单中的ID
|
||||
if (jlId) {
|
||||
@ -903,12 +933,15 @@ const saveDraft = async () => {
|
||||
}, 2000);
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: (response as any)?.resultMsg || "保存草稿失败",
|
||||
title: response?.resultMsg || "保存草稿失败",
|
||||
icon: "error"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error("保存草稿失败:", error);
|
||||
|
||||
// 处理数据验证错误
|
||||
if (error instanceof Error) {
|
||||
uni.showToast({
|
||||
title: error.message,
|
||||
@ -941,7 +974,7 @@ const publishNotice = async () => {
|
||||
// 如果有ID,检查接龙是否已存在且已发布
|
||||
if (formData.id) {
|
||||
try {
|
||||
const response = await jlFindByIdApi({ id: formData.id }) as ApiResponse<any>;
|
||||
const response = await jlFindByIdApi({ id: formData.id });
|
||||
if (response && response.resultCode === 1 && response.result) {
|
||||
const jlData = response.result;
|
||||
if (jlData.jlStatus === 'A') {
|
||||
@ -949,7 +982,9 @@ const publishNotice = async () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
console.error("检查接龙状态失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@ -959,21 +994,27 @@ const publishNotice = async () => {
|
||||
// 准备发布数据,构建 JlDto 对象
|
||||
const jlDto = buildJlDto("A"); // A表示已发布
|
||||
|
||||
// 添加调试信息
|
||||
console.log("=== 发布接龙调试信息 ===");
|
||||
console.log("jlDto:", jlDto);
|
||||
console.log("==========================");
|
||||
|
||||
// 调用后端接口,设置超时处理
|
||||
const response = await Promise.race([
|
||||
jlSaveApi(jlDto) as Promise<ApiResponse<any>>,
|
||||
jlSaveApi(jlDto),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("请求超时")), 300000) // 5分钟超时
|
||||
)
|
||||
]) as ApiResponse<any>;
|
||||
]);
|
||||
|
||||
uni.hideLoading();
|
||||
|
||||
if (response && response.resultCode === 1) {
|
||||
// 获取接龙ID,优先使用返回的ID,如果没有则使用请求中的ID
|
||||
const jlId = response.result || (response as any).data || jlDto.id;
|
||||
const jlId = response.result || response.data || jlDto.id;
|
||||
|
||||
if (!jlId) {
|
||||
console.error("发布成功但未获取到接龙ID");
|
||||
uni.showToast({
|
||||
title: "发布成功,但获取接龙ID失败",
|
||||
icon: "error"
|
||||
@ -1001,7 +1042,7 @@ const publishNotice = async () => {
|
||||
}, 2000);
|
||||
} else {
|
||||
// 处理业务错误
|
||||
const errorMsg = (response as any)?.resultMsg || (response as any)?.message || "发布失败";
|
||||
const errorMsg = response?.resultMsg || response?.message || "发布失败";
|
||||
uni.showToast({
|
||||
title: errorMsg,
|
||||
icon: "none",
|
||||
@ -1010,6 +1051,9 @@ const publishNotice = async () => {
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error("发布接龙失败:", error);
|
||||
|
||||
// 处理不同类型的错误
|
||||
if (error instanceof Error) {
|
||||
if (error.message === "请求超时") {
|
||||
uni.showToast({
|
||||
@ -1025,7 +1069,7 @@ const publishNotice = async () => {
|
||||
}
|
||||
} else if (error && typeof error === 'object' && 'errMsg' in error) {
|
||||
// uni-app 错误对象
|
||||
if ((error as any).errMsg && (error as any).errMsg.includes('timeout')) {
|
||||
if (error.errMsg && error.errMsg.includes('timeout')) {
|
||||
uni.showToast({
|
||||
title: "请求超时,请检查网络连接或稍后重试",
|
||||
icon: "none",
|
||||
@ -1033,7 +1077,7 @@ const publishNotice = async () => {
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: (error as any).errMsg || "发布失败,请重试",
|
||||
title: error.errMsg || "发布失败,请重试",
|
||||
icon: "error"
|
||||
});
|
||||
}
|
||||
|
||||
630
src/pages/view/routine/qd/confirm.vue
Normal file
630
src/pages/view/routine/qd/confirm.vue
Normal file
@ -0,0 +1,630 @@
|
||||
<template>
|
||||
<view class="confirm-page">
|
||||
<!-- 确认签到阶段 -->
|
||||
<view v-if="currentStep === 'confirm'" class="confirm-step">
|
||||
<view class="confirm-header">
|
||||
<text class="confirm-title">确认签到</text>
|
||||
</view>
|
||||
|
||||
<view class="meeting-info">
|
||||
<view class="info-item">
|
||||
<text class="info-label">会议名称:</text>
|
||||
<text class="info-value">{{ meetingInfo?.qdmc || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">会议时间:</text>
|
||||
<text class="info-value">{{ formatTime(meetingInfo?.qdkstime) }} - {{ formatTime(meetingInfo?.qdjstime) }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">会议地点:</text>
|
||||
<text class="info-value">{{ meetingInfo?.qdwz || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">签到人员:</text>
|
||||
<text class="info-value">{{ userInfo?.jsxm || '未设置' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="sign_file" class="signature-preview">
|
||||
<text class="preview-label">签名预览:</text>
|
||||
<image :src="sign_file" class="signature-preview-img" />
|
||||
</view>
|
||||
|
||||
<button @click="submitSignIn" class="submit-btn">确认签到</button>
|
||||
</view>
|
||||
|
||||
<!-- 签到成功阶段 -->
|
||||
<view v-else-if="currentStep === 'success'" class="success-step">
|
||||
<view class="success-content">
|
||||
<u-icon name="checkmark-circle" size="80" color="#67C23A" />
|
||||
<text class="success-title">签到成功</text>
|
||||
<text class="success-time">{{ formatTime(new Date()) }}</text>
|
||||
<text class="success-tip">您已成功完成签到</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 不在签到名单阶段 -->
|
||||
<view v-else-if="currentStep === 'notInList'" class="not-in-list-step">
|
||||
<view class="not-in-list-content">
|
||||
<u-icon name="close-circle" size="80" color="#F56C6C" />
|
||||
<text class="not-in-list-title">您不在签到名单中</text>
|
||||
<text class="not-in-list-subtitle">抱歉,您没有在此次会议的签到名单中</text>
|
||||
<text class="not-in-list-tip">请联系会议组织者确认您的参会资格</text>
|
||||
|
||||
<view class="meeting-info-card">
|
||||
<text class="card-title">会议信息</text>
|
||||
<view class="info-item">
|
||||
<text class="info-label">会议名称:</text>
|
||||
<text class="info-value">{{ meetingInfo?.qdmc || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">会议时间:</text>
|
||||
<text class="info-value">{{ formatTime(meetingInfo?.qdkstime) }} - {{ formatTime(meetingInfo?.qdjstime) }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">会议地点:</text>
|
||||
<text class="info-value">{{ meetingInfo?.qdwz || '未设置' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button @click="goBack" class="back-btn">返回</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 签到时间已结束阶段 -->
|
||||
<view v-else-if="currentStep === 'timeExpired'" class="time-expired-step">
|
||||
<view class="time-expired-content">
|
||||
<u-icon name="clock" size="80" color="#E6A23C" />
|
||||
<text class="time-expired-title">签到时间已结束</text>
|
||||
<text class="time-expired-subtitle">抱歉,本次签到的时间已经结束</text>
|
||||
<text class="time-expired-tip">请关注下次签到通知</text>
|
||||
|
||||
<view class="meeting-info-card">
|
||||
<text class="card-title">会议信息</text>
|
||||
<view class="info-item">
|
||||
<text class="info-label">会议名称:</text>
|
||||
<text class="info-value">{{ meetingInfo?.qdmc || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">会议时间:</text>
|
||||
<text class="info-value">{{ formatTime(meetingInfo?.qdkstime) }} - {{ formatTime(meetingInfo?.qdjstime) }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">会议地点:</text>
|
||||
<text class="info-value">{{ meetingInfo?.qdwz || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">当前时间:</text>
|
||||
<text class="info-value">{{ formatTime(new Date()) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button @click="goBack" class="back-btn">返回</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 二维码过期阶段 -->
|
||||
<view v-else-if="currentStep === 'qrExpired'" class="qr-expired-step">
|
||||
<view class="qr-expired-content">
|
||||
<u-icon name="close-circle" size="80" color="#F56C6C" />
|
||||
<text class="qr-expired-title">二维码已过期</text>
|
||||
<text class="qr-expired-subtitle">抱歉,您扫描的二维码已经过期</text>
|
||||
<text class="qr-expired-tip">请重新扫描最新的二维码</text>
|
||||
|
||||
<view class="qr-info-card">
|
||||
<text class="card-title">二维码信息</text>
|
||||
<view class="info-item">
|
||||
<text class="info-label">二维码有效期:</text>
|
||||
<text class="info-value">60秒</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">当前时间:</text>
|
||||
<text class="info-value">{{ formatTime(new Date()) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button @click="goBack" class="back-btn">返回</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 签名组件 -->
|
||||
<BasicSign v-if="showSignature" ref="signCompRef" :title="signTitle"></BasicSign>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { qdFindByIdApi, qdzxSignInApi, qdzxFindByQdParamsApi, qdzxFindByQdAndJsApi } from '@/api/base/server';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
// 页面参数
|
||||
const qdId = ref('');
|
||||
const qdzxId = ref('');
|
||||
|
||||
// 当前步骤
|
||||
const currentStep = ref<'sign' | 'confirm' | 'success' | 'notInList' | 'timeExpired' | 'qrExpired'>('confirm');
|
||||
|
||||
// 用户信息
|
||||
const userInfo = ref<any>(null);
|
||||
|
||||
// 会议信息
|
||||
const meetingInfo = ref<any>(null);
|
||||
|
||||
// 签到执行记录
|
||||
const qdzxRecord = ref<any>(null);
|
||||
|
||||
// 签名组件相关
|
||||
const signCompRef = ref<any>(null);
|
||||
const signTitle = ref<string>("签到签名");
|
||||
const sign_file = ref<string>("");
|
||||
const showSignature = ref<boolean>(false);
|
||||
|
||||
// 获取用户store
|
||||
const userStore = useUserStore();
|
||||
|
||||
onLoad(async (options) => {
|
||||
if (options?.qdId) {
|
||||
qdId.value = options.qdId;
|
||||
}
|
||||
if (options?.qdzxId) {
|
||||
qdzxId.value = options.qdzxId;
|
||||
}
|
||||
|
||||
// 第一步:获取缓存的教师信息
|
||||
const jsData = userStore.getJs;
|
||||
if (jsData && Object.keys(jsData).length > 0) {
|
||||
userInfo.value = jsData;
|
||||
console.log('获取到教师信息:', jsData);
|
||||
} else {
|
||||
console.error('未找到教师信息,请先登录');
|
||||
uni.showToast({ title: '请先登录', icon: 'none' });
|
||||
// 跳转到登录页面
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/system/login/login'
|
||||
});
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证二维码时间戳是否过期
|
||||
if (options?.timestamp) {
|
||||
const qrTimestamp = parseInt(options.timestamp);
|
||||
const currentTimestamp = Date.now();
|
||||
const timeDiff = currentTimestamp - qrTimestamp;
|
||||
const maxValidTime = 60 * 1000; // 60秒有效期
|
||||
|
||||
console.log('二维码时间戳验证:', {
|
||||
qrTimestamp,
|
||||
currentTimestamp,
|
||||
timeDiff,
|
||||
maxValidTime
|
||||
});
|
||||
|
||||
if (timeDiff > maxValidTime) {
|
||||
console.error('二维码已过期');
|
||||
currentStep.value = 'qrExpired';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 第二步:通过qdId和教师ID查询该教师是否能签到
|
||||
await checkTeacherSignInPermission();
|
||||
});
|
||||
|
||||
// 检查教师签到权限
|
||||
const checkTeacherSignInPermission = async () => {
|
||||
try {
|
||||
const result = await qdzxFindByQdAndJsApi({
|
||||
qdId: qdId.value,
|
||||
jsId: userInfo.value.id
|
||||
});
|
||||
|
||||
if (result && result.resultCode === 1 && result.result) {
|
||||
// 找到签到记录
|
||||
qdzxRecord.value = result.result;
|
||||
qdzxId.value = result.result.id;
|
||||
console.log('找到教师签到记录:', result.result);
|
||||
|
||||
// 第三步:获取签到详情,验证是否需要签名
|
||||
await loadMeetingInfo();
|
||||
} else {
|
||||
// 没有找到签到记录,显示"不在签到名单中"界面
|
||||
currentStep.value = 'notInList';
|
||||
await loadMeetingInfo(); // 仍然加载会议信息用于显示
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询教师签到权限失败:', error);
|
||||
uni.showToast({ title: '查询签到权限失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
// 加载会议信息
|
||||
const loadMeetingInfo = async () => {
|
||||
try {
|
||||
const result = await qdFindByIdApi({ id: qdId.value });
|
||||
if (result && result.resultCode === 1) {
|
||||
meetingInfo.value = result.result;
|
||||
|
||||
// 1. 验证签到时间是否已结束
|
||||
const currentTime = new Date();
|
||||
const endTime = meetingInfo.value?.qdjstime ? new Date(meetingInfo.value.qdjstime) : null;
|
||||
|
||||
if (endTime && currentTime > endTime) {
|
||||
// 签到时间已结束,显示"签到时间已结束"界面
|
||||
currentStep.value = 'timeExpired';
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有在找到qdzxId时才检查是否需要签名
|
||||
if (qdzxId.value) {
|
||||
// 检查是否需要签名
|
||||
if (meetingInfo.value?.mdqz === '1') {
|
||||
// 需要签名,调用签名组件
|
||||
await handleSignature();
|
||||
} else {
|
||||
// 不需要签名,直接到确认界面
|
||||
currentStep.value = 'confirm';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载会议信息失败:', error);
|
||||
uni.showToast({ title: '加载会议信息失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
// 处理签名
|
||||
const handleSignature = async () => {
|
||||
try {
|
||||
showSignature.value = true;
|
||||
sign_file.value = '';
|
||||
signTitle.value = "签到签名";
|
||||
|
||||
// 等待组件渲染完成
|
||||
await nextTick();
|
||||
const data = await signCompRef.value.getSyncSignature();
|
||||
sign_file.value = data.base64;
|
||||
showSignature.value = false;
|
||||
|
||||
// 签名完成后,跳转到确认界面
|
||||
currentStep.value = 'confirm';
|
||||
} catch (error) {
|
||||
showSignature.value = false;
|
||||
console.error('签名失败:', error);
|
||||
uni.showToast({ title: '签名失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
// 提交签到
|
||||
const submitSignIn = async () => {
|
||||
try {
|
||||
const params: any = {
|
||||
qdzxId: qdzxId.value,
|
||||
signInTime: new Date().toISOString() // 转换为ISO字符串格式
|
||||
};
|
||||
|
||||
// 如果有签名文件,添加到参数中
|
||||
if (sign_file.value) {
|
||||
params.signatureImage = sign_file.value;
|
||||
}
|
||||
|
||||
console.log('提交签到参数:', JSON.stringify(params, null, 2));
|
||||
console.log('参数类型检查:', {
|
||||
qdzxId: typeof params.qdzxId,
|
||||
signInTime: typeof params.signInTime,
|
||||
signatureImage: typeof params.signatureImage
|
||||
});
|
||||
|
||||
const result = await qdzxSignInApi(params);
|
||||
console.log('签到结果:', result);
|
||||
|
||||
if (result && result.resultCode === 1) {
|
||||
currentStep.value = 'success';
|
||||
} else {
|
||||
const errorMsg = result?.msg || result?.message || '签到失败';
|
||||
uni.showToast({ title: errorMsg, icon: 'none' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('签到失败:', error);
|
||||
uni.showToast({ title: '网络异常,请重试', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string | Date) => {
|
||||
if (!time) return '未设置';
|
||||
const date = new Date(time);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.confirm-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// 确认签到阶段
|
||||
.confirm-step {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.confirm-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.confirm-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.meeting-info {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.info-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.signature-preview {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.preview-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.signature-preview-img {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// 签到成功阶段
|
||||
.success-step {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 40px 24px;
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success-content {
|
||||
.success-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 16px 0 8px;
|
||||
}
|
||||
|
||||
.success-time {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.success-tip {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 不在签到名单阶段
|
||||
.not-in-list-step {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.not-in-list-content {
|
||||
.not-in-list-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 16px 0 8px;
|
||||
}
|
||||
|
||||
.not-in-list-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.not-in-list-tip {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.meeting-info-card {
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.info-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 签到时间已结束阶段 */
|
||||
.time-expired-step {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 40px 24px;
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.time-expired-content {
|
||||
.time-expired-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 16px 0 8px;
|
||||
}
|
||||
|
||||
.time-expired-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.time-expired-tip {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 二维码过期阶段 */
|
||||
.qr-expired-step {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 40px 24px;
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-expired-content {
|
||||
.qr-expired-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 16px 0 8px;
|
||||
}
|
||||
|
||||
.qr-expired-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.qr-expired-tip {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-info-card {
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.info-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
438
src/pages/view/routine/qd/detail.vue
Normal file
438
src/pages/view/routine/qd/detail.vue
Normal file
@ -0,0 +1,438 @@
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<scroll-view scroll-y class="detail-scroll-view">
|
||||
<view class="detail-container">
|
||||
<!-- 签到基本信息 -->
|
||||
<view class="info-card">
|
||||
<view class="card-header">
|
||||
<text class="qd-title">{{ qdInfo.qdmc }}</text>
|
||||
<text class="qd-status" :class="getStatusClass(qdInfo.qdStatus)">
|
||||
{{ getStatusText(qdInfo.qdStatus) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="info-list">
|
||||
<view class="info-item">
|
||||
<text class="info-label">签到地点:</text>
|
||||
<text class="info-value">{{ qdInfo.qdwz || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">开始时间:</text>
|
||||
<text class="info-value">{{ formatTime(qdInfo.qdkstime) }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">结束时间:</text>
|
||||
<text class="info-value">{{ formatTime(qdInfo.qdjstime) }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">发布者:</text>
|
||||
<text class="info-value">{{ qdInfo.jsxm || '未知' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">发布时间:</text>
|
||||
<text class="info-value">{{ formatTime(qdInfo.qdFbtime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 签到人员统计 -->
|
||||
<view class="stats-card">
|
||||
<view class="stats-header">
|
||||
<text class="stats-title">签到统计</text>
|
||||
</view>
|
||||
<view class="stats-content">
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{ totalCount }}</text>
|
||||
<text class="stat-label">总人数</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number signed">{{ signedCount }}</text>
|
||||
<text class="stat-label">已签到</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number unsigned">{{ unsignedCount }}</text>
|
||||
<text class="stat-label">未签到</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 签到人员列表 -->
|
||||
<view class="teacher-list-card">
|
||||
<view class="list-header">
|
||||
<text class="list-title">签到人员</text>
|
||||
</view>
|
||||
<view class="teacher-list">
|
||||
<view
|
||||
v-for="teacher in teacherList"
|
||||
:key="teacher.id"
|
||||
class="teacher-item"
|
||||
>
|
||||
<view class="teacher-info">
|
||||
<text class="teacher-name">{{ teacher.jsxm }}</text>
|
||||
<text class="teacher-position">{{ teacher.dzzw || '' }} {{ teacher.qtzw || '' }}</text>
|
||||
</view>
|
||||
<view class="teacher-status">
|
||||
<text class="status-text" :class="getStatusClass(teacher.qdStatus)">
|
||||
{{ getStatusText(teacher.qdStatus) }}
|
||||
</text>
|
||||
<text v-if="teacher.qdwctime" class="sign-time">
|
||||
{{ formatTime(teacher.qdwctime) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<template #bottom>
|
||||
<view class="bottom-actions">
|
||||
<button class="action-btn back-btn" @click="handleBack">
|
||||
返回列表
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { qdFindByIdApi, qdzxFindByQdParamsApi } from "@/api/base/server";
|
||||
|
||||
interface QdInfo {
|
||||
id: string;
|
||||
qdmc: string;
|
||||
qdwz: string;
|
||||
qdStatus: string;
|
||||
jsId: string;
|
||||
jsxm: string;
|
||||
qdFbtime: string;
|
||||
qdkstime: string;
|
||||
qdjstime: string;
|
||||
qdry: string;
|
||||
}
|
||||
|
||||
interface TeacherInfo {
|
||||
id: string;
|
||||
jsId: string;
|
||||
jsxm: string;
|
||||
dzzw: string;
|
||||
qtzw: string;
|
||||
qdStatus: string;
|
||||
qdwctime: string;
|
||||
}
|
||||
|
||||
const qdId = ref<string>('');
|
||||
const qdInfo = ref<QdInfo>({} as QdInfo);
|
||||
const teacherList = ref<TeacherInfo[]>([]);
|
||||
|
||||
const totalCount = computed(() => teacherList.value.length);
|
||||
const signedCount = computed(() => teacherList.value.filter(t => t.qdStatus === '1').length);
|
||||
const unsignedCount = computed(() => totalCount.value - signedCount.value);
|
||||
|
||||
onLoad((options) => {
|
||||
if (options && options.id) {
|
||||
qdId.value = options.id;
|
||||
loadQdDetail();
|
||||
loadTeacherList();
|
||||
} else {
|
||||
uni.showToast({ title: "缺少签到ID参数", icon: "error" });
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
|
||||
const loadQdDetail = async () => {
|
||||
try {
|
||||
const result = await qdFindByIdApi({ id: qdId.value });
|
||||
if (result.resultCode === 1 && result.result) {
|
||||
qdInfo.value = result.result;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载签到详情失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadTeacherList = async () => {
|
||||
try {
|
||||
const result = await qdzxFindByQdParamsApi({ qdId: qdId.value });
|
||||
if (result.resultCode === 1 && result.result) {
|
||||
teacherList.value = result.result;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载教师列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'A':
|
||||
return 'status-pending';
|
||||
case 'B':
|
||||
return 'status-draft';
|
||||
case 'C':
|
||||
return 'status-pushed';
|
||||
case '1':
|
||||
return 'status-signed';
|
||||
case '0':
|
||||
default:
|
||||
return 'status-unsigned';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'A':
|
||||
return '待推送';
|
||||
case 'B':
|
||||
return '暂存';
|
||||
case 'C':
|
||||
return '已推送';
|
||||
case '1':
|
||||
return '已签到';
|
||||
case '0':
|
||||
default:
|
||||
return '未签到';
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return '未设置';
|
||||
return new Date(time).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.detail-scroll-view {
|
||||
height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.qd-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.qd-status {
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-draft {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.status-pushed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-signed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-unsigned {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.info-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
min-width: 80px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
|
||||
&.signed {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
&.unsigned {
|
||||
color: #dc3545;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.teacher-list-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.list-header {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.teacher-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.teacher-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.teacher-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.teacher-name {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.teacher-position {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.teacher-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sign-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
558
src/pages/view/routine/qd/index.vue
Normal file
558
src/pages/view/routine/qd/index.vue
Normal file
@ -0,0 +1,558 @@
|
||||
<!-- src/pages/view/routine/qd/index.vue -->
|
||||
<template>
|
||||
<view class="qd-list-page">
|
||||
<!-- 查询组件 -->
|
||||
<view class="query-component">
|
||||
<view class="search-card">
|
||||
<view class="search-item">
|
||||
<picker
|
||||
mode="date"
|
||||
:value="searchForm.startTime"
|
||||
@change="onStartTimeChange"
|
||||
class="date-picker"
|
||||
>
|
||||
<view class="picker-text">{{ searchForm.startTime || '开始时间' }}</view>
|
||||
</picker>
|
||||
<text class="date-separator">~</text>
|
||||
<picker
|
||||
mode="date"
|
||||
:value="searchForm.endTime"
|
||||
@change="onEndTimeChange"
|
||||
class="date-picker"
|
||||
>
|
||||
<view class="picker-text">{{ searchForm.endTime || '结束时间' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="search-actions">
|
||||
<u-button
|
||||
text="查询"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleSearch"
|
||||
class="search-btn"
|
||||
/>
|
||||
<u-button
|
||||
text="重置"
|
||||
type="info"
|
||||
size="small"
|
||||
@click="handleReset"
|
||||
class="reset-btn"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表组件 -->
|
||||
<view class="list-component">
|
||||
<scroll-view scroll-y class="list-scroll-view">
|
||||
<view v-if="isLoading" class="loading-indicator">加载中...</view>
|
||||
<template v-else-if="dataList.length > 0">
|
||||
<view v-for="data in dataList" :key="data.id" class="qd-card">
|
||||
<view class="card-header">
|
||||
<text class="qd-title">{{ data.qdmc }}</text>
|
||||
<text class="qd-status" :class="getStatusClass(data.qdStatus)">
|
||||
{{ getStatusText(data.qdStatus) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="qd-info">
|
||||
<view class="info-item">
|
||||
<text class="info-label">签到地点:</text>
|
||||
<text class="info-value">{{ data.qdwz || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">开始时间:</text>
|
||||
<text class="info-value">{{ formatTime(data.qdkstime) }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">结束时间:</text>
|
||||
<text class="info-value">{{ formatTime(data.qdjstime) }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">发布者:</text>
|
||||
<text class="info-value">{{ data.jsxm || '未知' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">发布时间:</text>
|
||||
<text class="info-value">{{ formatTime(data.qdFbtime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-footer">
|
||||
<view class="footer-actions">
|
||||
<image
|
||||
src="/static/base/qr-code.png"
|
||||
class="footer-action-icon qr-icon"
|
||||
@click="goToQRCode(data.id)"
|
||||
/>
|
||||
<image
|
||||
src="/static/base/details.png"
|
||||
class="footer-action-icon details-icon"
|
||||
@click="goToFeedback(data.id)"
|
||||
/>
|
||||
<image
|
||||
v-if="data.qdStatus === 'A'"
|
||||
src="/static/base/push.png"
|
||||
class="footer-action-icon push-icon"
|
||||
@click="goToPush(data.id)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view v-else class="empty-state">暂无签到数据</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 - 固定在底部 -->
|
||||
<view class="add-button-fixed">
|
||||
<u-button
|
||||
text="新增签到"
|
||||
type="primary"
|
||||
@click="goToPublish"
|
||||
class="add-btn"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { qdFindPageApi } from "@/api/base/server";
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
|
||||
interface QdItem {
|
||||
id: string;
|
||||
qdmc: string; // 签到名称
|
||||
qdwz: string; // 签到地点
|
||||
qdStatus: string; // 发布状态:A待推送,B暂存,C已推送
|
||||
jsId: string; // 发布人ID
|
||||
jsxm: string; // 发布人姓名
|
||||
qdFbtime: string; // 发布时间
|
||||
qdkstime: string; // 签到开始时间
|
||||
qdjstime: string; // 签到结束时间
|
||||
qdry: string; // 签到人员
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
startTime: '',
|
||||
endTime: ''
|
||||
});
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<QdItem[]>([]);
|
||||
const isLoading = ref(false);
|
||||
|
||||
// 开始时间选择
|
||||
const onStartTimeChange = (e: any) => {
|
||||
searchForm.startTime = e.detail.value;
|
||||
};
|
||||
|
||||
// 结束时间选择
|
||||
const onEndTimeChange = (e: any) => {
|
||||
searchForm.endTime = e.detail.value;
|
||||
};
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
getQdList();
|
||||
};
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchForm.startTime = '';
|
||||
searchForm.endTime = '';
|
||||
getQdList();
|
||||
};
|
||||
|
||||
// 获取签到列表
|
||||
const getQdList = async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const res = await qdFindPageApi({
|
||||
qdkstime: searchForm.startTime,
|
||||
qdjstime: searchForm.endTime,
|
||||
page: 1,
|
||||
rows: 100
|
||||
});
|
||||
dataList.value = res.rows || [];
|
||||
} catch (error) {
|
||||
console.error('获取签到列表失败:', error);
|
||||
dataList.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态样式类
|
||||
const getStatusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'A':
|
||||
return 'status-pending';
|
||||
case 'B':
|
||||
return 'status-draft';
|
||||
case 'C':
|
||||
return 'status-pushed';
|
||||
default:
|
||||
return 'status-default';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'A':
|
||||
return '待推送';
|
||||
case 'B':
|
||||
return '暂存';
|
||||
case 'C':
|
||||
return '已推送';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return '未设置';
|
||||
return new Date(time).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
// 跳转到签到详情
|
||||
const goToFeedback = (id: string) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/qd/detail?id=${id}`
|
||||
});
|
||||
};
|
||||
|
||||
// 跳转到推送页面
|
||||
const goToPush = (id: string) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/qd/push-list?qdId=${id}`
|
||||
});
|
||||
};
|
||||
|
||||
// 跳转到新增页面
|
||||
const goToPublish = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/view/routine/qd/publish'
|
||||
});
|
||||
};
|
||||
|
||||
// 跳转到二维码页面
|
||||
const goToQRCode = (id: string) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/qd/qr-code?qdId=${id}`
|
||||
});
|
||||
};
|
||||
|
||||
// 页面显示时刷新数据
|
||||
onShow(() => {
|
||||
getQdList();
|
||||
});
|
||||
|
||||
// 页面加载时也调用一次
|
||||
onMounted(() => {
|
||||
getQdList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.qd-list-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
// 查询组件
|
||||
.query-component {
|
||||
padding: 12px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.search-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
gap: 8px;
|
||||
|
||||
.date-picker {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
font-size: 16px;
|
||||
color: #6c757d;
|
||||
margin: 0 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
|
||||
.search-btn, .reset-btn {
|
||||
flex: 1;
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表组件
|
||||
.list-component {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
padding-bottom: 80px; // 为底部按钮留出空间
|
||||
box-sizing: border-box;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.list-scroll-view {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-indicator,
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 30px 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.qd-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #f0f0f0;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: linear-gradient(135deg, #007aff 0%, #0056cc 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
gap: 12px;
|
||||
|
||||
.qd-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.qd-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-draft {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.status-pushed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-default {
|
||||
background: #f8f9fa;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.qd-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.info-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.footer-action-icon {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.qr-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.details-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.push-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增按钮 - 固定在底部
|
||||
.add-button-fixed {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 15px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
background: linear-gradient(135deg, #007aff 0%, #0056cc 100%);
|
||||
border-radius: 12px;
|
||||
padding: 12px 24px;
|
||||
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.3);
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式优化
|
||||
@media (max-width: 375px) {
|
||||
.query-component {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.qd-card {
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-header .qd-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.card-footer .footer-item {
|
||||
max-width: 150px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载动画
|
||||
.qd-card {
|
||||
animation: fadeInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
515
src/pages/view/routine/qd/publish.vue
Normal file
515
src/pages/view/routine/qd/publish.vue
Normal file
@ -0,0 +1,515 @@
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<scroll-view scroll-y class="form-scroll-view">
|
||||
<view class="form-container">
|
||||
<!-- 签到名称 -->
|
||||
<view class="info-card">
|
||||
<view class="form-item">
|
||||
<uni-easyinput
|
||||
type="textarea"
|
||||
autoHeight
|
||||
v-model="formData.qdmc"
|
||||
placeholder="请输入签到名称 (必填)"
|
||||
:inputBorder="false"
|
||||
placeholder-style="font-weight:bold; font-size: 18px; color: #999;"
|
||||
class="title-input"
|
||||
></uni-easyinput>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 签到地点 -->
|
||||
<view class="info-card">
|
||||
<view class="form-item">
|
||||
<uni-easyinput
|
||||
type="textarea"
|
||||
autoHeight
|
||||
v-model="formData.qdwz"
|
||||
placeholder="请输入签到地点 (必填)"
|
||||
:inputBorder="false"
|
||||
class="content-input"
|
||||
></uni-easyinput>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 签到人员 -->
|
||||
<view class="info-card">
|
||||
<view class="card-header picker-header" @click="showTeacherTree">
|
||||
<text class="section-title">签到人员</text>
|
||||
<view class="target-class">
|
||||
<text :class="{ placeholder: !formData.targetTeachers.length }">
|
||||
{{ formData.targetTeachers.length ? `已选择${formData.targetTeachers.length}人` : "请选择教师" }}
|
||||
</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="name-tags">
|
||||
<text
|
||||
v-for="(teacher, index) in displayTeachers"
|
||||
:key="teacher.id + index"
|
||||
class="name-tag"
|
||||
>{{ teacher.jsxm }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="formData.targetTeachers.length > maxDisplayCount"
|
||||
class="more-btn-container"
|
||||
>
|
||||
<button size="mini" class="more-btn-full" @click="showAllTeachers">
|
||||
更多({{ formData.targetTeachers.length - maxDisplayCount }})
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 是否启用签名 -->
|
||||
<view class="info-card list-item-card">
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="signatureOptions"
|
||||
@change="handleSignatureChange"
|
||||
>
|
||||
<view class="list-item-row">
|
||||
<text class="list-label">按名单签字</text>
|
||||
<view class="list-value">
|
||||
<text>{{ signatureStatusText }}</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 签到开始时间 -->
|
||||
<view class="info-card list-item-card">
|
||||
<uni-datetime-picker type="datetime" v-model="formData.qdkstime">
|
||||
<view class="list-item-row">
|
||||
<text class="list-label">开始时间</text>
|
||||
<view class="list-value">
|
||||
<text :class="{ placeholder: !formData.qdkstime }">
|
||||
{{ formData.qdkstime || '请选择开始时间' }}
|
||||
</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</uni-datetime-picker>
|
||||
</view>
|
||||
|
||||
<!-- 签到结束时间 -->
|
||||
<view class="info-card list-item-card">
|
||||
<uni-datetime-picker type="datetime" v-model="formData.qdjstime">
|
||||
<view class="list-item-row">
|
||||
<text class="list-label">结束时间</text>
|
||||
<view class="list-value">
|
||||
<text :class="{ placeholder: !formData.qdjstime }">
|
||||
{{ formData.qdjstime || '请选择结束时间' }}
|
||||
</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</uni-datetime-picker>
|
||||
</view>
|
||||
|
||||
<!-- 发布人 -->
|
||||
<view class="info-card list-item-card">
|
||||
<view class="list-item-row">
|
||||
<text class="list-label">发布人</text>
|
||||
<view class="list-value">
|
||||
<text>{{ formData.jsxm }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 发布时间 -->
|
||||
<view class="info-card list-item-card">
|
||||
<view class="list-item-row">
|
||||
<text class="list-label">发布时间</text>
|
||||
<view class="list-value">
|
||||
<text>{{ formData.qdFbtime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<template #bottom>
|
||||
<view class="bottom-actions">
|
||||
<button class="action-btn cancel-btn" @click="handleCancel">
|
||||
取消
|
||||
</button>
|
||||
<button class="action-btn confirm-btn" @click="handlePublish" :disabled="isPublishing">
|
||||
{{ isPublishing ? '发布中...' : '立即发布' }}
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { qdSaveApi } from "@/api/base/server";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
|
||||
const { getJs } = useUserStore();
|
||||
|
||||
interface TeacherInfo {
|
||||
id: string;
|
||||
jsxm: string;
|
||||
jsId: string;
|
||||
}
|
||||
|
||||
const formData = reactive({
|
||||
qdmc: '',
|
||||
qdwz: '',
|
||||
qdry: '',
|
||||
mdqz: '0',
|
||||
qdkstime: '',
|
||||
qdjstime: '',
|
||||
jsxm: '',
|
||||
qdFbtime: '',
|
||||
targetTeachers: [] as TeacherInfo[]
|
||||
});
|
||||
|
||||
const isPublishing = ref(false);
|
||||
const maxDisplayCount = 5;
|
||||
|
||||
const signatureOptions = ['不启用', '启用'];
|
||||
const signatureStatusText = computed(() => {
|
||||
return signatureOptions[parseInt(formData.mdqz)];
|
||||
});
|
||||
|
||||
const displayTeachers = computed(() => {
|
||||
return formData.targetTeachers.slice(0, maxDisplayCount);
|
||||
});
|
||||
|
||||
// 监听教师选择事件
|
||||
const handleTeacherSelected = (teachers: TeacherInfo[]) => {
|
||||
console.log('接收到教师选择事件:', teachers);
|
||||
formData.targetTeachers = teachers;
|
||||
};
|
||||
|
||||
// 初始化表单数据
|
||||
onMounted(() => {
|
||||
const js = getJs;
|
||||
formData.jsxm = js.jsxm;
|
||||
formData.qdFbtime = formatDateTime(new Date().toISOString());
|
||||
|
||||
// 监听教师选择事件
|
||||
uni.$on('teacherSelected', handleTeacherSelected);
|
||||
});
|
||||
|
||||
// 清理事件监听
|
||||
onUnmounted(() => {
|
||||
uni.$off('teacherSelected', handleTeacherSelected);
|
||||
});
|
||||
|
||||
// 页面显示时也尝试读取本地存储(但只在没有接收到事件数据时)
|
||||
onShow(() => {
|
||||
// 页面显示时的处理逻辑(如果需要的话)
|
||||
});
|
||||
|
||||
const handleSignatureChange = (e: any) => {
|
||||
formData.mdqz = e.detail.value.toString();
|
||||
};
|
||||
|
||||
const showTeacherTree = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/view/routine/qd/selectTeachers'
|
||||
});
|
||||
};
|
||||
|
||||
const showAllTeachers = () => {
|
||||
uni.showModal({
|
||||
title: '签到人员',
|
||||
content: formData.targetTeachers.map(t => t.jsxm).join('、'),
|
||||
showCancel: false
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 格式化时间为后台期望的格式
|
||||
const formatDateTime = (dateTime: string) => {
|
||||
if (!dateTime) return '';
|
||||
const date = new Date(dateTime);
|
||||
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');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const handlePublish = async () => {
|
||||
const requestStartTime = new Date().toISOString();
|
||||
console.log('=== handlePublish 开始 ===', requestStartTime);
|
||||
console.log('当前表单数据:', formData);
|
||||
console.log('选中的教师数量:', formData.targetTeachers.length);
|
||||
console.log('选中的教师列表:', formData.targetTeachers);
|
||||
|
||||
// 防重复提交检查 - 移到最前面
|
||||
if (isPublishing.value) {
|
||||
console.log('正在发布中,忽略重复点击', requestStartTime);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.qdmc.trim()) {
|
||||
uni.showToast({ title: '请输入签到名称', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.qdwz.trim()) {
|
||||
uni.showToast({ title: '请输入签到地点', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.qdkstime) {
|
||||
uni.showToast({ title: '请选择开始时间', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.qdjstime) {
|
||||
uni.showToast({ title: '请选择结束时间', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.targetTeachers.length === 0) {
|
||||
uni.showToast({ title: '请选择签到人员', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置发布状态
|
||||
isPublishing.value = true;
|
||||
console.log('设置发布状态为true', requestStartTime);
|
||||
|
||||
try {
|
||||
const js = getJs;
|
||||
|
||||
// 检查教师ID是否有重复
|
||||
const teacherIds = formData.targetTeachers.map(t => t.jsId);
|
||||
const uniqueIds = [...new Set(teacherIds)];
|
||||
console.log('教师ID列表:', teacherIds);
|
||||
console.log('去重后的教师ID列表:', uniqueIds);
|
||||
console.log('是否有重复ID:', teacherIds.length !== uniqueIds.length);
|
||||
|
||||
if (teacherIds.length !== uniqueIds.length) {
|
||||
console.warn('发现重复的教师ID,将进行去重处理');
|
||||
}
|
||||
|
||||
const params = {
|
||||
qdmc: formData.qdmc,
|
||||
qdwz: formData.qdwz,
|
||||
qdry: uniqueIds.join(','), // 使用去重后的ID列表
|
||||
mdqz: formData.mdqz,
|
||||
qdkstime: formatDateTime(formData.qdkstime),
|
||||
qdjstime: formatDateTime(formData.qdjstime),
|
||||
jsId: js.id,
|
||||
jsxm: formData.jsxm,
|
||||
qdFbtime: formData.qdFbtime,
|
||||
qdStatus: 'A',
|
||||
status: 'A'
|
||||
};
|
||||
|
||||
console.log('准备发送的参数:', params);
|
||||
console.log('qdry字段长度:', params.qdry.length);
|
||||
console.log('qdry字段内容:', params.qdry);
|
||||
console.log('开始调用API', requestStartTime);
|
||||
|
||||
const result = await qdSaveApi(params);
|
||||
|
||||
console.log('API返回结果:', result);
|
||||
console.log('API调用完成', new Date().toISOString());
|
||||
|
||||
if (result.resultCode === 1) {
|
||||
uni.showToast({ title: '发布成功', icon: 'success' });
|
||||
// 跳转到推送清单页面
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages/view/routine/qd/push-list?qdId=${result.result}`
|
||||
});
|
||||
}, 1500);
|
||||
} else {
|
||||
uni.showToast({ title: '发布失败', icon: 'none' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发布失败:', error);
|
||||
console.error('发布失败时间:', new Date().toISOString());
|
||||
uni.showToast({ title: '发布失败,请重试', icon: 'none' });
|
||||
} finally {
|
||||
isPublishing.value = false;
|
||||
console.log('=== handlePublish 结束 ===', new Date().toISOString());
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-scroll-view {
|
||||
height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.form-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 15px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
|
||||
:deep(.uni-easyinput__content) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.uni-easyinput__content-input) {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
:deep(.uni-easyinput__placeholder-class) {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.content-input {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
|
||||
:deep(.uni-easyinput__content) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.uni-easyinput__content-input) {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
:deep(.uni-easyinput__placeholder-class) {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.target-class {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.name-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.name-tag {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.more-btn-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.more-btn-full {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.list-item-card {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.list-item-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.list-item-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list-label {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.list-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background: linear-gradient(135deg, #007aff 0%, #0056cc 100%);
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.3);
|
||||
}
|
||||
|
||||
.confirm-btn:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
304
src/pages/view/routine/qd/push-list.vue
Normal file
304
src/pages/view/routine/qd/push-list.vue
Normal file
@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<scroll-view scroll-y class="push-list-scroll-view">
|
||||
<view class="push-list-container">
|
||||
<!-- 页面标题 -->
|
||||
<view class="page-header">
|
||||
<text class="page-title">推送清单</text>
|
||||
<text class="teacher-count">共 {{ teacherList.length }} 名教师</text>
|
||||
</view>
|
||||
|
||||
<!-- 教师信息卡片列表 -->
|
||||
<view class="teacher-cards">
|
||||
<view
|
||||
v-for="(teacher, index) in teacherList"
|
||||
:key="teacher.id || teacher.jsId || index"
|
||||
class="teacher-card"
|
||||
>
|
||||
<view class="teacher-main-info">
|
||||
<text class="teacher-name">{{ teacher.jsxm || '未知姓名' }}</text>
|
||||
<text class="teacher-position">{{ teacher.dzzw || '' }} {{ teacher.qtzw || '' }}</text>
|
||||
</view>
|
||||
<view class="teacher-status">
|
||||
<text class="status-label">状态:</text>
|
||||
<text class="status-value" :class="getStatusClass(teacher.qdStatus)">
|
||||
{{ getStatusText(teacher.qdStatus) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="teacherList.length === 0" class="empty-state">
|
||||
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
|
||||
<text class="empty-text">暂无教师信息</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- Bottom slot -->
|
||||
<template #bottom>
|
||||
<view class="bottom-actions">
|
||||
<button class="action-btn cancel-btn" @click="handleCancel">
|
||||
取消
|
||||
</button>
|
||||
<button class="action-btn confirm-btn" @click="handleConfirmPush" :disabled="isPushing">
|
||||
{{ isPushing ? '推送中...' : '确认推送' }}
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { qdzxFindByQdParamsApi, xxtsSaveByQdzxParamsApi } from "@/api/base/server";
|
||||
|
||||
interface TeacherInfo {
|
||||
id: string;
|
||||
jsId: string;
|
||||
jsxm: string;
|
||||
dzzw: string;
|
||||
qtzw: string;
|
||||
qdStatus: string;
|
||||
qdId: string;
|
||||
qdmc: string;
|
||||
}
|
||||
|
||||
const qdId = ref<string>('');
|
||||
const teacherList = ref<TeacherInfo[]>([]);
|
||||
const isPushing = ref(false);
|
||||
|
||||
onLoad((options) => {
|
||||
if (options && options.qdId && options.qdId !== 'null' && options.qdId !== 'undefined') {
|
||||
qdId.value = options.qdId;
|
||||
loadTeacherList();
|
||||
} else {
|
||||
uni.showToast({ title: "缺少签到ID参数", icon: "error" });
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
|
||||
const loadTeacherList = async () => {
|
||||
try {
|
||||
uni.showLoading({ title: "加载教师信息中..." });
|
||||
|
||||
console.log('开始调用API,qdId:', qdId.value);
|
||||
const result = await qdzxFindByQdParamsApi({ qdId: qdId.value });
|
||||
|
||||
uni.hideLoading();
|
||||
|
||||
console.log('API返回结果:', result);
|
||||
console.log('result.rows:', result?.rows);
|
||||
console.log('result.rows类型:', typeof result?.rows);
|
||||
console.log('result.rows是否为数组:', Array.isArray(result?.rows));
|
||||
console.log('result.rows长度:', result?.rows?.length);
|
||||
|
||||
// 直接处理Page对象,不依赖resultCode
|
||||
if (result && result.rows && Array.isArray(result.rows)) {
|
||||
teacherList.value = result.rows;
|
||||
console.log('成功从rows字段获取到教师数据:', result.rows.length, '条');
|
||||
console.log('教师数据详情:', result.rows);
|
||||
} else {
|
||||
console.warn('未找到有效的教师数据,API返回结构:', result);
|
||||
console.warn('result的所有字段:', Object.keys(result || {}));
|
||||
teacherList.value = [];
|
||||
uni.showToast({ title: "未找到教师数据", icon: "none" });
|
||||
}
|
||||
|
||||
console.log('最终teacherList.value:', teacherList.value);
|
||||
console.log('teacherList.value长度:', teacherList.value.length);
|
||||
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error('加载教师列表失败:', error);
|
||||
uni.showToast({ title: "加载教师列表失败", icon: "none" });
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return 'status-signed';
|
||||
case '0':
|
||||
default:
|
||||
return 'status-unsigned';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return '已推送';
|
||||
case '0':
|
||||
default:
|
||||
return '未推送';
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
const handleConfirmPush = async () => {
|
||||
isPushing.value = true;
|
||||
|
||||
try {
|
||||
const result = await xxtsSaveByQdzxParamsApi({ qdId: qdId.value });
|
||||
|
||||
if (result.resultCode === 1) {
|
||||
uni.showToast({ title: '推送成功', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
// 跳转回签到发布列表页面
|
||||
uni.redirectTo({
|
||||
url: '/pages/view/routine/qd/index'
|
||||
});
|
||||
}, 1500);
|
||||
} else {
|
||||
uni.showToast({ title: '推送失败', icon: 'none' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('推送失败:', error);
|
||||
uni.showToast({ title: '推送失败,请重试', icon: 'none' });
|
||||
} finally {
|
||||
isPushing.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.push-list-scroll-view {
|
||||
height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.push-list-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.teacher-count {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.teacher-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.teacher-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.teacher-main-info {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.teacher-name {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.teacher-position {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.teacher-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.status-signed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-unsigned {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
margin-top: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.confirm-btn:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
443
src/pages/view/routine/qd/qr-code.vue
Normal file
443
src/pages/view/routine/qd/qr-code.vue
Normal file
@ -0,0 +1,443 @@
|
||||
<template>
|
||||
<view class="qr-code-page">
|
||||
<view class="qr-container">
|
||||
<!-- 页面头部 -->
|
||||
<view class="page-header">
|
||||
<text class="page-title">签到二维码</text>
|
||||
<text class="page-subtitle">请使用微信扫描二维码进行签到</text>
|
||||
</view>
|
||||
|
||||
<!-- 二维码显示区域 -->
|
||||
<view class="qr-display">
|
||||
<view class="qr-card">
|
||||
<view class="qr-header">
|
||||
<text class="qr-title">{{ meetingInfo?.qdmc || '签到二维码' }}</text>
|
||||
<view class="qr-timer">
|
||||
<text class="timer-text">{{ qrTimer }}s</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="qr-content">
|
||||
<canvas
|
||||
canvas-id="qrcode"
|
||||
class="qr-canvas"
|
||||
:style="{ width: qrSize + 'px', height: qrSize + 'px' }"
|
||||
></canvas>
|
||||
</view>
|
||||
|
||||
<view class="qr-info">
|
||||
<view class="info-item">
|
||||
<text class="info-label">会议时间:</text>
|
||||
<text class="info-value">{{ formatTime(meetingInfo?.qdkstime) }} - {{ formatTime(meetingInfo?.qdjstime) }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">会议地点:</text>
|
||||
<text class="info-value">{{ meetingInfo?.qdwz || '未设置' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-buttons">
|
||||
<button @click="refreshQRCode" class="refresh-btn">
|
||||
<u-icon name="reload" size="16" color="#409EFF" />
|
||||
<text class="btn-text">刷新二维码</text>
|
||||
</button>
|
||||
<button @click="goBack" class="back-btn">
|
||||
<u-icon name="arrow-left" size="16" color="#666" />
|
||||
<text class="btn-text">返回</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 使用说明 -->
|
||||
<view class="usage-tips">
|
||||
<view class="tips-header">
|
||||
<u-icon name="info-circle" size="16" color="#409EFF" />
|
||||
<text class="tips-title">使用说明</text>
|
||||
</view>
|
||||
<view class="tips-content">
|
||||
<text class="tip-item">1. 二维码有效期为60秒,请及时扫描</text>
|
||||
<text class="tip-item">2. 使用微信扫一扫功能扫描二维码</text>
|
||||
<text class="tip-item">3. 扫描后将跳转到签到确认页面</text>
|
||||
<text class="tip-item">4. 请确保网络连接正常</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { qdFindByIdApi, generateQRCodeApi } from '@/api/base/server';
|
||||
|
||||
// 页面参数
|
||||
const qdId = ref('');
|
||||
|
||||
// 会议信息
|
||||
const meetingInfo = ref<any>(null);
|
||||
|
||||
// 二维码相关
|
||||
const qrTimer = ref(60);
|
||||
const qrSize = ref(200);
|
||||
let qrTimerInterval: any = null;
|
||||
let qrCanvas: any = null;
|
||||
|
||||
onLoad((options) => {
|
||||
if (options?.qdId) {
|
||||
qdId.value = options.qdId;
|
||||
}
|
||||
loadMeetingInfo();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
initQRCode();
|
||||
});
|
||||
|
||||
// 加载会议信息
|
||||
const loadMeetingInfo = async () => {
|
||||
try {
|
||||
const result = await qdFindByIdApi({ id: qdId.value });
|
||||
if (result && result.resultCode === 1) {
|
||||
meetingInfo.value = result.result;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载会议信息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化二维码
|
||||
const initQRCode = async () => {
|
||||
try {
|
||||
// 生成二维码数据,只传递qdId
|
||||
const result = await generateQRCodeApi({
|
||||
qdId: qdId.value
|
||||
});
|
||||
|
||||
if (result && result.resultCode === 1) {
|
||||
// 确保二维码数据是字符串格式
|
||||
const qrData = result.result || '';
|
||||
console.log('二维码数据:', qrData);
|
||||
generateQRCodeImage(qrData);
|
||||
} else {
|
||||
throw new Error('生成二维码失败');
|
||||
}
|
||||
|
||||
// 启动倒计时
|
||||
startTimer();
|
||||
} catch (error) {
|
||||
console.error('生成二维码失败:', error);
|
||||
uni.showToast({ title: '生成二维码失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
// 生成二维码图片
|
||||
const generateQRCodeImage = (qrData: string) => {
|
||||
try {
|
||||
// 使用qrcode库生成真正的二维码
|
||||
import('qrcode').then((QRCode) => {
|
||||
// 使用回调函数方式避免类型错误
|
||||
(QRCode as any).toDataURL(qrData, {
|
||||
width: qrSize.value,
|
||||
height: qrSize.value,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF'
|
||||
},
|
||||
errorCorrectionLevel: 'M'
|
||||
}, (err: any, url: string) => {
|
||||
if (err) {
|
||||
console.error('生成二维码失败:', err);
|
||||
// 如果生成失败,显示错误信息
|
||||
qrCanvas = uni.createCanvasContext('qrcode');
|
||||
qrCanvas.clearRect(0, 0, qrSize.value, qrSize.value);
|
||||
qrCanvas.setFillStyle('#ffffff');
|
||||
qrCanvas.fillRect(0, 0, qrSize.value, qrSize.value);
|
||||
qrCanvas.setFillStyle('#ff0000');
|
||||
qrCanvas.setFontSize(12);
|
||||
qrCanvas.fillText('二维码生成失败', qrSize.value / 2 - 40, qrSize.value / 2);
|
||||
qrCanvas.draw();
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用uni-app的方式处理图片
|
||||
// 将二维码数据直接绘制到canvas上
|
||||
qrCanvas = uni.createCanvasContext('qrcode');
|
||||
qrCanvas.clearRect(0, 0, qrSize.value, qrSize.value);
|
||||
|
||||
// 使用uni-app的drawImage方法
|
||||
qrCanvas.drawImage(url, 0, 0, qrSize.value, qrSize.value);
|
||||
qrCanvas.draw();
|
||||
|
||||
console.log('二维码生成成功');
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error('加载qrcode库失败:', error);
|
||||
// 如果无法加载qrcode库,显示提示信息
|
||||
qrCanvas = uni.createCanvasContext('qrcode');
|
||||
qrCanvas.clearRect(0, 0, qrSize.value, qrSize.value);
|
||||
qrCanvas.setFillStyle('#ffffff');
|
||||
qrCanvas.fillRect(0, 0, qrSize.value, qrSize.value);
|
||||
qrCanvas.setFillStyle('#666666');
|
||||
qrCanvas.setFontSize(12);
|
||||
qrCanvas.fillText('请安装qrcode库', qrSize.value / 2 - 30, qrSize.value / 2);
|
||||
qrCanvas.draw();
|
||||
});
|
||||
|
||||
console.log('二维码数据:', qrData);
|
||||
|
||||
} catch (error) {
|
||||
console.error('生成二维码异常:', error);
|
||||
// 异常处理,显示错误信息
|
||||
qrCanvas = uni.createCanvasContext('qrcode');
|
||||
qrCanvas.clearRect(0, 0, qrSize.value, qrSize.value);
|
||||
qrCanvas.setFillStyle('#ffffff');
|
||||
qrCanvas.fillRect(0, 0, qrSize.value, qrSize.value);
|
||||
qrCanvas.setFillStyle('#ff0000');
|
||||
qrCanvas.setFontSize(12);
|
||||
qrCanvas.fillText('二维码生成异常', qrSize.value / 2 - 40, qrSize.value / 2);
|
||||
qrCanvas.draw();
|
||||
}
|
||||
};
|
||||
|
||||
// 启动倒计时
|
||||
const startTimer = () => {
|
||||
qrTimer.value = 60;
|
||||
if (qrTimerInterval) {
|
||||
clearInterval(qrTimerInterval);
|
||||
}
|
||||
|
||||
qrTimerInterval = setInterval(() => {
|
||||
qrTimer.value--;
|
||||
if (qrTimer.value <= 0) {
|
||||
clearInterval(qrTimerInterval);
|
||||
uni.showToast({ title: '二维码已过期', icon: 'none' });
|
||||
// 可以在这里自动刷新二维码
|
||||
refreshQRCode();
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 刷新二维码
|
||||
const refreshQRCode = async () => {
|
||||
try {
|
||||
await initQRCode();
|
||||
uni.showToast({ title: '二维码已刷新', icon: 'success' });
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '刷新失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return '未设置';
|
||||
const date = new Date(time);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
// 组件销毁时清理定时器
|
||||
onUnmounted(() => {
|
||||
if (qrTimerInterval) {
|
||||
clearInterval(qrTimerInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.qr-code-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// 页面头部
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
// 二维码显示区域
|
||||
.qr-display {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.qr-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.qr-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.qr-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.qr-timer {
|
||||
background: linear-gradient(135deg, #ff4757 0%, #ff3742 100%);
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
|
||||
.timer-text {
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qr-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.qr-canvas {
|
||||
border: 2px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-info {
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.info-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 操作按钮
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.refresh-btn, .back-btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: #409EFF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用说明
|
||||
.usage-tips {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
.tips-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.tips-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.tips-content {
|
||||
.tip-item {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式优化
|
||||
@media (max-width: 375px) {
|
||||
.qr-code-page {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.qr-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.qr-size {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.page-header .page-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
844
src/pages/view/routine/qd/selectTeachers.vue
Normal file
844
src/pages/view/routine/qd/selectTeachers.vue
Normal file
@ -0,0 +1,844 @@
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<scroll-view scroll-y class="teacher-scroll-view">
|
||||
<view class="teacher-container">
|
||||
<!-- 筛选条件区域 -->
|
||||
<view class="filter-section">
|
||||
<view class="filter-title">📋 选择条件</view>
|
||||
|
||||
<!-- 选择类别 -->
|
||||
<view class="filter-row">
|
||||
<view class="filter-label">选择类别</view>
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="selectTypeOptions"
|
||||
range-key="label"
|
||||
@change="onSelectTypeChange"
|
||||
class="filter-picker"
|
||||
>
|
||||
<view class="picker-display">
|
||||
<text :class="{ placeholder: !selectType }">
|
||||
{{ getSelectTypeText() || '请选择类别' }}
|
||||
</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 职务类型选择器 -->
|
||||
<view class="filter-row" v-if="selectType === 4">
|
||||
<view class="filter-label">职务类型</view>
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="zwTypeOptions"
|
||||
range-key="label"
|
||||
@change="onZwTypeChange"
|
||||
class="filter-picker"
|
||||
>
|
||||
<view class="picker-display">
|
||||
<text>{{ getZwTypeText() }}</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 具体选择 -->
|
||||
<view class="filter-row" v-if="selectType && datas.length > 0">
|
||||
<view class="filter-label">{{ getSecondSelectLabel() }}</view>
|
||||
<view class="multi-select-container">
|
||||
<view class="selected-items-display" @click="showMultiSelectModal">
|
||||
<text :class="{ placeholder: !selectTwoType.length }">
|
||||
{{ getSecondSelectText() || '请选择' }}
|
||||
</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 多选弹窗 -->
|
||||
<view v-if="showModal" class="modal-overlay" @click="hideMultiSelectModal">
|
||||
<view class="modal-content" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ getSecondSelectLabel() }}</text>
|
||||
<view class="modal-actions">
|
||||
<text class="modal-btn cancel-btn" @click="hideMultiSelectModal">取消</text>
|
||||
<text class="modal-btn confirm-btn" @click="confirmMultiSelect">确定</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="checkbox-list">
|
||||
<view
|
||||
v-for="item in datas"
|
||||
:key="item.value"
|
||||
class="checkbox-item"
|
||||
@click="toggleItemSelection(item.value)"
|
||||
>
|
||||
<view class="checkbox">
|
||||
<uni-icons
|
||||
:type="selectTwoType.includes(item.value) ? 'checkbox-filled' : 'circle'"
|
||||
size="20"
|
||||
:color="selectTwoType.includes(item.value) ? '#007aff' : '#ccc'"
|
||||
></uni-icons>
|
||||
</view>
|
||||
<text class="checkbox-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 选择结果区域 -->
|
||||
<view class="result-section">
|
||||
<view class="result-title">
|
||||
👥 选择结果
|
||||
<text v-if="selectedTeachers.length > 0" class="result-count">
|
||||
({{ selectedTeachers.length }})
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 已选教师标签 -->
|
||||
<view class="name-tags" v-if="selectedTeachers.length > 0">
|
||||
<view
|
||||
v-for="teacher in selectedTeachers"
|
||||
:key="teacher.id"
|
||||
class="name-tag"
|
||||
>
|
||||
<text>{{ teacher.jsxm }}</text>
|
||||
<view class="remove-icon" @click="removeTeacher(teacher.id)">
|
||||
<uni-icons type="close" size="12" color="#ffffff"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="selectedTeachers.length === 0 && !isLoading" class="empty-state">
|
||||
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
|
||||
<text class="empty-text">
|
||||
{{ selectType ? '请先选择条件,加载教师列表' : '请选择筛选条件' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="isLoading" class="loading-state">
|
||||
<uni-icons type="spinner-cycle" size="40" color="#007aff"></uni-icons>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<template #bottom>
|
||||
<view class="bottom-actions">
|
||||
<button class="action-btn cancel-btn" @click="handleCancel">
|
||||
取消
|
||||
</button>
|
||||
<button class="action-btn confirm-btn" @click="handleConfirm">
|
||||
确定 ({{ selectedTeachers.length }})
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import {
|
||||
jsdfindJsByPhoneApi,
|
||||
jsFindByNjIdOrBjIdApi,
|
||||
jsFindByKmIdApi,
|
||||
jsFindByBzrNjIdApi,
|
||||
jsFindByZwIdApi,
|
||||
findAllNj,
|
||||
kmFindAllApi,
|
||||
zwFindAllApi
|
||||
} from "@/api/base/server";
|
||||
|
||||
interface TeacherInfo {
|
||||
id: string;
|
||||
jsxm: string;
|
||||
jsId: string;
|
||||
dzzw: string;
|
||||
qtzw: string;
|
||||
}
|
||||
|
||||
// 筛选相关状态
|
||||
const selectType = ref<number | null>(null);
|
||||
const selectTwoType = ref<string[]>([]);
|
||||
const zwType = ref<number>(1);
|
||||
const datas = ref<any[]>([]);
|
||||
const selectedTeachers = ref<TeacherInfo[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const showModal = ref(false); // 控制多选弹窗的显示
|
||||
|
||||
// 选择类别选项
|
||||
const selectTypeOptions = [
|
||||
{ value: 1, label: '所有老师' },
|
||||
{ value: 2, label: '科目' },
|
||||
{ value: 3, label: '班主任' },
|
||||
{ value: 4, label: '职务' }
|
||||
];
|
||||
|
||||
// 职务类型选项
|
||||
const zwTypeOptions = [
|
||||
{ value: 1, label: '党政职务' },
|
||||
{ value: 2, label: '其他职务' }
|
||||
];
|
||||
|
||||
// 获取选择类别文本
|
||||
const getSelectTypeText = () => {
|
||||
const option = selectTypeOptions.find(item => item.value === selectType.value);
|
||||
return option ? option.label : '';
|
||||
};
|
||||
|
||||
// 获取职务类型文本
|
||||
const getZwTypeText = () => {
|
||||
const option = zwTypeOptions.find(item => item.value === zwType.value);
|
||||
return option ? option.label : '';
|
||||
};
|
||||
|
||||
// 获取第二个选择框的标签
|
||||
const getSecondSelectLabel = () => {
|
||||
switch (selectType.value) {
|
||||
case 1:
|
||||
return '年级/班级';
|
||||
case 2:
|
||||
return '科目';
|
||||
case 3:
|
||||
return '年级';
|
||||
case 4:
|
||||
return '具体职务';
|
||||
default:
|
||||
return '请选择';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取第二个选择框的文本
|
||||
const getSecondSelectText = () => {
|
||||
if (!selectTwoType.value.length) return '';
|
||||
const selectedItems = datas.value.filter(item =>
|
||||
selectTwoType.value.includes(item.value)
|
||||
);
|
||||
if (selectedItems.length === 1) {
|
||||
return selectedItems[0].label;
|
||||
} else if (selectedItems.length > 1) {
|
||||
return `已选择${selectedItems.length}项`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// 选择类别改变
|
||||
const onSelectTypeChange = async (e: any) => {
|
||||
selectType.value = selectTypeOptions[e.detail.value].value;
|
||||
selectTwoType.value = [];
|
||||
selectedTeachers.value = [];
|
||||
|
||||
if (selectType.value === 1 || selectType.value === 3) {
|
||||
await loadNjData();
|
||||
} else if (selectType.value === 2) {
|
||||
await loadKmData();
|
||||
} else if (selectType.value === 4) {
|
||||
await loadZwData();
|
||||
}
|
||||
};
|
||||
|
||||
// 职务类型改变
|
||||
const onZwTypeChange = async (e: any) => {
|
||||
zwType.value = zwTypeOptions[e.detail.value].value;
|
||||
selectTwoType.value = [];
|
||||
selectedTeachers.value = [];
|
||||
await loadZwData();
|
||||
};
|
||||
|
||||
// 多选弹窗相关
|
||||
const showMultiSelectModal = () => {
|
||||
showModal.value = true;
|
||||
};
|
||||
|
||||
const hideMultiSelectModal = () => {
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
const toggleItemSelection = (value: string) => {
|
||||
const index = selectTwoType.value.indexOf(value);
|
||||
if (index > -1) {
|
||||
selectTwoType.value.splice(index, 1);
|
||||
} else {
|
||||
selectTwoType.value.push(value);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmMultiSelect = () => {
|
||||
hideMultiSelectModal();
|
||||
loadTeachers();
|
||||
};
|
||||
|
||||
// 加载年级数据
|
||||
const loadNjData = async () => {
|
||||
try {
|
||||
const result = await findAllNj();
|
||||
console.log('年级API返回结果:', result);
|
||||
|
||||
// 处理不同的返回格式
|
||||
let njData = [];
|
||||
if (Array.isArray(result)) {
|
||||
njData = result;
|
||||
} else if (result && typeof result === 'object') {
|
||||
// 检查各种可能的属性
|
||||
if (Array.isArray(result.result)) {
|
||||
njData = result.result;
|
||||
} else if (result.resultCode === 1 && Array.isArray(result.result)) {
|
||||
njData = result.result;
|
||||
} else if (Array.isArray(result.data)) {
|
||||
njData = result.data;
|
||||
} else {
|
||||
console.warn('年级数据格式异常:', result);
|
||||
njData = [];
|
||||
}
|
||||
} else {
|
||||
console.warn('年级数据格式异常:', result);
|
||||
njData = [];
|
||||
}
|
||||
|
||||
// 直接使用后端已排序的数据,不进行二次排序
|
||||
datas.value = njData.map((item: any) => ({
|
||||
label: item.njmc || item.name || item.label,
|
||||
value: item.id || item.value
|
||||
}));
|
||||
|
||||
console.log('处理后的年级数据:', datas.value);
|
||||
} catch (error) {
|
||||
console.error('加载年级数据失败:', error);
|
||||
uni.showToast({ title: '加载年级数据失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
// 加载科目数据
|
||||
const loadKmData = async () => {
|
||||
try {
|
||||
const result = await kmFindAllApi();
|
||||
console.log('科目API返回结果:', result);
|
||||
|
||||
// 处理不同的返回格式
|
||||
let kmData = [];
|
||||
if (Array.isArray(result)) {
|
||||
kmData = result;
|
||||
} else if (result && typeof result === 'object') {
|
||||
// 检查各种可能的属性
|
||||
if (Array.isArray(result.result)) {
|
||||
kmData = result.result;
|
||||
} else if (result.resultCode === 1 && Array.isArray(result.result)) {
|
||||
kmData = result.result;
|
||||
} else if (Array.isArray(result.data)) {
|
||||
kmData = result.data;
|
||||
} else {
|
||||
console.warn('科目数据格式异常:', result);
|
||||
kmData = [];
|
||||
}
|
||||
} else {
|
||||
console.warn('科目数据格式异常:', result);
|
||||
kmData = [];
|
||||
}
|
||||
|
||||
// 前端排序:按sort字段排序,如果没有sort字段则按名称排序
|
||||
const sortedData = kmData.sort((a: any, b: any) => {
|
||||
const sortA = a.sort || 0;
|
||||
const sortB = b.sort || 0;
|
||||
if (sortA !== sortB) {
|
||||
return sortA - sortB;
|
||||
} else {
|
||||
return (a.kmmc || a.name || '').localeCompare(b.kmmc || b.name || '');
|
||||
}
|
||||
});
|
||||
|
||||
datas.value = sortedData.map((item: any) => ({
|
||||
label: item.kmmc || item.name || item.label,
|
||||
value: item.id || item.value
|
||||
}));
|
||||
|
||||
console.log('处理后的科目数据:', datas.value);
|
||||
} catch (error) {
|
||||
console.error('加载科目数据失败:', error);
|
||||
uni.showToast({ title: '加载科目数据失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
// 加载职务数据
|
||||
const loadZwData = async () => {
|
||||
try {
|
||||
const result = await zwFindAllApi();
|
||||
console.log('职务API返回结果:', result);
|
||||
|
||||
// 处理不同的返回格式
|
||||
let allZwData = [];
|
||||
if (Array.isArray(result)) {
|
||||
allZwData = result;
|
||||
} else if (result && typeof result === 'object') {
|
||||
// 检查各种可能的属性
|
||||
if (Array.isArray(result.result)) {
|
||||
allZwData = result.result;
|
||||
} else if (result.resultCode === 1 && Array.isArray(result.result)) {
|
||||
allZwData = result.result;
|
||||
} else if (Array.isArray(result.data)) {
|
||||
allZwData = result.data;
|
||||
} else {
|
||||
console.warn('职务数据格式异常:', result);
|
||||
allZwData = [];
|
||||
}
|
||||
} else {
|
||||
console.warn('职务数据格式异常:', result);
|
||||
allZwData = [];
|
||||
}
|
||||
|
||||
// 根据职务类型过滤数据
|
||||
const filteredData = allZwData.filter((item: any) => {
|
||||
if (zwType.value === 1) {
|
||||
return item.zwlx === '1' || item.zwlx === '党政职务' || item.zwlx === 'A' || item.zwlx === 'dzzw';
|
||||
} else if (zwType.value === 2) {
|
||||
return item.zwlx === '2' || item.zwlx === '其他职务' || item.zwlx === 'B' || item.zwlx === 'qtzw';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 前端排序:按sort字段排序,如果没有sort字段则按名称排序
|
||||
const sortedData = filteredData.sort((a: any, b: any) => {
|
||||
const sortA = a.sort || 0;
|
||||
const sortB = b.sort || 0;
|
||||
if (sortA !== sortB) {
|
||||
return sortA - sortB;
|
||||
} else {
|
||||
return (a.zwmc || a.name || '').localeCompare(b.zwmc || b.name || '');
|
||||
}
|
||||
});
|
||||
|
||||
datas.value = sortedData.map((item: any) => ({
|
||||
label: item.zwmc || item.name || item.label,
|
||||
value: item.id || item.value
|
||||
}));
|
||||
|
||||
console.log('处理后的职务数据:', datas.value);
|
||||
} catch (error) {
|
||||
console.error('加载职务数据失败:', error);
|
||||
uni.showToast({ title: '加载职务数据失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
// 加载教师数据
|
||||
const loadTeachers = async () => {
|
||||
if (!selectType.value || !selectTwoType.value.length) return;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
let allTeacherData: any[] = [];
|
||||
|
||||
// 遍历所有选中的值,分别调用API
|
||||
for (const selectedValue of selectTwoType.value) {
|
||||
let result;
|
||||
|
||||
if (selectType.value === 1) {
|
||||
result = await jsFindByNjIdOrBjIdApi({
|
||||
njOrBjId: selectedValue
|
||||
});
|
||||
} else if (selectType.value === 2) {
|
||||
result = await jsFindByKmIdApi({
|
||||
kmId: selectedValue
|
||||
});
|
||||
} else if (selectType.value === 3) {
|
||||
result = await jsFindByBzrNjIdApi({
|
||||
njId: selectedValue
|
||||
});
|
||||
} else if (selectType.value === 4) {
|
||||
result = await jsFindByZwIdApi({
|
||||
zwId: selectedValue,
|
||||
zwType: zwType.value.toString()
|
||||
});
|
||||
}
|
||||
|
||||
console.log('教师API返回结果:', result);
|
||||
|
||||
// 处理不同的返回格式
|
||||
let teacherData = [];
|
||||
if (Array.isArray(result)) {
|
||||
teacherData = result;
|
||||
} else if (result && typeof result === 'object') {
|
||||
// 检查各种可能的属性
|
||||
if (Array.isArray(result.result)) {
|
||||
teacherData = result.result;
|
||||
} else if (result.resultCode === 1 && Array.isArray(result.result)) {
|
||||
teacherData = result.result;
|
||||
} else if (Array.isArray(result.data)) {
|
||||
teacherData = result.data;
|
||||
} else {
|
||||
console.warn('教师数据格式异常:', result);
|
||||
teacherData = [];
|
||||
}
|
||||
} else {
|
||||
console.warn('教师数据格式异常:', result);
|
||||
teacherData = [];
|
||||
}
|
||||
|
||||
// 将当前结果添加到总结果中
|
||||
allTeacherData = allTeacherData.concat(teacherData);
|
||||
}
|
||||
|
||||
// 去重(根据教师ID)
|
||||
const uniqueTeachers = allTeacherData.filter((teacher, index, self) =>
|
||||
index === self.findIndex(t => (t.id || t.jsId) === (teacher.id || teacher.jsId))
|
||||
);
|
||||
|
||||
console.log('API返回的教师数据总数:', allTeacherData.length);
|
||||
console.log('去重后的教师数据总数:', uniqueTeachers.length);
|
||||
console.log('是否有重复数据:', allTeacherData.length !== uniqueTeachers.length);
|
||||
|
||||
if (allTeacherData.length !== uniqueTeachers.length) {
|
||||
console.warn('发现重复的教师数据,已进行去重处理');
|
||||
}
|
||||
|
||||
// 直接使用后端已排序的数据,不进行二次排序
|
||||
selectedTeachers.value = uniqueTeachers.map((item: any) => ({
|
||||
id: item.id || item.jsId,
|
||||
jsxm: item.jsxm || item.name || item.label,
|
||||
jsId: item.id || item.jsId,
|
||||
dzzw: item.dzzw || '',
|
||||
qtzw: item.qtzw || ''
|
||||
}));
|
||||
|
||||
console.log('最终处理后的教师数据:', selectedTeachers.value);
|
||||
} catch (error) {
|
||||
console.error('加载教师列表失败:', error);
|
||||
uni.showToast({ title: '加载教师列表失败', icon: 'none' });
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 移除教师
|
||||
const removeTeacher = (teacherId: string) => {
|
||||
const index = selectedTeachers.value.findIndex(teacher => teacher.id === teacherId);
|
||||
if (index > -1) {
|
||||
selectedTeachers.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
console.log('=== handleConfirm 开始 ===');
|
||||
console.log('当前选中的教师数量:', selectedTeachers.value.length);
|
||||
console.log('选中的教师数据:', selectedTeachers.value);
|
||||
|
||||
// 检查是否有重复的教师ID
|
||||
const teacherIds = selectedTeachers.value.map(t => t.jsId);
|
||||
const uniqueIds = [...new Set(teacherIds)];
|
||||
console.log('教师ID列表:', teacherIds);
|
||||
console.log('去重后的教师ID列表:', uniqueIds);
|
||||
console.log('是否有重复ID:', teacherIds.length !== uniqueIds.length);
|
||||
|
||||
if (teacherIds.length !== uniqueIds.length) {
|
||||
console.warn('发现重复的教师ID,将进行去重处理');
|
||||
// 去重处理
|
||||
const uniqueTeachers = selectedTeachers.value.filter((teacher, index, self) =>
|
||||
index === self.findIndex(t => t.jsId === teacher.jsId)
|
||||
);
|
||||
selectedTeachers.value = uniqueTeachers;
|
||||
console.log('去重后的教师数据:', uniqueTeachers);
|
||||
}
|
||||
|
||||
// 使用全局事件传递教师数据
|
||||
console.log('准备发送 teacherSelected 事件');
|
||||
uni.$emit('teacherSelected', selectedTeachers.value);
|
||||
console.log('teacherSelected 事件已发送');
|
||||
|
||||
console.log('准备返回上一页');
|
||||
uni.navigateBack();
|
||||
console.log('=== handleConfirm 结束 ===');
|
||||
};
|
||||
|
||||
// 页面加载时初始化
|
||||
onMounted(() => {
|
||||
// 可以在这里加载默认数据或进行其他初始化
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.teacher-scroll-view {
|
||||
height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.teacher-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
width: 80px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.filter-picker {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.picker-display {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.multi-select-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.selected-items-display {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
border-radius: 12px 12px 0 0;
|
||||
width: 100%;
|
||||
max-height: 70%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
padding: 8px 15px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-btn.cancel-btn {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.modal-btn.confirm-btn {
|
||||
background: linear-gradient(135deg, #007aff 0%, #0056cc 100%);
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.3);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.checkbox-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.checkbox-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.result-count {
|
||||
color: #007aff;
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.name-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 10px; // Row gap, Column gap
|
||||
margin-bottom: 15px;
|
||||
|
||||
.name-tag {
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
flex-basis: calc((100% - 30px) / 4);
|
||||
height: 30px;
|
||||
line-height: 20px;
|
||||
border: 1px solid #f4f4f5;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
background-color: #f4f4f5;
|
||||
color: #909399;
|
||||
|
||||
.remove-icon {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #ff4d4f;
|
||||
border-bottom-left-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state, .loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-text, .loading-text {
|
||||
margin-top: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background: linear-gradient(135deg, #007aff 0%, #0056cc 100%);
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
BIN
src/static/base/details.png
Normal file
BIN
src/static/base/details.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
BIN
src/static/base/push.png
Normal file
BIN
src/static/base/push.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src/static/base/qr-code.png
Normal file
BIN
src/static/base/qr-code.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Loading…
x
Reference in New Issue
Block a user