Centralize typed resource functions under common/apis and return complete JSON envelopes while preserving raw responses, bearer session isolation, and single-owner errors. Keep settings state and queues inside the setup store. Use Element Plus forms and dialogs for login, settings, imports, moves, and images; preserve editor interactions and themes. Update callers, tests, generated declarations, and request import constraints together.
92 lines
3.1 KiB
TypeScript
92 lines
3.1 KiB
TypeScript
import type { Entry, TreeIcon } from '@@/apis/tree/type'
|
|
import { basename, dirname, hiddenAssetPath } from '@@/utils/paths'
|
|
import { computed, reactive, ref, shallowRef, watch } from 'vue'
|
|
import { rangePaths } from './selection'
|
|
import { ws } from './workspace'
|
|
|
|
export interface TreeNode {
|
|
name: string
|
|
path: string
|
|
dir: boolean
|
|
icon?: TreeIcon
|
|
children: TreeNode[]
|
|
}
|
|
|
|
const collator = new Intl.Collator('zh-CN', { numeric: true, sensitivity: 'base' })
|
|
|
|
function build(entries: Entry[]): TreeNode[] {
|
|
const root: TreeNode[] = []
|
|
const dirs = new Map<string, TreeNode>()
|
|
// 先按路径排序,保证父目录先于子项出现
|
|
for (const e of [...entries].sort((a, b) => a.path.length - b.path.length)) {
|
|
const node: TreeNode = { name: basename(e.path), path: e.path, dir: e.dir, icon: e.icon, children: [] }
|
|
if (e.dir) dirs.set(e.path, node)
|
|
;(dirs.get(dirname(e.path))?.children ?? root).push(node)
|
|
}
|
|
const sort = (nodes: TreeNode[]) => {
|
|
nodes.sort((a, b) => (a.dir !== b.dir ? (a.dir ? -1 : 1) : collator.compare(a.name, b.name)))
|
|
nodes.forEach(n => sort(n.children))
|
|
}
|
|
sort(root)
|
|
return root
|
|
}
|
|
|
|
export const tree = computed(() => build(ws.entries.filter(entry => !hiddenAssetPath(entry.path))))
|
|
|
|
export const treeIconTarget = shallowRef<{ path: string, anchor: HTMLElement } | null>(null)
|
|
|
|
// null 表示无接收目标,空字符串表示根目录;文件行指向它的直接父目录。
|
|
export const dropDirectory = ref<string | null>(null)
|
|
|
|
const EXPANDED_KEY = 'rawmark.expanded'
|
|
export const expanded = reactive(new Set<string>(JSON.parse(localStorage.getItem(EXPANDED_KEY) ?? '[]') as string[]))
|
|
watch(expanded, () => localStorage.setItem(EXPANDED_KEY, JSON.stringify([...expanded])))
|
|
|
|
// 打开或新建笔记时展开其所在的各级目录
|
|
watch(
|
|
() => [ws.active, ws.renaming, ws.image],
|
|
(paths) => {
|
|
for (const p of paths) {
|
|
for (let d = p ? dirname(p) : ''; d; d = dirname(d)) expanded.add(d)
|
|
}
|
|
},
|
|
)
|
|
|
|
export const selectedPaths = reactive(new Set<string>())
|
|
let anchor: string | null = null
|
|
export const visiblePaths = computed(() => {
|
|
const paths: string[] = []
|
|
const visit = (nodes: TreeNode[]) => {
|
|
for (const node of nodes) {
|
|
paths.push(node.path); if (expanded.has(node.path)) visit(node.children)
|
|
}
|
|
}
|
|
visit(tree.value)
|
|
return paths
|
|
})
|
|
watch(visiblePaths, (paths) => {
|
|
const visible = new Set(paths)
|
|
for (const path of selectedPaths) {
|
|
if (!visible.has(path)) selectedPaths.delete(path)
|
|
}
|
|
if (anchor && !visible.has(anchor)) anchor = null
|
|
})
|
|
|
|
export function selectPath(path: string, event?: Pick<MouseEvent, 'ctrlKey' | 'metaKey' | 'shiftKey'>) {
|
|
const additive = event?.ctrlKey || event?.metaKey
|
|
if (event?.shiftKey) {
|
|
const range = rangePaths(visiblePaths.value, anchor, path)
|
|
if (!additive) selectedPaths.clear()
|
|
range.forEach(item => selectedPaths.add(item))
|
|
return
|
|
}
|
|
if (!additive) selectedPaths.clear()
|
|
if (additive && selectedPaths.has(path)) selectedPaths.delete(path)
|
|
else selectedPaths.add(path)
|
|
anchor = path
|
|
}
|
|
|
|
export function contextSelection(path: string) {
|
|
if (!selectedPaths.has(path)) selectPath(path)
|
|
}
|