876 lines
20 KiB
Vue
Raw Normal View History

2025-10-07 08:58:02 +08:00
<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"
@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>
</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";
// 接口类型定义
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 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;
};
// 页面加载
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 response = await kccyFindByKcjbIdApi({ kcjbId: courseId.value });
console.log('课程成员API响应:', response);
// 处理API响应数据兼容多种格式
let memberData = [];
if (response) {
if (response.hasOwnProperty('resultCode')) {
if (response.resultCode === 1 || response.resultCode === 0) {
memberData = response.result || response.rows || response.data || [];
} else {
throw new Error(response?.message || response?.msg || '获取课程成员失败');
}
} else if (response.rows || response.data) {
memberData = response.rows || response.data || [];
} else if (Array.isArray(response)) {
memberData = response;
}
}
memberList.value = memberData;
console.log('解析后的成员列表:', memberList.value);
// 默认全选所有成员
memberList.value.forEach(member => {
selectedMembers[member.id] = true;
});
} catch (error) {
console.error('加载课程成员失败:', error);
uni.showToast({
title: '加载成员失败',
icon: 'error'
});
memberList.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 {
2025-10-11 21:11:16 +08:00
throw new Error(response?.message);
2025-10-07 08:58:02 +08:00
}
} catch (error) {
console.error('推送失败', error);
uni.showToast({
2025-10-11 21:11:16 +08:00
title: error?.message,
2025-10-07 08:58:02 +08:00
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;
&: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;
.member-name {
display: block;
font-size: 14px;
color: #1f2937;
font-weight: 600;
letter-spacing: 0.1px;
}
}
}
}
.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>