git-proxy / src /views /ContentView.vue
github-actions[bot]
Update from GitHub Actions
15ff6c7
raw
history blame
13.7 kB
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { MessagePlugin } from 'tdesign-vue-next';
import { repoApi, type Account } from '../services/repoApi';
import { useAccountStore } from '../stores/accountStorage';
import MonacoEditor from '../components/MonacoEditor.vue';
import RepoHeader from '../components/RepoHeader.vue';
const route = useRoute();
const router = useRouter();
const contentText = ref<string>('');
const loading = ref(false);
const showDiff = ref(false);
const originalText = ref('');
const fileSha = ref('');
const selectedAccount = ref<number | ''>('');
const currentPath = ref('');
const newFileName = ref('');
const store = useAccountStore();
const isNewFile = ref(false);
// Compute full path by combining directory path with new file name
const fullPath = computed(() => {
if (!isNewFile.value) return currentPath.value;
const basePath = route.query.path as string || '';
return basePath ? `${basePath}/${newFileName.value}` : newFileName.value;
});
// 提交对话框状态
const showCommitDialog = ref(false);
const commitMessage = ref('');
const decodeContent = (base64Content: string, contentType: 'text' | 'binary' = 'text'): string | Uint8Array => {
// 步骤1: 使用atob解码Base64字符串,获取原始二进制数据
const binaryContent = atob(base64Content);
if (contentType === 'binary') {
// 对于二进制文件,返回Uint8Array
const bytes = new Uint8Array(binaryContent.length);
for (let i = 0; i < binaryContent.length; i++) {
bytes[i] = binaryContent.charCodeAt(i);
}
return bytes;
} else {
// 对于文本内容,处理UTF-8编码
try {
// 方法1: 使用TextDecoder (推荐,更可靠)
const bytes = new Uint8Array(binaryContent.length);
for (let i = 0; i < binaryContent.length; i++) {
bytes[i] = binaryContent.charCodeAt(i);
}
return new TextDecoder('utf-8').decode(bytes);
} catch (e) {
// 方法2: 兼容性方案
return decodeURIComponent(escape(binaryContent));
}
}
};
const fetchContent = async () => {
const account = store.accounts.find(acc => acc.id === selectedAccount.value);
if (!account) {
MessagePlugin.error(`未找到账户信息${selectedAccount.value}`);
return;
}
try {
loading.value = true;
contentText.value = "";
originalText.value = "";
const result = await repoApi.getFileContent(account, currentPath.value);
if (result.content) {
// 不移除换行符,保持原始格式
contentText.value = result.encoding && result.encoding == "base64" ? decodeContent(result.content) as string : result.content;
originalText.value = contentText.value;
fileSha.value = result.sha;
}
} catch (error) {
MessagePlugin.error('获取文件内容失败');
} finally {
loading.value = false;
}
};
const handleSave = () => {
// For new files, we need a file name
if (isNewFile.value && !newFileName.value) {
MessagePlugin.error('请输入文件名');
return;
}
if (contentText.value === '') {
MessagePlugin.error('内容不能为空');
return;
}
// For existing files, check if content was modified
if (fileSha.value && originalText.value === contentText.value) {
MessagePlugin.success('内容未修改');
return;
}
showCommitDialog.value = true;
};
const handleConfirmSave = async () => {
// Use the full path for new files, or the route path for existing files
const path = isNewFile.value ? fullPath.value : (route.query.path as string);
if (!path) {
MessagePlugin.error('请输入文件名');
return;
}
loading.value = true;
try {
const account = store.accounts.find(acc => acc.id === selectedAccount.value);
if (!account) {
MessagePlugin.error(`未找到账户信息${selectedAccount.value}`);
return;
}
let response;
if (isNewFile.value) {
// Create new file
response = await repoApi.createFile(
account,
path,
contentText.value,
commitMessage.value.trim() || '创建文件'
);
} else {
// Update existing file
response = await repoApi.updateFile(
account,
path,
contentText.value,
fileSha.value,
commitMessage.value.trim() || '更新文件'
);
}
// Update fileSha with the new value from response
if (response && response.content && response.content.sha) {
fileSha.value = response.content.sha;
}
MessagePlugin.success(isNewFile.value ? '创建成功' : '保存成功');
originalText.value = contentText.value;
showCommitDialog.value = false;
commitMessage.value = '';
// If this was a new file, update the URL to include the path
if (isNewFile.value) {
router.replace({
path: '/content',
query: {
...route.query,
path,
newFile: undefined
}
});
}
} catch (error) {
console.error(error);
MessagePlugin.error('保存失败');
} finally {
loading.value = false;
}
};
const handleCancelSave = () => {
showCommitDialog.value = false;
commitMessage.value = '';
};
// 删除对话框状态
const showDeleteDialog = ref(false);
const handleDelete = () => {
showDeleteDialog.value = true;
};
const handleConfirmDelete = async () => {
loading.value = true;
try {
const account = store.accounts.find(acc => acc.id === selectedAccount.value);
if (!account) {
MessagePlugin.error(`未找到账户信息${selectedAccount.value}`);
return;
}
await repoApi.deleteFile(
account,
currentPath.value as string,
fileSha.value,
'删除文件'
);
MessagePlugin.success('删除成功');
showDeleteDialog.value = false;
router.back();
} catch (error) {
MessagePlugin.error('删除失败');
} finally {
loading.value = false;
}
};
const handleCancelDelete = () => {
showDeleteDialog.value = false;
};
// 定义按键事件处理函数
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
e.preventDefault();
handleSave();
}
};
// 在显示差异按钮点击处理中添加
const toggleDiff = () => {
showDiff.value = !showDiff.value;
if (showDiff.value) {
// 进入差异模式时,确保有原始文本作为比较
if (originalText.value === '') {
originalText.value = contentText.value;
}
}
};
// 在 AccountView.vue 中添加
watch(showDiff, (newVal) => {
if (newVal && originalText.value === contentText.value) {
// 如果开启差异模式但两个文本相同,可以考虑提示用户
MessagePlugin.info('当前没有差异可以显示');
}
}, { immediate: true });
const handlePathClick = (path: string) => {
router.push({
path: '/repo',
query: {
id: selectedAccount.value,
path
}
});
};
const handleRootClick = () => {
router.push({
path: '/repo',
query: {
id: selectedAccount.value,
}
});
};
const handleAccountChange = (val: number) => {
selectedAccount.value = val;
currentPath.value = ''; // Reset path
router.push({
path: '/repo',
query: {
id: selectedAccount.value,
}
});
}
const getLanguageFromPath = computed(() => {
const path = route.query.path as string;
if (!path) return 'plaintext';
const ext = path.split('.').pop()?.toLowerCase() || '';
// Map file extensions to Monaco editor languages
const languageMap: Record<string, string> = {
'js': 'javascript',
"mjs": 'javascript',
'ts': 'typescript',
'json': 'json',
'md': 'markdown',
'yml': 'yaml',
'yaml': 'yaml',
'py': 'python',
'html': 'html',
'css': 'css',
'scss': 'scss',
'less': 'less',
'xml': 'xml',
'sh': 'shell',
'bash': 'shell',
'vue': 'vue',
'jsx': 'javascript',
'tsx': 'typescript',
'gitignore': 'plaintext',
'env': 'plaintext',
'txt': 'plaintext',
"rs": 'rust',
'go': 'go',
};
return languageMap[ext] || 'plaintext';
});
// 监听路由变化同步本地状态
watch(() => route.query, async (query) => {
// Only respond to query changes when on the repo route
if (route.path !== '/content') return;
const { id, path, newFile } = query;
isNewFile.value = !!newFile;
if (id) {
await store.fetchAccounts();
const account = store.accounts.find(acc => acc.id === Number(id));
if (account) {
selectedAccount.value = account.id;
}
currentPath.value = path as string;
if (isNewFile.value) {
contentText.value = '';
originalText.value = '';
newFileName.value = ''; // Reset new file name when entering new file mode
return;
}
if (path) {
await fetchContent();
}
}
}, { immediate: true });
onMounted(() => {
window.addEventListener('keydown', handleKeyDown);
});
onUnmounted(() => {
// 注销全局按键监听
window.removeEventListener('keydown', handleKeyDown);
});
</script>
<template>
<div class="content-container h-full p-2 md:p-5">
<div class="flex flex-col gap-4 h-full">
<RepoHeader :selected-account="selectedAccount" :current-path="isNewFile ? fullPath : currentPath"
:is-new-file="isNewFile" :accounts="store.accounts" @path-click="handlePathClick"
@root-click="handleRootClick" @update:selected-account="handleAccountChange">
<div class="flex items-center justify-between w-full">
<div>
<t-input v-if="isNewFile" v-model="newFileName" placeholder="请输入文件名" class="w-20" />
</div>
<div class="flex gap-2">
<t-button variant="outline" @click="toggleDiff">
{{ showDiff ? '隐藏对比' : '显示对比' }}
</t-button>
<t-button theme="primary" @click="handleSave" :loading="loading">
保存
</t-button>
<t-button v-if="!isNewFile" theme="danger" @click="handleDelete" :loading="loading">
删除
</t-button>
</div>
</div>
</RepoHeader>
<t-card bordered class="h-full">
<template #content>
<div class="flex flex-col h-full">
<div class="editor-container flex-1">
<MonacoEditor v-model:value="contentText"
:original-value="showDiff ? originalText : undefined" :language="getLanguageFromPath"
:options="{ tabSize: 2 }" />
</div>
</div>
</template>
</t-card>
</div>
<t-dialog v-model:visible="showCommitDialog" header="提交更改" :confirm-on-enter="true" @confirm="handleConfirmSave"
@close="handleCancelSave">
<template #body>
<div class="flex flex-col gap-2">
<div class="mb-2">请输入提交信息:</div>
<t-input v-model:value="commitMessage" placeholder="描述此次更改的内容" :autofocus="true" />
</div>
</template>
<template #footer>
<t-button theme="default" @click="handleCancelSave">
取消
</t-button>
<t-button theme="primary" @click="handleConfirmSave" :loading="loading">
确认
</t-button>
</template>
</t-dialog>
<t-dialog v-model:visible="showDeleteDialog" header="确认删除" :confirm-on-enter="true"
@confirm="handleConfirmDelete" @close="handleCancelDelete">
<template #body>
<div class="p-2">
确定要删除此文件吗?此操作不可恢复。
</div>
</template>
<template #footer>
<t-button theme="default" @click="handleCancelDelete">
取消
</t-button>
<t-button theme="primary" @click="handleConfirmDelete" :loading="loading">
确认
</t-button>
</template>
</t-dialog>
</div>
</template>
<style scoped>
.content-container {
width: 100%;
}
:deep(.t-card__body) {
height: 100%;
}
.editor-container {
border: 1px solid var(--td-component-border);
}
</style>