Răsfoiți Sursa

我的通知、通知详情、发布通知

吴朋磊 3 săptămâni în urmă
părinte
comite
e32f0aa700

+ 59 - 0
components/common-header.vue

@@ -0,0 +1,59 @@
+<template>
+	<view class="common_header">
+		<view class="header_left" @click="handleBack">
+			<uni-icons type="back" size="24" color="#333333"></uni-icons>
+		</view>
+		<text class="header_title">{{ title }}</text>
+		<view class="header_right">
+			<slot name="right"></slot>
+		</view>
+	</view>
+</template>
+
+<script setup>
+const props = defineProps({
+	title: {
+		type: String,
+		default: ''
+	}
+});
+
+const emit = defineEmits(['back']);
+
+const handleBack = () => {
+	emit('back');
+};
+</script>
+
+<style lang="scss" scoped>
+.common_header {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	z-index: 100;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	height: 96rpx;
+	padding: 0 24rpx;
+	background-color: #FFFFFF;
+	border-bottom: 2rpx solid #F3F3F3;
+
+	.header_left {
+		width: 48rpx;
+	}
+
+	.header_title {
+		flex: 1;
+		text-align: center;
+		font-weight: 500;
+		font-size: 32rpx;
+		color: #333333;
+	}
+
+	.header_right {
+		width: 48rpx;
+	}
+}
+</style>

+ 177 - 0
components/confirm-modal.vue

@@ -0,0 +1,177 @@
+<template>
+	<view v-if="visible" class="modal_mask" >
+		<view class="modal_container" @click.stop>
+			<view class="modal_header">
+				<view class="header_left">
+					<image src="/static/image/icon/icon_warn.png" class="warning_icon" mode="aspectFit"></image>
+					<text class="header_title">{{ title }}</text>
+				</view>
+				<view class="close_btn" @click="handleClose">
+					<image src="/static/image/icon/close.png" class="close_icon" mode="aspectFit"></image>
+				</view>
+			</view>
+			<view class="modal_content">
+				<text class="content_text">{{ content }}</text>
+			</view>
+			<view class="modal_footer">
+				<view class="btn cancel_btn" @click="handleCancel">
+					<text class="btn_text">{{ cancelText }}</text>
+				</view>
+				<view class="btn confirm_btn" @click="handleConfirm">
+					<text class="btn_text">{{ confirmText }}</text>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+const props = defineProps({
+	visible: {
+		type: Boolean,
+		default: false
+	},
+	title: {
+		type: String,
+		default: '提示'
+	},
+	content: {
+		type: String,
+		default: ''
+	},
+	cancelText: {
+		type: String,
+		default: '取消'
+	},
+	confirmText: {
+		type: String,
+		default: '确定'
+	}
+});
+
+const emit = defineEmits(['close', 'cancel', 'confirm']);
+
+const handleClose = () => {
+	emit('close');
+	emit('cancel');
+};
+
+const handleCancel = () => {
+	emit('cancel');
+};
+
+const handleConfirm = () => {
+	emit('confirm');
+};
+</script>
+
+<style lang="scss" scoped>
+.modal_mask {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background-color: rgba(0, 0, 0, 0.5);
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	z-index: 1000;
+}
+
+.modal_container {
+	width: 90%;
+	background-color: #FFFFFF;
+	border-radius: 16rpx;
+	overflow: hidden;
+}
+
+.modal_header {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	height: 96rpx;
+	padding: 0 40rpx;
+	background-color: #F5F7FA;
+
+	.header_left {
+		display: flex;
+		align-items: center;
+	}
+
+	.warning_icon {
+		width: 40rpx;
+		height: 40rpx;
+		margin-right: 16rpx;
+	}
+
+	.header_title {
+		font-weight: 500;
+		font-size: 32rpx;
+		color: #333333;
+	}
+
+	.close_btn {
+		width: 48rpx;
+		height: 48rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+
+		.close_icon {
+			width: 48rpx;
+			height: 48rpx;
+		}
+	}
+}
+
+.modal_content {
+	height: 198rpx;
+	padding: 0 32rpx;
+	border-bottom: 2rpx solid #DCDFE6;
+	line-height: 198rpx;
+	.content_text {
+		font-size: 28rpx;
+		color: #000000;
+	}
+}
+
+.modal_footer {
+	display: flex;
+	padding: 28rpx 40rpx;
+	justify-content: space-around;
+	gap: 32rpx;
+	.btn {
+		height: 80rpx;
+		width: 280rpx;
+		border-radius: 8rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		margin-left: 0;
+
+		.btn_text {
+			font-size: 28rpx;
+			font-weight: 500;
+		}
+	}
+
+	.cancel_btn {
+		background-color: #FFFFFF;
+		border: 2rpx solid #DCDFE6;
+
+		.btn_text {
+			color: #666666;
+		}
+	}
+
+	.confirm_btn {
+		background-color: #2E64FA;
+		border: 2rpx solid #2E64FA;
+
+		.btn_text {
+			color: #FFFFFF;
+		}
+	}
+}
+</style>

+ 101 - 0
components/search-bar.vue

@@ -0,0 +1,101 @@
+<template>
+	<!-- 搜索栏 -->
+	<view class="search_container" :style="containerStyle">
+			<view class="search_box">
+				<image src="/static/image/icon/search.png" class="search_icon" mode="aspectFit"></image>
+				<input type="text" class="search_input" :value="modelValue" @input="handleInput"
+					placeholder="请输入关键字搜索" />
+			</view>
+		</view>
+
+</template>
+
+<script setup>
+import { computed } from 'vue';
+
+const props = defineProps({
+	modelValue: {
+		type: String,
+		default: ''
+	},
+	top: {
+		type: [String, Number],
+		default: ''
+	},
+	isFixed: {
+		type: Boolean,
+		default: false
+	},
+	zIndex: {
+		type: [String, Number],
+		default: '98'
+	},
+	border: {
+		type: Boolean,
+		default: true
+	}
+});
+
+const emit = defineEmits(['update:modelValue', 'search']);
+
+const containerStyle = computed(() => {
+	const style = {};
+	if (props.isFixed) {
+		style.position = 'fixed';
+		style.left = '0';
+		style.right = '0';
+		style.zIndex = props.zIndex;
+	}
+	if (props.top) {
+		style.top = typeof props.top === 'number' ? `${props.top}rpx` : props.top;
+	}
+	if (props.border) {
+		style.borderBottom = '2rpx solid #F3F3F3';
+	} else {
+		style.borderBottom = 'none';
+	}
+	return style;
+});
+
+const handleInput = (e) => {
+	const value = e.detail.value;
+	emit('update:modelValue', value);
+	emit('search', value);
+};
+</script>
+
+<style lang="scss">
+
+.search_container {
+	padding-bottom: 24rpx;
+	margin: 0 24rpx;
+	border-bottom: 2rpx solid #F3F3F3;
+	display: flex;
+
+	.search_box {
+		flex: 1;
+		display: flex;
+		align-items: center;
+		padding: 0rpx 24rpx;
+		height: 72rpx;
+		line-height: 72rpx;
+		border-radius: 8rpx;
+		border: 2rpx solid #DCDFE6;
+
+		.search_icon {
+			width: 32rpx;
+			height: 32rpx;
+			margin-right: 8rpx;
+		}
+
+		.search_input {
+			flex: 1;
+			height: 72rpx;
+			font-size: 28rpx;
+			color: #333333;
+			padding-right: 24rpx;
+			box-sizing: border-box;
+		}
+	}
+}
+</style>

+ 7 - 0
pages.json

@@ -84,6 +84,13 @@
 				"navigationStyle": "custom"
 			}
 		},
+		{
+			"path": "pages/notice/publish/personList",
+			"style": {
+				"navigationBarTitleText": "人员列表",
+				"navigationStyle": "custom"
+			}
+		},
 		{
 			"path": "pages/notice/overview/noticeDetail",
 			"style": {

+ 75 - 63
pages/notice/mine/index.vue

@@ -12,29 +12,14 @@
 							<text class="notice_title">{{ item.title }}</text>
 						</view>
 						<view class="notice_meta">
+							<view class="meta_left">
+								<text class="meta_text">{{ item.category }}</text>
+								<text class="meta_text">{{ item.department }}</text>
+								<text class="meta_text">{{ item.time }}</text>
+							</view>
 							<view class="status_tag" :class="item.statusClass">
 								<text>{{ item.status }}</text>
 							</view>
-							<text class="meta_text">{{ item.category }}</text>
-							<text class="meta_text">{{ item.department }}</text>
-							<text class="meta_text">{{ item.time }}</text>
-						</view>
-					</view>
-					<view class="item_bottom">
-						<view class="notice_stats">
-							<text class="stats_text">已读: {{ item.readCount }}人</text>
-							<text class="stats_text">未读: {{ item.unreadCount }}人</text>
-						</view>
-						<view class="notice_actions">
-							<view class="action_btn" :class="getDeleteClass(item)" @click="handleDelete(item.id)">
-								<text>删除</text>
-							</view>
-							<view class="action_btn edit" @click="handleEdit(item.id)">
-								<text>编辑</text>
-							</view>
-							<view class="action_btn detail" @click="handleDetail(item.id)">
-								<text>详情</text>
-							</view>
 						</view>
 					</view>
 				</scroll-view>
@@ -49,50 +34,18 @@
 
 <script setup>
 import { ref } from 'vue';
+import { onShow } from '@dcloudio/uni-app';
 import NoticeHeader from '@/components/notice-header.vue';
 import NoData from '@/components/no-data.vue';
+import overviewApi from '@/reqApi/overview.js';
 
 const tabs = ['未读', '已读', '全部'];
 const activeTab = ref(0);
+const typeId = ref('0');
+const timeType = ref(0);
+const word = ref('');
 
-const noticeList = ref([
-	{
-		id: '1',
-		title: '25-26学年第一学期初一期中考试安排通知',
-		status: '已发布',
-		statusClass: 'status-published',
-		category: '教学教务',
-		department: '教务处',
-		time: '2024-06-03 18:00:00',
-		readCount: 1500,
-		unreadCount: 50,
-		icon: '/static/image/todo/xiaoxi.png'
-	},
-	{
-		id: '2',
-		title: '关于开展教师培训的通知',
-		status: '已发布',
-		statusClass: 'status-published',
-		category: '教师培训',
-		department: '人事处',
-		time: '2024-06-02 10:00:00',
-		readCount: 280,
-		unreadCount: 15,
-		icon: '/static/image/todo/xiaoxi.png'
-	},
-	{
-		id: '3',
-		title: '暑期放假安排通知',
-		status: '待发布',
-		statusClass: 'status-pending',
-		category: '学校通知',
-		department: '校长办公室',
-		time: '2024-06-01 09:00:00',
-		readCount: 0,
-		unreadCount: 0,
-		icon: '/static/image/todo/xiaoxi.png'
-	}
-]);
+const noticeList = ref([]);
 
 const getDeleteClass = (item) => {
 	if (item.noticeStatus !== 2) {
@@ -101,17 +54,68 @@ const getDeleteClass = (item) => {
 	return 'delete';
 };
 
+const fetchNoticeList = async () => {
+	try {
+		const params = {
+			noticeStatus: activeTab.value,
+			pageParam: {
+				pageNum: 1,
+				pageSize: 9999
+			},
+			typeId: typeId.value,
+			timeType: timeType.value,
+			word: word.value
+		};
+		const res = await overviewApi.teacher_notices_page(params);
+		if (res.data.records && res.data.records[0] && res.data.records[0].noticePageVOList) {
+			noticeList.value = res.data.records[0].noticePageVOList.map(item => ({
+				id: item.id,
+				title: item.noticeName,
+				status: item.readStatus === 1 ? '已读' : '未读',
+				noticeStatus: item.readStatus,
+				statusClass: item.readStatus === 1 ? 'status-published' : 'status-closed',
+				category: item.typeName,
+				department: item.createTeacherName,
+				time: item.releaseTime,
+				readCount: item.readNum,
+				unreadCount: item.unReadNum,
+				icon: '/static/image/todo/xiaoxi.png'
+			}));
+		} else {
+			noticeList.value = [];
+		}
+	} catch (error) {
+		console.error('获取通知列表失败:', error);
+	}
+};
+
+onShow(() => {
+	fetchNoticeList();
+});
+
 const goBack = () => {
 	uni.navigateBack();
 };
 
 const handleTabChange = (idx) => {
 	activeTab.value = idx;
+	fetchNoticeList();
+};
+
+const handleTypeChange = (type) => {
+	typeId.value = type;
+	fetchNoticeList();
+};
+
+const handleTimeChange = (time) => {
+	timeType.value = time;
+	fetchNoticeList();
 };
 
-const handleTypeChange = () => {};
-const handleTimeChange = () => {};
-const handleSearchChange = () => {};
+const handleSearchChange = (keyword) => {
+	word.value = keyword;
+	fetchNoticeList();
+};
 
 const handleDelete = (id) => {
 	uni.showModal({
@@ -188,8 +192,15 @@ const handleDetail = (id) => {
 		.notice_meta {
 			display: flex;
 			align-items: center;
-			flex-wrap: wrap;
-			gap: 16rpx;
+			justify-content: space-between;
+
+			.meta_left {
+				display: flex;
+				align-items: center;
+				flex-wrap: wrap;
+				gap: 16rpx;
+				
+			}
 
 			.status_tag {
 				font-size: 24rpx;
@@ -219,6 +230,7 @@ const handleDetail = (id) => {
 			.meta_text {
 				font-size: 24rpx;
 				color: #999999;
+				height: 48rpx;
 				line-height: 48rpx;
 			}
 		}

+ 35 - 10
pages/notice/overview/index.vue

@@ -50,6 +50,16 @@
 
 		<common-tabbar></common-tabbar>
 
+		<ConfirmModal
+			:visible="showDeleteModal"
+			title="提示"
+			content="确定要删除该通知吗?"
+			cancelText="取消"
+			confirmText="确定"
+			@cancel="closeDeleteModal"
+			@confirm="confirmDelete"
+		></ConfirmModal>
+
 	</view>
 </template>
 
@@ -57,6 +67,7 @@
 import { ref } from 'vue';
 import { onShow } from '@dcloudio/uni-app';
 import NoticeHeader from '@/components/notice-header.vue';
+import ConfirmModal from '@/components/confirm-modal.vue';
 import overviewApi from '@/reqApi/overview.js';
 import store from '@/store/index.js';
 import NoData from '@/components/no-data.vue';
@@ -67,6 +78,8 @@ const typeId = ref('0');
 const timeType = ref(0);
 const word = ref('');
 const noticeList = ref([]);
+const showDeleteModal = ref(false);
+const deleteId = ref(null);
 
 const statusMap = {
 	0: '待发布',
@@ -151,16 +164,28 @@ const handleDelete = (id) => {
 	if (item && item.noticeStatus !== 2) {
 		return;
 	}
-	uni.showModal({
-		title: '确认删除',
-		content: '确定要删除这条通知吗?',
-		success: (res) => {
-			if (res.confirm) {
-				noticeList.value = noticeList.value.filter(item => item.id !== id);
-				uni.showToast({ title: '删除成功', icon: 'success' });
-			}
-		}
-	});
+	deleteId.value = id;
+	showDeleteModal.value = true;
+};
+
+const closeDeleteModal = () => {
+	showDeleteModal.value = false;
+	deleteId.value = null;
+};
+
+const confirmDelete = async () => {
+	if (!deleteId.value) {
+		closeDeleteModal();
+		return;
+	}
+	try {
+		await overviewApi.delete_notices({ id: deleteId.value });
+		noticeList.value = noticeList.value.filter(item => item.id !== deleteId.value);
+		uni.showToast({ title: '删除成功', icon: 'success' });
+	} catch (error) {
+		uni.showToast({ title: '删除失败', icon: 'none' });
+	}
+	closeDeleteModal();
 };
 
 const handleEdit = (id) => {

+ 67 - 9
pages/notice/publish/index.vue

@@ -65,7 +65,7 @@
 					<text class="form_label">通知对象</text>
 					<view class="form_content">
 						<view class="object_row">
-							<view class="object_info">
+							<view class="object_info" @click="goPersonList">
 								<text class="object_count">{{ objectTypeLabel || '请选择' }}</text>
 								<text class="object_count">已选{{ selectedCount }}人</text>
 							</view>
@@ -199,7 +199,8 @@ const fetchNoticeDetail = async (id) => {
 		const res = await overviewApi.notices_edit({ id });
 		if (res.data) {
 			const data = res.data;
-				formData.title = data.noticeName || '';
+			console.log('userList原始数据:', data.userList);
+			formData.title = data.noticeName || '';
 			const content = data.noticeContent || '';
 			formData.content = content.replace(/<[^>]+>/g, '');
 			formData.status = data.isClose !== true;
@@ -226,10 +227,15 @@ const fetchNoticeDetail = async (id) => {
 			}
 			
 			if (data.userList && Array.isArray(data.userList)) {
-				selectedUsers.value = data.userList.map(user => ({
-					id: user.userId,
-					name: user.thirdName
-				}));
+				selectedUsers.value = data.userList.map(user => {
+					const thirdCode = user.thirdCode || user.userAccount || user.teacherId || user.studentCode || '';
+					return {
+						id: user.userId,
+						name: user.thirdName,
+						thirdCode: thirdCode,
+						userAccount: user.userAccount || thirdCode
+					};
+				});
 				selectedCount.value = selectedUsers.value.length;
 			}
 		}
@@ -253,6 +259,20 @@ onShow(() => {
 		}
 		uni.removeStorageSync('selectObjectResult');
 	}
+
+	const personResult = uni.getStorageSync('personListResult');
+	if (personResult) {
+		try {
+			const data = JSON.parse(personResult);
+			if (data && data.users) {
+				selectedUsers.value = data.users;
+				selectedCount.value = data.users.length;
+			}
+		} catch (e) {
+			console.error('解析人员列表结果失败:', e);
+		}
+		uni.removeStorageSync('personListResult');
+	}
 });
 
 const handleTypeChange = (e) => {
@@ -286,6 +306,18 @@ const selectObject = () => {
 	uni.navigateTo({ url: '/pages/notice/publish/selectObject' });
 };
 
+const goPersonList = () => {
+	if (selectedUsers.value.length === 0) {
+		return;
+	}
+	const data = {
+		tab: objectTypeLabel.value,
+		users: selectedUsers.value
+	};
+	uni.setStorageSync('personListData', JSON.stringify(data));
+	uni.navigateTo({ url: '/pages/notice/publish/personList' });
+};
+
 const handleObjectSelect = (data) => {
 	if (data) {
 		objectTypeLabel.value = data.tab;
@@ -371,10 +403,36 @@ const handleSave = async () => {
 			const userInfo = {
 				userType: objectType === 5 ? 2 : 1,
 				userId: user.id,
-				thirdName: user.name.split('(')[0].trim(),
-				thirdCode: user.name.match(/\((.*?)\)/)?.[1] || ''
-				
+				thirdName: user.name,
+				thirdCode: user.thirdCode || user.userAccount || '',
+				gradeName: '',
+				classTypeName: '',
+				className: '',
+				departmentName: '',
+				groupName: '',
+				subjectName: '',
+				roleName: ''
 			};
+
+			switch (objectType) {
+				case 1:
+					userInfo.departmentName = user.departmentName || '';
+					userInfo.groupName = user.groupName || '';
+					break;
+				case 2:
+					userInfo.subjectName = user.subjectName || '';
+					userInfo.gradeName = user.gradeName || '';
+					break;
+				case 3:
+					userInfo.gradeName = user.gradeName || '';
+					userInfo.classTypeName = user.classTypeName || '';
+					userInfo.className = user.className || '';
+					break;
+				case 4:
+					userInfo.roleName = user.roleName || '';
+					break;
+			}
+
 			return userInfo;
 		});
 		

+ 172 - 0
pages/notice/publish/personList.vue

@@ -0,0 +1,172 @@
+<template>
+	<view class="page_person_list">
+		<CommonHeader :title="headerTitle" @back="goBack"></CommonHeader>
+
+		<SearchBar v-model="searchWord" @search="handleSearch" :isFixed="true" :top="114" :border="false"></SearchBar>
+
+		<scroll-view scroll-y class="person_list">
+			<view v-for="(person, index) in filteredList" :key="person.userId || person.id" class="person_item">
+				<view class="person_info">
+					<text class="person_name">{{ person.thirdName || person.name }}</text>
+					<text class="person_code">({{ person.thirdCode || person.userAccount || '' }})</text>
+				</view>
+				<view class="person_action" @click="openDeleteModal(index)">
+					<text class="delete_text">删除</text>
+				</view>
+			</view>
+
+			<view v-if="filteredList.length === 0" class="empty_state">
+				<text class="empty_text">暂无人员</text>
+			</view>
+		</scroll-view>
+
+		<ConfirmModal
+			:visible="showDeleteModal"
+			title="提示"
+			:content="`确定要删除【${deletePersonName}】吗?`"
+			cancelText="取消"
+			confirmText="确定"
+			@cancel="closeDeleteModal"
+			@confirm="confirmDelete"
+		></ConfirmModal>
+	</view>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue';
+import CommonHeader from '@/components/common-header.vue';
+import SearchBar from '@/components/search-bar.vue';
+import ConfirmModal from '@/components/confirm-modal.vue';
+
+const searchWord = ref('');
+
+const personList = ref([]);
+const objectTypeLabel = ref('');
+
+const showDeleteModal = ref(false);
+const deleteIndex = ref(-1);
+const deletePersonName = ref('');
+
+const filteredList = computed(() => {
+	console.log(personList.value)
+	if (!searchWord.value) return personList.value;
+	const keyword = searchWord.value.toLowerCase();
+	return personList.value.filter(person => {
+		const name = (person.thirdName || person.name || '').toLowerCase();
+		const code = (person.thirdCode || person.userAccount || '').toLowerCase();
+		return name.includes(keyword) || code.includes(keyword);
+	});
+	
+});
+
+const headerTitle = computed(() => {
+	return `${objectTypeLabel.value || ''} 已选${personList.value.length}人`;
+});
+
+const handleSearch = (keyword) => {
+	searchWord.value = keyword;
+};
+
+const openDeleteModal = (index) => {
+	const person = personList.value[index];
+	deletePersonName.value = person.thirdName || person.name || '';
+	deleteIndex.value = index;
+	showDeleteModal.value = true;
+};
+
+const closeDeleteModal = () => {
+	showDeleteModal.value = false;
+	deleteIndex.value = -1;
+	deletePersonName.value = '';
+};
+
+const confirmDelete = () => {
+	if (deleteIndex.value >= 0) {
+		personList.value.splice(deleteIndex.value, 1);
+	}
+	closeDeleteModal();
+};
+
+const goBack = () => {
+	uni.setStorageSync('personListResult', JSON.stringify({
+		users: personList.value
+	}));
+	uni.navigateBack();
+};
+
+onMounted(() => {
+	const data = uni.getStorageSync('personListData');
+	if (data) {
+		try {
+			const parsed = JSON.parse(data);
+			const users = parsed.users || [];
+			const seen = new Set();
+			personList.value = users.filter(user => {
+				const userId = user.userId || user.id;
+				if (!userId || seen.has(userId)) return false;
+				seen.add(userId);
+				return true;
+			});
+			objectTypeLabel.value = parsed.tab || '';
+		} catch (e) {
+			console.error('解析人员列表数据失败:', e);
+		}
+	}
+});
+</script>
+
+<style lang="scss">
+.page_person_list {
+	display: flex;
+	flex-direction: column;
+	min-height: 100vh;
+}
+
+.person_list {
+	flex: 1;
+	padding: 222rpx 24rpx 24rpx 24rpx;
+	box-sizing: border-box;
+}
+
+.person_item {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	height: 86rpx;
+	border-bottom: 2rpx solid #F3F3F3;
+	.person_info {
+		display: flex;
+		align-items: center;
+
+		.person_name {
+			font-size: 28rpx;
+			color: #666;
+		}
+
+		.person_code {
+			font-size: 28rpx;
+			color: #666;
+			margin-left: 16rpx;
+		}
+	}
+
+	.person_action {
+		.delete_text {
+			font-size: 28rpx;
+			color: #F56C6C;
+		}
+	}
+}
+
+.empty_state {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	padding: 100rpx 0;
+
+	.empty_text {
+		font-size: 28rpx;
+		color: #999999;
+	}
+}
+</style>

+ 68 - 100
pages/notice/publish/selectObject.vue

@@ -1,10 +1,6 @@
 <template>
 	<view class="page_select_object">
-		<view class="select_header">
-			<uni-icons type="back" size="24" color="#333333" @click="goBack"></uni-icons>
-			<text class="header_title">选择通知对象</text>
-			<text class="header_placeholder"></text>
-		</view>
+		<CommonHeader title="选择通知对象" @back="goBack"></CommonHeader>
 
 		<view class="tabs_container">
 			<view v-for="tab in tabs" :key="tab.key" class="tab_item" :class="{ active: activeTab === tab.key, disabled: editMode }"
@@ -13,12 +9,7 @@
 			</view>
 		</view>
 
-		<view class="search_container">
-			<view class="search_box">
-				<image src="/static/image/icon/search.png" class="search_icon" mode="aspectFit"></image>
-				<input type="text" class="search_input" v-model="searchKeyword" @input="searchTree(searchKeyword)" placeholder="请输入关键字搜索" />
-			</view>
-		</view>
+		<SearchBar v-model="searchKeyword" @search="searchTree" :border="true" :isFixed="true" :top="212" :zIndex="100"></SearchBar>
 
 		<scroll-view scroll-y class="content_list">
 			<TreeNode :tree-data="treeData" :expanded-ids="expandedIds" @toggle-expand="toggleExpand"
@@ -37,6 +28,8 @@
 import { ref, computed, onMounted } from 'vue';
 import uniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue';
 import TreeNode from '@/components/tree-node.vue';
+import SearchBar from '@/components/search-bar.vue';
+import CommonHeader from '@/components/common-header.vue';
 import overview from '@/reqApi/overview.js';
 
 const activeTab = ref('department');
@@ -63,11 +56,12 @@ const deduplicateUsers = (users) => {
 	if (!users || !Array.isArray(users)) return [];
 	const seen = new Set();
 	return users.filter(user => {
-		if (user.teacherId && seen.has(user.teacherId)) {
+		const userId = user.userId;
+		if (userId && seen.has(userId)) {
 			return false;
 		}
-		if (user.teacherId) {
-			seen.add(user.teacherId);
+		if (userId) {
+			seen.add(userId);
 		}
 		return true;
 	});
@@ -79,14 +73,15 @@ const transformDepartmentData = (data) => {
 	
 	return data.map(dept => {
 		const groups = (dept.groupVOS || []).map(group => ({
-			id: group.groupId,
+			id: `${dept.id}_${group.groupId}`,
 			name: group.groupName,
 			checked: false,
 			halfChecked: false,
 			children: deduplicateUsers(group.groupUserVOS || []).map(user => ({
 				id: user.userId,
-				name: `${user.teacherName} (${user.userAccount})`,
+				name: user.teacherName,
 				userId: user.userId,
+				userAccount: user.userAccount,
 				checked: false,
 				halfChecked: false
 			}))
@@ -124,21 +119,22 @@ const transformSubjectData = (data) => {
 	
 	return data.map(subject => {
 		const grades = (subject.gradePersonVos || []).map(grade => ({
-			id: grade.schoolYearGradeId || grade.gradeCode,
+			id: `${subject.subjectCode}_${grade.schoolYearGradeId || grade.gradeCode}`,
 			name: grade.gradeName,
 			checked: false,
 			halfChecked: false,
 			children: deduplicateUsers(grade.personVoList || []).map(user => ({
 				id: user.userId,
-				name: `${user.teacherName} (${user.userAccount})`,
+				name: user.teacherName,
 				userId: user.userId,
+				userAccount: user.userAccount,
 				checked: false,
 				halfChecked: false
 			}))
 		}));
 		
 		return {
-			id: subject.subjectCode,
+			id: `subject_${subject.subjectCode}`,
 			name: subject.subjectName,
 			checked: false,
 			halfChecked: false,
@@ -154,21 +150,22 @@ const transformGradeData = (data) => {
 	return data.map(grade => {
 		const classTypes = (grade.classTypePersonVoList || []).map(classType => {
 			const classes = (classType.classInfoPersonVoList || []).map(cls => ({
-				id: cls.classCode,
+				id: `${classType.classType}_${cls.classCode}`,
 				name: cls.className,
 				checked: false,
 				halfChecked: false,
 				children: deduplicateUsers(cls.personVoList || []).map(user => ({
 					id: user.userId,
-					name: `${user.teacherName} (${user.userAccount})`,
+					name: user.teacherName,
 					userId: user.userId,
+					userAccount: user.userAccount,
 					checked: false,
 					halfChecked: false
 				}))
 			}));
 			
 			return {
-				id: classType.classType,
+				id: `${grade.schoolYearGradeId || grade.gradeCode}_${classType.classType}`,
 				name: classType.classTypeName,
 				checked: false,
 				halfChecked: false,
@@ -198,8 +195,9 @@ const transformPersonalData = (data) => {
 			halfChecked: false,
 			children: deduplicateUsers(item.personVoList || []).map(user => ({
 				id: user.userId,
-				name: `${user.teacherName} (${user.userAccount})`,
+				name: user.teacherName,
 				userId: user.userId,
+				userAccount: user.userAccount,
 				checked: false,
 				halfChecked: false
 			}))
@@ -213,21 +211,22 @@ const transformStudentData = (data) => {
 	
 	return data.map(grade => {
 		const classes = (grade.clsVOS || []).map(cls => ({
-			id: cls.classId,
+			id: `${grade.schoolYearGradeId || grade.gradeCode}_${cls.classId}`,
 			name: cls.className,
 			checked: false,
 			halfChecked: false,
-			children: (cls.studentTblVOS || []).map(student => ({
+			children: deduplicateUsers(cls.studentTblVOS || []).map(student => ({
 				id: student.userId,
-				name: `${student.studentName} (${student.studentCode})`,
+				name: student.studentName,
 				userId: student.userId,
+				userAccount: student.studentCode,
 				checked: false,
 				halfChecked: false
 			}))
 		}));
 		
 		return {
-			id: grade.schoolYearGradeId || grade.gradeCode,
+			id: `student_${grade.schoolYearGradeId || grade.gradeCode}`,
 			name: grade.gradeName,
 			checked: false,
 			halfChecked: false,
@@ -374,14 +373,10 @@ const toggleExpand = ({ itemId, siblingIds, isExpanded }) => {
 	if (isExpanded) {
 		expandedIds.value = expandedIds.value.filter(id => id !== itemId);
 	} else {
-		const descendantIds = getDescendantIds(treeData.value, itemId);
 		expandedIds.value = expandedIds.value.filter(id => {
 			if (siblingIds.includes(id)) {
 				return false;
 			}
-			if (descendantIds.includes(id)) {
-				return false;
-			}
 			return true;
 		});
 		expandedIds.value = [...expandedIds.value, itemId];
@@ -486,15 +481,51 @@ const goBack = () => {
 
 const collectSelectedUsers = () => {
 	const selectedUsers = [];
-	const collect = (nodes) => {
+	const seen = new Set();
+	const collect = (nodes, parentInfo = {}) => {
 		nodes.forEach(node => {
 			if (node.children && node.children.length > 0) {
-				collect(node.children);
-			} else if (node.checked && node.userId) {
+				const newParentInfo = { ...parentInfo };
+				if (node.name && !node.userId) {
+					if (!newParentInfo.departmentName && !newParentInfo.subjectName && !newParentInfo.gradeName && !newParentInfo.roleName) {
+						if (activeTab.value === 1) {
+							newParentInfo.departmentName = node.name;
+						} else if (activeTab.value === 2) {
+							newParentInfo.subjectName = node.name;
+						} else if (activeTab.value === 3) {
+							newParentInfo.gradeName = node.name;
+						} else if (activeTab.value === 4) {
+							newParentInfo.roleName = node.name;
+						} else if (activeTab.value === 5) {
+							newParentInfo.gradeName = node.name;
+						}
+					} else if (!newParentInfo.groupName && !newParentInfo.classTypeName && !newParentInfo.className) {
+						if (activeTab.value === 1) {
+							newParentInfo.groupName = node.name;
+						} else if (activeTab.value === 3) {
+							newParentInfo.classTypeName = node.name;
+						} else if (activeTab.value === 5) {
+							newParentInfo.className = node.name;
+						}
+					} else if (!newParentInfo.className && activeTab.value === 3) {
+						newParentInfo.className = node.name;
+					}
+				}
+				collect(node.children, newParentInfo);
+			} else if (node.checked && node.userId && !seen.has(node.userId)) {
+				seen.add(node.userId);
 				selectedUsers.push({
 					id: node.id,
 					name: node.name,
-					userId: node.userId
+					userId: node.userId,
+					thirdCode: node.userAccount,
+					departmentName: parentInfo.departmentName || '',
+					groupName: parentInfo.groupName || '',
+					subjectName: parentInfo.subjectName || '',
+					gradeName: parentInfo.gradeName || '',
+					classTypeName: parentInfo.classTypeName || '',
+					className: parentInfo.className || '',
+					roleName: parentInfo.roleName || ''
 				});
 			}
 		});
@@ -592,7 +623,7 @@ onMounted(async () => {
 			}
 			
 			if (data.users) {
-				selectedUserIds.value = data.users.map(u => u.id);
+				selectedUserIds.value = data.users.map(u => u.userId || u.id);
 			}
 		} catch (e) {
 			console.error('解析上次选择失败:', e);
@@ -631,31 +662,6 @@ onMounted(async () => {
 	min-height: 100vh;
 }
 
-.select_header {
-	position: fixed;
-	top: 0;
-	left: 0;
-	right: 0;
-	z-index: 100;
-	display: flex;
-	align-items: center;
-	justify-content: space-between;
-	height: 96rpx;
-	padding: 0 24rpx;
-	background-color: #FFFFFF;
-	border-bottom: 2rpx solid #F3F3F3;
-
-	.header_title {
-		font-weight: 500;
-		font-size: 32rpx;
-		color: #333333;
-	}
-
-	.header_placeholder {
-		width: 48rpx;
-	}
-}
-
 .tabs_container {
 	position: fixed;
 	top: 96rpx;
@@ -663,7 +669,7 @@ onMounted(async () => {
 	right: 0;
 	z-index: 99;
 	display: flex;
-	padding: 24rpx;
+	padding: 24rpx ;
 	background-color: #FFFFFF;
 	justify-content: space-between;
 
@@ -700,44 +706,6 @@ onMounted(async () => {
 	}
 }
 
-.search_container {
-	position: fixed;
-	top: 192rpx;
-	left: 0;
-	right: 0;
-	z-index: 98;
-	padding: 32rpx 0rpx;
-	margin: 0 24rpx;
-	background-color: #FFFFFF;
-	border-bottom: 2rpx solid #F3F3F3;
-
-	.search_box {
-		flex: 1;
-		display: flex;
-		align-items: center;
-		padding: 0rpx 24rpx;
-		height: 72rpx;
-		line-height: 72rpx;
-		border-radius: 8rpx;
-		border: 2rpx solid #DCDFE6;
-
-		.search_icon {
-			width: 32rpx;
-			height: 32rpx;
-			margin-right: 8rpx;
-		}
-
-		.search_input {
-			flex: 1;
-			height: 72rpx;
-			font-size: 28rpx;
-			color: #333333;
-			padding-right:24rpx;
-			box-sizing: border-box;
-		}
-	}
-}
-
 .content_list {
 	flex: 1;
 	padding: 330rpx 0 150rpx 0;