!70 czh-20260630-修复计划滚动条问题

* czh-20260630-修复计划滚动条问题
* czh-AI-20260630-新增功能-帮助中心菜单项
* czh-AI-20260630-新增功能-帮助中心常量导出
* czh-AI-20260630-新增功能-帮助中心环境变量配置
* czh-20260630-优化拖拽效果
* czh-20260630-增加乐观更新;优化抽屉界面
This commit is contained in:
陈志浩
2026-06-30 06:12:28 +00:00
parent 9c9d5e0096
commit e20fa25000
8 changed files with 138 additions and 70 deletions
+3
View File
@@ -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=#
+3
View File
@@ -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;
+3
View File
@@ -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 || '#';
+38 -7
View File
@@ -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 {
+64 -10
View File
@@ -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);
}
}
}