11<script setup lang="ts">
22import { exportMergedTheme } from ' @md/core'
33import { themeMap , themeOptionsMap } from ' @md/shared'
4- import { Download , Edit3 , Ellipsis , Eye , Plus , X } from ' lucide-vue-next'
4+ import { Check , CheckSquare , Download , Edit3 , Ellipsis , Eye , Plus , X } from ' lucide-vue-next'
55import { useCssEditorStore } from ' @/stores/cssEditor'
66import { useEditorStore } from ' @/stores/editor'
77import { useRenderStore } from ' @/stores/render'
@@ -16,7 +16,7 @@ const editorStore = useEditorStore()
1616const themeStore = useThemeStore ()
1717
1818const { isMobile } = storeToRefs (uiStore )
19- const { cssContentConfig } = storeToRefs (cssEditorStore )
19+ const { cssContentConfig, isSelectMode, selectedIds } = storeToRefs (cssEditorStore )
2020
2121// 控制是否启用动画
2222const enableAnimation = ref (false )
@@ -176,11 +176,89 @@ function addHandler() {
176176 isOpenAddDialog .value = true
177177}
178178
179- // 查看内置主题功能
180179const isOpenViewThemeDialog = ref (false )
181180const selectedViewTheme = ref <' default' | ' grace' | ' simple' >(' default' )
182181
183- // 打开查看内置主题对话框
182+ const contextMenuTargetId = ref <string | null >(null )
183+ const showContextMenu = ref (false )
184+ const contextMenuPos = ref ({ x: 0 , y: 0 })
185+
186+ function onContextMenu(e : MouseEvent , tabId : string ) {
187+ e .preventDefault ()
188+ e .stopPropagation ()
189+ contextMenuTargetId .value = tabId
190+ contextMenuPos .value = { x: e .clientX , y: e .clientY }
191+ showContextMenu .value = true
192+ }
193+
194+ function closeContextMenu() {
195+ showContextMenu .value = false
196+ contextMenuTargetId .value = null
197+ }
198+
199+ function contextMenuRename() {
200+ const tab = cssContentConfig .value .tabs .find (t => t .id === contextMenuTargetId .value )
201+ if (tab ) {
202+ rename (tab .name )
203+ }
204+ closeContextMenu ()
205+ }
206+
207+ function contextMenuExport() {
208+ if (contextMenuTargetId .value ) {
209+ cssEditorStore .exportSingleTab (contextMenuTargetId .value )
210+ }
211+ closeContextMenu ()
212+ }
213+
214+ function contextMenuDelete() {
215+ if (contextMenuTargetId .value ) {
216+ removeHandler (contextMenuTargetId .value )
217+ }
218+ closeContextMenu ()
219+ }
220+
221+ function enterSelectModeFromContextMenu() {
222+ if (contextMenuTargetId .value ) {
223+ cssEditorStore .toggleSelectMode ()
224+ cssEditorStore .toggleSelectTab (contextMenuTargetId .value )
225+ }
226+ closeContextMenu ()
227+ }
228+
229+ const allSelected = computed (
230+ () => cssContentConfig .value .tabs .length > 0 && selectedIds .value .length === cssContentConfig .value .tabs .length ,
231+ )
232+
233+ const isOpenBatchDelConfirmDialog = ref (false )
234+
235+ const batchDelConfirmText = computed (() => {
236+ const n = selectedIds .value .length
237+ if (n === 1 ) {
238+ const tab = cssContentConfig .value .tabs .find (t => t .id === selectedIds .value [0 ])
239+ return ` 此操作将删除「${tab ?.title ?? ` ` }」,是否继续? `
240+ }
241+ return ` 此操作将删除已选的 ${n } 个方案,是否继续? `
242+ })
243+
244+ function confirmBatchDelete() {
245+ cssEditorStore .batchDeleteTabs ()
246+ isOpenBatchDelConfirmDialog .value = false
247+ }
248+
249+ function handleTabClick(tabId : string ) {
250+ if (isSelectMode .value ) {
251+ cssEditorStore .toggleSelectTab (tabId )
252+ }
253+ else {
254+ tabChanged (tabId )
255+ }
256+ }
257+
258+ function exitSelectMode() {
259+ cssEditorStore .toggleSelectMode ()
260+ }
261+
184262function openViewThemeDialog() {
185263 selectedViewTheme .value = ' default'
186264 isOpenViewThemeDialog .value = true
@@ -229,6 +307,13 @@ onMounted(() => {
229307
230308 // 初始化时滚动到活跃的 tab
231309 scrollToActiveTab ()
310+
311+ // 点击外部关闭右键菜单
312+ document .addEventListener (' click' , closeContextMenu )
313+ })
314+
315+ onUnmounted (() => {
316+ document .removeEventListener (' click' , closeContextMenu )
232317})
233318
234319// 导出合并后的主题
@@ -279,17 +364,25 @@ function exportCurrentTheme() {
279364 <!-- Tab 栏 + 工具栏合并 -->
280365 <div class =" flex items-center h-9 px-2 shrink-0 border-b border-border" >
281366 <div class =" flex-1 flex items-center gap-0 overflow-x-auto custom-scrollbar min-w-0 h-full" >
282- <button
367+ <div
283368 v-for =" item in cssContentConfig.tabs"
284- :key =" item.name "
369+ :key =" item.id "
285370 class =" group/tab relative flex items-center gap-1.5 shrink-0 h-full px-3 text-xs transition-colors duration-150"
286- :class =" {
287- 'css-tab-active text-foreground font-medium': cssContentConfig.active === item.id ,
288- 'text-muted-foreground hover: text-foreground': cssContentConfig.active !== item.id ,
289- } "
290- @click =" tabChanged (item.id)"
371+ :class =" [
372+ cssContentConfig.active === item.id && !isSelectMode ? 'css-tab-active text-foreground font-medium' : 'text-muted-foreground hover:text-foreground' ,
373+ isSelectMode && selectedIds.includes(item.id) ? 'bg-accent text-accent- foreground' : '' ,
374+ ] "
375+ @click =" handleTabClick (item.id)"
291376 @dblclick.stop =" startInlineRename(item)"
377+ @contextmenu =" onContextMenu($event, item.id)"
292378 >
379+ <span
380+ v-if =" isSelectMode"
381+ class =" inline-flex items-center justify-center size-4 rounded border transition-colors mr-1"
382+ :class =" selectedIds.includes(item.id) ? 'bg-primary border-primary text-primary-foreground' : 'border-border'"
383+ >
384+ <Check v-if =" selectedIds.includes(item.id)" class =" size-3" />
385+ </span >
293386 <input
294387 v-if =" inlineEditId === item.id"
295388 :ref =" setInlineInputRef"
@@ -302,14 +395,12 @@ function exportCurrentTheme() {
302395 >
303396 <span v-else class =" truncate max-w-[100px]" >{{ item.title }}</span >
304397
305- <!-- 活跃 tab 下划线指示器 -->
306398 <span
307- v-if =" cssContentConfig.active === item.id"
399+ v-if =" cssContentConfig.active === item.id && !isSelectMode "
308400 class =" absolute bottom-0 left-2 right-2 h-[2px] rounded-full bg-primary"
309401 />
310402
311- <!-- 活跃 tab 操作: 更多菜单 -->
312- <DropdownMenu v-if =" cssContentConfig.active === item.id" >
403+ <DropdownMenu v-if =" cssContentConfig.active === item.id && !isSelectMode" >
313404 <DropdownMenuTrigger as-child >
314405 <span
315406 class =" inline-flex items-center justify-center size-4 rounded text-muted-foreground/60 hover:text-foreground hover:bg-black/5 dark:hover:bg-white/10 transition-colors duration-100 cursor-pointer"
@@ -322,6 +413,9 @@ function exportCurrentTheme() {
322413 <DropdownMenuItem @click.stop =" rename(item.name)" >
323414 <Edit3 class =" mr-2 size-4" /> 重命名
324415 </DropdownMenuItem >
416+ <DropdownMenuItem @click.stop =" cssEditorStore.exportSingleTab(item.id)" >
417+ <Download class =" mr-2 size-4" /> 导出
418+ </DropdownMenuItem >
325419 <DropdownMenuSeparator />
326420 <DropdownMenuItem
327421 v-if =" cssContentConfig.tabs.length > 1"
@@ -332,7 +426,7 @@ function exportCurrentTheme() {
332426 </DropdownMenuItem >
333427 </DropdownMenuContent >
334428 </DropdownMenu >
335- </button >
429+ </div >
336430 </div >
337431
338432 <!-- 工具按钮组 -->
@@ -380,6 +474,90 @@ function exportCurrentTheme() {
380474 />
381475 </div >
382476
477+ <!-- 选择模式底部操作栏 -->
478+ <Transition name =" slide-up" >
479+ <div
480+ v-if =" isSelectMode"
481+ class =" shrink-0 border-t border-border bg-background px-3 pt-2 pb-3 space-y-2"
482+ >
483+ <div class =" flex items-center justify-between text-xs" >
484+ <span class =" text-muted-foreground" >
485+ 已选
486+ <strong class =" text-foreground font-semibold" >{{ selectedIds.length }}</strong >
487+ 个
488+ </span >
489+ <div class =" flex items-center gap-2 text-muted-foreground" >
490+ <button
491+ class =" hover:text-foreground transition-colors"
492+ @click =" allSelected ? cssEditorStore.clearSelection() : cssEditorStore.selectAllTabs()"
493+ >
494+ {{ allSelected ? '取消全选' : '全选' }}
495+ </button >
496+ <span class =" opacity-30" >·</span >
497+ <button class =" hover:text-foreground transition-colors" @click =" exitSelectMode" >
498+ 完成
499+ </button >
500+ </div >
501+ </div >
502+ <div class =" flex" >
503+ <button
504+ class =" flex flex-1 items-center justify-center rounded-md py-2 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-35"
505+ title =" 导出"
506+ :disabled =" !selectedIds.length"
507+ @click =" cssEditorStore.batchExportTabs"
508+ >
509+ <Download class =" size-4" />
510+ </button >
511+ <div class =" mx-1 self-center h-5 w-px bg-border/60 shrink-0" />
512+ <button
513+ class =" flex flex-1 items-center justify-center rounded-md py-2 text-destructive/60 transition-colors hover:bg-destructive/8 hover:text-destructive disabled:pointer-events-none disabled:opacity-35"
514+ :title =" selectedIds.length >= cssContentConfig.tabs.length ? '至少保留一个方案' : '删除'"
515+ :disabled =" !selectedIds.length || selectedIds.length >= cssContentConfig.tabs.length"
516+ @click =" isOpenBatchDelConfirmDialog = true"
517+ >
518+ <X class =" size-4" />
519+ </button >
520+ </div >
521+ </div >
522+ </Transition >
523+
524+ <!-- 右键菜单 -->
525+ <Teleport to =" body" >
526+ <div
527+ v-if =" showContextMenu"
528+ class =" fixed z-50 min-w-[120px] rounded-md border border-border bg-background p-1 shadow-md animate-in fade-in-0 zoom-in-95"
529+ :style =" { left: `${contextMenuPos.x}px`, top: `${contextMenuPos.y}px` }"
530+ @click.stop
531+ >
532+ <button
533+ class =" flex w-full items-center rounded-sm px-2 py-1.5 text-xs text-foreground hover:bg-accent transition-colors"
534+ @click =" contextMenuRename"
535+ >
536+ <Edit3 class =" mr-2 size-3.5" /> 重命名
537+ </button >
538+ <button
539+ class =" flex w-full items-center rounded-sm px-2 py-1.5 text-xs text-foreground hover:bg-accent transition-colors"
540+ @click =" contextMenuExport"
541+ >
542+ <Download class =" mr-2 size-3.5" /> 导出
543+ </button >
544+ <div class =" my-1 h-px bg-border" />
545+ <button
546+ v-if =" cssContentConfig.tabs.length > 1"
547+ class =" flex w-full items-center rounded-sm px-2 py-1.5 text-xs text-destructive hover:bg-destructive/8 transition-colors"
548+ @click =" contextMenuDelete"
549+ >
550+ <X class =" mr-2 size-3.5" /> 删除
551+ </button >
552+ <button
553+ class =" flex w-full items-center rounded-sm px-2 py-1.5 text-xs text-foreground hover:bg-accent transition-colors"
554+ @click =" enterSelectModeFromContextMenu"
555+ >
556+ <CheckSquare class =" mr-2 size-3.5" /> 多选
557+ </button >
558+ </div >
559+ </Teleport >
560+
383561 <!-- 新增弹窗 -->
384562 <Dialog v-model:open =" isOpenAddDialog" >
385563 <DialogContent class =" sm:max-w-[425px]" >
@@ -468,6 +646,25 @@ function exportCurrentTheme() {
468646 </AlertDialogFooter >
469647 </AlertDialogContent >
470648 </AlertDialog >
649+
650+ <!-- 批量删除确认 -->
651+ <AlertDialog v-model:open =" isOpenBatchDelConfirmDialog" >
652+ <AlertDialogContent >
653+ <AlertDialogHeader >
654+ <AlertDialogTitle >提示</AlertDialogTitle >
655+ <AlertDialogDescription >{{ batchDelConfirmText }}</AlertDialogDescription >
656+ </AlertDialogHeader >
657+ <AlertDialogFooter >
658+ <AlertDialogCancel >取消</AlertDialogCancel >
659+ <AlertDialogAction
660+ class =" bg-destructive text-destructive-foreground hover:bg-destructive/90"
661+ @click =" confirmBatchDelete"
662+ >
663+ 确定删除
664+ </AlertDialogAction >
665+ </AlertDialogFooter >
666+ </AlertDialogContent >
667+ </AlertDialog >
471668 </div >
472669
473670 <!-- 查看内置主题对话框 -->
@@ -539,4 +736,16 @@ function exportCurrentTheme() {
539736.mobile-css-editor.animate {
540737 transition : transform 300ms cubic-bezier (0.16 , 1 , 0.3 , 1 );
541738}
739+
740+ /* 底部操作栏动画 */
741+ .slide-up-enter-active ,
742+ .slide-up-leave-active {
743+ transition : transform 200ms ease , opacity 200ms ease ;
744+ }
745+
746+ .slide-up-enter-from ,
747+ .slide-up-leave-to {
748+ transform : translateY (100% );
749+ opacity : 0 ;
750+ }
542751 </style >
0 commit comments