fix 选择器
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
<button class="btn-back" @click="handleCancel">
|
||||
<span class="arrow-left">←</span> 返回
|
||||
</button>
|
||||
<h2 class="page-title">{{ taskID ? '编辑学习任务' : '创建学习任务' }}</h2>
|
||||
<h2 class="page-title">{{ taskId ? '编辑学习任务' : '创建学习任务' }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="task-form">
|
||||
@@ -152,7 +152,7 @@
|
||||
<!-- 操作按钮 -->
|
||||
<div class="form-actions">
|
||||
<button class="btn-primary" @click="handleSubmit" :disabled="submitting">
|
||||
{{ submitting ? '提交中...' : (taskID ? '更新任务' : '创建任务') }}
|
||||
{{ submitting ? '提交中...' : (taskId ? '更新任务' : '创建任务') }}
|
||||
</button>
|
||||
<button class="btn-default" @click="handleCancel">取消</button>
|
||||
</div>
|
||||
@@ -252,52 +252,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户选择器弹窗 -->
|
||||
<div v-if="showUserSelector" class="modal-overlay" @click.self="showUserSelector = false">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">选择学习用户</h3>
|
||||
<button class="modal-close" @click="showUserSelector = false">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-model="userSearchKeyword"
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="搜索用户名或手机号"
|
||||
@input="searchUsers"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="userLoading" class="loading-tip">加载中...</div>
|
||||
<div v-else-if="availableUsers.length === 0" class="empty-tip">暂无可选用户</div>
|
||||
<div v-else class="selector-list">
|
||||
<label
|
||||
v-for="user in availableUsers"
|
||||
:key="user.id"
|
||||
class="selector-item"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="user.id"
|
||||
:checked="isUserSelected(user.id)"
|
||||
@change="toggleUser(user)"
|
||||
/>
|
||||
<div class="selector-item-content">
|
||||
<h4 class="selector-item-title">{{ user.username }}</h4>
|
||||
<p class="selector-item-meta">邮箱: {{ user.email || '未知' }} | {{ user.phone || '-' }}</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn-primary" @click="showUserSelector = false">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 用户选择器组件 -->
|
||||
<GenericSelector
|
||||
v-model:visible="showUserSelector"
|
||||
mode="add"
|
||||
title="选择学习用户"
|
||||
left-title="可选用户"
|
||||
right-title="已选用户"
|
||||
:fetch-available-api="fetchAllUsers"
|
||||
:fetch-selected-api="fetchSelectedUsers"
|
||||
:filter-selected="filterUsers"
|
||||
:loading="userLoading"
|
||||
:item-config="{ id: 'id', label: 'username', sublabel: 'deptName' }"
|
||||
:use-tree="true"
|
||||
:tree-transform="transformUsersToTree"
|
||||
:tree-props="{ children: 'children', label: 'displayName', id: 'id' }"
|
||||
:only-leaf-selectable="true"
|
||||
unit-name="人"
|
||||
search-placeholder="搜索用户..."
|
||||
@confirm="handleUserSelectConfirm"
|
||||
@cancel="showUserSelector = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -308,6 +283,7 @@ import { courseApi } from '@/apis/study';
|
||||
import { resourceApi } from '@/apis/resource';
|
||||
import { userApi } from '@/apis/system';
|
||||
import { learningTaskApi } from '@/apis/study';
|
||||
import { GenericSelector } from '@/components/base';
|
||||
import type { TaskVO, Course, TaskItemVO } from '@/types/study';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import type { SysUser } from '@/types/user';
|
||||
@@ -321,7 +297,7 @@ defineExpose({
|
||||
});
|
||||
|
||||
interface Props {
|
||||
taskID?: string;
|
||||
taskId?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
@@ -369,7 +345,6 @@ const showUserSelector = ref(false);
|
||||
// 搜索关键词
|
||||
const courseSearchKeyword = ref('');
|
||||
const resourceSearchKeyword = ref('');
|
||||
const userSearchKeyword = ref('');
|
||||
|
||||
// 加载状态
|
||||
const courseLoading = ref(false);
|
||||
@@ -377,22 +352,52 @@ const resourceLoading = ref(false);
|
||||
const userLoading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.taskID) {
|
||||
loadTask();
|
||||
onMounted(async () => {
|
||||
// 先加载所有可选项
|
||||
await Promise.all([
|
||||
loadCourses(),
|
||||
loadResources(),
|
||||
loadUsers()
|
||||
]);
|
||||
|
||||
// 如果是编辑模式,加载任务数据并恢复选择
|
||||
if (props.taskId) {
|
||||
await loadTask();
|
||||
}
|
||||
loadCourses();
|
||||
loadResources();
|
||||
loadUsers();
|
||||
});
|
||||
|
||||
// 加载任务数据
|
||||
async function loadTask() {
|
||||
try {
|
||||
const res = await learningTaskApi.getTaskById(props.taskID!);
|
||||
const res = await learningTaskApi.getTaskById(props.taskId!);
|
||||
if (res.success && res.data) {
|
||||
taskData.value = res.data as TaskVO;
|
||||
// TODO: 根据 taskCourses、taskResources、taskUsers 恢复选中状态
|
||||
|
||||
// 转换时间格式为 datetime-local 可接受的格式
|
||||
if (taskData.value.learningTask.startTime) {
|
||||
taskData.value.learningTask.startTime = formatDateTimeLocal(taskData.value.learningTask.startTime);
|
||||
}
|
||||
if (taskData.value.learningTask.endTime) {
|
||||
taskData.value.learningTask.endTime = formatDateTimeLocal(taskData.value.learningTask.endTime);
|
||||
}
|
||||
|
||||
// 恢复课程选择
|
||||
if (taskData.value.taskCourses && taskData.value.taskCourses.length > 0) {
|
||||
const courseIds = taskData.value.taskCourses.map(tc => tc.courseID);
|
||||
selectedCourses.value = availableCourses.value.filter(c => courseIds.includes(c.courseID));
|
||||
}
|
||||
|
||||
// 恢复资源选择
|
||||
if (taskData.value.taskResources && taskData.value.taskResources.length > 0) {
|
||||
const resourceIds = taskData.value.taskResources.map(tr => tr.resourceID);
|
||||
selectedResources.value = availableResources.value.filter(r => resourceIds.includes(r.resourceID));
|
||||
}
|
||||
|
||||
// 恢复用户选择
|
||||
if (taskData.value.taskUsers && taskData.value.taskUsers.length > 0) {
|
||||
const userIds = taskData.value.taskUsers.map(tu => tu.userID);
|
||||
selectedUsers.value = availableUsers.value.filter(u => userIds.includes(u.id));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载任务失败:', error);
|
||||
@@ -400,6 +405,29 @@ async function loadTask() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 ISO 8601 时间格式转换为 datetime-local 输入框需要的格式
|
||||
* @param dateTimeString ISO 8601 格式的时间字符串
|
||||
* @returns YYYY-MM-DDTHH:mm 格式的字符串
|
||||
*/
|
||||
function formatDateTimeLocal(dateTimeString: string): string {
|
||||
if (!dateTimeString) return '';
|
||||
|
||||
const date = new Date(dateTimeString);
|
||||
|
||||
// 检查日期是否有效
|
||||
if (isNaN(date.getTime())) return '';
|
||||
|
||||
// 获取本地时间的各个部分
|
||||
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');
|
||||
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
// 加载课程列表
|
||||
async function loadCourses() {
|
||||
courseLoading.value = true;
|
||||
@@ -474,19 +502,132 @@ async function loadUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索用户
|
||||
function searchUsers() {
|
||||
if (!userSearchKeyword.value) {
|
||||
loadUsers();
|
||||
return;
|
||||
/**
|
||||
* 获取所有用户的API函数
|
||||
*/
|
||||
async function fetchAllUsers() {
|
||||
const filter: any = { username: undefined };
|
||||
return await userApi.getUserList(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已选用户的API函数
|
||||
*/
|
||||
async function fetchSelectedUsers() {
|
||||
return {
|
||||
success: true,
|
||||
dataList: selectedUsers.value,
|
||||
code: 200,
|
||||
message: '',
|
||||
login: true,
|
||||
auth: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤已选用户
|
||||
*/
|
||||
function filterUsers(available: any[], selected: any[]) {
|
||||
const selectedIds = new Set(selected.map(item => item.id));
|
||||
return available.filter(item => !selectedIds.has(item.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将用户扁平数据转换为树形结构(按部门分组)
|
||||
*/
|
||||
function transformUsersToTree(flatData: any[]): any[] {
|
||||
if (!flatData || flatData.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const keyword = userSearchKeyword.value.toLowerCase();
|
||||
availableUsers.value = availableUsers.value.filter(user =>
|
||||
user.username?.toLowerCase().includes(keyword) ||
|
||||
user.phone?.includes(keyword) ||
|
||||
user.email?.toLowerCase().includes(keyword)
|
||||
);
|
||||
// 按部门分组
|
||||
const deptMap = new Map<string, any>();
|
||||
const tree: any[] = [];
|
||||
|
||||
flatData.forEach(item => {
|
||||
const deptID = item.deptID || 'unknown';
|
||||
const deptName = item.deptName || '未分配部门';
|
||||
// 优先使用 parentID,如果不存在则使用 parentDeptID
|
||||
const parentID = item.parentID || item.parentDeptID;
|
||||
|
||||
if (!deptMap.has(deptID)) {
|
||||
// 创建部门节点
|
||||
deptMap.set(deptID, {
|
||||
id: `dept_${deptID}`,
|
||||
displayName: deptName,
|
||||
deptID: deptID,
|
||||
deptName: deptName,
|
||||
parentDeptID: parentID,
|
||||
children: [],
|
||||
isDept: true
|
||||
});
|
||||
}
|
||||
|
||||
// 添加用户到部门的children中
|
||||
const deptNode = deptMap.get(deptID);
|
||||
if (deptNode) {
|
||||
deptNode.children.push({
|
||||
...item,
|
||||
displayName: item.username || item.id,
|
||||
isDept: false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 构建部门层级关系
|
||||
const deptNodes = Array.from(deptMap.values());
|
||||
const deptTreeMap = new Map<string, any>();
|
||||
|
||||
// 初始化所有部门节点
|
||||
deptNodes.forEach(dept => {
|
||||
deptTreeMap.set(dept.deptID, { ...dept });
|
||||
});
|
||||
|
||||
// 构建部门树
|
||||
deptNodes.forEach(dept => {
|
||||
const node = deptTreeMap.get(dept.deptID);
|
||||
if (!node) return;
|
||||
|
||||
if (!dept.parentDeptID || dept.parentDeptID === '0' || dept.parentDeptID === '') {
|
||||
// 根部门
|
||||
tree.push(node);
|
||||
} else {
|
||||
// 子部门
|
||||
const parent = deptTreeMap.get(dept.parentDeptID);
|
||||
if (parent) {
|
||||
// 将用户节点暂存
|
||||
const users = node.children || [];
|
||||
|
||||
// 添加部门到父部门(先添加部门)
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
}
|
||||
|
||||
// 找到部门子节点的插入位置(部门应该在用户之前)
|
||||
const firstUserIndex = parent.children.findIndex((child: any) => !child.isDept);
|
||||
const insertIndex = firstUserIndex === -1 ? parent.children.length : firstUserIndex;
|
||||
|
||||
// 插入部门节点
|
||||
parent.children.splice(insertIndex, 0, {
|
||||
...node,
|
||||
children: users
|
||||
});
|
||||
} else {
|
||||
// 找不到父节点,作为根节点
|
||||
tree.push(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户选择器确认事件
|
||||
*/
|
||||
function handleUserSelectConfirm(users: any[]) {
|
||||
selectedUsers.value = users as SysUser[];
|
||||
showUserSelector.value = false;
|
||||
}
|
||||
|
||||
// 课程选择相关
|
||||
@@ -526,19 +667,6 @@ function removeResource(index: number) {
|
||||
}
|
||||
|
||||
// 用户选择相关
|
||||
function isUserSelected(userID?: string) {
|
||||
return selectedUsers.value.some(u => u.id === userID);
|
||||
}
|
||||
|
||||
function toggleUser(user: SysUser) {
|
||||
const index = selectedUsers.value.findIndex(u => u.id === user.id);
|
||||
if (index > -1) {
|
||||
selectedUsers.value.splice(index, 1);
|
||||
} else {
|
||||
selectedUsers.value.push(user);
|
||||
}
|
||||
}
|
||||
|
||||
function removeUser(index: number) {
|
||||
selectedUsers.value.splice(index, 1);
|
||||
}
|
||||
@@ -623,14 +751,14 @@ async function handleSubmit() {
|
||||
} as TaskItemVO));
|
||||
|
||||
let res;
|
||||
if (props.taskID) {
|
||||
if (props.taskId) {
|
||||
res = await learningTaskApi.updateTask(taskData.value);
|
||||
} else {
|
||||
res = await learningTaskApi.createTask(taskData.value);
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
ElMessage.success(props.taskID ? '任务更新成功' : '任务创建成功');
|
||||
ElMessage.success(props.taskId ? '任务更新成功' : '任务创建成功');
|
||||
emit('success');
|
||||
} else {
|
||||
ElMessage.error(res.message || '操作失败');
|
||||
|
||||
Reference in New Issue
Block a user