github-actions[bot] commited on
Commit
1e6d48a
·
1 Parent(s): 2aa6de4

Update from GitHub Actions

Browse files
components.d.ts CHANGED
@@ -40,5 +40,6 @@ declare module 'vue' {
40
  TSelect: typeof import('tdesign-vue-next')['Select']
41
  TTable: typeof import('tdesign-vue-next')['Table']
42
  TTextarea: typeof import('tdesign-vue-next')['Textarea']
 
43
  }
44
  }
 
40
  TSelect: typeof import('tdesign-vue-next')['Select']
41
  TTable: typeof import('tdesign-vue-next')['Table']
42
  TTextarea: typeof import('tdesign-vue-next')['Textarea']
43
+ TUpload: typeof import('tdesign-vue-next')['Upload']
44
  }
45
  }
functions/api/github/[[path]].ts CHANGED
@@ -78,7 +78,9 @@ export const onRequest = async (context: RouteContext): Promise<Response> => {
78
  encodedContent = content;
79
  } catch (e) {
80
  // 如果不是,则编码它
81
- encodedContent = btoa(unescape(encodeURIComponent(content)));
 
 
82
  }
83
 
84
  const githubUrl = `${githubApiBase}/repos/${owner}/${repo}/contents/${path}`;
 
78
  encodedContent = content;
79
  } catch (e) {
80
  // 如果不是,则编码它
81
+ const encoder = new TextEncoder();
82
+ const bytes = encoder.encode(content);
83
+ encodedContent = btoa(String.fromCharCode.apply(null, [...new Uint8Array(bytes)]));
84
  }
85
 
86
  const githubUrl = `${githubApiBase}/repos/${owner}/${repo}/contents/${path}`;
src/components/RepoHeader.vue CHANGED
@@ -8,6 +8,7 @@ const props = defineProps<{
8
  currentPath: string;
9
  accounts: Account[];
10
  isNewFile?: boolean;
 
11
  }>();
12
 
13
  const emit = defineEmits<{
@@ -49,7 +50,7 @@ const pathBreadcrumbs = computed(() => {
49
  <t-option v-for="acc in accounts" :key="acc.id" :value="acc.id" :label="`${acc.owner}/${acc.repo}`" />
50
  </t-select>
51
 
52
- <t-breadcrumb v-if="currentPath && pathBreadcrumbs.length > 0 || isNewFile" class="mt-3">
53
  <t-breadcrumb-item @click="emit('rootClick')" class="cursor-pointer hover:text-blue-500">
54
  <span class="flex items-center gap-1">
55
  <FolderIcon /> 根目录
 
8
  currentPath: string;
9
  accounts: Account[];
10
  isNewFile?: boolean;
11
+ isUploadFile?: boolean;
12
  }>();
13
 
14
  const emit = defineEmits<{
 
50
  <t-option v-for="acc in accounts" :key="acc.id" :value="acc.id" :label="`${acc.owner}/${acc.repo}`" />
51
  </t-select>
52
 
53
+ <t-breadcrumb v-if="currentPath && pathBreadcrumbs.length > 0 || isNewFile || isUploadFile" class="mt-3">
54
  <t-breadcrumb-item @click="emit('rootClick')" class="cursor-pointer hover:text-blue-500">
55
  <span class="flex items-center gap-1">
56
  <FolderIcon /> 根目录
src/router/index.ts CHANGED
@@ -27,6 +27,12 @@ const router = createRouter({
27
  component: () => import('../views/ContentView.vue'),
28
  meta: { requiresAuth: true }
29
  },
 
 
 
 
 
 
30
  {
31
  path: '/setting',
32
  name: 'Setting',
 
27
  component: () => import('../views/ContentView.vue'),
28
  meta: { requiresAuth: true }
29
  },
30
+ {
31
+ path: '/upload',
32
+ name: 'Upload',
33
+ component: () => import('../views/UploadView.vue'),
34
+ meta: { requiresAuth: true }
35
+ },
36
  {
37
  path: '/setting',
38
  name: 'Setting',
src/services/repoApi.ts CHANGED
@@ -95,9 +95,19 @@ class GitHubRepoApi implements IRepoApi {
95
  }
96
 
97
  async createFile(account: Account, path: string, content: string, message?: string) {
98
- const encoder = new TextEncoder();
99
- const bytes = encoder.encode(content);
100
- const base64Content = btoa(String.fromCharCode.apply(null, [...new Uint8Array(bytes)]));
 
 
 
 
 
 
 
 
 
 
101
  const response = await fetch(
102
  `${API_BASE_URL}/api/github/${account.owner}/${account.repo}/${path}`,
103
  {
@@ -106,7 +116,7 @@ class GitHubRepoApi implements IRepoApi {
106
  'Content-Type': 'application/json',
107
  Authorization: `Bearer ${account.token}`
108
  },
109
- body: JSON.stringify({ branch: account.ref, content: base64Content, message })
110
  }
111
  );
112
  return handleResponse(response);
 
95
  }
96
 
97
  async createFile(account: Account, path: string, content: string, message?: string) {
98
+
99
+ let encodedContent;
100
+ try {
101
+ // 检查内容是否已经是 Base64 编码
102
+ atob(content);
103
+ encodedContent = content;
104
+ } catch (e) {
105
+ // 如果不是,则编码它
106
+ const encoder = new TextEncoder();
107
+ const bytes = encoder.encode(content);
108
+ encodedContent = btoa(String.fromCharCode.apply(null, [...new Uint8Array(bytes)]));
109
+ }
110
+
111
  const response = await fetch(
112
  `${API_BASE_URL}/api/github/${account.owner}/${account.repo}/${path}`,
113
  {
 
116
  'Content-Type': 'application/json',
117
  Authorization: `Bearer ${account.token}`
118
  },
119
+ body: JSON.stringify({ branch: account.ref, content: encodedContent, message })
120
  }
121
  );
122
  return handleResponse(response);
src/views/ContentView.vue CHANGED
@@ -73,21 +73,26 @@ const fetchContent = async () => {
73
  imageUrl.value = "";
74
 
75
  const result = await repoApi.getFileContent(account, currentPath.value);
76
- if (result.content) {
77
  if (isImageFile.value) {
78
- // 如果是图片,将base64转换为Blob URL
79
- const base64Data = result.encoding === 'base64' ?
80
- result.content :
81
- btoa(result.content);
82
- const byteCharacters = atob(base64Data);
83
- const byteNumbers = new Array(byteCharacters.length);
84
- for (let i = 0; i < byteCharacters.length; i++) {
85
- byteNumbers[i] = byteCharacters.charCodeAt(i);
 
 
 
 
 
 
 
 
86
  }
87
- const byteArray = new Uint8Array(byteNumbers);
88
- const blob = new Blob([byteArray]);
89
- imageUrl.value = URL.createObjectURL(blob);
90
- } else {
91
  // 文本文件处理
92
  contentText.value = result.encoding === 'base64' ?
93
  decodeContent(result.content) as string :
@@ -381,7 +386,7 @@ onUnmounted(() => {
381
 
382
  // 组件销毁时清理 ObjectURL
383
  onBeforeUnmount(() => {
384
- if (imageUrl.value) {
385
  URL.revokeObjectURL(imageUrl.value);
386
  }
387
  });
 
73
  imageUrl.value = "";
74
 
75
  const result = await repoApi.getFileContent(account, currentPath.value);
76
+ if (result) {
77
  if (isImageFile.value) {
78
+ if (result.content) {
79
+ // 如果有 content,将 base64 转换为 Blob URL
80
+ const base64Data = result.encoding === 'base64' ?
81
+ result.content :
82
+ btoa(result.content);
83
+ const byteCharacters = atob(base64Data);
84
+ const byteNumbers = new Array(byteCharacters.length);
85
+ for (let i = 0; i < byteCharacters.length; i++) {
86
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
87
+ }
88
+ const byteArray = new Uint8Array(byteNumbers);
89
+ const blob = new Blob([byteArray]);
90
+ imageUrl.value = URL.createObjectURL(blob);
91
+ } else if (result.download_url) {
92
+ // 如果没有 content 但有 download_url,直接使用
93
+ imageUrl.value = result.download_url;
94
  }
95
+ } else if (result.content) {
 
 
 
96
  // 文本文件处理
97
  contentText.value = result.encoding === 'base64' ?
98
  decodeContent(result.content) as string :
 
386
 
387
  // 组件销毁时清理 ObjectURL
388
  onBeforeUnmount(() => {
389
+ if (imageUrl.value && imageUrl.value.startsWith('blob:')) {
390
  URL.revokeObjectURL(imageUrl.value);
391
  }
392
  });
src/views/RepoView.vue CHANGED
@@ -5,7 +5,7 @@ import { useRoute, useRouter } from 'vue-router';
5
  import { MessagePlugin } from 'tdesign-vue-next';
6
  import { useAccountStore } from '../stores/accountStorage';
7
  import { repoApi, type RepoContent, type Account } from '../services/repoApi';
8
- import { FolderIcon, FileIcon, CodeIcon, ImageIcon } from 'tdesign-icons-vue-next';
9
  import RepoHeader from '../components/RepoHeader.vue';
10
 
11
  const router = useRouter();
@@ -184,6 +184,20 @@ const handleNewFile = () => {
184
  }
185
  });
186
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  </script>
188
 
189
  <template>
@@ -198,6 +212,12 @@ const handleNewFile = () => {
198
  </template>
199
  新建文件
200
  </t-button>
 
 
 
 
 
 
201
  </div>
202
  </RepoHeader>
203
 
 
5
  import { MessagePlugin } from 'tdesign-vue-next';
6
  import { useAccountStore } from '../stores/accountStorage';
7
  import { repoApi, type RepoContent, type Account } from '../services/repoApi';
8
+ import { FolderIcon, FileIcon, CodeIcon, ImageIcon, UploadIcon } from 'tdesign-icons-vue-next';
9
  import RepoHeader from '../components/RepoHeader.vue';
10
 
11
  const router = useRouter();
 
184
  }
185
  });
186
  };
187
+
188
+ const handleUpload = () => {
189
+ if (!selectedAccount.value) {
190
+ MessagePlugin.error('请先选择仓库');
191
+ return;
192
+ }
193
+ router.push({
194
+ path: '/upload',
195
+ query: {
196
+ id: selectedAccount.value,
197
+ path: currentPath.value,
198
+ }
199
+ });
200
+ };
201
  </script>
202
 
203
  <template>
 
212
  </template>
213
  新建文件
214
  </t-button>
215
+ <t-button theme="primary" @click="handleUpload" class="flex items-center gap-1">
216
+ <template #icon>
217
+ <UploadIcon />
218
+ </template>
219
+ 上传文件
220
+ </t-button>
221
  </div>
222
  </RepoHeader>
223
 
src/views/UploadView.vue ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { ref, watch } from 'vue';
3
+ import { useRoute, useRouter } from 'vue-router';
4
+ import { MessagePlugin, type UploadFile, type UploadInstanceFunctions, type UploadProps } from 'tdesign-vue-next';
5
+ import { UploadIcon } from 'tdesign-icons-vue-next';
6
+ import { useAccountStore } from '../stores/accountStorage';
7
+ import { repoApi } from '../services/repoApi';
8
+ import RepoHeader from '../components/RepoHeader.vue';
9
+
10
+ const route = useRoute();
11
+ const router = useRouter();
12
+ const store = useAccountStore();
13
+
14
+ const selectedAccount = ref<number | ''>('');
15
+ const currentPath = ref('');
16
+ const loading = ref(false);
17
+ const uploadRef = ref<UploadInstanceFunctions>();
18
+ const uploadFiles = ref<UploadProps['value']>([]);
19
+ const commitMessage = ref('');
20
+ const commitDescription = ref('');
21
+ const selectedFile = ref<File | null>(null);
22
+
23
+ // Watch route query params and sync with local state
24
+ watch(() => route.query, async (query) => {
25
+ if (route.path !== '/upload') return;
26
+ const { id, path } = query;
27
+ if (id) {
28
+ await store.fetchAccounts();
29
+ const account = store.accounts.find(acc => acc.id === Number(id));
30
+ if (account) {
31
+ selectedAccount.value = account.id;
32
+ }
33
+ currentPath.value = path ? path as string : '';
34
+ }
35
+ uploadFiles.value = [];
36
+ }, { immediate: true });
37
+
38
+ const handlePathClick = (path: string) => {
39
+ router.push({
40
+ path: '/repo',
41
+ query: {
42
+ id: selectedAccount.value,
43
+ path
44
+ }
45
+ });
46
+ };
47
+
48
+ const handleRootClick = () => {
49
+ router.push({
50
+ path: '/repo',
51
+ query: {
52
+ id: selectedAccount.value,
53
+ }
54
+ });
55
+ };
56
+
57
+ const handleAccountChange = (val: number) => {
58
+ selectedAccount.value = val;
59
+ currentPath.value = '';
60
+ router.push({
61
+ path: '/repo',
62
+ query: {
63
+ id: selectedAccount.value,
64
+ }
65
+ });
66
+ };
67
+
68
+ const handleUpload = async (files: UploadFile[]) => {
69
+ if (!selectedAccount.value) {
70
+ MessagePlugin.error('请先选择仓库');
71
+ return;
72
+ }
73
+
74
+ if (!files || files.length === 0) {
75
+ MessagePlugin.error('请选择要上传的文件');
76
+ return;
77
+ }
78
+ selectedFile.value = files[0].raw as File;
79
+ };
80
+
81
+ const handleConfirmUpload = async () => {
82
+ if (!selectedFile.value || !selectedAccount.value) return;
83
+
84
+ const account = store.accounts.find(acc => acc.id === selectedAccount.value);
85
+ if (!account) {
86
+ MessagePlugin.error('未找到账户信息');
87
+ return;
88
+ }
89
+
90
+ loading.value = true;
91
+ try {
92
+ // Verify that selectedFile.value is actually a File object
93
+ if (!(selectedFile.value instanceof File)) {
94
+ throw new Error('Invalid file object');
95
+ }
96
+
97
+ const content = await new Promise<string>((resolve, reject) => {
98
+ const reader = new FileReader();
99
+ reader.onload = (e) => {
100
+ if (e.target?.result) {
101
+ // 获取 base64 字符串,移除 "data:*/*;base64," 前缀
102
+ const dataUrl = e.target.result.toString();
103
+
104
+ const base64 = dataUrl.split(',')[1];
105
+
106
+ resolve(base64);
107
+ } else {
108
+ reject(new Error('Failed to read file content'));
109
+ }
110
+ };
111
+ reader.onerror = () => reject(reader.error);
112
+ // 使用 readAsDataURL 来处理所有类型的文件
113
+ reader.readAsDataURL(selectedFile.value as File);
114
+ });
115
+
116
+ const filePath = currentPath.value
117
+ ? `${currentPath.value}/${selectedFile.value.name}`
118
+ : selectedFile.value.name;
119
+
120
+ const finalCommitMessage = commitMessage.value.trim() +
121
+ (commitDescription.value ? '\n\n' + commitDescription.value.trim() : '') ||
122
+ `上传文件: ${selectedFile.value.name}`;
123
+
124
+ // GitHub API 要求 base64 编码的内容
125
+ const response = await repoApi.createFile(
126
+ account,
127
+ filePath,
128
+ content,
129
+ finalCommitMessage
130
+ );
131
+
132
+ MessagePlugin.success('上传成功');
133
+
134
+ commitMessage.value = '';
135
+ commitDescription.value = '';
136
+ selectedFile.value = null;
137
+
138
+ router.push({
139
+ path: '/repo',
140
+ query: {
141
+ id: selectedAccount.value,
142
+ path: currentPath.value
143
+ }
144
+ });
145
+ } catch (error) {
146
+ console.error(error);
147
+ MessagePlugin.error('上传失败');
148
+ } finally {
149
+ loading.value = false;
150
+ }
151
+ };
152
+
153
+ const handleCancelUpload = () => {
154
+ commitMessage.value = '';
155
+ commitDescription.value = '';
156
+ selectedFile.value = null;
157
+ };
158
+ </script>
159
+
160
+ <template>
161
+ <div class="upload-container w-full flex flex-col p-3 md:p-5 gap-3 md:gap-5 bg-gray-50">
162
+ <RepoHeader :selected-account="selectedAccount" :current-path="currentPath" :accounts="store.accounts"
163
+ :is-upload-file="true" @path-click="handlePathClick" @root-click="handleRootClick"
164
+ @update:selected-account="handleAccountChange" />
165
+
166
+ <div class="content-section flex-1 bg-white rounded-lg shadow-sm p-6">
167
+ <t-upload ref="uploadRef" v-model="uploadFiles" class="w-full flex items-center justify-center" theme="image" :auto-upload="false"
168
+ :multiple="false" :draggable="true" :allow-upload-duplicate-file="true" @change="handleUpload">
169
+ </t-upload>
170
+
171
+ <!-- 提交表单 -->
172
+ <div class="mt-6 pt-4">
173
+ <div class="flex flex-col gap-4">
174
+ <t-input v-model:value="commitMessage" placeholder="请输入提交标题" :autofocus="true" />
175
+ <t-textarea v-model:value="commitDescription" placeholder="请输入详细的提交说明(可选)"
176
+ class="commit-description-textarea" />
177
+ <div class="flex gap-2 justify-end">
178
+ <t-button theme="default" @click="handleCancelUpload">
179
+ 取消
180
+ </t-button>
181
+ <t-button theme="primary" @click="handleConfirmUpload" :loading="loading">
182
+ 确认上传
183
+ </t-button>
184
+ </div>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ </template>
190
+
191
+ <style scoped>
192
+ .upload-container {
193
+ min-height: 600px;
194
+ }
195
+ :deep(.t-upload__dragger){
196
+ @apply w-full;
197
+ }
198
+ </style>