876 lines
20 KiB
Vue
876 lines
20 KiB
Vue
|
|
<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 {
|
|||
|
|
throw new Error(response?.message || '推送失败');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('推送失败', error);
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '推送失败,请重试',
|
|||
|
|
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>
|