821 lines
23 KiB
Vue
821 lines
23 KiB
Vue
<template>
|
|
<div class="task-sidebar">
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-quick-header" @click="planExpanded = !planExpanded">
|
|
<Icon icon="tasklist-arrow-down|svg" class="quick-access-arrow" :class="{ expanded: planExpanded }" />
|
|
<span class="sidebar-quick-title">计划总览</span>
|
|
</div>
|
|
<template v-if="planExpanded">
|
|
<div
|
|
v-for="item in QUICK_ACCESS_ITEMS"
|
|
:key="item.id"
|
|
class="sidebar-item sidebar-quick-access-sub"
|
|
:class="{ active: currentView === item.id }"
|
|
@click="onQuickAccessClick(item)"
|
|
>
|
|
<Icon :icon="item.icon" class="sidebar-item-icon" />
|
|
<span>{{ item.label }}</span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="sidebar-divider"></div>
|
|
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-quick-header" @click="taskExpanded = !taskExpanded">
|
|
<Icon icon="tasklist-arrow-down|svg" class="quick-access-arrow" :class="{ expanded: taskExpanded }" />
|
|
<span class="sidebar-quick-title">事项总览</span>
|
|
</div>
|
|
<template v-if="taskExpanded">
|
|
<div
|
|
v-for="item in NAV_ITEMS"
|
|
:key="item.id"
|
|
class="sidebar-item sidebar-quick-access-sub"
|
|
:class="{ active: currentView === item.id }"
|
|
@click="emit('view-select', item.id)"
|
|
>
|
|
<Icon :icon="item.icon" class="sidebar-item-icon" />
|
|
<span>{{ item.label }}</span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="sidebar-divider"></div>
|
|
|
|
<div class="sidebar-section sidebar-lists-section">
|
|
<div class="sidebar-favorites-header">
|
|
<Icon icon="tasklist-task-list|svg" class="sidebar-favorites-icon" />
|
|
<span class="sidebar-favorites-name">常用计划</span>
|
|
<a-dropdown :trigger="['click']">
|
|
<Icon icon="ant-design:plus-outlined" class="sidebar-group-add" @click.stop />
|
|
<template #overlay>
|
|
<a-menu @click="onAddMenuClick">
|
|
<a-menu-item key="new-list">
|
|
<Icon icon="ant-design:file-add-outlined" class="mr-1" />
|
|
新建计划
|
|
</a-menu-item>
|
|
<a-menu-item key="new-group">
|
|
<Icon icon="ant-design:folder-add-outlined" class="mr-1" />
|
|
新建分组
|
|
</a-menu-item>
|
|
</a-menu>
|
|
</template>
|
|
</a-dropdown>
|
|
</div>
|
|
<div class="sidebar-favorites-items">
|
|
<draggable :list="localUngroupedTaskLists" group="tasklists" item-key="id" handle=".list-drag-handle" :animation="200" @end="onDragEnd">
|
|
<template #item="{ element: taskList }">
|
|
<div
|
|
v-if="renamingId !== taskList.id"
|
|
class="sidebar-item"
|
|
:class="{ active: currentListId === taskList.id }"
|
|
:data-list-id="taskList.id"
|
|
@click="emit('list-select', taskList.id)"
|
|
@mouseenter="hoveredListId = taskList.id"
|
|
@mouseleave="hoveredListId = ''"
|
|
>
|
|
<span class="list-drag-handle">
|
|
<Icon icon="tasklist-list-drag-handle|svg" />
|
|
</span>
|
|
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" />
|
|
<span class="sidebar-item-name">{{ taskList.name }}</span>
|
|
<a-dropdown :trigger="['click']">
|
|
<Icon v-show="hoveredListId === taskList.id" icon="ant-design:ellipsis-outlined" class="sidebar-item-more" @click.stop />
|
|
<template #overlay>
|
|
<a-menu @click="onListMenuClick($event, taskList.id, taskList.name)">
|
|
<a-menu-item v-if="canEditList(taskList.id)" key="rename">
|
|
<Icon icon="ant-design:edit-outlined" class="mr-1" />
|
|
重命名
|
|
</a-menu-item>
|
|
<a-menu-item key="remove">
|
|
<Icon icon="ant-design:minus-circle-outlined" class="mr-1" />
|
|
移除
|
|
</a-menu-item>
|
|
</a-menu>
|
|
</template>
|
|
</a-dropdown>
|
|
</div>
|
|
<div v-else class="sidebar-item sidebar-item-renaming">
|
|
<span class="list-drag-handle-placeholder"></span>
|
|
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" style="opacity: 0.4" />
|
|
<a-input ref="renameInputRef" v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
|
|
</div>
|
|
</template>
|
|
</draggable>
|
|
</div>
|
|
|
|
<draggable :list="localUserGroups" group="tasklistgroups" item-key="id" :animation="200" drag-class="group-drag-ghost" @start="onGroupDragStart" @end="onGroupDragEnd">
|
|
<template #item="{ element: group }">
|
|
<div :data-group-id="group.id" :class="{ 'group-being-dragged': draggingGroupId === group.id }">
|
|
<div
|
|
v-if="group.delFlag !== 1 && renamingId !== group.id"
|
|
class="sidebar-group-header"
|
|
@mousedown="onGroupMouseDown($event, group.id)"
|
|
@mousemove="onGroupMouseMove($event)"
|
|
@mouseup="onGroupMouseUp($event, group.id)"
|
|
>
|
|
<Icon icon="tasklist-arrow-down|svg" class="group-collapse-arrow" :class="{ collapsed: collapsedGroups[group.id] || draggingGroupId === group.id }" />
|
|
<span class="sidebar-group-name">{{ group.name }}</span>
|
|
<a-dropdown :trigger="['click']" @click.stop>
|
|
<Icon icon="ant-design:ellipsis-outlined" class="sidebar-group-more" @click.stop />
|
|
<template #overlay>
|
|
<a-menu @click="onGroupMenuClick($event, group.id, group.name)">
|
|
<a-menu-item key="rename">
|
|
<Icon icon="ant-design:edit-outlined" class="mr-1" />
|
|
重命名
|
|
</a-menu-item>
|
|
<a-menu-item key="delete" danger>
|
|
<Icon icon="ant-design:delete-outlined" class="mr-1" />
|
|
删除
|
|
</a-menu-item>
|
|
</a-menu>
|
|
</template>
|
|
</a-dropdown>
|
|
<Icon icon="ant-design:plus-outlined" class="sidebar-group-add" @click.stop="startNewList(group.id)" />
|
|
</div>
|
|
<div v-if="group.delFlag !== 1 && renamingId === group.id" class="sidebar-group-header sidebar-group-renaming" @click.stop>
|
|
<Icon icon="tasklist-arrow-down|svg" class="group-collapse-arrow" :class="{ collapsed: collapsedGroups[group.id] || draggingGroupId === group.id }" />
|
|
<a-input ref="renameInputRef" v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
|
|
</div>
|
|
<template v-if="group.delFlag !== 1 && !collapsedGroups[group.id]">
|
|
<div class="sidebar-group-items" :data-group-id="group.id">
|
|
<draggable
|
|
:list="getLocalGroupTaskLists(group)"
|
|
group="tasklists"
|
|
item-key="id"
|
|
handle=".list-drag-handle"
|
|
:animation="200"
|
|
@end="onDragEnd"
|
|
>
|
|
<template #item="{ element: taskList }">
|
|
<div
|
|
v-if="renamingId !== taskList.id"
|
|
class="sidebar-item"
|
|
:class="{ active: currentListId === taskList.id }"
|
|
:data-list-id="taskList.id"
|
|
@click="emit('list-select', taskList.id)"
|
|
@mouseenter="hoveredListId = taskList.id"
|
|
@mouseleave="hoveredListId = ''"
|
|
>
|
|
<span class="list-drag-handle">
|
|
<Icon icon="tasklist-list-drag-handle|svg" />
|
|
</span>
|
|
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" />
|
|
<span class="sidebar-item-name">{{ taskList.name }}</span>
|
|
<a-dropdown :trigger="['click']">
|
|
<Icon v-show="hoveredListId === taskList.id" icon="ant-design:ellipsis-outlined" class="sidebar-item-more" @click.stop />
|
|
<template #overlay>
|
|
<a-menu @click="onListMenuClick($event, taskList.id, taskList.name)">
|
|
<a-menu-item v-if="canEditList(taskList.id)" key="rename">
|
|
<Icon icon="ant-design:edit-outlined" class="mr-1" />
|
|
重命名
|
|
</a-menu-item>
|
|
<a-menu-item key="remove">
|
|
<Icon icon="ant-design:minus-circle-outlined" class="mr-1" />
|
|
移除
|
|
</a-menu-item>
|
|
</a-menu>
|
|
</template>
|
|
</a-dropdown>
|
|
</div>
|
|
<div v-else class="sidebar-item sidebar-item-renaming">
|
|
<span class="list-drag-handle-placeholder"></span>
|
|
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" style="opacity: 0.4" />
|
|
<a-input ref="renameInputRef" v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
|
|
</div>
|
|
</template>
|
|
</draggable>
|
|
<div
|
|
v-if="getLocalGroupTaskLists(group).length === 0"
|
|
class="sidebar-empty-hint"
|
|
>
|
|
可拖拽计划加入该分组
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</draggable>
|
|
|
|
<div v-if="showNewGroupInput" class="sidebar-new-group-row">
|
|
<Icon icon="ant-design:plus-outlined" class="sidebar-new-icon" />
|
|
<a-input
|
|
v-model:value="newGroupName"
|
|
size="small"
|
|
placeholder="输入分组名称..."
|
|
autofocus
|
|
style="flex: 1"
|
|
@press-enter="submitNewGroup"
|
|
@blur="showNewGroupInput = false"
|
|
/>
|
|
</div>
|
|
<div v-else class="sidebar-new-group-btn" @click="showNewGroupInput = true">
|
|
<Icon icon="ant-design:plus-outlined" class="sidebar-new-icon" />
|
|
<span>新建分组</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { ref, computed, watch, nextTick } from 'vue';
|
|
import Icon from '/@/components/Icon/index';
|
|
import draggable from 'vuedraggable';
|
|
import { useMessage } from '/@/hooks/web/useMessage';
|
|
import { NAV_ITEMS, QUICK_ACCESS_ITEMS, QUICK_ACCESS_TAB_MAP } from '../types';
|
|
import type { TaskListGroup, ViewType } from '../types';
|
|
|
|
const { createConfirm } = useMessage();
|
|
|
|
const props = defineProps<{
|
|
taskListGroups: TaskListGroup[];
|
|
currentView: ViewType;
|
|
currentListId: string;
|
|
favoriteIdMap: Map<string, string>;
|
|
permissionMap: Map<string, string>;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
'view-select': [view: ViewType];
|
|
'list-select': [listId: string];
|
|
'create-list-modal': [groupId: string];
|
|
'create-group': [name: string];
|
|
'rename-list': [listId: string, newName: string];
|
|
'delete-list': [listId: string];
|
|
'remove-favorite': [listId: string];
|
|
'rename-group': [groupId: string, newName: string];
|
|
'delete-group': [groupId: string];
|
|
'quick-access-tab-click': [tabKey: string];
|
|
'move-list': [favoriteId: string, targetGroupId: string, sortOrder: number];
|
|
'move-group': [groupId: string, targetSortOrder: number];
|
|
}>();
|
|
|
|
function onQuickAccessClick(item: { id: string }) {
|
|
const tabKey = QUICK_ACCESS_TAB_MAP[item.id];
|
|
if (tabKey) {
|
|
emit('quick-access-tab-click', tabKey);
|
|
}
|
|
}
|
|
|
|
const planExpanded = ref(true);
|
|
const taskExpanded = ref(true);
|
|
|
|
const ungroupedTaskLists = computed(() => {
|
|
const group = props.taskListGroups.find((g) => g.id === '__ungrouped__');
|
|
if (group) {
|
|
return [...group.taskLists].filter((l) => l.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
|
}
|
|
return [];
|
|
});
|
|
|
|
const userGroups = computed(() => {
|
|
return props.taskListGroups.filter((g) => g.id !== '__ungrouped__' && g.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
|
});
|
|
|
|
// 本地可变副本,供 draggable :list 模式直接修改,实现乐观更新
|
|
const localUngroupedTaskLists = ref<any[]>([]);
|
|
const localUserGroups = ref<any[]>([]);
|
|
const localGroupTaskListsMap = ref<Record<string, any[]>>({});
|
|
|
|
watch(ungroupedTaskLists, (val) => {
|
|
localUngroupedTaskLists.value = [...val];
|
|
}, { immediate: true });
|
|
|
|
watch(userGroups, (val) => {
|
|
localUserGroups.value = val.map((g) => ({
|
|
...g,
|
|
taskLists: [...(g.taskLists || [])],
|
|
}));
|
|
// 同步分组内清单到 map
|
|
const newMap: Record<string, any[]> = {};
|
|
for (const g of val) {
|
|
newMap[g.id] = [...(g.taskLists || [])].filter((l) => l.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
|
}
|
|
localGroupTaskListsMap.value = newMap;
|
|
}, { immediate: true });
|
|
|
|
function getGroupTaskLists(group: any) {
|
|
return [...group.taskLists].filter((l) => l.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
|
}
|
|
|
|
function getLocalGroupTaskLists(group: any) {
|
|
if (localGroupTaskListsMap.value[group.id]) {
|
|
return localGroupTaskListsMap.value[group.id];
|
|
}
|
|
// 兜底:如果没有本地缓存,从原始数据计算
|
|
const list = getGroupTaskLists(group);
|
|
localGroupTaskListsMap.value[group.id] = list;
|
|
return list;
|
|
}
|
|
|
|
const hoveredListId = ref('');
|
|
const collapsedGroups = ref<Record<string, boolean>>({});
|
|
const showNewGroupInput = ref(false);
|
|
const newGroupName = ref('');
|
|
const renamingId = ref('');
|
|
const renamingType = ref<'list' | 'group'>('list');
|
|
const renameValue = ref('');
|
|
const renameInputRef = ref<HTMLInputElement | null>(null);
|
|
|
|
watch(renamingId, async (val) => {
|
|
if (val) {
|
|
await nextTick();
|
|
renameInputRef.value?.focus();
|
|
}
|
|
});
|
|
|
|
const draggingGroupId = ref('');
|
|
const dragStartX = ref(0);
|
|
const dragStartY = ref(0);
|
|
const isDragging = ref(false);
|
|
const DRAG_THRESHOLD = 5;
|
|
|
|
function toggleGroup(groupId: string) {
|
|
collapsedGroups.value = {
|
|
...collapsedGroups.value,
|
|
[groupId]: !collapsedGroups.value[groupId],
|
|
};
|
|
}
|
|
|
|
function onDragEnd(evt: any) {
|
|
const itemEl = evt.item;
|
|
const listId = itemEl?.dataset?.listId;
|
|
if (!listId) return;
|
|
|
|
const favoriteId = props.favoriteIdMap.get(listId);
|
|
if (!favoriteId) return;
|
|
|
|
const toContainer = evt.to;
|
|
const targetGroupId = toContainer?.closest('[data-group-id]')?.dataset?.groupId || '';
|
|
|
|
const newIndex = evt.newIndex ?? 0;
|
|
|
|
emit('move-list', favoriteId, targetGroupId, newIndex + 1);
|
|
}
|
|
|
|
function onGroupMouseDown(evt: MouseEvent, groupId: string) {
|
|
dragStartX.value = evt.clientX;
|
|
dragStartY.value = evt.clientY;
|
|
isDragging.value = false;
|
|
}
|
|
|
|
function onGroupMouseMove(evt: MouseEvent) {
|
|
const dx = Math.abs(evt.clientX - dragStartX.value);
|
|
const dy = Math.abs(evt.clientY - dragStartY.value);
|
|
|
|
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {
|
|
isDragging.value = true;
|
|
}
|
|
}
|
|
|
|
function onGroupMouseUp(evt: MouseEvent, groupId: string) {
|
|
if (!isDragging.value) {
|
|
toggleGroup(groupId);
|
|
}
|
|
isDragging.value = false;
|
|
dragStartX.value = 0;
|
|
dragStartY.value = 0;
|
|
}
|
|
|
|
function onGroupDragStart(evt: any) {
|
|
const itemEl = evt.item;
|
|
const groupId = itemEl?.dataset?.groupId;
|
|
if (groupId) {
|
|
draggingGroupId.value = groupId;
|
|
}
|
|
}
|
|
|
|
function onGroupDragEnd(evt: any) {
|
|
draggingGroupId.value = '';
|
|
isDragging.value = false;
|
|
const itemEl = evt.item;
|
|
const groupId = itemEl?.dataset?.groupId || itemEl?.closest('[data-group-id]')?.dataset?.groupId;
|
|
if (!groupId) return;
|
|
// 拖拽结束后保持分组折叠状态,防止展开跳动
|
|
collapsedGroups.value[groupId] = true;
|
|
const newIndex = evt.newIndex ?? 0;
|
|
emit('move-group', groupId, newIndex + 1);
|
|
}
|
|
|
|
function canEditList(listId: string): boolean {
|
|
const perm = props.permissionMap.get(listId);
|
|
return perm === '1' || perm === '2';
|
|
}
|
|
|
|
function onAddMenuClick({ key }: { key: string }) {
|
|
if (key === 'new-list') {
|
|
startNewList('');
|
|
} else if (key === 'new-group') {
|
|
showNewGroupInput.value = true;
|
|
newGroupName.value = '';
|
|
}
|
|
}
|
|
|
|
function startNewList(groupId: string) {
|
|
emit('create-list-modal', groupId);
|
|
}
|
|
|
|
function onListMenuClick({ key }: { key: string }, listId: string, currentName: string) {
|
|
if (key === 'rename') {
|
|
renamingId.value = listId;
|
|
renamingType.value = 'list';
|
|
renameValue.value = currentName;
|
|
} else if (key === 'remove') {
|
|
createConfirm({
|
|
iconType: 'warning',
|
|
title: '确认移除',
|
|
content: '确定从常用计划移除吗?',
|
|
okText: '确认',
|
|
cancelText: '取消',
|
|
onOk: () => {
|
|
emit('remove-favorite', listId);
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
function onGroupMenuClick({ key }: { key: string }, groupId: string, currentName: string) {
|
|
if (key === 'rename') {
|
|
renamingId.value = groupId;
|
|
renamingType.value = 'group';
|
|
renameValue.value = currentName;
|
|
} else if (key === 'delete') {
|
|
emit('delete-group', groupId);
|
|
}
|
|
}
|
|
|
|
function confirmInlineRename() {
|
|
const name = renameValue.value.trim();
|
|
if (name && renamingId.value) {
|
|
if (renamingType.value === 'list') {
|
|
emit('rename-list', renamingId.value, name);
|
|
} else {
|
|
emit('rename-group', renamingId.value, name);
|
|
}
|
|
}
|
|
renamingId.value = '';
|
|
renameValue.value = '';
|
|
}
|
|
|
|
function submitNewGroup() {
|
|
if (newGroupName.value.trim()) {
|
|
emit('create-group', newGroupName.value.trim());
|
|
newGroupName.value = '';
|
|
showNewGroupInput.value = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.task-sidebar {
|
|
width: 240px;
|
|
min-width: 240px;
|
|
height: 100%;
|
|
background: #fff;
|
|
border-right: 1px solid #f0f0f0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
user-select: none;
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: 14px 16px 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.sidebar-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #1f1f1f;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.sidebar-nav {
|
|
padding: 4px 8px;
|
|
}
|
|
|
|
.sidebar-divider {
|
|
height: 1px;
|
|
background: #f0f0f0;
|
|
margin: 4px 12px;
|
|
}
|
|
|
|
.sidebar-section {
|
|
padding: 4px 8px;
|
|
}
|
|
|
|
.sidebar-quick-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 10px;
|
|
cursor: pointer;
|
|
color: #8c8c8c;
|
|
font-size: 14px;
|
|
user-select: none;
|
|
|
|
&:hover {
|
|
color: #595959;
|
|
}
|
|
}
|
|
|
|
.sidebar-quick-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #595959;
|
|
}
|
|
|
|
.quick-access-arrow {
|
|
font-size: 12px;
|
|
color: #8c8c8c;
|
|
transition: transform 0.2s ease;
|
|
transform: rotate(-90deg);
|
|
|
|
&.expanded {
|
|
transform: rotate(0deg);
|
|
}
|
|
}
|
|
|
|
.sidebar-item.sidebar-quick-access-sub {
|
|
padding-left: 30px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.sidebar-section-header {
|
|
padding: 6px 8px 2px;
|
|
}
|
|
|
|
.sidebar-section-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #8c8c8c;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.sidebar-lists-section {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
.sidebar-favorites-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 6px 8px 2px;
|
|
position: relative;
|
|
}
|
|
|
|
.sidebar-favorites-icon {
|
|
color: #8f959e;
|
|
flex-shrink: 0;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.sidebar-favorites-name {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #595959;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.sidebar-favorites-items {
|
|
.sidebar-item {
|
|
padding-left: 8px;
|
|
}
|
|
}
|
|
|
|
.sidebar-group-items {
|
|
.sidebar-item {
|
|
padding-left: 8px;
|
|
}
|
|
}
|
|
|
|
.sidebar-group-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 6px 8px 2px;
|
|
cursor: pointer;
|
|
position: relative;
|
|
|
|
&:hover .sidebar-group-name {
|
|
color: #333;
|
|
}
|
|
|
|
.sidebar-group-more,
|
|
.sidebar-group-add {
|
|
opacity: 0;
|
|
transition: opacity 0.15s;
|
|
}
|
|
|
|
&:hover .sidebar-group-more,
|
|
&:hover .sidebar-group-add {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.group-collapse-arrow {
|
|
font-size: 12px;
|
|
color: #8f959e;
|
|
transition: transform 0.2s ease;
|
|
flex-shrink: 0;
|
|
width: 16px;
|
|
text-align: center;
|
|
|
|
&.collapsed {
|
|
transform: rotate(-90deg);
|
|
}
|
|
}
|
|
|
|
.sidebar-group-name {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #595959;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.sidebar-group-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 16px;
|
|
height: 16px;
|
|
font-size: 10px;
|
|
color: #999;
|
|
}
|
|
|
|
.sidebar-group-add {
|
|
margin-left: auto;
|
|
font-size: 14px;
|
|
color: #999;
|
|
cursor: pointer;
|
|
padding: 2px 4px;
|
|
border-radius: 4px;
|
|
&:hover {
|
|
color: #1677ff;
|
|
background: #e6f4ff;
|
|
}
|
|
}
|
|
|
|
.sidebar-group-more {
|
|
font-size: 16px;
|
|
color: #999;
|
|
padding: 2px;
|
|
border-radius: 4px;
|
|
flex-shrink: 0;
|
|
&:hover {
|
|
color: #1677ff;
|
|
background: #e6f4ff;
|
|
}
|
|
}
|
|
|
|
.sidebar-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 10px;
|
|
cursor: pointer;
|
|
border-radius: 6px;
|
|
margin: 1px 0;
|
|
font-size: 14px;
|
|
color: #595959;
|
|
transition: all 0.15s ease;
|
|
position: relative;
|
|
|
|
&:hover {
|
|
background: #f5f5f5;
|
|
color: #333;
|
|
}
|
|
|
|
&.active {
|
|
background: #e6f4ff;
|
|
color: #1677ff;
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
|
|
.sidebar-item-icon {
|
|
font-size: 14px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sidebar-item-name {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
flex: 1;
|
|
}
|
|
|
|
.sidebar-item-more {
|
|
font-size: 14px;
|
|
color: #999;
|
|
padding: 2px;
|
|
border-radius: 4px;
|
|
flex-shrink: 0;
|
|
&:hover {
|
|
color: #1677ff;
|
|
background: #e6f4ff;
|
|
}
|
|
}
|
|
|
|
.list-drag-handle {
|
|
cursor: grab;
|
|
font-size: 12px;
|
|
color: transparent;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 14px;
|
|
height: 14px;
|
|
flex-shrink: 0;
|
|
transition: color 0.15s;
|
|
&:active {
|
|
cursor: grabbing;
|
|
}
|
|
}
|
|
|
|
.sidebar-item:hover .list-drag-handle {
|
|
color: #c0c4cc;
|
|
&:hover {
|
|
color: #8c8c8c;
|
|
}
|
|
}
|
|
|
|
.list-drag-handle-placeholder {
|
|
width: 14px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sidebar-empty-hint {
|
|
padding: 8px 10px 4px 38px;
|
|
font-size: 12px;
|
|
color: #bfbfbf;
|
|
font-style: italic;
|
|
}
|
|
|
|
.sidebar-new-group-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 8px 10px;
|
|
margin: 4px 0;
|
|
cursor: pointer;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
color: #8c8c8c;
|
|
&:hover {
|
|
background: #f5f5f5;
|
|
color: #333;
|
|
}
|
|
}
|
|
|
|
.sidebar-new-group-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 10px;
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.sidebar-new-item-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 4px 10px 4px 8px;
|
|
margin: 2px 0;
|
|
}
|
|
|
|
.group-being-dragged > .sidebar-group-items {
|
|
display: none !important;
|
|
}
|
|
|
|
.group-drag-ghost {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.sidebar-item-renaming {
|
|
background: #f5f6f7;
|
|
}
|
|
|
|
.sidebar-group-renaming {
|
|
background: #f5f6f7;
|
|
}
|
|
|
|
.sidebar-new-icon {
|
|
font-size: 12px;
|
|
color: #8c8c8c;
|
|
}
|
|
</style>
|