2026-01-10 10:09:15 +08:00

942 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="push-page">
<!-- 任务信息 -->
<view class="task-info-card">
<view class="task-title">{{ taskInfo.rwmc }}</view>
<view class="task-meta">
<text class="meta-item">截止时间{{ formatDate(taskInfo.rwjstime) }}</text>
<text class="meta-item">负责人{{ taskInfo.rwfzrxm }}</text>
</view>
</view>
<!-- 推送对象选择 -->
<view class="push-section">
<view class="section-header">
<text class="section-title">选择推送对象</text>
<view class="section-actions">
<text class="action-btn" @click="selectAllMembers">全选</text>
<text class="action-btn" @click="unselectAllMembers">取消全选</text>
</view>
</view>
<!-- 成员分组列表 -->
<scroll-view scroll-y class="members-scroll">
<view
v-for="(groupMembers, groupName) in groupedMembers"
:key="groupName"
class="member-group-section"
>
<!-- 分组头部 -->
<view class="group-header">
<view class="group-checkbox-container" @click="handleGroupCheckChange(String(groupName))">
<view
:class="['checkbox-box', {
checked: isGroupChecked(String(groupName)),
indeterminate: isGroupIndeterminate(String(groupName))
}]"
>
<text v-if="isGroupChecked(String(groupName))" class="check-icon">✓</text>
<text v-else-if="isGroupIndeterminate(String(groupName))" class="check-icon">-</text>
</view>
<view class="group-info">
<text class="group-title">{{ groupName }}</text>
<text class="group-count">({{ groupMembers.length }}人)</text>
</view>
</view>
</view>
<!-- 分组成员列表 -->
<view class="group-members-list">
<view class="members-grid">
<view
v-for="member in groupMembers"
:key="member.id"
:class="['member-check-item', { 'is-pushed': isPushed(member.jsId) }]"
@click="handleMemberCheckChange(member.id)"
>
<view :class="['checkbox-box', { checked: selectedMembers[member.id] }]">
<text v-if="selectedMembers[member.id]" class="check-icon">✓</text>
</view>
<view class="member-info">
<view class="member-avatar">
<text class="avatar-text">{{ member.jsxm?.charAt(0) || '?' }}</text>
</view>
<view class="member-details">
<text class="member-name">{{ member.jsxm }}</text>
<text v-if="isPushed(member.jsId)" class="pushed-badge">已推送</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="Object.keys(groupedMembers).length === 0" class="empty-state">
<text class="empty-text">暂无课程成员</text>
<text class="empty-hint">请先添加课程成员</text>
</view>
</scroll-view>
<!-- 推送统计 -->
<view class="push-summary">
<text class="summary-text">已选择 {{ selectedCount }} 名成员</text>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="bottom-actions">
<view class="action-buttons">
<button class="cancel-btn" @click="goBack" :disabled="isSubmitting">
取消
</button>
<button
class="confirm-btn"
@click="handleConfirmPush"
:disabled="isSubmitting || selectedCount === 0"
>
{{ isSubmitting ? '推送中...' : '确认推送' }}
</button>
</view>
</view>
<!-- 推送遮罩层 -->
<view v-if="isSubmitting" class="push-overlay">
<view class="push-loading">
<view class="loading-spinner"></view>
<text class="loading-text">正在推送任务...</text>
<text class="loading-hint">请稍候不要重复点击</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { kccyFindByKcjbIdApi } from "@/api/base/kccyApi";
import { rwPushJsApi } from "@/api/base/rwApi";
import { executedInfoByRwIdApi } from "@/api/base/rwzxApi";
// 接口类型定义
interface TaskInfo {
id: string;
rwmc: string;
rwms: string;
rwjstime: string;
rwfzrxm: string;
rwfzr: string;
rwStatus: string;
rwlyId: string;
}
interface MemberItem {
id: string;
jsId: string;
jsxm: string;
kcjbId: string;
fzmc: string;
}
// 响应式数<E5BC8F>?
const taskInfo = ref<TaskInfo>({
id: '',
rwmc: '',
rwms: '',
rwjstime: '',
rwfzrxm: '',
rwfzr: '',
rwStatus: '',
rwlyId: ''
});
const courseId = ref('');
const memberList = ref<MemberItem[]>([]);
const selectedMembers = reactive<{ [key: string]: boolean }>({});
const pushedJsIds = ref<string[]>([]); // 已推送的教师ID列表
const isSubmitting = ref(false);
const lastClickTime = ref(0);
const DEBOUNCE_DELAY = 2000; // 防抖延迟2秒
// 计算属<E7AE97>?
const groupedMembers = computed(() => {
const groups: { [key: string]: MemberItem[] } = {};
memberList.value.forEach(member => {
const groupName = member.fzmc || '未分组';
if (!groups[groupName]) {
groups[groupName] = [];
}
groups[groupName].push(member);
});
return groups;
});
const selectedCount = computed(() => {
return Object.values(selectedMembers).filter(Boolean).length;
});
// 检查分组是否全
const isGroupChecked = (groupName: string) => {
const groupMembers = groupedMembers.value[groupName] || [];
return groupMembers.length > 0 && groupMembers.every(member => selectedMembers[member.id]);
};
// 检查分组是否半选
const isGroupIndeterminate = (groupName: string) => {
const groupMembers = groupedMembers.value[groupName] || [];
const checkedCount = groupMembers.filter(member => selectedMembers[member.id]).length;
return checkedCount > 0 && checkedCount < groupMembers.length;
};
// 检查教师是否已推送
const isPushed = (jsId: string) => {
return pushedJsIds.value.includes(jsId);
};
// 页面加载
onLoad((options: any) => {
console.log('推送页面接收到的参数:', options);
// 从全局存储中获取任务数据
const storedTaskData = uni.getStorageSync('pushTaskData');
if (storedTaskData) {
try {
taskInfo.value = storedTaskData;
courseId.value = taskInfo.value.rwlyId || options.courseId || '';
console.log('任务信息:', taskInfo.value);
console.log('课程ID:', courseId.value);
// 清除存储的任务数据
uni.removeStorageSync('pushTaskData');
loadCourseMembers();
} catch (error) {
console.error('解析任务信息失败:', error);
uni.showToast({
title: '参数解析失败',
icon: 'error'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
} else {
console.error('缺少任务信息参数');
uni.showToast({
title: '缺少任务信息',
icon: 'error'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
});
// 加载课程成员
const loadCourseMembers = async () => {
try {
console.log('开始加载课程成员课程ID:', courseId.value);
// 并发请求:获取成员列表和任务执行记录(用于判断是否已推送)
const [memberResponse, executionResponse] = await Promise.all([
kccyFindByKcjbIdApi({ kcjbId: courseId.value }),
executedInfoByRwIdApi({ rwId: taskInfo.value.id })
]);
console.log('课程成员API响应:', memberResponse);
console.log('任务执行记录API响应:', executionResponse);
// 处理成员数据
let memberData = [];
if (memberResponse) {
if (memberResponse.hasOwnProperty('resultCode')) {
if (memberResponse.resultCode === 1 || memberResponse.resultCode === 0) {
memberData = memberResponse.result || memberResponse.rows || memberResponse.data || [];
} else {
throw new Error(memberResponse?.message || memberResponse?.msg || '获取课程成员失败');
}
} else if (memberResponse.rows || memberResponse.data) {
memberData = memberResponse.rows || memberResponse.data || [];
} else if (Array.isArray(memberResponse)) {
memberData = memberResponse;
}
}
memberList.value = memberData;
// 处理任务执行记录提取已推送的教师IDiszx = 1
let executionData = [];
if (executionResponse) {
if (executionResponse.hasOwnProperty('resultCode')) {
if (executionResponse.resultCode === 1 || executionResponse.resultCode === 0) {
executionData = executionResponse.result || executionResponse.rows || executionResponse.data || [];
}
} else if (executionResponse.rows || executionResponse.data) {
executionData = executionResponse.rows || executionResponse.data || [];
} else if (Array.isArray(executionResponse)) {
executionData = executionResponse;
}
}
// 从任务执行记录中提取已推送的教师IDiszx = 1
pushedJsIds.value = executionData
.filter((item: any) => item.iszx === 1) // 筛选 iszx = 1已推送的记录
.map((item: any) => item.rwzxfzr) // 提取教师ID
.filter((jsId: string) => jsId != null && jsId.trim() !== ''); // 过滤空值
console.log('已推送的教师ID基于iszx字段:', pushedJsIds.value);
// 默认勾选未推送的成员,不勾选已推送的成员
memberList.value.forEach(member => {
const isPushed = pushedJsIds.value.includes(member.jsId);
selectedMembers[member.id] = !isPushed; // 未推送的默认勾选,已推送的不勾选
});
console.log('解析后的成员列表:', memberList.value);
console.log('默认选中状态:', selectedMembers);
} catch (error) {
console.error('加载课程成员失败:', error);
uni.showToast({
title: '加载成员失败',
icon: 'error'
});
memberList.value = [];
pushedJsIds.value = [];
}
};
// 处理分组选择变化
const handleGroupCheckChange = (groupName: string) => {
const groupMembers = groupedMembers.value[groupName] || [];
const isChecked = isGroupChecked(groupName);
groupMembers.forEach(member => {
selectedMembers[member.id] = !isChecked;
});
};
// 处理单个成员选择变化
const handleMemberCheckChange = (memberId: string) => {
selectedMembers[memberId] = !selectedMembers[memberId];
};
// 全
const selectAllMembers = () => {
memberList.value.forEach(member => {
selectedMembers[member.id] = true;
});
};
// 取消全
const unselectAllMembers = () => {
memberList.value.forEach(member => {
selectedMembers[member.id] = false;
});
};
// 防抖处理确认推送
const handleConfirmPush = () => {
const currentTime = Date.now();
// 防抖检查如果在2秒内重复点击则忽略
if (currentTime - lastClickTime.value < DEBOUNCE_DELAY) {
uni.showToast({
title: '请勿重复点击,请稍候',
icon: 'none',
duration: 1500
});
return;
}
// 如果正在提交中,直接返回
if (isSubmitting.value) {
uni.showToast({
title: '正在推送中,请稍候',
icon: 'none',
duration: 1500
});
return;
}
lastClickTime.value = currentTime;
confirmPush();
};
// 确认推送
const confirmPush = async () => {
if (selectedCount.value === 0) {
uni.showToast({
title: '请选择推送对象',
icon: 'error'
});
return;
}
// 获取选中的成员
const selectedMemberList = memberList.value.filter(member => selectedMembers[member.id]);
uni.showModal({
title: '确认推送',
content: `确定要推送任务"${taskInfo.value.rwmc}"给${selectedCount.value}名成员吗?`,
success: async (res) => {
if (res.confirm) {
await executePush(selectedMemberList);
}
}
});
};
// 执行推
const executePush = async (selectedMemberList: MemberItem[]) => {
isSubmitting.value = true;
try {
// 调用推送API
const pushData = {
rwId: taskInfo.value.id,
jsIds: selectedMemberList.map(member => member.jsId).join(','),
rwlyId: courseId.value
};
const response = await rwPushJsApi(pushData);
console.log('推送API响应:', response);
if (response && response.resultCode === 1) {
uni.showToast({
title: `推送成功!已推送给${selectedCount.value}名成员`,
icon: 'success',
duration: 2000
});
setTimeout(() => {
uni.navigateBack();
}, 2000);
} else {
throw new Error(response?.message);
}
} catch (error) {
console.error('推送失败', error);
uni.showToast({
title: error?.message,
icon: 'error'
});
} finally {
isSubmitting.value = false;
}
};
// 工具函数
const formatDate = (dateStr: string) => {
if (!dateStr) return '';
const date = new Date(dateStr);
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
};
// 返回上一<E4B88A>?
const goBack = () => {
uni.navigateBack();
};
</script>
<style lang="scss" scoped>
.push-page {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #e8f4fd 100%);
display: flex;
flex-direction: column;
}
.task-info-card {
margin: 16px;
padding: 16px;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border-radius: 12px;
box-shadow:
0 2px 12px rgba(0, 0, 0, 0.06),
0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #409eff 0%, #67c23a 50%, #e6a23c 100%);
}
.task-title {
font-size: 16px;
font-weight: 700;
color: #1f2937;
margin-bottom: 8px;
line-height: 1.4;
letter-spacing: 0.2px;
}
.task-meta {
display: flex;
flex-direction: column;
gap: 8px;
.meta-item {
font-size: 14px;
color: #6b7280;
font-weight: 500;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.03);
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
}
}
.push-section {
flex: 1;
margin: 0 16px 80px 16px;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border-radius: 12px;
box-shadow:
0 2px 12px rgba(0, 0, 0, 0.06),
0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #409eff 0%, #67c23a 50%, #e6a23c 100%);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
margin-top: 3px;
.section-title {
font-size: 16px;
font-weight: 700;
color: #1f2937;
letter-spacing: 0.2px;
}
.section-actions {
display: flex;
gap: 12px;
.action-btn {
color: #409eff;
font-size: 14px;
font-weight: 600;
padding: 8px 16px;
border-radius: 8px;
background: rgba(64, 158, 255, 0.1);
border: 1px solid rgba(64, 158, 255, 0.2);
transition: all 0.3s ease;
&:active {
background: rgba(64, 158, 255, 0.2);
transform: translateY(1px);
}
}
}
}
}
.members-scroll {
flex: 1;
max-height: 60vh;
padding: 16px;
}
.member-group-section {
margin-bottom: 16px;
border: 1px solid #e5e7eb;
border-radius: 10px;
background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
transition: all 0.3s ease;
position: relative;
&:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.1);
}
.group-header {
padding: 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
.group-checkbox-container {
display: flex;
align-items: center;
gap: 12px;
.group-info {
display: flex;
align-items: center;
gap: 8px;
.group-title {
font-weight: 700;
color: #409eff;
font-size: 16px;
letter-spacing: 0.3px;
}
.group-count {
color: #6b7280;
font-size: 14px;
font-weight: 500;
}
}
}
}
.group-members-list {
padding: 8px 12px;
}
.members-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
}
.member-check-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border-radius: 8px;
border: 1px solid #e5e7eb;
transition: all 0.3s ease;
position: relative;
&.is-pushed {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border-color: #67c23a;
opacity: 0.85;
.member-avatar {
background: linear-gradient(135deg, #67c23a 0%, #5daf34 100%);
opacity: 0.8;
}
}
&:active {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-color: #409eff;
transform: translateY(1px);
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
}
.member-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
.member-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 4px rgba(64, 158, 255, 0.3);
.avatar-text {
color: #fff;
font-size: 14px;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
}
.member-details {
flex: 1;
display: flex;
align-items: center;
gap: 6px;
.member-name {
display: block;
font-size: 14px;
color: #1f2937;
font-weight: 600;
letter-spacing: 0.1px;
}
.pushed-badge {
display: inline-block;
padding: 2px 6px;
background: linear-gradient(135deg, #67c23a 0%, #5daf34 100%);
color: #fff;
font-size: 10px;
border-radius: 4px;
font-weight: 600;
white-space: nowrap;
box-shadow: 0 1px 3px rgba(103, 194, 58, 0.3);
}
}
}
}
.checkbox-box {
width: 18px;
height: 18px;
border: 2px solid #d1d5db;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
transition: all 0.3s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&.checked {
background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
border-color: #409eff;
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.3);
.check-icon {
color: #fff;
font-size: 12px;
font-weight: bold;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
}
&.indeterminate {
background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
border-color: #409eff;
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.3);
.check-icon {
color: #fff;
font-size: 16px;
font-weight: bold;
line-height: 1;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: #666;
.empty-text {
font-size: 16px;
margin-bottom: 8px;
}
.empty-hint {
font-size: 12px;
color: #999;
}
}
.push-summary {
padding: 12px 16px;
background: linear-gradient(135deg, #f6ffed 0%, #f0f9ff 100%);
border-top: 1px solid rgba(0, 0, 0, 0.05);
text-align: center;
.summary-text {
color: #52c41a;
font-weight: 600;
font-size: 14px;
letter-spacing: 0.1px;
}
}
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
border-top: 1px solid #e5e5e5;
padding: 12px 16px;
z-index: 999;
.action-buttons {
display: flex;
gap: 12px;
.cancel-btn, .confirm-btn {
flex: 1;
height: 40px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: translateY(1px);
}
}
.cancel-btn {
background-color: #909399;
color: #fff;
&:hover {
background-color: #82848a;
}
}
.confirm-btn {
background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
color: #fff;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
&:hover:not(:disabled) {
background: linear-gradient(135deg, #3a8ee6 0%, #337ecc 100%);
}
}
}
}
// 响应式调<E5BC8F>?
@media screen and (max-width: 375px) {
.section-header {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.section-actions {
justify-content: center;
}
.member-check-item {
padding: 6px;
.member-avatar {
width: 28px;
height: 28px;
.avatar-text {
font-size: 12px;
}
}
}
}
/* 推送遮罩层 */
.push-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
backdrop-filter: blur(4px);
.push-loading {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 32px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.1),
0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
min-width: 200px;
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(64, 158, 255, 0.2);
border-top: 3px solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
font-size: 16px;
font-weight: 600;
color: #1f2937;
text-align: center;
letter-spacing: 0.2px;
}
.loading-hint {
font-size: 12px;
color: #6b7280;
text-align: center;
opacity: 0.8;
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 遮罩层响应式调整 */
@media screen and (max-width: 375px) {
.push-overlay .push-loading {
margin: 20px;
padding: 24px;
.loading-spinner {
width: 32px;
height: 32px;
}
.loading-text {
font-size: 14px;
}
.loading-hint {
font-size: 11px;
}
}
}
</style>