Files
llm-in-text/src/views/DocsView.vue
T
ydy0615 70152c61b1 feat: enhance Milkdown editor and file system functionality
- Normalize line endings in Markdown export for DOCX files.
- Improve selection serialization to Markdown with better handling of empty documents.
- Add a new `updateFile` function to the file system for updating file properties.
- Introduce video transcoding capabilities using FFmpeg, supporting various video formats.
- Update AGENTS.md for clearer plugin structure and responsibilities.
- Add scoped styles for TreeNodeItem component to improve UI consistency.
- Implement cross-origin isolation headers in Vite configuration for enhanced security.
- Remove obsolete test_cross.py file.
2026-05-01 20:55:02 +08:00

441 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { computed, onMounted, ref } from 'vue'
import FileTree from '../components/FileTree.vue'
import FileContent from '../components/FileContent.vue'
import ContextMenu from '../components/ContextMenu.vue'
import { useFileSystem } from '../composables/useFileSystem'
const fs = useFileSystem()
const sidebarCollapsed = ref(false)
const confirmDialog = ref(null)
onMounted(() => {
fs.load()
})
const selectedNode = computed(() => fs.getSelectedNode())
const breadcrumb = computed(() => (fs.selectedId.value ? fs.getBreadcrumbPath(fs.selectedId.value) : []))
const storageSummary = computed(() => {
const bytes = fs.stats.value.usedBytes
if (!bytes) return '0 B'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let value = bytes
let index = 0
while (value >= 1024 && index < units.length - 1) {
value /= 1024
index += 1
}
return `${value >= 100 || index === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[index]}`
})
function handleCreateFile(parentId, name = 'untitled.md') {
fs.createFile(parentId, name)
}
function handleCreateFolder(parentId, name = '新建文件夹') {
fs.createFolder(parentId, name)
}
function handleRename(id, newName) {
fs.rename(id, newName)
}
function findNode(nodes, id) {
for (const node of nodes) {
if (node.id === id) return node
if (node.type === 'folder') {
const found = findNode(node.children || [], id)
if (found) return found
}
}
return null
}
function handleDelete(id) {
const node = findNode(fs.tree.value, id)
if (!node) return
confirmDialog.value = {
id,
name: node.name,
type: node.type
}
}
function confirmDelete() {
if (!confirmDialog.value) return
fs.remove(confirmDialog.value.id)
confirmDialog.value = null
}
function handleContextMenu(x, y, node) {
fs.showContextMenu(x, y, node)
}
function handleDrop(draggedId, targetParentId) {
if (draggedId === targetParentId) return
fs.cut(draggedId)
fs.paste(targetParentId)
}
async function handleUploadFiles(files) {
const parentId = selectedNode.value?.type === 'folder' ? selectedNode.value.id : selectedNode.value?.parentId || null
const result = await fs.uploadFiles(files, parentId)
if (result.failed.length > 0) {
fs.error.value = result.failed.map((item) => `${item.name}${item.reason}`).join('')
}
}
function closeConfirm() {
confirmDialog.value = null
}
</script>
<template>
<div class="docs-view">
<div class="docs-layout">
<aside v-show="!sidebarCollapsed" class="docs-sidebar">
<FileTree
:nodes="fs.tree.value"
:selected-id="fs.selectedId.value"
:expanded-ids="fs.expandedIds.value"
:clipboard="fs.clipboard.value"
:get-file-icon="fs.getFileIcon"
:loading="fs.loading.value"
:stats="fs.stats.value"
@select="fs.select"
@toggle="fs.toggleFolder"
@create-file="handleCreateFile"
@create-folder="handleCreateFolder"
@rename="handleRename"
@remove="handleDelete"
@copy="fs.copy"
@cut="fs.cut"
@paste="fs.paste"
@context-menu="handleContextMenu"
@drop="handleDrop"
@drag-start="() => {}"
@drag-over="() => {}"
@upload-files="handleUploadFiles"
/>
</aside>
<section class="docs-main">
<header v-if="selectedNode?.type !== 'file'" class="docs-toolbar">
<div class="toolbar-left">
<button
class="sidebar-toggle"
type="button"
:title="sidebarCollapsed ? '展开左侧栏' : '收起左侧栏'"
@click="sidebarCollapsed = !sidebarCollapsed"
>
<svg viewBox="0 0 16 16" width="15" height="15" fill="currentColor"><path d="M2.75 2A1.75 1.75 0 001 3.75v8.5C1 13.216 1.784 14 2.75 14h10.5A1.75 1.75 0 0015 12.25v-8.5A1.75 1.75 0 0013.25 2H2.75zm0 1.5h2.5v9h-2.5a.25.25 0 01-.25-.25v-8.5a.25.25 0 01.25-.25zm4 9v-9h6.5a.25.25 0 01.25.25v8.5a.25.25 0 01-.25.25h-6.5z"/></svg>
</button>
<div class="toolbar-breadcrumb">
<button class="crumb-link" type="button" @click="fs.select(null)">workspace</button>
<template v-for="item in breadcrumb" :key="item.id">
<span class="crumb-sep">/</span>
<button
v-if="item.id !== selectedNode?.id"
class="crumb-link"
type="button"
@click="fs.select(item.id)"
>
{{ item.name }}
</button>
<span v-else class="crumb-current">{{ item.name }}</span>
</template>
</div>
</div>
<div class="toolbar-right">
<span class="storage-pill">本地存储 {{ storageSummary }}</span>
<button
v-if="fs.canPaste()"
class="toolbar-btn"
type="button"
@click="fs.paste(selectedNode?.type === 'folder' ? selectedNode.id : selectedNode?.parentId || null)"
>
粘贴
</button>
</div>
</header>
<FileContent
:node="selectedNode"
:breadcrumb="breadcrumb"
:root-nodes="fs.tree.value"
:get-file-icon="fs.getFileIcon"
:get-file-blob="fs.getFileBlob"
:update-file="fs.updateFile"
:show-sidebar-toggle="selectedNode?.type === 'file'"
@navigate="fs.select"
@toggle-sidebar="sidebarCollapsed = !sidebarCollapsed"
/>
</section>
</div>
<ContextMenu
:visible="!!fs.contextMenu.value"
:x="fs.contextMenu.value?.x || 0"
:y="fs.contextMenu.value?.y || 0"
:node="fs.contextMenu.value?.node || null"
:can-paste="fs.canPaste()"
@close="fs.hideContextMenu()"
@rename="fs.hideContextMenu()"
@delete="(id) => { fs.hideContextMenu(); handleDelete(id) }"
@copy="(id) => { fs.hideContextMenu(); fs.copy(id) }"
@cut="(id) => { fs.hideContextMenu(); fs.cut(id) }"
@paste="(parentId) => { fs.hideContextMenu(); fs.paste(parentId) }"
@new-file="(parentId) => { fs.hideContextMenu(); handleCreateFile(parentId) }"
@new-folder="(parentId) => { fs.hideContextMenu(); handleCreateFolder(parentId) }"
/>
<Teleport to="body">
<div v-if="confirmDialog" class="confirm-overlay">
<div class="confirm-dialog">
<h3>确认删除</h3>
<p>
确定要删除 <strong>{{ confirmDialog.name }}</strong>
{{ confirmDialog.type === 'folder' ? ' 以及其中的全部内容' : '' }} 吗?
</p>
<div class="confirm-actions">
<button class="btn btn-secondary" type="button" @click="closeConfirm">取消</button>
<button class="btn btn-danger" type="button" @click="confirmDelete">删除</button>
</div>
</div>
</div>
</Teleport>
<Teleport to="body">
<div v-if="fs.error.value" class="error-toast">
<div class="error-content">
<span>{{ fs.error.value }}</span>
<button class="error-close" type="button" @click="fs.error.value = null">×</button>
</div>
</div>
</Teleport>
</div>
</template>
<style scoped>
.docs-view {
width: 100%;
height: 100vh;
overflow: hidden;
background: var(--github-bg);
}
.docs-layout {
display: grid;
grid-template-columns: 320px minmax(0, 1fr);
height: 100%;
}
.docs-sidebar {
min-width: 0;
border-right: 1px solid var(--github-border);
background: var(--github-bg);
}
.docs-main {
min-width: 0;
display: flex;
flex-direction: column;
}
.docs-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 56px;
padding: 0 18px;
border-bottom: 1px solid var(--github-border);
background: var(--github-bg);
}
.toolbar-left,
.toolbar-right,
.toolbar-breadcrumb {
display: flex;
align-items: center;
gap: 10px;
}
.toolbar-breadcrumb {
min-width: 0;
flex-wrap: wrap;
}
.sidebar-toggle,
.toolbar-btn,
.crumb-link {
border: none;
background: transparent;
cursor: pointer;
}
.sidebar-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border: 1px solid var(--github-border);
border-radius: 8px;
color: var(--github-text-secondary);
background: var(--github-bg);
}
.sidebar-toggle:hover,
.toolbar-btn:hover {
background: var(--github-hover);
}
.crumb-link {
color: #0969da;
font-size: 14px;
}
.crumb-current {
color: var(--github-text);
font-size: 14px;
font-weight: 700;
}
.crumb-sep {
color: var(--github-text-secondary);
}
.storage-pill {
display: inline-flex;
align-items: center;
height: 32px;
padding: 0 12px;
border: 1px solid var(--github-border);
border-radius: 999px;
background: var(--github-hover);
color: var(--github-text-secondary);
font-size: 12px;
font-weight: 700;
}
.toolbar-btn {
height: 32px;
padding: 0 12px;
border: 1px solid var(--github-border);
border-radius: 8px;
background: var(--github-bg);
color: var(--github-text);
font-size: 13px;
font-weight: 600;
}
.confirm-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(15, 23, 42, 0.35);
z-index: 10000;
}
.confirm-dialog {
width: min(420px, calc(100vw - 32px));
padding: 24px;
border: 1px solid var(--github-border);
border-radius: 16px;
background: var(--github-bg);
box-shadow: 0 28px 60px rgba(15, 23, 42, 0.22);
}
.confirm-dialog h3 {
margin: 0 0 10px;
font-size: 1.1rem;
}
.confirm-dialog p {
margin: 0;
color: var(--github-text-secondary);
line-height: 1.7;
}
.confirm-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 18px;
}
.btn {
height: 36px;
padding: 0 14px;
border: 1px solid var(--github-border);
border-radius: 10px;
font-weight: 700;
cursor: pointer;
}
.btn-secondary {
background: var(--github-bg);
color: var(--github-text);
}
.btn-danger {
border-color: #cf222e;
background: #cf222e;
color: #fff;
}
.error-toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translateX(-50%);
z-index: 10001;
}
.error-content {
display: flex;
align-items: center;
gap: 12px;
max-width: min(720px, calc(100vw - 32px));
padding: 12px 16px;
border-radius: 12px;
background: #cf222e;
color: #fff;
box-shadow: 0 18px 40px rgba(207, 34, 46, 0.28);
}
.error-close {
border: none;
background: transparent;
color: inherit;
font-size: 18px;
cursor: pointer;
}
@media (max-width: 900px) {
.docs-layout {
grid-template-columns: minmax(0, 1fr);
}
.docs-sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 999;
width: min(88vw, 320px);
box-shadow: 20px 0 40px rgba(15, 23, 42, 0.16);
}
.docs-toolbar {
padding: 10px 14px;
align-items: flex-start;
flex-direction: column;
}
}
</style>