!70 czh-20260630-修复计划滚动条问题
* czh-20260630-修复计划滚动条问题 * czh-AI-20260630-新增功能-帮助中心菜单项 * czh-AI-20260630-新增功能-帮助中心常量导出 * czh-AI-20260630-新增功能-帮助中心环境变量配置 * czh-20260630-优化拖拽效果 * czh-20260630-增加乐观更新;优化抽屉界面
This commit is contained in:
@@ -37,3 +37,6 @@ VITE_AUTOLOGIN_URL=http://localhost:9100/api/sys/autoLogin
|
||||
|
||||
# 联系我们
|
||||
VITE_GLOB_CONTACT_URL=http://localhost:3100/contact
|
||||
|
||||
# 帮助中心
|
||||
VITE_GLOB_HELP_CENTER_URL=#
|
||||
|
||||
@@ -38,3 +38,6 @@ VITE_AUTOLOGIN_URL=http://200.100.65.101:9100/api/sys/autoLogin
|
||||
|
||||
# 联系我们
|
||||
VITE_GLOB_CONTACT_URL=#
|
||||
|
||||
# 帮助中心
|
||||
VITE_GLOB_HELP_CENTER_URL=#
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<MenuItem itemKey="depart" :text="t('layout.header.dropdownItemSwitchDepart')" icon="ant-design:cluster-outlined" />
|
||||
<MenuItem itemKey="cache" :text="t('layout.header.dropdownItemRefreshCache')" icon="ion:sync-outline" />
|
||||
<MenuItem itemKey="contact" text="联系我们" icon="ant-design:phone-outlined" />
|
||||
<MenuItem itemKey="helpCenter" text="帮助中心" icon="ant-design:question-circle-outlined" />
|
||||
|
||||
<!-- <MenuItem
|
||||
v-if="getUseLockPage"
|
||||
@@ -34,7 +35,7 @@
|
||||
|
||||
import { defineComponent, computed, ref } from 'vue';
|
||||
|
||||
import { SITE_URL, CONTACT_URL } from '/@/settings/siteSetting';
|
||||
import { SITE_URL, CONTACT_URL, HELP_CENTER_URL } from '/@/settings/siteSetting';
|
||||
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||
@@ -56,7 +57,7 @@ import deptLogo from '/@/assets/images/department-logo.png';
|
||||
// import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
import { getRefPromise } from '/@/utils/index';
|
||||
|
||||
type MenuEvent = 'logout' | 'doc' | 'lock' | 'cache' | 'depart' | 'contact';
|
||||
type MenuEvent = 'logout' | 'doc' | 'lock' | 'cache' | 'depart' | 'contact' | 'helpCenter';
|
||||
const { createMessage } = useMessage();
|
||||
export default defineComponent({
|
||||
name: 'UserDropdown',
|
||||
@@ -122,6 +123,11 @@ import deptLogo from '/@/assets/images/department-logo.png';
|
||||
openWindow(CONTACT_URL);
|
||||
}
|
||||
|
||||
// open help center
|
||||
function openHelpCenter() {
|
||||
openWindow(HELP_CENTER_URL);
|
||||
}
|
||||
|
||||
// 清除缓存
|
||||
async function clearCache() {
|
||||
const result = await refreshCache();
|
||||
@@ -169,6 +175,9 @@ import deptLogo from '/@/assets/images/department-logo.png';
|
||||
case 'contact':
|
||||
openContact();
|
||||
break;
|
||||
case 'helpCenter':
|
||||
openHelpCenter();
|
||||
break;
|
||||
case 'depart':
|
||||
updateCurrentDepart();
|
||||
break;
|
||||
|
||||
@@ -9,3 +9,6 @@ export const SITE_URL = 'http://www.jeecg.com';
|
||||
|
||||
// contact us url
|
||||
export const CONTACT_URL = import.meta.env.VITE_GLOB_CONTACT_URL || '#';
|
||||
|
||||
// help center url
|
||||
export const HELP_CENTER_URL = import.meta.env.VITE_GLOB_HELP_CENTER_URL || '#';
|
||||
|
||||
@@ -433,7 +433,7 @@
|
||||
|
||||
<div v-show="!group.collapsed">
|
||||
<draggable
|
||||
:model-value="flattenGroupTasks(group.tasks)"
|
||||
:list="flatListCache[group.id]"
|
||||
:item-key="(ft: any) => ft.task.id"
|
||||
group="tasks"
|
||||
handle=".drag-handle"
|
||||
@@ -836,7 +836,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { ref, shallowRef, computed, watch, nextTick } from 'vue';
|
||||
import HelpModal from './HelpModal.vue';
|
||||
import dayjs from 'dayjs';
|
||||
import Icon from '/@/components/Icon/index';
|
||||
@@ -954,6 +954,27 @@
|
||||
|
||||
const localGroups = ref<TaskGroup[]>([]);
|
||||
|
||||
const flatListCache = shallowRef<Record<string, FlatTaskItem[]>>({});
|
||||
|
||||
function rebuildFlatListCache() {
|
||||
const cache: Record<string, FlatTaskItem[]> = {};
|
||||
for (const g of localGroups.value) {
|
||||
cache[g.id] = flattenGroupTasks(g.tasks);
|
||||
}
|
||||
flatListCache.value = cache;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => localGroups.value,
|
||||
() => rebuildFlatListCache(),
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => expandedTaskIds.value.size,
|
||||
() => rebuildFlatListCache()
|
||||
);
|
||||
|
||||
function showDragBlockedMessage() {
|
||||
import('ant-design-vue').then(({ message }) => {
|
||||
message.warning('子事项只能在当前父事项下移动');
|
||||
@@ -1489,13 +1510,13 @@
|
||||
const targetGroupEl = toContainer?.closest('[data-group-id]');
|
||||
const targetGroupId = targetGroupEl?.dataset?.groupId || '';
|
||||
|
||||
const targetGroup = computedGroups.value.find((g) => g.id === targetGroupId);
|
||||
const targetGroup = localGroups.value.find((g) => g.id === targetGroupId);
|
||||
if (!targetGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawFlatList = flattenGroupTasks(targetGroup.tasks);
|
||||
const groupIds = new Set(computedGroups.value.map((g) => g.id));
|
||||
const rawFlatList = flatListCache.value[targetGroupId] || [];
|
||||
const groupIds = new Set(localGroups.value.map((g) => g.id));
|
||||
|
||||
const draggedIdx = rawFlatList.findIndex((f) => f.task.id === taskId);
|
||||
let flatList: FlatTaskItem[];
|
||||
@@ -1511,8 +1532,8 @@
|
||||
if (draggedIdx !== -1) {
|
||||
draggedTask = flatList[newIndex]?.task || null;
|
||||
} else {
|
||||
for (const g of computedGroups.value) {
|
||||
const found = flattenGroupTasks(g.tasks).find((f) => f.task.id === taskId);
|
||||
for (const g of localGroups.value) {
|
||||
const found = (flatListCache.value[g.id] || []).find((f) => f.task.id === taskId);
|
||||
if (found) {
|
||||
draggedTask = found.task;
|
||||
break;
|
||||
@@ -1904,6 +1925,16 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.ant-spin-nested-loading) {
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.ant-spin-container) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.content-header {
|
||||
|
||||
@@ -282,12 +282,12 @@
|
||||
</button>
|
||||
<span class="feishu-subtask-name" :class="{ done: child.completed }">{{ child.taskName }}</span>
|
||||
<div v-if="!isFullReadOnly" class="feishu-subtask-actions">
|
||||
<a-tooltip title="设置日期">
|
||||
<span class="feishu-subtask-action-btn" @click.stop="openSubTaskDateEditor(child.id)">
|
||||
<Icon icon="ant-design:calendar-outlined" />
|
||||
<a-tooltip v-if="child.assigneeName" :title="child.assigneeName" placement="top">
|
||||
<span class="feishu-subtask-avatar" @click.stop="openSubTaskUserSelect(child)">
|
||||
<AvatarDisplay :names="child.assigneeName" :size="20" />
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="添加负责人">
|
||||
<a-tooltip v-else title="添加负责人">
|
||||
<span class="feishu-subtask-action-btn" @click.stop="openSubTaskUserSelect(child)">
|
||||
<Icon icon="ant-design:user-add-outlined" />
|
||||
</span>
|
||||
@@ -298,26 +298,13 @@
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div v-if="child.assigneeName && !isFullReadOnly" class="feishu-subtask-avatar" @click.stop="openSubTaskUserSelect(child)">
|
||||
<a-tooltip :title="child.assigneeName" placement="top"><AvatarDisplay :names="child.assigneeName" :size="20" /></a-tooltip>
|
||||
</div>
|
||||
<div v-else-if="child.assigneeName && isFullReadOnly" class="feishu-subtask-avatar">
|
||||
<div v-else-if="child.assigneeName && isFullReadOnly" class="feishu-subtask-avatar">
|
||||
<a-tooltip :title="child.assigneeName" placement="top"><AvatarDisplay :names="child.assigneeName" :size="20" /></a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<div v-if="editingSubTask && editingSubTaskField === 'dateRange'" ref="subTaskDateTriggerRef" class="feishu-subtask-date-panel">
|
||||
<TaskDateRangePicker
|
||||
v-model:visible="subTaskDatePickerVisible"
|
||||
:trigger-ref="subTaskDateTriggerRef"
|
||||
:start-value="editingSubTaskStartValue"
|
||||
:end-value="editingSubTaskEndValue"
|
||||
@change="onSubTaskDateRangeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="showNewSubTaskInput && !isFullReadOnly" class="feishu-subtask-row feishu-subtask-input-row">
|
||||
<span class="feishu-subtask-drag-placeholder"></span>
|
||||
<span class="feishu-subtask-check feishu-subtask-check-empty"></span>
|
||||
@@ -379,7 +366,7 @@
|
||||
import SzUserSelectBySecLevel from '/@/components/semri/userComponent/SzUserSelectBySecLevel.vue';
|
||||
import SzDeptUserModal from '/@/components/semri/deptUserComponent/SzDeptUserModal.vue';
|
||||
import TaskAttachmentUpload from './TaskAttachmentUpload.vue';
|
||||
import TaskDateRangePicker from './TaskDateRangePicker.vue';
|
||||
|
||||
import draggable from 'vuedraggable';
|
||||
import dayjs from 'dayjs';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
@@ -428,13 +415,7 @@
|
||||
const showRemarkEditor = ref(false);
|
||||
const subtaskExpanded = ref(true);
|
||||
|
||||
const editingSubTask = ref<string | null>(null);
|
||||
const editingSubTaskField = ref<string | null>(null);
|
||||
const editingSubTaskValue = ref<any>(null);
|
||||
const editingSubTaskStartValue = ref<string | null>(null);
|
||||
const editingSubTaskEndValue = ref<string | null>(null);
|
||||
const subTaskUserSelectId = ref<string>('');
|
||||
const subTaskDatePickerOpen = ref(false);
|
||||
const drawerDatePickerVisible = ref(false);
|
||||
|
||||
const drawerDateEditMode = ref<'start' | 'end' | null>(null);
|
||||
@@ -540,9 +521,6 @@
|
||||
showDescEditor.value = false;
|
||||
showRemarkEditor.value = false;
|
||||
subtaskExpanded.value = true;
|
||||
editingSubTask.value = null;
|
||||
editingSubTaskField.value = null;
|
||||
editingSubTaskValue.value = null;
|
||||
drawerDatePickerVisible.value = false;
|
||||
startTimeDayjs.value = data?.task?.startTime || null;
|
||||
endTimeDayjs.value = data?.task?.endTime || null;
|
||||
@@ -772,29 +750,6 @@
|
||||
subTaskUserSelectId.value = '';
|
||||
}
|
||||
|
||||
function openSubTaskDateEditor(childId: string) {
|
||||
editingSubTask.value = childId;
|
||||
editingSubTaskField.value = 'dateRange';
|
||||
const child = allTasks.value.find((t) => t.id === childId);
|
||||
editingSubTaskStartValue.value = child?.startTime ? child.startTime.substring(0, 10) : null;
|
||||
editingSubTaskEndValue.value = child?.endTime ? child.endTime.substring(0, 10) : null;
|
||||
subTaskDatePickerOpen.value = true;
|
||||
}
|
||||
|
||||
function onSubTaskDateRangeChange(value: [string | null, string | null]) {
|
||||
if (editingSubTask.value) {
|
||||
emit('update-sub-task-fields', editingSubTask.value, {
|
||||
startTime: value[0] ? value[0] + ' 00:00:00' : '',
|
||||
endTime: value[1] ? value[1] + ' 00:00:00' : '',
|
||||
});
|
||||
editingSubTask.value = null;
|
||||
editingSubTaskField.value = null;
|
||||
editingSubTaskStartValue.value = null;
|
||||
editingSubTaskEndValue.value = null;
|
||||
subTaskDatePickerOpen.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onSubTaskDragEnd(evt: any) {
|
||||
const { oldIndex, newIndex } = evt;
|
||||
if (oldIndex === newIndex) return;
|
||||
|
||||
@@ -708,6 +708,16 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.ant-spin-nested-loading) {
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.ant-spin-container) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.flat-view-header {
|
||||
|
||||
@@ -844,19 +844,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
function findTaskInGroups(taskId: string): { group: TaskGroup; task: TaskItem } | null {
|
||||
for (const g of taskListGroups.value) {
|
||||
for (const l of g.taskLists) {
|
||||
for (const tg of l.groups) {
|
||||
const t = tg.tasks.find((t: TaskItem) => t.id === taskId);
|
||||
if (t) return { group: tg, task: t };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function onToggleTask(taskId: string) {
|
||||
// 乐观更新:立刻切换 UI
|
||||
if (isFlatView.value) {
|
||||
const idx = flatTasks.value.findIndex((t) => t.id === taskId);
|
||||
if (idx >= 0) {
|
||||
flatTasks.value[idx] = { ...flatTasks.value[idx], completed: !flatTasks.value[idx].completed };
|
||||
}
|
||||
} else {
|
||||
const found = findTaskInGroups(taskId);
|
||||
if (found) {
|
||||
found.task.completed = !found.task.completed;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await toggleTaskStatus({ id: taskId });
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 切换任务状态失败', e);
|
||||
// 回滚
|
||||
if (isFlatView.value) {
|
||||
const idx = flatTasks.value.findIndex((t) => t.id === taskId);
|
||||
if (idx >= 0) {
|
||||
flatTasks.value[idx] = { ...flatTasks.value[idx], completed: !flatTasks.value[idx].completed };
|
||||
}
|
||||
} else if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
} else {
|
||||
const found = findTaskInGroups(taskId);
|
||||
if (found) {
|
||||
found.task.completed = !found.task.completed;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 切换任务状态失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1057,16 +1086,41 @@
|
||||
}
|
||||
|
||||
async function onToggleSubTask(taskId: string) {
|
||||
// 乐观更新:立刻切换子任务完成状态
|
||||
if (isFlatView.value) {
|
||||
const idx = flatTasks.value.findIndex((t) => t.id === taskId);
|
||||
if (idx >= 0) {
|
||||
flatTasks.value[idx] = { ...flatTasks.value[idx], completed: !flatTasks.value[idx].completed };
|
||||
}
|
||||
} else {
|
||||
const found = findTaskInGroups(taskId);
|
||||
if (found) {
|
||||
found.task.completed = !found.task.completed;
|
||||
}
|
||||
}
|
||||
refreshDrawer();
|
||||
|
||||
try {
|
||||
await toggleTaskStatus({ id: taskId });
|
||||
if (isFlatView.value) {
|
||||
await refreshFlatViewTasks();
|
||||
} else if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
refreshDrawer();
|
||||
}
|
||||
refreshDrawer();
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 切换子任务状态失败', e);
|
||||
// 回滚
|
||||
if (isFlatView.value) {
|
||||
const idx = flatTasks.value.findIndex((t) => t.id === taskId);
|
||||
if (idx >= 0) {
|
||||
flatTasks.value[idx] = { ...flatTasks.value[idx], completed: !flatTasks.value[idx].completed };
|
||||
}
|
||||
} else {
|
||||
const found = findTaskInGroups(taskId);
|
||||
if (found) {
|
||||
found.task.completed = !found.task.completed;
|
||||
}
|
||||
}
|
||||
refreshDrawer();
|
||||
if (currentListId.value) {
|
||||
await loadTasks(currentListId.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user