czh-20260429-实现任务管理的前端功能
This commit is contained in:
@@ -0,0 +1,794 @@
|
||||
<template>
|
||||
<div class="task-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<span class="sidebar-title">
|
||||
<Icon icon="ant-design:menu-outlined" class="mr-1" />
|
||||
任务
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-nav">
|
||||
<div
|
||||
v-for="item in NAV_ITEMS"
|
||||
:key="item.id"
|
||||
class="sidebar-item"
|
||||
:class="{ active: currentView === item.id }"
|
||||
@click="emit('view-select', item.id)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="sidebar-item-icon" />
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-divider"></div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-quick-header" @click="quickAccessExpanded = !quickAccessExpanded">
|
||||
<Icon icon="tasklist-arrow-down|svg" class="quick-access-arrow" :class="{ expanded: quickAccessExpanded }" />
|
||||
<span class="sidebar-quick-title">快速访问</span>
|
||||
</div>
|
||||
<template v-if="quickAccessExpanded">
|
||||
<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 sidebar-lists-section">
|
||||
<div class="sidebar-favorites-header">
|
||||
<Icon icon="tasklist-task-list|svg" class="sidebar-favorites-icon" />
|
||||
<span class="sidebar-favorites-name">收藏清单</span>
|
||||
<Icon icon="ant-design:plus-outlined" class="sidebar-group-add" @click.stop="startNewList('')" />
|
||||
</div>
|
||||
<div class="sidebar-favorites-items">
|
||||
<div v-if="showNewListInput && newListGroupId === ''" class="sidebar-new-item-row">
|
||||
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" style="opacity: 0.4" />
|
||||
<a-input
|
||||
v-model:value="newListName"
|
||||
size="small"
|
||||
placeholder="输入清单名称..."
|
||||
autofocus
|
||||
style="flex: 1"
|
||||
@press-enter="submitNewList"
|
||||
@blur="showNewListInput = false"
|
||||
/>
|
||||
</div>
|
||||
<draggable :model-value="ungroupedTaskLists" 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 v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
|
||||
<draggable :model-value="userGroups" 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 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">
|
||||
<div v-if="showNewListInput && newListGroupId === group.id" class="sidebar-new-item-row">
|
||||
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" style="opacity: 0.4" />
|
||||
<a-input
|
||||
v-model:value="newListName"
|
||||
size="small"
|
||||
placeholder="输入清单名称..."
|
||||
autofocus
|
||||
style="flex: 1"
|
||||
@press-enter="submitNewList"
|
||||
@blur="showNewListInput = false"
|
||||
/>
|
||||
</div>
|
||||
<draggable
|
||||
:model-value="getGroupTaskLists(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 v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<div
|
||||
v-if="group.taskLists.filter((l) => l.delFlag !== 1).length === 0 && !(showNewListInput && newListGroupId === group.id)"
|
||||
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 } 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': [name: string, groupId: string];
|
||||
'create-group': [name: string];
|
||||
'rename-list': [listId: string, newName: string];
|
||||
'delete-list': [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 quickAccessExpanded = 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 [];
|
||||
});
|
||||
|
||||
function getGroupTaskLists(group: TaskListGroup) {
|
||||
return [...group.taskLists].filter((l) => l.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||
}
|
||||
|
||||
const userGroups = computed(() => {
|
||||
return props.taskListGroups.filter((g) => g.id !== '__ungrouped__' && g.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||
});
|
||||
|
||||
const hoveredListId = ref('');
|
||||
const collapsedGroups = ref<Record<string, boolean>>({});
|
||||
const showNewGroupInput = ref(false);
|
||||
const newGroupName = ref('');
|
||||
const showNewListInput = ref(false);
|
||||
const newListGroupId = ref('');
|
||||
const newListName = ref('');
|
||||
const renamingId = ref('');
|
||||
const renamingType = ref<'list' | 'group'>('list');
|
||||
const renameValue = ref('');
|
||||
|
||||
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;
|
||||
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 startNewList(groupId: string) {
|
||||
showNewListInput.value = true;
|
||||
newListGroupId.value = groupId;
|
||||
newListName.value = '';
|
||||
}
|
||||
|
||||
function submitNewList() {
|
||||
const name = newListName.value.trim();
|
||||
if (!name) {
|
||||
showNewListInput.value = false;
|
||||
return;
|
||||
}
|
||||
emit('create-list', name, newListGroupId.value);
|
||||
showNewListInput.value = false;
|
||||
newListName.value = '';
|
||||
}
|
||||
|
||||
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('delete-list', 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>
|
||||
Reference in New Issue
Block a user