refactor: 重构前端

This commit is contained in:
Baobhan Sith
2025-04-15 11:11:01 +08:00
parent d1f874d38b
commit 2072bff331
16 changed files with 2361 additions and 768 deletions
@@ -0,0 +1,173 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import MonacoEditor from './MonacoEditor.vue'; // 导入 Monaco Editor 组件
import type { SaveStatus } from '../types/sftp.types'; // 导入保存状态类型
const props = defineProps<{
isVisible: boolean; // 控制可见性
filePath: string | null; // 当前编辑文件路径
language: string; // 编辑器语言
isLoading: boolean; // 是否正在加载文件内容
loadingError: string | null; // 加载错误信息
isSaving: boolean; // 是否正在保存
saveStatus: SaveStatus; // 保存状态
saveError: string | null; // 保存错误信息
modelValue: string; // 文件内容 (用于 v-model)
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void; // v-model 更新事件
(e: 'request-save'): void; // 请求保存事件
(e: 'close'): void; // 关闭编辑器事件
}>();
const { t } = useI18n();
// 计算属性,用于 v-model 绑定到 MonacoEditor
const editorContent = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
const handleSaveRequest = () => {
emit('request-save');
};
const handleClose = () => {
emit('close');
};
</script>
<template>
<div v-if="isVisible" class="editor-overlay">
<div class="editor-header">
<span>{{ t('fileManager.editingFile') }}: {{ filePath }}</span>
<div class="editor-actions">
<!-- 保存状态显示 -->
<span v-if="saveStatus === 'saving'" class="save-status saving">{{ t('fileManager.saving') }}...</span>
<span v-if="saveStatus === 'success'" class="save-status success"> {{ t('fileManager.saveSuccess') }}</span>
<span v-if="saveStatus === 'error'" class="save-status error"> {{ t('fileManager.saveError') }}: {{ saveError }}</span>
<!-- 保存按钮 -->
<button @click="handleSaveRequest" :disabled="isSaving || isLoading || !!loadingError" class="save-btn">
{{ isSaving ? t('fileManager.saving') : t('fileManager.actions.save') }}
</button>
<!-- 关闭按钮 -->
<button @click="handleClose" class="close-editor-btn"></button>
</div>
</div>
<!-- 加载状态 -->
<div v-if="isLoading" class="editor-loading">{{ t('fileManager.loadingFile') }}</div>
<!-- 加载错误 -->
<div v-else-if="loadingError" class="editor-error">{{ loadingError }}</div>
<!-- Monaco 编辑器实例 -->
<MonacoEditor
v-else
v-model="editorContent"
:language="language"
theme="vs-dark"
class="editor-instance"
@request-save="handleSaveRequest"
/>
</div>
</template>
<style scoped>
/* 样式从 FileManager.vue 迁移并保持一致 */
.editor-overlay {
position: absolute; /* 相对于父容器定位 */
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(40, 40, 40, 0.95); /* 深色半透明背景 */
z-index: 1000; /* 确保在文件列表之上,但在上传弹窗之下 */
display: flex;
flex-direction: column;
color: #f0f0f0;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem;
background-color: #333;
border-bottom: 1px solid #555;
font-size: 0.9em;
flex-shrink: 0; /* 防止头部被压缩 */
}
.close-editor-btn {
background: none;
border: none;
color: #ccc;
font-size: 1.2em;
cursor: pointer;
padding: 0.2rem 0.5rem;
}
.close-editor-btn:hover {
color: white;
}
.editor-loading, .editor-error {
padding: 2rem;
text-align: center;
font-size: 1.1em;
flex-grow: 1; /* 占据剩余空间 */
display: flex;
align-items: center;
justify-content: center;
}
.editor-error {
color: #ff8a8a;
}
.editor-actions {
display: flex;
align-items: center;
gap: 1rem; /* 添加按钮间距 */
}
.save-btn {
background-color: #4CAF50;
color: white;
border: none;
padding: 0.4rem 0.8rem;
/* margin-left: 1rem; */ /* 使用 gap 代替 */
cursor: pointer;
border-radius: 3px;
font-size: 0.9em;
}
.save-btn:disabled {
background-color: #aaa;
cursor: not-allowed;
}
.save-btn:hover:not(:disabled) {
background-color: #45a049;
}
.save-status {
/* margin-left: 1rem; */ /* 使用 gap 代替 */
font-size: 0.9em;
padding: 0.2rem 0.5rem;
border-radius: 3px;
white-space: nowrap; /* 防止状态文本换行 */
}
.save-status.saving {
color: #888;
}
.save-status.success {
color: #4CAF50;
background-color: #e8f5e9;
}
.save-status.error {
color: #f44336;
background-color: #ffebee;
}
.editor-instance {
flex-grow: 1; /* 让编辑器占据剩余空间 */
min-height: 0; /* 对 flex 布局中的子元素很重要 */
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,112 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import type { UploadItem } from '../types/upload.types'; // 导入上传项类型
const props = defineProps<{
uploads: Record<string, UploadItem>; // 接收上传任务字典
}>();
const emit = defineEmits<{
(e: 'cancel-upload', uploadId: string): void; // 定义取消上传事件
}>();
const { t } = useI18n();
// 计算是否有可见的上传任务(非已完成或已取消的)
const hasVisibleUploads = computed(() => {
return Object.values(props.uploads).some(
upload => upload.status !== 'success' && upload.status !== 'cancelled'
);
});
// 计算显示的上传列表(可以过滤掉已完成/取消的,或者全部显示)
// 这里选择全部显示,让用户能看到最终状态
const uploadList = computed(() => Object.values(props.uploads));
const handleCancel = (uploadId: string) => {
emit('cancel-upload', uploadId);
};
</script>
<template>
<!-- 仅当有上传任务时显示 -->
<div v-if="uploadList.length > 0" class="upload-popup">
<h4>{{ t('fileManager.uploadTasks') }}:</h4>
<ul>
<li v-for="upload in uploadList" :key="upload.id">
<span>{{ upload.filename }} ({{ t(`fileManager.uploadStatus.${upload.status}`) }})</span>
<progress v-if="upload.status === 'uploading' || upload.status === 'pending'" :value="upload.progress" max="100"></progress>
<span v-if="upload.status === 'uploading'"> {{ upload.progress }}%</span>
<span v-if="upload.status === 'error'" class="error"> {{ t('fileManager.errors.generic') }}: {{ upload.error }}</span>
<span v-if="upload.status === 'success'"> </span>
<span v-if="upload.status === 'cancelled'"> {{ t('fileManager.uploadStatus.cancelled') }}</span>
<!-- 只有在可取消状态时显示取消按钮 -->
<button v-if="['pending', 'uploading', 'paused'].includes(upload.status)" @click="handleCancel(upload.id)" class="cancel-btn">{{ t('fileManager.actions.cancel') }}</button>
</li>
</ul>
</div>
</template>
<style scoped>
/* 样式从 FileManager.vue 迁移并保持一致 */
.upload-popup {
position: fixed;
bottom: 1rem;
right: 1rem;
background-color: white;
border: 1px solid #ccc;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
padding: 0.8rem;
max-width: 300px;
max-height: 200px;
overflow-y: auto;
z-index: 1001; /* 确保在文件列表之上 */
font-size: 0.9rem; /* 保持字体大小一致 */
}
.upload-popup h4 {
margin: 0 0 0.5rem 0;
font-size: 0.9em;
border-bottom: 1px solid #eee;
padding-bottom: 0.3rem;
}
.upload-popup ul {
list-style: none;
padding: 0;
margin: 0;
}
.upload-popup li {
margin-bottom: 0.4rem;
font-size: 0.85em;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem; /* 添加一些间隙 */
}
.upload-popup progress {
/* margin: 0 0.5rem; */ /* 使用 gap 代替 */
width: 80px;
height: 0.8em;
flex-shrink: 0; /* 防止进度条被压缩 */
}
.upload-popup .error {
color: red;
/* margin-left: 0.5rem; */ /* 使用 gap 代替 */
flex-basis: 100%; /* 错误信息换行 */
font-size: 0.8em;
}
.upload-popup .cancel-btn {
margin-left: auto; /* 将按钮推到右侧 */
padding: 0.1rem 0.4rem;
font-size: 0.8em;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
cursor: pointer;
border-radius: 3px;
}
.upload-popup .cancel-btn:hover {
background-color: #f5c6cb;
}
</style>