feat(filelib-web): 文件编辑改为模态框并集成 CodeMirror 语法高亮

This commit is contained in:
2026-07-27 20:53:16 +08:00
parent e194670d74
commit fe38d0b8a9
5 changed files with 476 additions and 45 deletions
+65 -35
View File
@@ -1,11 +1,22 @@
<script lang="ts">
/**
* 文件编辑器(模态框形式)。打开后加载文件内容并使用 CodeMirror 编辑,
* 支持语法高亮、版本冲突处理、历史查看与删除。
*/
import { api, ApiError } from "./api.js";
import { toastOk, toastErr, toast } from "./stores.js";
import type { FileContent, VersionInfo, Role } from "./types.js";
import Modal from "./Modal.svelte";
import Icon from "./Icon.svelte";
import CodeEditor from "./CodeEditor.svelte";
let { projectId, path, role, onchanged, onclose }: { projectId: string; path: string; role: Role; onchanged: () => void; onclose?: () => void } = $props();
let { projectId, path, role, onchanged, onclose }: {
projectId: string;
path: string;
role: Role;
onchanged: () => void;
onclose: () => void;
} = $props();
let file = $state<FileContent | null>(null);
let draft = $state("");
@@ -15,6 +26,7 @@
let history = $state<VersionInfo[]>([]);
const canEdit = $derived(role !== "VIEW");
const filename = $derived(path.split("/").pop() ?? "");
async function load(): Promise<void> {
try {
@@ -81,6 +93,7 @@
toastOk("已删除");
file = null;
onchanged();
onclose();
} catch (e) {
toastErr(e instanceof Error ? e.message : String(e));
}
@@ -104,50 +117,67 @@
}
</script>
{#if loadError}
<div class="panel text-xs text-danger">{loadError}</div>
{:else if file}
<div class="panel">
<div class="mb-2.5 flex items-center justify-between">
<span class="file-meta">{file.path} @ {file.version}</span>
<!-- 主编辑器模态框:宽屏 overlay -->
<div
class="fixed inset-0 z-40 flex items-center justify-center bg-black/30 p-4"
role="presentation"
onclick={(e) => { if (e.target === e.currentTarget) onclose(); }}
>
<div class="flex h-[85vh] w-full max-w-[900px] flex-col rounded-2xl border border-line-soft bg-panel shadow-[0_4px_20px_rgba(26,26,24,.07)]">
<!-- 顶栏 -->
<div class="flex shrink-0 items-center justify-between border-b border-line-soft px-6 py-4">
<div class="flex min-w-0 items-center gap-3">
<span class="text-[15px] font-semibold text-ink truncate">{filename}</span>
{#if file}
<span class="file-meta">@ {file.version}</span>
{/if}
</div>
<div class="flex items-center gap-1.5">
<a class="btn" href="/database/api/projects/{projectId}/file/raw?path={encodeURIComponent(file.path)}" download>下载</a>
<button class="btn" onclick={openHistory}><Icon name="clock" size={13} /> 历史</button>
{#if canEdit}
<button class="btn btn-danger" onclick={remove}><Icon name="trash" size={13} /> 删除文件</button>
{/if}
{#if onclose}
<button class="btn !px-2.5" onclick={onclose} title="关闭预览" aria-label="关闭预览"></button>
{#if file}
<a class="btn" href="/database/api/projects/{projectId}/file/raw?path={encodeURIComponent(file.path)}" download>
<Icon name="download" size={13} /> 下载
</a>
<button class="btn" onclick={openHistory}><Icon name="clock" size={13} /> 历史</button>
{#if canEdit}
<button class="btn btn-danger" onclick={remove}><Icon name="trash" size={13} /> 删除</button>
{/if}
{/if}
<button class="btn !px-2.5" onclick={onclose} title="关闭" aria-label="关闭编辑器"></button>
</div>
</div>
{#if file.encoding === "base64"}
<div class="quiet">二进制文件({file.size} B),不支持在线编辑</div>
{:else}
<textarea rows="14" class="textarea !leading-7" bind:value={draft} readonly={!canEdit}></textarea>
{/if}
<!-- 编辑器主体 -->
<div class="flex-1 overflow-y-auto px-6 py-4">
{#if loadError}
<div class="text-xs text-danger">{loadError}</div>
{:else if file === null}
<div class="quiet">加载中…</div>
{:else if file.encoding === "base64"}
<div class="quiet">二进制文件({file.size} B),不支持在线编辑</div>
{:else}
<CodeEditor value={draft} filename={path} readonly={!canEdit} onchange={(v) => (draft = v)} />
{/if}
{#if canEdit && file.encoding !== "base64"}
<div class="mt-3 flex justify-end">
{#if conflict}
<div class="mt-4 rounded-xl border border-[#E8E2C8] bg-[#FCFBF4] p-4">
<div class="mb-2 text-[13px] font-semibold text-[#6E6329]">冲突:他人已提交 {conflict.currentVersion},差异如下</div>
<pre class="diff rounded-lg border border-line-soft bg-panel p-3">{@html renderDiff(conflict.diff)}</pre>
<div class="mt-2 text-[11.5px] text-[#8A8059]">请人工合并后重新提交(基版已更新为 {conflict.currentVersion})</div>
<div class="mt-2 flex justify-end">
<button class="btn" onclick={acceptLatest}>载入最新内容</button>
</div>
</div>
{/if}
</div>
<!-- 底栏 -->
{#if canEdit && file && file.encoding !== "base64"}
<div class="flex shrink-0 justify-end border-t border-line-soft px-6 py-3">
<button class="btn btn-primary" onclick={save}>提交修改</button>
</div>
{/if}
{#if conflict}
<div class="mt-3.5 rounded-xl border border-[#E8E2C8] bg-[#FCFBF4] p-4">
<div class="mb-2 text-[13px] font-semibold text-[#6E6329]">冲突:他人已提交 {conflict.currentVersion},差异如下(你的基版 → 最新版)</div>
<pre class="diff rounded-lg border border-line-soft bg-panel p-3">{@html renderDiff(conflict.diff)}</pre>
<div class="mt-2 text-[11.5px] text-[#8A8059]">请人工合并后,以最新内容为全文重新提交(基版将更新为 {conflict.currentVersion})</div>
<div class="mt-2 flex justify-end">
<button class="btn" onclick={acceptLatest}>载入最新内容</button>
</div>
</div>
{/if}
</div>
{:else}
<div class="panel quiet">加载中…</div>
{/if}
</div>
{#if showHistory}
<Modal title="版本历史" onclose={() => (showHistory = false)}>