github-actions[bot] commited on
Commit
15ff6c7
·
1 Parent(s): 992ad96

Update from GitHub Actions

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.cnb.yml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ main:
2
+ push:
3
+ - docker:
4
+ image: node:18
5
+ imports: https://cnb.cool/godgodgame/oci-private-key/-/blob/main/envs.yml
6
+ stages:
7
+ - name: 环境检查
8
+ script: echo $GITHUB_TOKEN_GK && echo $GITHUB_TOKEN && node -v && npm -v
9
+ - name: 将master分支同步更新到github的master分支
10
+ script: git push https://$GITHUB_TOKEN_GK:[email protected]/zhezzma/git-proxy.git HEAD:main
.editorconfig ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ indent_style = tab
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.yml]
12
+ indent_style = space
.prettierrc ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "printWidth": 140,
3
+ "singleQuote": true,
4
+ "semi": true,
5
+ "useTabs": true
6
+ }
.vscode/settings.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "files.associations": {
3
+ "wrangler.json": "jsonc"
4
+ }
5
+ }
Dockerfile ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 基础镜像:使用 Node.js 20 的 Alpine Linux 版本
2
+ FROM node:20-alpine
3
+
4
+ # 设置工作目录
5
+ WORKDIR /app
6
+
7
+ # 安装系统依赖
8
+ RUN apk add --no-cache \
9
+ # 基本构建工具
10
+ python3 \
11
+ make \
12
+ g++ \
13
+ # Playwright 依赖
14
+ chromium \
15
+ nss \
16
+ freetype \
17
+ freetype-dev \
18
+ harfbuzz \
19
+ ca-certificates \
20
+ ttf-freefont \
21
+ # 其他依赖
22
+ gcompat
23
+
24
+ # 设置 Playwright 的环境变量
25
+ ENV PLAYWRIGHT_BROWSERS_PATH=/usr/bin
26
+ ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
27
+ ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser
28
+ ENV PLAYWRIGHT_SKIP_BROWSER_VALIDATION=1
29
+
30
+ # 复制依赖文件并安装
31
+ COPY package*.json ./
32
+ COPY tsconfig*.json ./
33
+ COPY vite.config.ts ./
34
+ COPY index.html ./
35
+ COPY index.ts ./
36
+ COPY src/ ./src/
37
+ COPY public/ ./public/
38
+ COPY functions/ ./functions/
39
+
40
+
41
+ RUN npm install
42
+ RUN npm run build:server
43
+
44
+ # 创建非 root 用户和用户组
45
+ RUN addgroup -S -g 1001 nodejs && \
46
+ adduser -S -D -H -u 1001 -G nodejs hono
47
+
48
+ # 设置应用文件的所有权
49
+ RUN chown -R hono:nodejs /app
50
+
51
+ # 切换到非 root 用户
52
+ USER hono
53
+
54
+ # 声明容器要暴露的端口
55
+ EXPOSE 7860
56
+ ENV PORT=7860
57
+
58
+ # 启动应用
59
+ CMD ["node", "dist-server/index.js"]
auto-imports.d.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable */
2
+ /* prettier-ignore */
3
+ // @ts-nocheck
4
+ // noinspection JSUnusedGlobalSymbols
5
+ // Generated by unplugin-auto-import
6
+ // biome-ignore lint: disable
7
+ export {}
8
+ declare global {
9
+
10
+ }
components.d.ts ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable */
2
+ // @ts-nocheck
3
+ // Generated by unplugin-vue-components
4
+ // Read more: https://github.com/vuejs/core/pull/3399
5
+ // biome-ignore lint: disable
6
+ export {}
7
+
8
+ /* prettier-ignore */
9
+ declare module 'vue' {
10
+ export interface GlobalComponents {
11
+ MonacoEditor: typeof import('./src/components/MonacoEditor.vue')['default']
12
+ RepoHeader: typeof import('./src/components/RepoHeader.vue')['default']
13
+ RouterLink: typeof import('vue-router')['RouterLink']
14
+ RouterView: typeof import('vue-router')['RouterView']
15
+ TAside: typeof import('tdesign-vue-next')['Aside']
16
+ TBreadcrumb: typeof import('tdesign-vue-next')['Breadcrumb']
17
+ TBreadcrumbItem: typeof import('tdesign-vue-next')['BreadcrumbItem']
18
+ TButton: typeof import('tdesign-vue-next')['Button']
19
+ TCard: typeof import('tdesign-vue-next')['Card']
20
+ TCheckbox: typeof import('tdesign-vue-next')['Checkbox']
21
+ TContent: typeof import('tdesign-vue-next')['Content']
22
+ TDialog: typeof import('tdesign-vue-next')['Dialog']
23
+ TDivider: typeof import('tdesign-vue-next')['Divider']
24
+ TDrawer: typeof import('tdesign-vue-next')['Drawer']
25
+ TDropdown: typeof import('tdesign-vue-next')['Dropdown']
26
+ TFooter: typeof import('tdesign-vue-next')['Footer']
27
+ TForm: typeof import('tdesign-vue-next')['Form']
28
+ TFormItem: typeof import('tdesign-vue-next')['FormItem']
29
+ THeader: typeof import('tdesign-vue-next')['Header']
30
+ TIcon: typeof import('tdesign-vue-next')['Icon']
31
+ TInput: typeof import('tdesign-vue-next')['Input']
32
+ TLayout: typeof import('tdesign-vue-next')['Layout']
33
+ TList: typeof import('tdesign-vue-next')['List']
34
+ TListItem: typeof import('tdesign-vue-next')['ListItem']
35
+ TLoading: typeof import('tdesign-vue-next')['Loading']
36
+ TMenu: typeof import('tdesign-vue-next')['Menu']
37
+ TMenuItem: typeof import('tdesign-vue-next')['MenuItem']
38
+ TOption: typeof import('tdesign-vue-next')['Option']
39
+ TPagination: typeof import('tdesign-vue-next')['Pagination']
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
+ }
functions/api/_middleware.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ /**
4
+ * CORS 相关的响应头配置
5
+ */
6
+ export const CORS_HEADERS = {
7
+ 'Access-Control-Allow-Origin': '*', // 允许所有来源
8
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', // 允许的HTTP方法
9
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With', // 允许的请求头
10
+ 'Access-Control-Max-Age': '86400', // 预检请求的有效期
11
+ };
12
+
13
+ /**
14
+ * 处理 OPTIONS 预检请求
15
+ */
16
+ export function handleOptions(): Response {
17
+ return new Response(null, {
18
+ status: 204,
19
+ headers: CORS_HEADERS
20
+ });
21
+ }
22
+
23
+ /**
24
+ * 为响应添加 CORS 头
25
+ * @param response 原始响应对象
26
+ * @returns 添加了 CORS 头的新响应对象
27
+ */
28
+ export function addCorsHeaders(response: Response): Response {
29
+ const newHeaders = new Headers(response.headers);
30
+ Object.entries(CORS_HEADERS).forEach(([key, value]) => {
31
+ newHeaders.set(key, value);
32
+ });
33
+
34
+ return new Response(response.body, {
35
+ status: response.status,
36
+ statusText: response.statusText,
37
+ headers: newHeaders
38
+ });
39
+ }
40
+
41
+
42
+ //cloundflare functions的中间件,处理跨域请求
43
+ export async function onRequest(context: any) {
44
+ // 处理预检请求
45
+ if (context.request.method === "OPTIONS") {
46
+ return handleOptions();
47
+ }
48
+ try {
49
+ const response = await context.next();
50
+ return addCorsHeaders(response);
51
+ } catch (err: any) {
52
+ return new Response(`${err.message}\n${err.stack}`, {
53
+ status: 500,
54
+ headers: CORS_HEADERS
55
+ });
56
+ }
57
+ }
functions/api/account.ts ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authMiddleware } from "../utils/auth.js";
2
+
3
+
4
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
5
+ const request = context.request;
6
+ const env = context.env as Env;
7
+
8
+ const authResponse = await authMiddleware(request, env);
9
+ if (authResponse) {
10
+ return authResponse;
11
+ }
12
+
13
+ const KV_KEY = "accounts"
14
+
15
+ try {
16
+ // GET 请求处理
17
+ if (request.method === 'GET') {
18
+ const accounts = await env.KV.get(KV_KEY);
19
+ return new Response(accounts || '[]', {
20
+ status: 200,
21
+ headers: { 'Content-Type': 'application/json' }
22
+ });
23
+ }
24
+
25
+ // POST 请求处理
26
+ if (request.method === 'POST') {
27
+ const data = await request.json();
28
+
29
+ // 验证数据格式
30
+ if (!Array.isArray(data)) {
31
+ return new Response(JSON.stringify({ error: '无效的数据格式' }), {
32
+ status: 400,
33
+ headers: { 'Content-Type': 'application/json' }
34
+ });
35
+ }
36
+
37
+ // 存储账号数据
38
+ await env.KV.put(KV_KEY, JSON.stringify(data));
39
+
40
+ return new Response(JSON.stringify({ message: '保存成功' }), {
41
+ status: 200,
42
+ headers: { 'Content-Type': 'application/json' }
43
+ });
44
+ }
45
+
46
+ // 不支持的请求方法
47
+ return new Response(JSON.stringify({ error: '不支持的请求方法' }), {
48
+ status: 405,
49
+ headers: { 'Content-Type': 'application/json' }
50
+ });
51
+
52
+ } catch (error) {
53
+ return new Response(JSON.stringify({ error: '服务器内部错误' }), {
54
+ status: 500,
55
+ headers: { 'Content-Type': 'application/json' }
56
+ });
57
+ }
58
+ };
functions/api/github/[[path]].ts ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //https://docs.github.com/en/rest/repos/contents
2
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
3
+ const request = context.request;
4
+ const env = context.env as Env;
5
+
6
+ // 从 Authorization header 中获取 token
7
+ const authHeader = request.headers.get('Authorization');
8
+ if (!authHeader || (!authHeader.startsWith('Bearer ') && !authHeader.startsWith('token '))) {
9
+ return new Response(JSON.stringify({ error: '未提供有效的授权令牌' }), {
10
+ status: 401,
11
+ headers: { 'Content-Type': 'application/json' }
12
+ });
13
+ }
14
+ const githubToken = authHeader.startsWith('Bearer ')
15
+ ? authHeader.replace('Bearer ', '')
16
+ : authHeader.replace('token ', '');
17
+
18
+ console.log('Request URL:', request.url);
19
+ try {
20
+ const url = new URL(request.url);
21
+
22
+ // 提取仓库所有者和仓库名称
23
+ const pathParts = url.pathname.split('/').filter(Boolean);
24
+ const owner = pathParts[2] || url.searchParams.get('owner'); // 仓库所有者
25
+ const repo = pathParts[3] || url.searchParams.get('repo'); // 仓库名称
26
+ const path = pathParts[4] || url.searchParams.get('path') || ""; // 文件路径
27
+ if (!owner || !repo) {
28
+ return new Response(JSON.stringify({ error: '缺少仓库所有者或仓库名称' }), {
29
+ status: 400,
30
+ headers: { 'Content-Type': 'application/json' }
31
+ });
32
+ }
33
+
34
+
35
+ // GitHub API 基础 URL
36
+ const githubApiBase = 'https://api.github.com';
37
+ if (request.method === 'GET') {
38
+ const ref = url.searchParams.get('ref'); // 分支或标签
39
+ // 获取文件内容或列出目录
40
+ const githubUrl = `${githubApiBase}/repos/${owner}/${repo}/contents/${path}${ref ? '?ref=' + ref : ''}`;
41
+ console.log(githubUrl);
42
+ const response = await fetch(githubUrl, {
43
+ headers: {
44
+ 'Authorization': `token ${githubToken}`,
45
+ 'Accept': 'application/vnd.github.v3+json',
46
+ 'User-Agent': 'Cloudflare-Worker'
47
+ }
48
+ });
49
+
50
+ const data = await response.json();
51
+
52
+ return new Response(JSON.stringify(data), {
53
+ status: response.status,
54
+ headers: { 'Content-Type': 'application/json' }
55
+ });
56
+ }
57
+
58
+ if (request.method === 'POST') {
59
+
60
+ // 创建新文件
61
+ const body = await request.json() as any;
62
+
63
+ const { content, message, branch } = body;
64
+
65
+ if (!path || content === undefined) {
66
+ return new Response(JSON.stringify({ error: '缺少必要参数: path, content' }), {
67
+ status: 400,
68
+ headers: { 'Content-Type': 'application/json' }
69
+ });
70
+ }
71
+
72
+ // Base64 编码内容
73
+ let encodedContent;
74
+ try {
75
+ // 检查内容是否已经是 Base64 编码
76
+ atob(content);
77
+ encodedContent = content;
78
+ } catch (e) {
79
+ // 如果不是,则编码它
80
+ encodedContent = btoa(unescape(encodeURIComponent(content)));
81
+ }
82
+
83
+ const githubUrl = `${githubApiBase}/repos/${owner}/${repo}/contents/${path}`;
84
+
85
+ const response = await fetch(githubUrl, {
86
+ method: 'PUT',
87
+ headers: {
88
+ 'Authorization': `token ${githubToken}`,
89
+ 'Accept': 'application/vnd.github.v3+json',
90
+ 'Content-Type': 'application/json',
91
+ 'User-Agent': 'Cloudflare-Worker'
92
+ },
93
+ body: JSON.stringify({
94
+ message: message || `Create file ${path}`,
95
+ content: encodedContent,
96
+ branch: branch
97
+ })
98
+ });
99
+
100
+ const data = await response.json();
101
+
102
+ return new Response(JSON.stringify(data), {
103
+ status: response.status,
104
+ headers: { 'Content-Type': 'application/json' }
105
+ });
106
+ }
107
+
108
+ if (request.method === 'PUT') {
109
+ // 更新现有文件
110
+
111
+ const body = await request.json() as any;
112
+ const { content, message, sha, branch } = body;
113
+
114
+ if (!path || content === undefined || !sha) {
115
+ return new Response(JSON.stringify({ error: '缺少必要参数: path, content, sha' }), {
116
+ status: 400,
117
+ headers: { 'Content-Type': 'application/json' }
118
+ });
119
+ }
120
+
121
+ // Base64 编码内容
122
+ let encodedContent;
123
+ try {
124
+ atob(content);
125
+ encodedContent = content;
126
+ } catch (e) {
127
+ encodedContent = btoa(unescape(encodeURIComponent(content)));
128
+ }
129
+
130
+ const githubUrl = `${githubApiBase}/repos/${owner}/${repo}/contents/${path}`;
131
+
132
+ const response = await fetch(githubUrl, {
133
+ method: 'PUT',
134
+ headers: {
135
+ 'Authorization': `token ${githubToken}`,
136
+ 'Accept': 'application/vnd.github.v3+json',
137
+ 'Content-Type': 'application/json',
138
+ 'User-Agent': 'Cloudflare-Worker'
139
+ },
140
+ body: JSON.stringify({
141
+ message: message || `Update file ${path}`,
142
+ content: encodedContent,
143
+ sha: sha,
144
+ branch: branch
145
+ })
146
+ });
147
+
148
+ const data = await response.json();
149
+
150
+ return new Response(JSON.stringify(data), {
151
+ status: response.status,
152
+ headers: { 'Content-Type': 'application/json' }
153
+ });
154
+ }
155
+
156
+ if (request.method === 'DELETE') {
157
+
158
+ const body = await request.json() as any;
159
+ // 单个文件删除
160
+ const { message, sha, branch } = body;
161
+
162
+ if (!path || !sha) {
163
+ return new Response(JSON.stringify({ error: '缺少必要参数: path, sha' }), {
164
+ status: 400,
165
+ headers: { 'Content-Type': 'application/json' }
166
+ });
167
+ }
168
+
169
+ const githubUrl = `${githubApiBase}/repos/${owner}/${repo}/contents/${path}`;
170
+
171
+ const response = await fetch(githubUrl, {
172
+ method: 'DELETE',
173
+ headers: {
174
+ 'Authorization': `token ${githubToken}`,
175
+ 'Accept': 'application/vnd.github.v3+json',
176
+ 'Content-Type': 'application/json',
177
+ 'User-Agent': 'Cloudflare-Worker'
178
+ },
179
+ body: JSON.stringify({
180
+ message: message || `Delete file ${path}`,
181
+ sha: sha,
182
+ branch: branch
183
+ })
184
+ });
185
+
186
+ const data = await response.json();
187
+
188
+ return new Response(JSON.stringify(data), {
189
+ status: response.status,
190
+ headers: { 'Content-Type': 'application/json' }
191
+ });
192
+
193
+ }
194
+
195
+ // 不支持的请求方法
196
+ return new Response(JSON.stringify({ error: '不支持的请求方法' }), {
197
+ status: 405,
198
+ headers: { 'Content-Type': 'application/json' }
199
+ });
200
+ } catch (error: any) {
201
+ return new Response(JSON.stringify({ error: '服务器内部错误', details: error.message }), {
202
+ status: 500,
203
+ headers: { 'Content-Type': 'application/json' }
204
+ });
205
+ }
206
+ };
functions/api/hf/[[path]].ts ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ //没有固定的文档
3
+ //接口是通过查看huggingface.co的请求来实现的
4
+ //有个huggingface hub api
5
+ //https://huggingface.co/docs/huggingface.js/hub/README
6
+ //https://github.com/huggingface/huggingface.js/tree/main/packages/hub
7
+ //https://github.com/huggingface/huggingface.js/blob/main/packages/hub/src/lib/list-files.ts
8
+ //https://github.com/huggingface/huggingface.js/blob/main/packages/hub/src/lib/download-file.ts
9
+ //https://github.com/huggingface/huggingface.js/blob/main/packages/hub/src/lib/commit.ts
10
+ //https://github.com/huggingface/huggingface.js/blob/main/packages/hub/src/lib/delete-files.ts
11
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
12
+ const request = context.request;
13
+ const env = context.env as Env;
14
+
15
+ // 从 Authorization header 中获取 token
16
+ const authHeader = request.headers.get('Authorization');
17
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
18
+ return new Response(JSON.stringify({ error: '未提供有效的授权令牌' }), {
19
+ status: 401,
20
+ headers: { 'Content-Type': 'application/json' }
21
+ });
22
+ }
23
+ const hfToken = authHeader.replace('Bearer ', '');
24
+
25
+ console.log('Request URL:', request.url);
26
+ try {
27
+ const url = new URL(request.url);
28
+ const pathParts = url.pathname.split('/').filter(Boolean);
29
+
30
+ // 提取路径参数
31
+ const owner = pathParts[2] || url.searchParams.get('owner'); // 仓库所有者
32
+ const repo = pathParts[3] || url.searchParams.get('repo'); // 仓库名称
33
+ const operation = pathParts[4] || url.searchParams.get('op'); // 操作类型: raw, upload, tree, delete
34
+ const ref = pathParts[5] || url.searchParams.get('ref') || 'main';
35
+
36
+ if (!owner || !repo) {
37
+ return new Response(JSON.stringify({ error: '缺少仓库所有者或仓库名称' }), {
38
+ status: 400,
39
+ headers: { 'Content-Type': 'application/json' }
40
+ });
41
+ }
42
+ // Hugging Face API 基础 URL
43
+ const hfApiBaseUrl = 'https://huggingface.co/api/datasets';
44
+
45
+ // 处理 GET 请求 - 获取文件内容或列出文件
46
+
47
+
48
+ if (operation === 'raw' && request.method === 'GET') {
49
+ const path = pathParts.length > 6 ? pathParts.slice(6).join('/') : '';
50
+ //这里没有错..getraw就是没有api
51
+ const hfUrl = `https://huggingface.co/datasets/${owner}/${repo}/raw/${ref}/${path}`;
52
+
53
+ const response = await fetch(hfUrl, {
54
+ headers: {
55
+ 'Authorization': `Bearer ${hfToken}`,
56
+ 'User-Agent': 'Cloudflare-Worker'
57
+ }
58
+ });
59
+
60
+ return new Response(await response.text(), {
61
+ status: response.status,
62
+ headers: response.headers
63
+ });
64
+ }
65
+
66
+ if (operation === 'tree' && request.method === 'GET') {
67
+ const path = pathParts.length > 6 ? pathParts.slice(6).join('/') : '';
68
+ // 4. 列出文件
69
+ const hfUrl = `${hfApiBaseUrl}/${owner}/${repo}/tree/${ref}/${path}`;
70
+
71
+ const response = await fetch(hfUrl, {
72
+ headers: {
73
+ 'Authorization': `Bearer ${hfToken}`,
74
+ 'User-Agent': 'Cloudflare-Worker'
75
+ }
76
+ });
77
+
78
+ const data = await response.json();
79
+ return new Response(JSON.stringify(data), {
80
+ status: response.status,
81
+ headers: { 'Content-Type': 'application/json' }
82
+ });
83
+ }
84
+
85
+ // 处理 POST 请求 - 上传文件
86
+ if (operation === 'commit' && (request.method === 'POST' || request.method === 'PUT' || request.method === 'DELETE')) {
87
+ const hfUrl = `${hfApiBaseUrl}/${owner}/${repo}/commit/${ref}`;
88
+ const body = await request.json();
89
+ const response = await fetch(hfUrl, {
90
+ method: 'POST',
91
+ headers: {
92
+ 'Authorization': `Bearer ${hfToken}`,
93
+ 'Content-Type': 'application/json',
94
+ 'User-Agent': 'Cloudflare-Worker'
95
+ },
96
+ body: JSON.stringify(body)
97
+ });
98
+
99
+ const data = await response.json();
100
+ return new Response(JSON.stringify(data), {
101
+ status: response.status,
102
+ headers: { 'Content-Type': 'application/json' }
103
+ });
104
+ }
105
+
106
+
107
+ // 不支持的请求方法或路径
108
+ return new Response(JSON.stringify({ error: '不支持的请求方法或路径' }), {
109
+ status: 400,
110
+ headers: { 'Content-Type': 'application/json' }
111
+ });
112
+ } catch (error: any) {
113
+ console.error('Error:', error);
114
+ return new Response(JSON.stringify({ error: '服务器内部错误', details: error.message }), {
115
+ status: 500,
116
+ headers: { 'Content-Type': 'application/json' }
117
+ });
118
+ }
119
+ };
functions/api/huggingface.ts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { authMiddleware } from "../utils/auth.js";
3
+
4
+
5
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
6
+ const request = context.request;
7
+ const env = context.env as Env;
8
+
9
+ const authResponse = await authMiddleware(request, env);
10
+ if (authResponse) {
11
+ return authResponse;
12
+ }
13
+
14
+ try {
15
+
16
+ if (request.method === 'GET') {
17
+
18
+ }
19
+
20
+ if (request.method === 'POST') {
21
+
22
+ }
23
+
24
+ if (request.method === 'PUT') {
25
+
26
+ }
27
+
28
+ if (request.method === 'DELETE') {
29
+
30
+ }
31
+
32
+ // 不支持的请求方法
33
+ return new Response(JSON.stringify({ error: '不支持的请求方法' }), {
34
+ status: 405,
35
+ headers: { 'Content-Type': 'application/json' }
36
+ });
37
+
38
+
39
+ } catch (error) {
40
+ return new Response(JSON.stringify({ error: '服务器内部错误' }), {
41
+ status: 500,
42
+ headers: { 'Content-Type': 'application/json' }
43
+ });
44
+ }
45
+ };
functions/api/login.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { generateToken } from '../utils/jwt.js';
2
+
3
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
4
+ const request = context.request;
5
+ const env = context.env as Env;
6
+ try {
7
+ // 解析登录凭证
8
+ const credentials = await request.json() as LoginCredentials;
9
+ // 验证用户名和密码
10
+ if (credentials.username === env.USER_NAME && credentials.password === env.PASSWORD) {
11
+ // 生成JWT令牌
12
+ const token = await generateToken(credentials.username, env.JWT_SECRET);
13
+ return new Response(
14
+ JSON.stringify({
15
+ success: true,
16
+ token,
17
+ user: { username: credentials.username }
18
+ }),
19
+ {
20
+ status: 200,
21
+ headers: { 'Content-Type': 'application/json' }
22
+ }
23
+ );
24
+ }
25
+
26
+ // 登录失败: 无效的凭证
27
+ return new Response(
28
+ JSON.stringify({
29
+ success: false,
30
+ error: 'Invalid credentials'
31
+ }),
32
+ {
33
+ status: 401,
34
+ headers: { 'Content-Type': 'application/json' }
35
+ }
36
+ );
37
+ } catch (error) {
38
+ // 登录处理失败
39
+ console.error(`登录处理失败:`, error);
40
+ throw error;
41
+ }
42
+ }
functions/api/setting.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { authMiddleware } from "../utils/auth.js";
3
+
4
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
5
+ const request = context.request;
6
+ const env = context.env as Env;
7
+
8
+ const authResponse = await authMiddleware(request, env);
9
+ if (authResponse) {
10
+ return authResponse;
11
+ }
12
+
13
+ const KV_KEY = "settings"
14
+
15
+ try {
16
+ // GET 请求处理
17
+ if (request.method === 'GET') {
18
+ const settings = await env.KV.get(KV_KEY);
19
+ return new Response(settings || '{}', {
20
+ status: 200,
21
+ headers: { 'Content-Type': 'application/json' }
22
+ });
23
+ }
24
+
25
+ // POST 请求处理
26
+ if (request.method === 'POST') {
27
+ const data = await request.json();
28
+ // 存储账号数据
29
+ await env.KV.put(KV_KEY, JSON.stringify(data));
30
+
31
+ return new Response(JSON.stringify({ message: '保存成功' }), {
32
+ status: 200,
33
+ headers: { 'Content-Type': 'application/json' }
34
+ });
35
+ }
36
+
37
+ // 不支持的请求方法
38
+ return new Response(JSON.stringify({ error: '不支持的请求方法' }), {
39
+ status: 405,
40
+ headers: { 'Content-Type': 'application/json' }
41
+ });
42
+
43
+ } catch (error) {
44
+ return new Response(JSON.stringify({ error: '服务器内部错误' }), {
45
+ status: 500,
46
+ headers: { 'Content-Type': 'application/json' }
47
+ });
48
+ }
49
+ };
functions/types.d.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ interface KVNamespace {
3
+ put: (key: string, value: string, options?: { expiration?: number, expirationTtl?: number, metadata?: object }) => Promise<void>;
4
+ get: (key: string) => Promise<string | null>;
5
+ delete: (key: string) => Promise<void>;
6
+ }
7
+
8
+ interface Env {
9
+ SEND_PASSWORD:string;
10
+ API_TOKEN: string; // API 访问令牌
11
+ JWT_SECRET: string; // JWT 密钥
12
+ USER_NAME: string; // 用户名
13
+ KV: KVNamespace;
14
+ ASSETS:any;
15
+ }
16
+
17
+ /**
18
+ * 登录凭证接口
19
+ */
20
+ interface LoginCredentials {
21
+ /** 用户名 */
22
+ username: string;
23
+ /** 密码 */
24
+ password: string;
25
+ }
26
+
27
+
28
+ interface RouteContext {
29
+ request: Request;
30
+ functionPath: string;
31
+ waitUntil: (promise: Promise<any>) => void;
32
+ passThroughOnException: () => void;
33
+ next: (input?: Request | string, init?: RequestInit) => Promise<Response>;
34
+ env: Env;
35
+ params: any;
36
+ data: any;
37
+ }
functions/utils/auth.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { verifyToken } from './jwt.js';
2
+
3
+ /**
4
+ * 认证中间件
5
+ * @param request 请求对象
6
+ * @param env 环境变量
7
+ * @param requestId 请求ID
8
+ * @returns 如果认证失败返回错误响应,否则返回 null
9
+ */
10
+ export async function authMiddleware(request: Request, env: Env): Promise<Response | null> {
11
+ const isValid = await verifyToken(request, env.JWT_SECRET);
12
+ if (!isValid) {
13
+ return new Response(
14
+ JSON.stringify({ error: 'Unauthorized' }),
15
+ {
16
+ status: 401,
17
+ headers: { 'Content-Type': 'application/json' }
18
+ }
19
+ );
20
+ }
21
+
22
+ return null;
23
+ }
24
+
25
+ export async function authApiToken(request: Request, env: Env): Promise<Response | null> {
26
+ // 验证API令牌
27
+ const authHeader = request.headers.get('Authorization');
28
+ if (authHeader !== `Bearer ${env.API_TOKEN}`) {
29
+ return new Response(
30
+ JSON.stringify({ error: 'Unauthorized' }),
31
+ {
32
+ status: 401,
33
+ headers: { 'Content-Type': 'application/json' }
34
+ }
35
+ );
36
+ }
37
+
38
+ return null;
39
+ }
40
+
41
+
functions/utils/jwt.ts ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 生成 JWT token
3
+ * @param username 用户名
4
+ * @param secret 密钥
5
+ * @returns 生成的 token 字符串
6
+ */
7
+ export async function generateToken(username: string, secret: string): Promise<string> {
8
+ // JWT 头部信息
9
+ const header = { alg: 'HS256', typ: 'JWT' };
10
+ // JWT 载荷信息
11
+ const payload = {
12
+ sub: username,
13
+ exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60), //30天后过期
14
+ iat: Math.floor(Date.now() / 1000) // 签发时间
15
+ };
16
+
17
+ const encodedHeader = btoa(JSON.stringify(header));
18
+ const encodedPayload = btoa(JSON.stringify(payload));
19
+ const signature = await createHmacSignature(
20
+ `${encodedHeader}.${encodedPayload}`,
21
+ secret
22
+ );
23
+
24
+ return `${encodedHeader}.${encodedPayload}.${signature}`;
25
+ }
26
+
27
+ /**
28
+ * 验证 JWT token
29
+ * @param request 请求对象
30
+ * @param secret 密钥
31
+ * @returns 验证是否通过
32
+ */
33
+ export async function verifyToken(request: Request, secret: string): Promise<boolean> {
34
+ const authHeader = request.headers.get('Authorization');
35
+ if (!authHeader?.startsWith('Bearer ')) {
36
+ return false;
37
+ }
38
+
39
+ const token = authHeader.split(' ')[1];
40
+ try {
41
+ const [headerB64, payloadB64, signatureB64] = token.split('.');
42
+ const expectedSignature = await createHmacSignature(
43
+ `${headerB64}.${payloadB64}`,
44
+ secret
45
+ );
46
+
47
+ if (signatureB64 !== expectedSignature) {
48
+ return false;
49
+ }
50
+
51
+ const payload = JSON.parse(atob(payloadB64));
52
+ const now = Math.floor(Date.now() / 1000);
53
+
54
+ return payload.exp > now;
55
+ } catch (error) {
56
+ console.error('Token verification failed:', error);
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 创建 HMAC 签名
63
+ * @param message 需要签名的消息
64
+ * @param secret 密钥
65
+ * @returns 签名字符串
66
+ */
67
+ async function createHmacSignature(message: string, secret: string): Promise<string> {
68
+ const encoder = new TextEncoder();
69
+ const keyData = encoder.encode(secret);
70
+ const messageData = encoder.encode(message);
71
+
72
+ const cryptoKey = await crypto.subtle.importKey(
73
+ 'raw',
74
+ keyData,
75
+ { name: 'HMAC', hash: 'SHA-256' },
76
+ false,
77
+ ['sign']
78
+ );
79
+
80
+ const signature = await crypto.subtle.sign(
81
+ 'HMAC',
82
+ cryptoKey,
83
+ messageData
84
+ );
85
+
86
+ return btoa(String.fromCharCode(...new Uint8Array(signature)));
87
+ }
index.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>微软邮箱管理平台</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
index.ts ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Context, Hono } from 'hono'
2
+ import * as dotenv from 'dotenv'
3
+ import { cors } from "hono/cors";
4
+ import { compress } from "hono/compress";
5
+ import { prettyJSON } from "hono/pretty-json";
6
+ import { trimTrailingSlash } from "hono/trailing-slash";
7
+ import { serve } from '@hono/node-server'
8
+ import { createStorage } from "unstorage";
9
+ import cloudflareKVHTTPDriver from "unstorage/drivers/cloudflare-kv-http";
10
+ import { serveStatic } from '@hono/node-server/serve-static'
11
+
12
+ import path from 'path'
13
+ import { fileURLToPath } from 'url'
14
+ import { dirname } from 'path'
15
+
16
+ // 导入所有路由处理函数
17
+ import { onRequest as handleAccount } from './functions/api/account.js'
18
+ import { onRequest as handleLogin } from './functions/api/login.js'
19
+ import { onRequest as handleSetting } from './functions/api/setting.js'
20
+
21
+
22
+ dotenv.config({ path: ['.env', '.env.local'], override: true });
23
+ const isDev = process.env.NODE_ENV === 'development'
24
+
25
+ const app = new Hono<{ Bindings: Env }>()
26
+ app.use(compress());
27
+ app.use(prettyJSON());
28
+ app.use(trimTrailingSlash());
29
+ app.use('*', cors({
30
+ origin: '*',
31
+ allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
32
+ allowHeaders: ['Content-Type', 'Authorization'],
33
+ exposeHeaders: ['Content-Length'],
34
+ credentials: true,
35
+ }));
36
+
37
+
38
+ const storage = createStorage({
39
+ driver: cloudflareKVHTTPDriver({
40
+ accountId: process.env.CLOUDFLARE_ACCOUNT_ID || "",
41
+ namespaceId: process.env.CLOUDFLARE_NAMESPACE_ID || "",
42
+ apiToken: process.env.CLOUDFLARE_API_TOKEN || "",
43
+ }),
44
+ });
45
+
46
+ var kv: KVNamespace = {
47
+ get: async (key: string) => {
48
+ const value = await storage.getItemRaw(key);
49
+ return value as string;
50
+ },
51
+ put: async (key: string, value: string) => {
52
+ await storage.setItem(key, value);
53
+ },
54
+ delete:async(key:string)=>{
55
+ await storage.removeItem(key);
56
+ }
57
+ };
58
+
59
+ app.use('*', async (c, next) => {
60
+ c.env.KV = kv;
61
+ await next()
62
+ })
63
+
64
+
65
+ const scriptPath = fileURLToPath(import.meta.url)
66
+ const scriptDir = dirname(scriptPath)
67
+ const rootDir = isDev ? dirname(scriptPath) : dirname(scriptDir)
68
+ const currentDir = process.cwd();
69
+ let staticPath = path.relative(currentDir, rootDir);
70
+ if (!isDev) {
71
+ staticPath = path.relative(currentDir, path.join(rootDir, "dist"))
72
+ }
73
+ console.log('Script dir:', scriptDir)
74
+ console.log('Root dir:', rootDir)
75
+ console.log('Current dir:', currentDir);
76
+ console.log('Relative path for static files:', staticPath || '.');
77
+
78
+
79
+
80
+ const createContext = (c: Context) => {
81
+ const eventContext: RouteContext = {
82
+ request: c.req.raw,
83
+ functionPath: c.req.path,
84
+ waitUntil: (promise: Promise<any>) => {
85
+ if (c.executionCtx?.waitUntil) {
86
+ c.executionCtx.waitUntil(promise);
87
+ }
88
+ },
89
+ passThroughOnException: () => {
90
+ if (c.executionCtx?.passThroughOnException) {
91
+ c.executionCtx.passThroughOnException();
92
+ }
93
+ },
94
+ next: async (input?: Request | string, init?: RequestInit) => {
95
+ if (typeof input === 'string') {
96
+ return fetch(input, init);
97
+ } else if (input instanceof Request) {
98
+ return fetch(input);
99
+ }
100
+ return new Response('Not Found', { status: 404 });
101
+ },
102
+ env: {
103
+ ...c.env,
104
+ ASSETS: {
105
+ fetch: fetch.bind(globalThis)
106
+ }
107
+ },
108
+ params: c.req.param(),
109
+ // 可以从 c.get() 获取数据,或者传入空对象
110
+ data: c.get('data') || {}
111
+ };
112
+ return eventContext;
113
+ }
114
+
115
+ app.all('/api/*', async (c) => {
116
+ try {
117
+ const context = createContext(c);
118
+ const path = c.req.path;
119
+ // 根据路径匹配对应的处理函数
120
+ let response: Response;
121
+ switch (path) {
122
+ case '/api/account':
123
+ response = await handleAccount(context);
124
+ break;
125
+ case '/api/login':
126
+ response = await handleLogin(context);
127
+ break;
128
+ case '/api/setting':
129
+ response = await handleSetting(context);
130
+ break;
131
+ default:
132
+ return c.json({ error: 'Route not found' }, 404);
133
+ }
134
+ return response;
135
+ } catch (error) {
136
+ return c.json({ error: (error as Error).message }, 500);
137
+ }
138
+ })
139
+
140
+
141
+ // 中间件配置
142
+ app.get('/*', serveStatic({
143
+ root: staticPath,
144
+ rewriteRequestPath: (path) => {
145
+ return path === '/' ? '/index.html' : path;
146
+ },
147
+ onFound: async (path, c) => {
148
+ console.log('Found:', path)
149
+ },
150
+ onNotFound: async (path, c) => {
151
+ console.log('Not Found:', path)
152
+ }
153
+ }))
154
+
155
+
156
+ // 启动服务器
157
+ const port = parseInt(process.env.PORT || '8788')
158
+ serve({
159
+ fetch: (request: Request, env) => app.fetch(request, { ...env, ...process.env }),
160
+ port
161
+ }, () => {
162
+ console.log(`Server running at http://localhost:${port}`)
163
+ })
164
+
165
+ export default app
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "msmail",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite --host --port 5009",
8
+ "dev:server": "cross-env NODE_ENV=development tsx watch --no-cache index.ts",
9
+ "dev:pages": "wrangler pages dev dist",
10
+ "build": "vue-tsc -b && vite build",
11
+ "build:server": "npm run build && tsc -p ./tsconfig.functions.json",
12
+ "preview": "npm run build && wrangler pages dev ./dist",
13
+ "deploy": "npm run build && wrangler pages deploy ./dist",
14
+ "test": "vitest",
15
+ "cf-typegen": "wrangler types"
16
+ },
17
+ "dependencies": {
18
+ "@hono/node-server": "^1.13.8",
19
+ "@monaco-editor/loader": "^1.5.0",
20
+ "@tailwindcss/vite": "^4.0.14",
21
+ "dotenv": "^16.4.7",
22
+ "hono": "^4.7.4",
23
+ "monaco-editor": "^0.52.2",
24
+ "pinia": "^3.0.1",
25
+ "tailwindcss": "^4.0.14",
26
+ "tdesign-vue-next": "^1.11.4",
27
+ "unstorage": "^1.15.0",
28
+ "vue": "^3.5.13",
29
+ "vue-router": "^4.5.0"
30
+ },
31
+ "devDependencies": {
32
+ "@cloudflare/vitest-pool-workers": "^0.7.5",
33
+ "@cloudflare/workers-types": "^4.20250313.0",
34
+ "@types/node": "^22.10.2",
35
+ "@vitejs/plugin-vue": "^5.2.1",
36
+ "@vue/tsconfig": "^0.7.0",
37
+ "concurrently": "^8.2.2",
38
+ "cross-env": "^7.0.3",
39
+ "tsx": "^4.7.1",
40
+ "typescript": "^5.5.2",
41
+ "unplugin-auto-import": "^19.0.0",
42
+ "unplugin-vue-components": "^28.0.0",
43
+ "vite": "^6.2.0",
44
+ "vitest": "~3.0.7",
45
+ "vue-tsc": "^2.2.4",
46
+ "wrangler": "^4.0.0"
47
+ }
48
+ }
public/vite.svg ADDED
src/App.vue ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { useRoute, useRouter } from 'vue-router';
3
+ import { ref } from 'vue';
4
+ import logo from './assets/logo.png'
5
+ const router = useRouter();
6
+ const route = useRoute();
7
+ const menuVisible = ref(false);
8
+
9
+ const menu = [
10
+ {
11
+ name: '仓库',
12
+ path: '/repo',
13
+ icon: 'system-log'
14
+ },
15
+ {
16
+ name: '账号',
17
+ path: '/account',
18
+ icon: 'user-list'
19
+ },
20
+ {
21
+ name: '设置',
22
+ path: '/setting',
23
+ icon: 'setting-1'
24
+ }
25
+ ];
26
+
27
+ const logout = () => {
28
+ window.localStorage.removeItem('isAuthenticated');
29
+ router.push('/login');
30
+ };
31
+
32
+ const toggleMenu = () => {
33
+ menuVisible.value = !menuVisible.value;
34
+ };
35
+
36
+
37
+ //解析token
38
+ const parseToken = () => {
39
+ try {
40
+ const token = localStorage.getItem('token') as string;
41
+ const [, payload] = token.split('.');
42
+ const data = JSON.parse(atob(payload));
43
+ return data;
44
+ } catch (error) {
45
+ return null;
46
+ }
47
+ };
48
+ const jwtToken = parseToken();
49
+
50
+ </script>
51
+
52
+ <template>
53
+ <template v-if="route.path === '/login'">
54
+ <router-view />
55
+ </template>
56
+ <t-layout v-else class="h-screen">
57
+ <!-- 移动端抽屉菜单 -->
58
+ <t-drawer v-model:visible="menuVisible" placement="left" size="232" :footer="false" :header="false"
59
+ :close-on-overlay-click="true" class="lg:hidden">
60
+ <t-menu :value="route.path" theme="dark" class="h-full bg-transparent ">
61
+ <template #logo>
62
+ <router-link to="/" class="flex items-center gap-2 p-4">
63
+ <img :src="logo" alt="logo" class="w-8 h-8" />
64
+ <h1 class="text-xl font-bold text-white">SEED LOG</h1>
65
+ </router-link>
66
+ </template>
67
+ <t-menu-item v-for="item in menu" :key="item.path" :value="item.path" :to="item.path"
68
+ @click="menuVisible = false" class="menu-item mx-4 my-2 rounded-xl">
69
+ <template #icon>
70
+ <t-icon :name="item.icon" />
71
+ </template>
72
+ {{ item.name }}
73
+ </t-menu-item>
74
+ </t-menu>
75
+ </t-drawer>
76
+
77
+ <!-- 桌面端侧边栏 -->
78
+ <t-aside class="sidebar backdrop-blur-lg hidden lg:block">
79
+ <t-menu :value="route.path" theme="dark" class="bg-transparent">
80
+ <template #logo>
81
+ <router-link to="/" class="flex items-center gap-2">
82
+ <img :src="logo" alt="logo" class="w-10 h-10 transition-transform hover:scale-110" />
83
+ <h1 class="text-2xl font-bold text-white tracking-wider">SEED LOG</h1>
84
+ </router-link>
85
+ </template>
86
+ <t-menu-item v-for="item in menu" :key="item.path" :value="item.path" :to="item.path"
87
+ class="menu-item mx-4 my-2 rounded-xl">
88
+ <template #icon>
89
+ <t-icon :name="item.icon" class="text-xl" />
90
+ </template>
91
+ <span class="font-medium">{{ item.name }}</span>
92
+ </t-menu-item>
93
+ </t-menu>
94
+ </t-aside>
95
+
96
+
97
+ <t-layout class="w-full">
98
+ <t-header class="header backdrop-blur-xl border-b border-gray-100">
99
+ <div class="flex items-center justify-between h-full px-2 sm:px-4 lg:px-8">
100
+ <!-- 移动端菜单按钮和标题 -->
101
+ <div class="flex items-center gap-2 sm:gap-4 ">
102
+ <div class="lg:hidden">
103
+ <t-button theme="default" variant="text" class=" min-w-[40px]" @click="toggleMenu">
104
+ <t-icon name="menu" size="20px" sm:size="24px" />
105
+ </t-button>
106
+ </div>
107
+ <h1
108
+ class="text-base sm:text-xl lg:text-2xl font-bold bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-transparent truncate max-w-[150px] sm:max-w-full">
109
+ {{ jwtToken?.sub }}
110
+ </h1>
111
+ </div>
112
+ <div class="flex items-center gap-2 sm:gap-4">
113
+ <t-button theme="danger" @click="logout" class="logout-btn text-sm sm:text-base py-1 px-2 sm:px-4">
114
+ <template #icon>
115
+ <t-icon name="logout" class="text-sm sm:text-base" />
116
+ </template>
117
+ <span>退出</span>
118
+ </t-button>
119
+ </div>
120
+ </div>
121
+ </t-header>
122
+
123
+ <!-- 内容区域 -->
124
+ <t-content class="content bg-gray-50/30 flex-1 overflow-y-auto w-full">
125
+ <router-view v-slot="{ Component }">
126
+ <transition name="fade-slide" mode="out-in">
127
+ <keep-alive>
128
+ <component :is="Component" />
129
+ </keep-alive>
130
+ </transition>
131
+ </router-view>
132
+ </t-content>
133
+
134
+ <!-- 页脚 -->
135
+ <t-footer class="footer backdrop-blur-sm py-4 text-center text-sm text-gray-600">
136
+ <span class="opacity-75">© 微软邮箱管理系统</span>
137
+ </t-footer>
138
+ </t-layout>
139
+
140
+ </t-layout>
141
+ </template>
142
+
143
+ <style scoped>
144
+ @reference "./assets/base.css";
145
+
146
+ .sidebar {
147
+ @apply bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-800;
148
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
149
+ }
150
+
151
+ .header {
152
+ @apply bg-white/80 sticky top-0 z-10;
153
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
154
+ }
155
+
156
+ .menu-item {
157
+ @apply transition-all duration-300 hover:bg-white/15 active:scale-95;
158
+ @apply flex items-center gap-3 px-4 py-3;
159
+ }
160
+
161
+ .logout-btn {
162
+ @apply transition-transform hover:scale-105 active:scale-95;
163
+ }
164
+
165
+ .fade-slide-enter-active,
166
+ .fade-slide-leave-active {
167
+ transition: all 0.3s ease;
168
+ }
169
+
170
+ .fade-slide-enter-from {
171
+ opacity: 0;
172
+ transform: translateY(20px);
173
+ }
174
+
175
+ .fade-slide-leave-to {
176
+ opacity: 0;
177
+ transform: translateY(-20px);
178
+ }
179
+
180
+ /* :deep(.t-default-menu.t-menu--dark){
181
+ @apply bg-transparent;
182
+ } */
183
+
184
+ :deep(.t-drawer__body) {
185
+ @apply p-0;
186
+ }
187
+
188
+ :deep(.t-drawer__body) {
189
+ @apply p-0;
190
+ }
191
+
192
+ :deep(::-webkit-scrollbar) {
193
+ @apply w-2;
194
+ }
195
+
196
+ :deep(::-webkit-scrollbar-thumb) {
197
+ @apply bg-gray-400/30 rounded-full transition-colors hover:bg-gray-400/50;
198
+ }
199
+
200
+ :deep(::-webkit-scrollbar-track) {
201
+ @apply bg-transparent;
202
+ }
203
+
204
+ .content {
205
+ background-image: radial-gradient(circle at 50% 50%,
206
+ rgba(255, 255, 255, 0.8) 0%,
207
+ rgba(240, 240, 250, 0.6) 100%);
208
+ }
209
+
210
+ .footer {
211
+ @apply bg-white/60;
212
+ }
213
+
214
+ /* 添加移动端响应式样式 */
215
+ @media (max-width: 1024px) {
216
+ .sidebar {
217
+ display: none;
218
+ }
219
+ }
220
+
221
+ .drawer-menu {
222
+ @apply bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-800;
223
+ }
224
+ </style>
src/assets/base.css ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ @import "tailwindcss";
2
+ @import "tailwindcss/preflight" layer(base);
3
+ @import "tailwindcss/utilities" layer(utilities);
src/assets/logo.png ADDED
src/assets/main.css ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ @import './base.css';
2
+
src/assets/vue.svg ADDED
src/components/MonacoEditor.vue ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div ref="editorContainer" class="h-full"></div>
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { ref, onMounted, watch, onBeforeUnmount } from 'vue';
7
+ import monaco from '@monaco-editor/loader';
8
+
9
+ const props = defineProps<{
10
+ value: string,
11
+ originalValue?: string,
12
+ language?: string,
13
+ options?: Record<string, any>,
14
+ }>();
15
+ const emit = defineEmits(['update:value']);
16
+
17
+ const editorContainer = ref<HTMLElement>();
18
+ let editorInstance: any = null;
19
+ let monacoInstance: any = null;
20
+ let editorModel: any = null;
21
+
22
+ // 创建编辑器的函数
23
+ const createEditor = () => {
24
+ if (!editorContainer.value || !monacoInstance) return;
25
+
26
+ // 如果已存在编辑器实例,先销毁
27
+ if (editorInstance) {
28
+ editorInstance.dispose();
29
+ editorInstance = null;
30
+ }
31
+
32
+ const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
33
+ const commonOptions = {
34
+ language: props.language || 'json',
35
+ minimap: { enabled: !isMobile },
36
+ fontSize: isMobile ? 16 : 14,
37
+ lineNumbers: isMobile ? 'off' as const : 'on' as const,
38
+ scrollBeyondLastLine: false,
39
+ automaticLayout: true,
40
+ ...props.options
41
+ };
42
+
43
+ // 每次都创建新模型,确保内容刷新
44
+ if (editorModel) {
45
+ editorModel.dispose();
46
+ }
47
+ editorModel = monacoInstance.editor.createModel(props.value, props.language);
48
+
49
+ if (props.originalValue !== undefined) {
50
+ // 创建差异编辑器
51
+ const originalModel = monacoInstance.editor.createModel(props.originalValue, props.language);
52
+ editorInstance = monacoInstance.editor.createDiffEditor(editorContainer.value, {
53
+ ...commonOptions,
54
+ readOnly: false,
55
+ renderSideBySide: true,
56
+ ignoreTrimWhitespace: false,
57
+ renderOverviewRuler: true,
58
+ diffWordWrap: 'on',
59
+ });
60
+
61
+ editorInstance.setModel({
62
+ original: originalModel,
63
+ modified: editorModel
64
+ });
65
+ const modifiedEditor = editorInstance.getModifiedEditor();
66
+ modifiedEditor.onDidChangeModelContent(() => {
67
+ emit('update:value', modifiedEditor.getValue());
68
+ });
69
+ } else {
70
+ // 创建普通编辑器
71
+ editorInstance = monacoInstance.editor.create(editorContainer.value, {
72
+ ...commonOptions,
73
+ model: editorModel
74
+ });
75
+ editorInstance.onDidChangeModelContent(() => {
76
+ emit('update:value', editorInstance.getValue());
77
+ });
78
+ }
79
+ };
80
+
81
+ onMounted(() => {
82
+ monaco.init().then(instance => {
83
+ monacoInstance = instance;
84
+ createEditor();
85
+ });
86
+ });
87
+
88
+ // 监听 value, originalValue 和 language 的变化
89
+ watch([() => props.value, () => props.originalValue, () => props.language], ([newVal, newOriginalVal, newLanguage], [oldVal, oldOriginalVal]) => {
90
+ // 判断是否需要重建编辑器
91
+ const modeChanged =
92
+ (oldOriginalVal === undefined && newOriginalVal !== undefined) ||
93
+ (oldOriginalVal !== undefined && newOriginalVal === undefined);
94
+
95
+ if (modeChanged || oldVal === undefined) {
96
+ // 编辑器模式变化或首次加载,重新创建
97
+ createEditor();
98
+ } else if (props.language && editorModel && editorModel.getLanguageId() !== props.language) {
99
+ // 语言改变时,更新模型的语言
100
+ monacoInstance.editor.setModelLanguage(editorModel, props.language);
101
+ } else if (editorInstance) {
102
+ if (props.originalValue !== undefined) {
103
+ // 差异模式下更新内容
104
+ const model = editorInstance.getModel();
105
+ if (model && model.original && model.original.getValue() !== newOriginalVal) {
106
+ model.original.setValue(newOriginalVal || '');
107
+ }
108
+ if (model && model.modified && model.modified.getValue() !== newVal) {
109
+ model.modified.setValue(newVal || '');
110
+ }
111
+ } else {
112
+ // 普通模式下更新内容
113
+ if (editorInstance.getValue() !== newVal) {
114
+ editorInstance.setValue(newVal || '');
115
+ }
116
+ }
117
+ }
118
+ }, { deep: true });
119
+
120
+ onBeforeUnmount(() => {
121
+ if (editorInstance) {
122
+ editorInstance.dispose();
123
+ }
124
+ if (editorModel) {
125
+ editorModel.dispose();
126
+ }
127
+ editorInstance = null;
128
+ editorModel = null;
129
+ });
130
+ </script>
131
+
132
+ <style scoped>
133
+ :deep(.monaco-diff-editor),
134
+ :deep(.monaco-editor) {
135
+ height: 100%;
136
+ }
137
+ </style>
src/components/RepoHeader.vue ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { FolderIcon } from 'tdesign-icons-vue-next';
4
+ import type { Account } from '../services/accountApi';
5
+
6
+ const props = defineProps<{
7
+ selectedAccount: number | '';
8
+ currentPath: string;
9
+ accounts: Account[];
10
+ isNewFile?: boolean;
11
+ }>();
12
+
13
+ const emit = defineEmits<{
14
+ (e: 'pathClick', path: string): void;
15
+ (e: 'rootClick'): void;
16
+ (e: 'update:selectedAccount', value: number): void;
17
+ }>();
18
+
19
+ const pathBreadcrumbs = computed(() => {
20
+ const { currentPath, isNewFile } = props;
21
+ if (isNewFile) {
22
+ // For new files, show "新建文件" at the end of the breadcrumb
23
+ const paths = currentPath ? currentPath.split('/').filter(Boolean) : [];
24
+ const breadcrumbs = paths.map((path: string, index: number) => ({
25
+ text: path,
26
+ path: paths.slice(0, index + 1).join('/')
27
+ }));
28
+
29
+ return breadcrumbs;
30
+ }
31
+
32
+ // Regular file browsing
33
+ if (!currentPath || typeof currentPath !== 'string') return [];
34
+ const paths = currentPath.split('/').filter(Boolean);
35
+ if (paths.length === 0) return [];
36
+ return paths.map((path: string, index: number) => ({
37
+ text: path,
38
+ path: paths.slice(0, index + 1).join('/')
39
+ }));
40
+ });
41
+ </script>
42
+
43
+ <template>
44
+ <div class="header-section bg-white p-4 rounded-lg shadow-sm">
45
+ <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
46
+ <div class="flex flex-col md:flex-row md:items-center gap-3">
47
+ <t-select :value="selectedAccount" class="w-full md:w-64" placeholder="选择仓库"
48
+ @update:value="(val: number) => emit('update:selectedAccount', val)">
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 /> 根目录
56
+ </span>
57
+ </t-breadcrumb-item>
58
+ <t-breadcrumb-item v-for="item in pathBreadcrumbs" :key="item.path" @click="emit('pathClick', item.path)"
59
+ class="cursor-pointer hover:text-blue-500">
60
+ {{ item.text }}
61
+ </t-breadcrumb-item>
62
+ </t-breadcrumb>
63
+ </div>
64
+ <slot></slot>
65
+ </div>
66
+ </div>
67
+ </template>
src/main.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 引入组件库的少量全局样式变量
2
+ import 'tdesign-vue-next/es/style/index.css';
3
+ import './assets/main.css'
4
+
5
+ import { createApp } from 'vue'
6
+ import { createPinia } from 'pinia'
7
+
8
+ import App from './App.vue'
9
+ import router from './router'
10
+
11
+ const app = createApp(App)
12
+
13
+ app.use(createPinia())
14
+ app.use(router)
15
+
16
+ app.mount('#app')
src/router/index.ts ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createRouter, createWebHistory } from 'vue-router'
2
+
3
+
4
+ const router = createRouter({
5
+ history: createWebHistory(import.meta.env.BASE_URL),
6
+ routes: [
7
+ {
8
+ path: '/',
9
+ redirect: '/repo',
10
+ meta: { requiresAuth: true }
11
+ },
12
+ {
13
+ path: '/login',
14
+ name: 'Login',
15
+ component: () => import('../views/LoginView.vue'),
16
+ meta: { requiresAuth: false }
17
+ },
18
+ {
19
+ path: '/repo',
20
+ name: 'Repo',
21
+ component: () => import('../views/RepoView.vue'),
22
+ meta: { requiresAuth: true }
23
+ },
24
+ {
25
+ path: '/content',
26
+ name: 'Content',
27
+ component: () => import('../views/ContentView.vue'),
28
+ meta: { requiresAuth: true }
29
+ },
30
+ {
31
+ path: '/setting',
32
+ name: 'Setting',
33
+ component: () => import('../views/SettingView.vue'),
34
+ meta: { requiresAuth: true }
35
+ },
36
+ {
37
+ path: '/account',
38
+ name: 'Account',
39
+ component: () => import('../views/AccountView.vue'),
40
+ meta: { requiresAuth: true }
41
+ },
42
+ ],
43
+ })
44
+ // 添加路由守卫
45
+ router.beforeEach((to, from, next) => {
46
+ const isAuthenticated = localStorage.getItem('isAuthenticated') === 'true'
47
+ if (to.meta.requiresAuth && !isAuthenticated) {
48
+ next('/login')
49
+ } else if (to.path === '/login' && isAuthenticated) {
50
+ next('/')
51
+ } else {
52
+ next()
53
+ }
54
+ })
55
+
56
+ export default router
src/services/accountApi.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { API_BASE_URL, getHeaders, handleResponse } from './util';
2
+
3
+ export interface Account {
4
+ id: number,
5
+ owner: string,
6
+ repo: string,
7
+ ref: string,
8
+ type: "github" | "hf",
9
+ token: string,
10
+ }
11
+
12
+ export const accountApi = {
13
+ async post(accounts: Account[]) {
14
+ const response = await fetch(
15
+ `${API_BASE_URL}/api/account`,
16
+ {
17
+ headers: getHeaders(),
18
+ method: 'POST',
19
+ body: JSON.stringify(accounts)
20
+ }
21
+ );
22
+ return handleResponse(response);
23
+ },
24
+
25
+ async get(): Promise<Account[]> {
26
+ const response = await fetch(
27
+ `${API_BASE_URL}/api/account`,
28
+ {
29
+ headers: getHeaders()
30
+ }
31
+ );
32
+ return handleResponse(response);
33
+ },
34
+
35
+ }
src/services/repoApi.ts ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import router from '../router';
2
+ import { API_BASE_URL, getHeaders, handleResponse } from './util';
3
+
4
+ export interface Account {
5
+ owner: string;
6
+ repo: string;
7
+ ref: string;
8
+ type: "github" | "hf";
9
+ token: string;
10
+ }
11
+
12
+ export interface RepoContent {
13
+ name: string;
14
+ path: string;
15
+ sha: string;
16
+ size: number;
17
+ url: string;
18
+ html_url: string;
19
+ git_url: string;
20
+ download_url: string | null;
21
+ type: "file" | "dir";
22
+ content?: string;
23
+ encoding?: string;
24
+ }
25
+
26
+ // Base interface for repo operations
27
+ interface IRepoApi {
28
+ getContents(account: Account, path: string): Promise<RepoContent[]>;
29
+ getFileContent(account: Account, path: string): Promise<RepoContent>;
30
+ updateFile(account: Account, path: string, content: string, sha: string, message?: string): Promise<any>;
31
+ deleteFile(account: Account, path: string, sha: string, message?: string): Promise<any>;
32
+ createFile(account: Account, path: string, content: string, message?: string): Promise<any>;
33
+ }
34
+
35
+ // GitHub implementation
36
+ class GitHubRepoApi implements IRepoApi {
37
+ async getContents(account: Account, path: string = ''): Promise<RepoContent[]> {
38
+ const response = await fetch(
39
+ `${API_BASE_URL}/api/github/${account.owner}/${account.repo}/${path}?ref=${account.ref}`,
40
+ {
41
+ headers: {
42
+ Authorization: `Bearer ${account.token}`
43
+ }
44
+ }
45
+ );
46
+ return await handleResponse(response);
47
+ }
48
+
49
+ async getFileContent(account: Account, path: string): Promise<RepoContent> {
50
+ const response = await fetch(
51
+ `${API_BASE_URL}/api/github/${account.owner}/${account.repo}/${path}?ref=${account.ref}`,
52
+ {
53
+ headers: {
54
+ 'Content-Type': 'application/json',
55
+ Authorization: `Bearer ${account.token}`
56
+ }
57
+ }
58
+ );
59
+ return await handleResponse(response);
60
+ }
61
+
62
+ async updateFile(account: Account, path: string, content: string, sha: string, message?: string) {
63
+
64
+ const encoder = new TextEncoder();
65
+ const bytes = encoder.encode(content);
66
+ const base64Content = btoa(String.fromCharCode.apply(null, [...new Uint8Array(bytes)]));
67
+
68
+ const response = await fetch(
69
+ `${API_BASE_URL}/api/github/${account.owner}/${account.repo}/${path}`,
70
+ {
71
+ method: 'PUT',
72
+ headers: {
73
+ 'Content-Type': 'application/json',
74
+ Authorization: `Bearer ${account.token}`
75
+ },
76
+ body: JSON.stringify({ branch: account.ref, content: base64Content, message, sha })
77
+ }
78
+ );
79
+ return handleResponse(response);
80
+ }
81
+
82
+ async deleteFile(account: Account, path: string, sha: string, message?: string) {
83
+ const response = await fetch(
84
+ `${API_BASE_URL}/api/github/${account.owner}/${account.repo}/${path}`,
85
+ {
86
+ method: 'DELETE',
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ Authorization: `Bearer ${account.token}`
90
+ },
91
+ body: JSON.stringify({ branch: account.ref, sha, message })
92
+ }
93
+ );
94
+ return handleResponse(response);
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
+ {
104
+ method: 'POST',
105
+ headers: {
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);
113
+ }
114
+ }
115
+
116
+ // HuggingFace implementation
117
+ class HuggingFaceRepoApi implements IRepoApi {
118
+
119
+ async getContents(account: Account, path: string = ''): Promise<RepoContent[]> {
120
+ const response = await fetch(
121
+ `${API_BASE_URL}/api/hf/${account.owner}/${account.repo}/tree/${account.ref}/${path}`,
122
+ {
123
+ method: 'GET',
124
+ headers: {
125
+ Authorization: `Bearer ${account.token}`
126
+ }
127
+ }
128
+ );
129
+ let result = await handleResponse(response);
130
+
131
+ return result.map((item: any) => {
132
+ item.sha = item.oid;
133
+ item.name = item.path.split('/').pop() || '';
134
+ return item;
135
+ });
136
+ }
137
+
138
+ async getFileContent(account: Account, path: string): Promise<RepoContent> {
139
+ const response = await fetch(
140
+ `${API_BASE_URL}/api/hf/${account.owner}/${account.repo}/raw/${account.ref}/${path}`,
141
+ {
142
+ method: 'GET',
143
+ headers: {
144
+ Authorization: `Bearer ${account.token}`
145
+ }
146
+ }
147
+ );
148
+
149
+ if (response.status === 401) {
150
+ localStorage.removeItem('isAuthenticated');
151
+ localStorage.removeItem('token');
152
+ router.push('/login');
153
+ throw new Error('认证失败,请重新登录');
154
+ }
155
+
156
+ return {
157
+ content: await response.text(),
158
+ encoding: 'utf-8',
159
+ name: '',
160
+ path: path,
161
+ sha: '',
162
+ size: 0,
163
+ url: '',
164
+ html_url: '',
165
+ git_url: '',
166
+ download_url: null,
167
+ type: 'file'
168
+ };
169
+ }
170
+
171
+ async updateFile(account: Account, path: string, content: string, sha: string, message?: string) {
172
+ const response = await fetch(
173
+ `${API_BASE_URL}/api/hf/${account.owner}/${account.repo}/commit/${account.ref}/${path}`,
174
+ {
175
+ method: 'PUT',
176
+ headers: {
177
+ 'Content-Type': 'application/json',
178
+ Authorization: `Bearer ${account.token}`
179
+ },
180
+ body: JSON.stringify({
181
+ "description": "",
182
+ "summary": message,
183
+ "files": [
184
+ {
185
+ "content": content,
186
+ "encoding": "utf-8",
187
+ "path": path
188
+ }
189
+ ]
190
+ })
191
+ }
192
+ );
193
+ const result = await handleResponse(response);
194
+ return {
195
+ content: {
196
+ sha: result.commitOid
197
+ }
198
+ };
199
+ }
200
+
201
+ async deleteFile(account: Account, path: string, sha: string, message?: string) {
202
+ const response = await fetch(
203
+ `${API_BASE_URL}/api/hf/${account.owner}/${account.repo}/commit/${account.ref}/${path}`,
204
+ {
205
+ method: 'DELETE',
206
+ headers: {
207
+ 'Content-Type': 'application/json',
208
+ Authorization: `Bearer ${account.token}`
209
+ },
210
+ body: JSON.stringify({
211
+ "description": "",
212
+ "summary": message,
213
+ "deletedFiles": [
214
+ {
215
+ "path": path
216
+ }
217
+ ]
218
+ })
219
+ }
220
+ );
221
+ return handleResponse(response);
222
+ }
223
+
224
+ async createFile(account: Account, path: string, content: string, message?: string) {
225
+ const response = await fetch(
226
+ `${API_BASE_URL}/api/hf/${account.owner}/${account.repo}/commit/${account.ref}/${path}`,
227
+ {
228
+ method: 'POST',
229
+ headers: {
230
+ 'Content-Type': 'application/json',
231
+ Authorization: `Bearer ${account.token}`
232
+ },
233
+ body: JSON.stringify({
234
+ "description": "",
235
+ "summary": message,
236
+ "files": [
237
+ {
238
+ "content": content,
239
+ "encoding": "utf-8",
240
+ "path": path
241
+ }
242
+ ]
243
+ })
244
+ }
245
+ );
246
+ const result = await handleResponse(response);
247
+ return {
248
+ content: {
249
+ sha: result.commitOid
250
+ }
251
+ };
252
+ }
253
+ }
254
+
255
+ // Factory to get the appropriate repo API implementation
256
+ class RepoApiFactory {
257
+ private static githubInstance: GitHubRepoApi;
258
+ private static huggingFaceInstance: HuggingFaceRepoApi;
259
+
260
+ static getRepoApi(type: "github" | "hf"): IRepoApi {
261
+ switch (type) {
262
+ case "github":
263
+ if (!this.githubInstance) {
264
+ this.githubInstance = new GitHubRepoApi();
265
+ }
266
+ return this.githubInstance;
267
+ case "hf":
268
+ if (!this.huggingFaceInstance) {
269
+ this.huggingFaceInstance = new HuggingFaceRepoApi();
270
+ }
271
+ return this.huggingFaceInstance;
272
+ default:
273
+ throw new Error(`Unsupported repository type: ${type}`);
274
+ }
275
+ }
276
+ }
277
+
278
+ // Exported API object that uses the factory
279
+ export const repoApi = {
280
+ async getContents(account: Account, path: string = '') {
281
+ const api = RepoApiFactory.getRepoApi(account.type);
282
+ return api.getContents(account, path);
283
+ },
284
+
285
+ async getFileContent(account: Account, path: string): Promise<RepoContent> {
286
+ const api = RepoApiFactory.getRepoApi(account.type);
287
+ return api.getFileContent(account, path);
288
+ },
289
+
290
+ async updateFile(account: Account, path: string, content: string, sha: string, message?: string) {
291
+ const api = RepoApiFactory.getRepoApi(account.type);
292
+ return api.updateFile(account, path, content, sha, message);
293
+ },
294
+
295
+ async deleteFile(account: Account, path: string, sha: string, message?: string) {
296
+ const api = RepoApiFactory.getRepoApi(account.type);
297
+ return api.deleteFile(account, path, sha, message);
298
+ },
299
+
300
+ async createFile(account: Account, path: string, content: string, message?: string) {
301
+ const api = RepoApiFactory.getRepoApi(account.type);
302
+ return api.createFile(account, path, content, message);
303
+ }
304
+ };
src/services/settingApi.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { API_BASE_URL, getHeaders, handleResponse } from './util';
2
+
3
+ export interface Settings {
4
+ /** 飞书配置 */
5
+ feishu: {
6
+ /** 飞书应用ID */
7
+ app_id: string;
8
+ /** 飞书应用密钥 */
9
+ app_secret: string;
10
+ /** 飞书应用验证Token */
11
+ verification_token: string;
12
+ /** 飞书应用加密Key */
13
+ encrypt_key: string;
14
+ /** 飞书机器人接收ID */
15
+ receive_id: string;
16
+ }
17
+ }
18
+
19
+
20
+ export const settingApi = {
21
+ async update(settings: Settings) {
22
+ const response = await fetch(
23
+ `${API_BASE_URL}/api/setting`,
24
+ {
25
+ headers: getHeaders(),
26
+ method: 'POST',
27
+ body: JSON.stringify(settings)
28
+ }
29
+ );
30
+ return handleResponse(response);
31
+ },
32
+
33
+ async get(): Promise<Settings> {
34
+ const response = await fetch(
35
+ `${API_BASE_URL}/api/setting`,
36
+ {
37
+ headers: getHeaders()
38
+ }
39
+ );
40
+ return handleResponse(response);
41
+ },
42
+ }
src/services/userApi.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { API_BASE_URL, handleResponse } from './util';
2
+
3
+ export interface LoginResponse {
4
+ success: boolean;
5
+ token: string;
6
+ message?: string;
7
+ }
8
+
9
+ export const userApi = {
10
+ async login(username: string, password: string): Promise<LoginResponse> {
11
+ const response = await fetch(`${API_BASE_URL}/api/login`, {
12
+ method: 'POST',
13
+ headers: {
14
+ 'Content-Type': 'application/json',
15
+ },
16
+ body: JSON.stringify({ username, password }),
17
+ });
18
+ return handleResponse(response);
19
+ }
20
+ };
21
+
22
+
src/services/util.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import router from "../router";
2
+
3
+ export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
4
+
5
+ export function getHeaders() {
6
+ return {
7
+ 'Content-Type': 'application/json',
8
+ Authorization: `Bearer ${localStorage.getItem('token')}`
9
+ };
10
+ }
11
+ export async function handleResponse(response: Response) {
12
+ if (response.status === 401) {
13
+ localStorage.removeItem('isAuthenticated');
14
+ localStorage.removeItem('token');
15
+ router.push('/login');
16
+ throw new Error('认证失败,请重新登录');
17
+ }
18
+ if (!response.ok) {
19
+ const contentType = response.headers.get('content-type');
20
+ if (contentType && contentType.includes('application/json')) {
21
+ const errorData = await response.json();
22
+ throw new Error(errorData.error || '请求失败');
23
+ } else {
24
+ const errorText = await response.text();
25
+ throw new Error(errorText || '请求失败');
26
+ }
27
+ }
28
+ return response.json();
29
+ }
src/stores/accountStorage.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineStore } from 'pinia'
2
+ import { accountApi, type Account } from '../services/accountApi'
3
+
4
+ export const useAccountStore = defineStore('server', {
5
+ state: () => ({
6
+ accounts: [] as Account[],
7
+ initialized: false
8
+ }),
9
+
10
+ actions: {
11
+ async fetchAccounts(force = false) {
12
+ if (!force && this.initialized) return;
13
+ try {
14
+ const data = await accountApi.get();
15
+ this.accounts = Array.isArray(data) ? data : [];
16
+ this.initialized = true;
17
+ } catch (error) {
18
+ console.error('Failed to fetch accounts:', error);
19
+ this.accounts = [];
20
+ }
21
+ },
22
+
23
+ updateAccounts(newAccounts: Account[]) {
24
+ this.accounts = Array.isArray(newAccounts) ? newAccounts : [];
25
+ }
26
+ }
27
+ })
src/style.css ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ a {
17
+ font-weight: 500;
18
+ color: #646cff;
19
+ text-decoration: inherit;
20
+ }
21
+ a:hover {
22
+ color: #535bf2;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ display: flex;
28
+ place-items: center;
29
+ min-width: 320px;
30
+ min-height: 100vh;
31
+ }
32
+
33
+ h1 {
34
+ font-size: 3.2em;
35
+ line-height: 1.1;
36
+ }
37
+
38
+ button {
39
+ border-radius: 8px;
40
+ border: 1px solid transparent;
41
+ padding: 0.6em 1.2em;
42
+ font-size: 1em;
43
+ font-weight: 500;
44
+ font-family: inherit;
45
+ background-color: #1a1a1a;
46
+ cursor: pointer;
47
+ transition: border-color 0.25s;
48
+ }
49
+ button:hover {
50
+ border-color: #646cff;
51
+ }
52
+ button:focus,
53
+ button:focus-visible {
54
+ outline: 4px auto -webkit-focus-ring-color;
55
+ }
56
+
57
+ .card {
58
+ padding: 2em;
59
+ }
60
+
61
+ #app {
62
+ max-width: 1280px;
63
+ margin: 0 auto;
64
+ padding: 2rem;
65
+ text-align: center;
66
+ }
67
+
68
+ @media (prefers-color-scheme: light) {
69
+ :root {
70
+ color: #213547;
71
+ background-color: #ffffff;
72
+ }
73
+ a:hover {
74
+ color: #747bff;
75
+ }
76
+ button {
77
+ background-color: #f9f9f9;
78
+ }
79
+ }
src/views/AccountView.vue ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { ref, onMounted, onUnmounted, watch } from 'vue';
3
+ import { MessagePlugin } from 'tdesign-vue-next';
4
+ import { accountApi } from '../services/accountApi';
5
+ import { useAccountStore } from '../stores/accountStorage';
6
+ import MonacoEditor from '../components/MonacoEditor.vue';
7
+ const accountStore = useAccountStore();
8
+ const accountsText = ref<string>('');
9
+ const loading = ref(false);
10
+ const showDiff = ref(false);
11
+ const originalText = ref('');
12
+
13
+ const fetchAccounts = async (force: boolean) => {
14
+ try {
15
+ loading.value = true;
16
+ accountsText.value = "";
17
+ await accountStore.fetchAccounts(force);
18
+ const formattedData = JSON.stringify(accountStore.accounts, null, 2);
19
+ accountsText.value = formattedData;
20
+ originalText.value = formattedData;
21
+ } catch (error) {
22
+ MessagePlugin.error('获取账号数据失败');
23
+ } finally {
24
+ loading.value = false;
25
+ }
26
+ };
27
+
28
+
29
+ const handleSave = async () => {
30
+ loading.value = true;
31
+ try {
32
+ if (accountsText.value === '') {
33
+ MessagePlugin.error('账号数据不能为空');
34
+ return;
35
+ }
36
+ if (originalText.value === accountsText.value) {
37
+ MessagePlugin.success('数据未修改');
38
+ return;
39
+ }
40
+ const accounts = JSON.parse(accountsText.value);
41
+ const result = await accountApi.post(accounts);
42
+ if (result.error) {
43
+ MessagePlugin.error(`${result.error}`);
44
+ return;
45
+ }
46
+ accountStore.updateAccounts(accounts || []);
47
+ MessagePlugin.success('保存成功');
48
+ } catch (error) {
49
+ if (error instanceof SyntaxError) {
50
+ MessagePlugin.error('JSON格式错误');
51
+ } else {
52
+ MessagePlugin.error('保存失败');
53
+ }
54
+ } finally {
55
+ loading.value = false;
56
+ }
57
+ };
58
+
59
+ const handleReload = async () => {
60
+ loading.value = true;
61
+ try {
62
+ await fetchAccounts(true);
63
+ MessagePlugin.success('重新加载成功');
64
+ } catch (error) {
65
+ MessagePlugin.error('重新加载失败');
66
+ } finally {
67
+ loading.value = false;
68
+ }
69
+ };
70
+
71
+ // 定义按键事件处理函数
72
+ const handleKeyDown = (e: KeyboardEvent) => {
73
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
74
+ e.preventDefault();
75
+ handleSave();
76
+ }
77
+ };
78
+
79
+
80
+ // 在显示差异按钮点击处理中添加
81
+ const toggleDiff = () => {
82
+ showDiff.value = !showDiff.value;
83
+
84
+ if (showDiff.value) {
85
+ // 进入差异模式时,确保有原始文本作为比较
86
+ if (originalText.value === '') {
87
+ originalText.value = accountsText.value;
88
+ }
89
+ }
90
+ };
91
+ // 在 AccountView.vue 中添加
92
+ watch(showDiff, (newVal) => {
93
+ if (newVal && originalText.value === accountsText.value) {
94
+ // 如果开启差异模式但两个文本相同,可以考虑提示用户
95
+ MessagePlugin.info('当前没有差异可以显示');
96
+ }
97
+ }, { immediate: true });
98
+
99
+ onMounted(async () => {
100
+ // 注册全局按键监听
101
+ window.addEventListener('keydown', handleKeyDown);
102
+ await fetchAccounts(false);
103
+ });
104
+
105
+ onUnmounted(() => {
106
+ // 注销全局按键监听
107
+ window.removeEventListener('keydown', handleKeyDown);
108
+ });
109
+ </script>
110
+
111
+ <template>
112
+ <div class="account-container h-full p-2 md:p-5">
113
+ <t-card bordered class="h-full">
114
+ <template #content>
115
+ <div class=" flex flex-col h-full">
116
+ <div class="flex justify-end items-center mb-4 gap-4">
117
+ <div class="flex gap-2">
118
+ <t-button variant="outline" @click="handleReload">
119
+ 重新加载
120
+ </t-button>
121
+ <t-button variant="outline" @click="toggleDiff">
122
+ {{ showDiff ? '隐藏对比' : '显示对比' }}
123
+ </t-button>
124
+ <t-button theme="primary" @click="handleSave" :loading="loading">
125
+ 保存账号
126
+ </t-button>
127
+ </div>
128
+ </div>
129
+
130
+ <div class="editor-container flex-1">
131
+ <MonacoEditor v-model:value="accountsText" :original-value="showDiff ? originalText : undefined"
132
+ language="json" :options="{ tabSize: 2 }" />
133
+ </div>
134
+ </div>
135
+ </template>
136
+ </t-card>
137
+ </div>
138
+ </template>
139
+
140
+ <style scoped>
141
+ .account-container {
142
+ width: 100%;
143
+ }
144
+
145
+ :deep(.t-card__body) {
146
+ height: 100%;
147
+ }
148
+
149
+ .editor-container {
150
+ border: 1px solid var(--td-component-border);
151
+ }
152
+ </style>
src/views/ContentView.vue ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
3
+ import { useRoute, useRouter } from 'vue-router';
4
+ import { MessagePlugin } from 'tdesign-vue-next';
5
+ import { repoApi, type Account } from '../services/repoApi';
6
+ import { useAccountStore } from '../stores/accountStorage';
7
+ import MonacoEditor from '../components/MonacoEditor.vue';
8
+ import RepoHeader from '../components/RepoHeader.vue';
9
+
10
+ const route = useRoute();
11
+ const router = useRouter();
12
+
13
+ const contentText = ref<string>('');
14
+ const loading = ref(false);
15
+ const showDiff = ref(false);
16
+ const originalText = ref('');
17
+ const fileSha = ref('');
18
+ const selectedAccount = ref<number | ''>('');
19
+ const currentPath = ref('');
20
+ const newFileName = ref('');
21
+ const store = useAccountStore();
22
+ const isNewFile = ref(false);
23
+
24
+ // Compute full path by combining directory path with new file name
25
+ const fullPath = computed(() => {
26
+ if (!isNewFile.value) return currentPath.value;
27
+ const basePath = route.query.path as string || '';
28
+ return basePath ? `${basePath}/${newFileName.value}` : newFileName.value;
29
+ });
30
+
31
+ // 提交对话框状态
32
+ const showCommitDialog = ref(false);
33
+ const commitMessage = ref('');
34
+
35
+ const decodeContent = (base64Content: string, contentType: 'text' | 'binary' = 'text'): string | Uint8Array => {
36
+ // 步骤1: 使用atob解码Base64字符串,获取原始二进制数据
37
+ const binaryContent = atob(base64Content);
38
+
39
+ if (contentType === 'binary') {
40
+ // 对于二进制文件,返回Uint8Array
41
+ const bytes = new Uint8Array(binaryContent.length);
42
+ for (let i = 0; i < binaryContent.length; i++) {
43
+ bytes[i] = binaryContent.charCodeAt(i);
44
+ }
45
+ return bytes;
46
+ } else {
47
+ // 对于文本内容,处理UTF-8编码
48
+ try {
49
+ // 方法1: 使用TextDecoder (推荐,更可靠)
50
+ const bytes = new Uint8Array(binaryContent.length);
51
+ for (let i = 0; i < binaryContent.length; i++) {
52
+ bytes[i] = binaryContent.charCodeAt(i);
53
+ }
54
+ return new TextDecoder('utf-8').decode(bytes);
55
+ } catch (e) {
56
+ // 方法2: 兼容性方案
57
+ return decodeURIComponent(escape(binaryContent));
58
+ }
59
+ }
60
+ };
61
+
62
+ const fetchContent = async () => {
63
+ const account = store.accounts.find(acc => acc.id === selectedAccount.value);
64
+ if (!account) {
65
+ MessagePlugin.error(`未找到账户信息${selectedAccount.value}`);
66
+ return;
67
+ }
68
+ try {
69
+ loading.value = true;
70
+ contentText.value = "";
71
+ originalText.value = "";
72
+ const result = await repoApi.getFileContent(account, currentPath.value);
73
+ if (result.content) {
74
+ // 不移除换行符,保持原始格式
75
+ contentText.value = result.encoding && result.encoding == "base64" ? decodeContent(result.content) as string : result.content;
76
+ originalText.value = contentText.value;
77
+ fileSha.value = result.sha;
78
+ }
79
+ } catch (error) {
80
+ MessagePlugin.error('获取文件内容失败');
81
+ } finally {
82
+ loading.value = false;
83
+ }
84
+ };
85
+
86
+ const handleSave = () => {
87
+
88
+
89
+ // For new files, we need a file name
90
+ if (isNewFile.value && !newFileName.value) {
91
+ MessagePlugin.error('请输入文件名');
92
+ return;
93
+ }
94
+
95
+ if (contentText.value === '') {
96
+ MessagePlugin.error('内容不能为空');
97
+ return;
98
+ }
99
+
100
+ // For existing files, check if content was modified
101
+ if (fileSha.value && originalText.value === contentText.value) {
102
+ MessagePlugin.success('内容未修改');
103
+ return;
104
+ }
105
+
106
+ showCommitDialog.value = true;
107
+ };
108
+
109
+ const handleConfirmSave = async () => {
110
+
111
+
112
+ // Use the full path for new files, or the route path for existing files
113
+ const path = isNewFile.value ? fullPath.value : (route.query.path as string);
114
+ if (!path) {
115
+ MessagePlugin.error('请输入文件名');
116
+ return;
117
+ }
118
+
119
+ loading.value = true;
120
+ try {
121
+ const account = store.accounts.find(acc => acc.id === selectedAccount.value);
122
+
123
+ if (!account) {
124
+ MessagePlugin.error(`未找到账户信息${selectedAccount.value}`);
125
+ return;
126
+ }
127
+
128
+ let response;
129
+ if (isNewFile.value) {
130
+ // Create new file
131
+ response = await repoApi.createFile(
132
+ account,
133
+ path,
134
+ contentText.value,
135
+ commitMessage.value.trim() || '创建文件'
136
+ );
137
+ } else {
138
+ // Update existing file
139
+ response = await repoApi.updateFile(
140
+ account,
141
+ path,
142
+ contentText.value,
143
+ fileSha.value,
144
+ commitMessage.value.trim() || '更新文件'
145
+ );
146
+ }
147
+
148
+ // Update fileSha with the new value from response
149
+ if (response && response.content && response.content.sha) {
150
+ fileSha.value = response.content.sha;
151
+ }
152
+
153
+ MessagePlugin.success(isNewFile.value ? '创建成功' : '保存成功');
154
+ originalText.value = contentText.value;
155
+ showCommitDialog.value = false;
156
+ commitMessage.value = '';
157
+
158
+ // If this was a new file, update the URL to include the path
159
+ if (isNewFile.value) {
160
+ router.replace({
161
+ path: '/content',
162
+ query: {
163
+ ...route.query,
164
+ path,
165
+ newFile: undefined
166
+ }
167
+ });
168
+ }
169
+ } catch (error) {
170
+ console.error(error);
171
+ MessagePlugin.error('保存失败');
172
+ } finally {
173
+ loading.value = false;
174
+ }
175
+ };
176
+
177
+ const handleCancelSave = () => {
178
+ showCommitDialog.value = false;
179
+ commitMessage.value = '';
180
+ };
181
+
182
+ // 删除对话框状态
183
+ const showDeleteDialog = ref(false);
184
+
185
+ const handleDelete = () => {
186
+
187
+
188
+ showDeleteDialog.value = true;
189
+ };
190
+
191
+ const handleConfirmDelete = async () => {
192
+
193
+
194
+ loading.value = true;
195
+ try {
196
+ const account = store.accounts.find(acc => acc.id === selectedAccount.value);
197
+
198
+ if (!account) {
199
+ MessagePlugin.error(`未找到账户信息${selectedAccount.value}`);
200
+ return;
201
+ }
202
+
203
+ await repoApi.deleteFile(
204
+ account,
205
+ currentPath.value as string,
206
+ fileSha.value,
207
+ '删除文件'
208
+ );
209
+ MessagePlugin.success('删除成功');
210
+ showDeleteDialog.value = false;
211
+ router.back();
212
+ } catch (error) {
213
+ MessagePlugin.error('删除失败');
214
+ } finally {
215
+ loading.value = false;
216
+ }
217
+ };
218
+
219
+ const handleCancelDelete = () => {
220
+ showDeleteDialog.value = false;
221
+ };
222
+
223
+ // 定义按键事件处理函数
224
+ const handleKeyDown = (e: KeyboardEvent) => {
225
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
226
+ e.preventDefault();
227
+ handleSave();
228
+ }
229
+ };
230
+
231
+ // 在显示差异按钮点击处理中添加
232
+ const toggleDiff = () => {
233
+ showDiff.value = !showDiff.value;
234
+
235
+ if (showDiff.value) {
236
+ // 进入差异模式时,确保有原始文本作为比较
237
+ if (originalText.value === '') {
238
+ originalText.value = contentText.value;
239
+ }
240
+ }
241
+ };
242
+
243
+ // 在 AccountView.vue 中添加
244
+ watch(showDiff, (newVal) => {
245
+ if (newVal && originalText.value === contentText.value) {
246
+ // 如果开启差异模式但两个文本相同,可以考虑提示用户
247
+ MessagePlugin.info('当前没有差异可以显示');
248
+ }
249
+ }, { immediate: true });
250
+
251
+ const handlePathClick = (path: string) => {
252
+ router.push({
253
+ path: '/repo',
254
+ query: {
255
+ id: selectedAccount.value,
256
+ path
257
+ }
258
+ });
259
+ };
260
+
261
+ const handleRootClick = () => {
262
+ router.push({
263
+ path: '/repo',
264
+ query: {
265
+ id: selectedAccount.value,
266
+ }
267
+ });
268
+ };
269
+
270
+ const handleAccountChange = (val: number) => {
271
+ selectedAccount.value = val;
272
+ currentPath.value = ''; // Reset path
273
+ router.push({
274
+ path: '/repo',
275
+ query: {
276
+ id: selectedAccount.value,
277
+ }
278
+ });
279
+ }
280
+
281
+ const getLanguageFromPath = computed(() => {
282
+ const path = route.query.path as string;
283
+ if (!path) return 'plaintext';
284
+
285
+ const ext = path.split('.').pop()?.toLowerCase() || '';
286
+
287
+ // Map file extensions to Monaco editor languages
288
+ const languageMap: Record<string, string> = {
289
+ 'js': 'javascript',
290
+ "mjs": 'javascript',
291
+ 'ts': 'typescript',
292
+ 'json': 'json',
293
+ 'md': 'markdown',
294
+ 'yml': 'yaml',
295
+ 'yaml': 'yaml',
296
+ 'py': 'python',
297
+ 'html': 'html',
298
+ 'css': 'css',
299
+ 'scss': 'scss',
300
+ 'less': 'less',
301
+ 'xml': 'xml',
302
+ 'sh': 'shell',
303
+ 'bash': 'shell',
304
+ 'vue': 'vue',
305
+ 'jsx': 'javascript',
306
+ 'tsx': 'typescript',
307
+ 'gitignore': 'plaintext',
308
+ 'env': 'plaintext',
309
+ 'txt': 'plaintext',
310
+ "rs": 'rust',
311
+ 'go': 'go',
312
+ };
313
+
314
+ return languageMap[ext] || 'plaintext';
315
+ });
316
+
317
+ // 监听路由变化同步本地状态
318
+ watch(() => route.query, async (query) => {
319
+ // Only respond to query changes when on the repo route
320
+ if (route.path !== '/content') return;
321
+ const { id, path, newFile } = query;
322
+ isNewFile.value = !!newFile;
323
+ if (id) {
324
+ await store.fetchAccounts();
325
+ const account = store.accounts.find(acc => acc.id === Number(id));
326
+ if (account) {
327
+ selectedAccount.value = account.id;
328
+ }
329
+ currentPath.value = path as string;
330
+
331
+ if (isNewFile.value) {
332
+ contentText.value = '';
333
+ originalText.value = '';
334
+ newFileName.value = ''; // Reset new file name when entering new file mode
335
+ return;
336
+ }
337
+ if (path) {
338
+ await fetchContent();
339
+ }
340
+ }
341
+ }, { immediate: true });
342
+
343
+ onMounted(() => {
344
+ window.addEventListener('keydown', handleKeyDown);
345
+
346
+
347
+
348
+ });
349
+
350
+ onUnmounted(() => {
351
+ // 注销全局按键监听
352
+ window.removeEventListener('keydown', handleKeyDown);
353
+ });
354
+ </script>
355
+
356
+ <template>
357
+ <div class="content-container h-full p-2 md:p-5">
358
+ <div class="flex flex-col gap-4 h-full">
359
+ <RepoHeader :selected-account="selectedAccount" :current-path="isNewFile ? fullPath : currentPath"
360
+ :is-new-file="isNewFile" :accounts="store.accounts" @path-click="handlePathClick"
361
+ @root-click="handleRootClick" @update:selected-account="handleAccountChange">
362
+ <div class="flex items-center justify-between w-full">
363
+ <div>
364
+ <t-input v-if="isNewFile" v-model="newFileName" placeholder="请输入文件名" class="w-20" />
365
+ </div>
366
+ <div class="flex gap-2">
367
+ <t-button variant="outline" @click="toggleDiff">
368
+ {{ showDiff ? '隐藏对比' : '显示对比' }}
369
+ </t-button>
370
+ <t-button theme="primary" @click="handleSave" :loading="loading">
371
+ 保存
372
+ </t-button>
373
+ <t-button v-if="!isNewFile" theme="danger" @click="handleDelete" :loading="loading">
374
+ 删除
375
+ </t-button>
376
+ </div>
377
+ </div>
378
+ </RepoHeader>
379
+ <t-card bordered class="h-full">
380
+ <template #content>
381
+ <div class="flex flex-col h-full">
382
+ <div class="editor-container flex-1">
383
+ <MonacoEditor v-model:value="contentText"
384
+ :original-value="showDiff ? originalText : undefined" :language="getLanguageFromPath"
385
+ :options="{ tabSize: 2 }" />
386
+ </div>
387
+ </div>
388
+ </template>
389
+ </t-card>
390
+ </div>
391
+
392
+ <t-dialog v-model:visible="showCommitDialog" header="提交更改" :confirm-on-enter="true" @confirm="handleConfirmSave"
393
+ @close="handleCancelSave">
394
+ <template #body>
395
+ <div class="flex flex-col gap-2">
396
+ <div class="mb-2">请输入提交信息:</div>
397
+ <t-input v-model:value="commitMessage" placeholder="描述此次更改的内容" :autofocus="true" />
398
+ </div>
399
+ </template>
400
+ <template #footer>
401
+ <t-button theme="default" @click="handleCancelSave">
402
+ 取消
403
+ </t-button>
404
+ <t-button theme="primary" @click="handleConfirmSave" :loading="loading">
405
+ 确认
406
+ </t-button>
407
+ </template>
408
+ </t-dialog>
409
+
410
+ <t-dialog v-model:visible="showDeleteDialog" header="确认删除" :confirm-on-enter="true"
411
+ @confirm="handleConfirmDelete" @close="handleCancelDelete">
412
+ <template #body>
413
+ <div class="p-2">
414
+ 确定要删除此文件吗?此操作不可恢复。
415
+ </div>
416
+ </template>
417
+ <template #footer>
418
+ <t-button theme="default" @click="handleCancelDelete">
419
+ 取消
420
+ </t-button>
421
+ <t-button theme="primary" @click="handleConfirmDelete" :loading="loading">
422
+ 确认
423
+ </t-button>
424
+ </template>
425
+ </t-dialog>
426
+ </div>
427
+ </template>
428
+
429
+ <style scoped>
430
+ .content-container {
431
+ width: 100%;
432
+ }
433
+
434
+ :deep(.t-card__body) {
435
+ height: 100%;
436
+ }
437
+
438
+ .editor-container {
439
+ border: 1px solid var(--td-component-border);
440
+ }
441
+ </style>
src/views/LoginView.vue ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+ import { MessagePlugin } from 'tdesign-vue-next'
5
+ import logo from '../assets/logo.png'
6
+ import { userApi } from '../services/userApi'
7
+
8
+ const router = useRouter()
9
+ const username = ref('')
10
+ const password = ref('')
11
+ const loading = ref(false)
12
+
13
+
14
+ const handleLogin = async () => {
15
+ if (!username.value || !password.value) {
16
+ MessagePlugin.warning('请输入用户名和密码')
17
+ return
18
+ }
19
+
20
+ loading.value = true
21
+ try {
22
+ const data = await userApi.login(username.value, password.value)
23
+
24
+ if (data.success) {
25
+ localStorage.setItem('token', data.token)
26
+ localStorage.setItem('isAuthenticated', 'true')
27
+
28
+ MessagePlugin.success('登录成功')
29
+ router.push('/')
30
+ } else {
31
+ MessagePlugin.error(data.message || '登录失败')
32
+ }
33
+ } catch (error) {
34
+ MessagePlugin.error('网络错误,请稍后重试')
35
+ console.error('Login error:', error)
36
+ } finally {
37
+ loading.value = false
38
+ }
39
+ }
40
+ </script>
41
+
42
+
43
+ <template>
44
+ <div
45
+ class="min-h-screen w-full flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50">
46
+ <div class="relative w-full max-w-md">
47
+ <!-- 装饰背景 -->
48
+ <div
49
+ class="absolute -top-4 -left-4 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob">
50
+ </div>
51
+ <div
52
+ class="absolute -bottom-8 -right-4 w-72 h-72 bg-blue-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000">
53
+ </div>
54
+ <div
55
+ class="absolute -bottom-8 left-20 w-72 h-72 bg-indigo-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000">
56
+ </div>
57
+
58
+ <!-- 登录卡片 -->
59
+ <div class="relative bg-white/80 backdrop-blur-lg rounded-2xl shadow-xl p-8 m-4">
60
+ <!-- Logo和标题 -->
61
+ <div class="flex flex-col items-center mb-8">
62
+ <img :src="logo" alt="logo" class="w-16 h-16 mb-4" />
63
+ <h2 class="text-2xl font-bold text-gray-800">微软账号管理</h2>
64
+ <!-- <p class="text-gray-500 mt-2">登录以继续使用</p> -->
65
+ </div>
66
+
67
+ <!-- 登录表单 -->
68
+ <form @submit.prevent="handleLogin" class="space-y-6">
69
+ <div class="space-y-2">
70
+ <t-input v-model="username" size="large" placeholder="请输入用户名" :autofocus="true" class="w-full">
71
+ <template #prefix-icon>
72
+ <t-icon name="user" />
73
+ </template>
74
+ </t-input>
75
+ </div>
76
+
77
+ <div class="space-y-2">
78
+ <t-input v-model="password" type="password" size="large" placeholder="请输入密码" class="w-full">
79
+ <template #prefix-icon>
80
+ <t-icon name="lock-on" />
81
+ </template>
82
+ </t-input>
83
+ </div>
84
+
85
+ <!-- <div class="flex items-center justify-between text-sm">
86
+ <t-checkbox>记住我</t-checkbox>
87
+ <a href="#" class="text-blue-600 hover:text-blue-700 transition-colors">
88
+ 忘记密码?
89
+ </a>
90
+ </div> -->
91
+
92
+ <t-button type="submit" theme="primary" :loading="loading" size="large" class="w-full" :disabled="loading">
93
+ {{ loading ? '登录中...' : '登录' }}
94
+ </t-button>
95
+ </form>
96
+
97
+ <!-- 额外信息 -->
98
+ <div class="mt-6 text-center text-sm text-gray-500">
99
+ 测试账号: admin / password
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </template>
105
+
106
+ <style scoped>
107
+ .animate-blob {
108
+ animation: blob 7s infinite;
109
+ }
110
+
111
+ .animation-delay-2000 {
112
+ animation-delay: 2s;
113
+ }
114
+
115
+ .animation-delay-4000 {
116
+ animation-delay: 4s;
117
+ }
118
+
119
+ @keyframes blob {
120
+ 0% {
121
+ transform: translate(0px, 0px) scale(1);
122
+ }
123
+
124
+ 33% {
125
+ transform: translate(30px, -50px) scale(1.1);
126
+ }
127
+
128
+ 66% {
129
+ transform: translate(-20px, 20px) scale(0.9);
130
+ }
131
+
132
+ 100% {
133
+ transform: translate(0px, 0px) scale(1);
134
+ }
135
+ }
136
+ </style>
src/views/RepoView.vue ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { type TableProps } from 'tdesign-vue-next';
3
+ import { computed, ref, watch, onMounted } from 'vue';
4
+ 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();
12
+ const route = useRoute();
13
+
14
+ // Local state
15
+ const loading = ref(false);
16
+ const allFiles = ref<RepoContent[]>([]);
17
+ const selectedAccount = ref<number>(0);
18
+ const currentPath = ref('');
19
+ const store = useAccountStore();
20
+
21
+ // Sort files with directories first then files
22
+ const sortedFiles = computed(() => {
23
+ // First separate directories and files
24
+ const dirs = allFiles.value.filter(item => item.type === 'dir');
25
+ const files = allFiles.value.filter(item => item.type === 'file');
26
+
27
+ // Sort directories and files alphabetically by name
28
+ dirs.sort((a, b) => a.name.localeCompare(b.name));
29
+ files.sort((a, b) => a.name.localeCompare(b.name));
30
+
31
+ // Combine with directories first
32
+ return [...dirs, ...files];
33
+ });
34
+
35
+ // Helper function to determine file icon based on file extension
36
+ const getFileIcon = (filename: string) => {
37
+ const extension = filename.split('.').pop()?.toLowerCase();
38
+
39
+ if (!extension) return FileIcon;
40
+
41
+ const codeExtensions = ['js', 'ts', 'py', 'java', 'c', 'cpp', 'cs', 'go', 'php', 'html', 'css', 'vue', 'jsx', 'tsx'];
42
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp'];
43
+ const textExtensions = ['txt', 'md', 'json', 'xml', 'csv', 'yml', 'yaml'];
44
+
45
+ if (codeExtensions.includes(extension)) return CodeIcon;
46
+ if (imageExtensions.includes(extension)) return ImageIcon;
47
+ // if (textExtensions.includes(extension)) return TextIcon;
48
+ // if (extension === 'pdf') return PdfIcon;
49
+
50
+ return FileIcon;
51
+ };
52
+
53
+ const columns = ref<TableProps['columns']>([
54
+ {
55
+ colKey: 'name',
56
+ title: '文件名',
57
+ width: 300,
58
+ cell: (h, { row }) => {
59
+ const IconComponent = row.type === 'dir' ? FolderIcon : getFileIcon(row.name);
60
+ return h('div', { class: 'flex items-center gap-2' }, [
61
+ h(IconComponent, {
62
+ class: row.type === 'dir' ? 'text-blue-500' : 'text-gray-600',
63
+ style: { fontSize: '1.25rem' }
64
+ }),
65
+ h('span', row.name)
66
+ ]);
67
+ }
68
+ },
69
+ { colKey: 'type', title: '类型', width: 100 },
70
+ {
71
+ colKey: 'size',
72
+ title: '大小',
73
+ width: 100,
74
+ cell: (h, { row }) => {
75
+ if (row.type === 'dir') return '-';
76
+ // Format file size
77
+ const size = Number(row.size);
78
+ if (size < 1024) return `${size} B`;
79
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
80
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`;
81
+ }
82
+ },
83
+ { colKey: 'sha', title: 'SHA', width: 200 }
84
+ ]);
85
+
86
+ const handleRowClick = (e: any) => {
87
+ const item: RepoContent = e.row;
88
+ if (item.type === 'dir') {
89
+ currentPath.value = item.path;
90
+ fetchRepo();
91
+ } else if (item.type === 'file') {
92
+ router.push({
93
+ path: '/content',
94
+ query: {
95
+ id: selectedAccount.value,
96
+ path: item.path
97
+ }
98
+ });
99
+ }
100
+ };
101
+
102
+ const fetchRepo = async () => {
103
+ if (!selectedAccount.value) return;
104
+
105
+ loading.value = true;
106
+ try {
107
+ const account = store.accounts.find(acc => acc.id === selectedAccount.value);
108
+ if (!account) {
109
+ MessagePlugin.error(`未找到账户信息${selectedAccount.value}`);
110
+ return;
111
+ }
112
+ const result = await repoApi.getContents(account, currentPath.value);
113
+ allFiles.value = Array.isArray(result) ? result : [result];
114
+ } catch (error) {
115
+ MessagePlugin.error('获取仓库内容失败');
116
+ } finally {
117
+ loading.value = false;
118
+ }
119
+ };
120
+
121
+ const handleBreadcrumbClick = (path: string) => {
122
+ currentPath.value = path;
123
+ fetchRepo();
124
+ };
125
+
126
+ const handleRootClick = () => {
127
+ currentPath.value = '';
128
+ fetchRepo();
129
+ };
130
+
131
+ const handleAccountChange = (val: number) => {
132
+ selectedAccount.value = val;
133
+ currentPath.value = ''; // Reset path
134
+ router.push({
135
+ path: '/repo',
136
+ query: {
137
+ id: selectedAccount.value,
138
+ }
139
+ });
140
+ }
141
+
142
+ // Watch route query params and sync with local state
143
+ watch(() => route.query, async (query) => {
144
+ // Only respond to query changes when on the repo route
145
+ if (route.path !== '/repo') return;
146
+ await store.fetchAccounts();
147
+ const { id, path } = query;
148
+
149
+ if (id) {
150
+
151
+ selectedAccount.value = Number(id);
152
+ currentPath.value = path ? path as string : '';
153
+ }
154
+ else {
155
+
156
+ if (store.accounts.length > 0) {
157
+ selectedAccount.value = store.accounts[0].id;
158
+ }
159
+ currentPath.value = '';
160
+ }
161
+ // Fetch data after updating both account and path
162
+ await fetchRepo();
163
+ }, { immediate: true });
164
+
165
+
166
+ onMounted(async () => {
167
+
168
+ });
169
+
170
+
171
+
172
+ const handleNewFile = () => {
173
+ if (!selectedAccount.value) {
174
+ MessagePlugin.error('请先选择仓库');
175
+ return;
176
+ }
177
+ let path = currentPath.value;
178
+ router.push({
179
+ path: '/content',
180
+ query: {
181
+ id: selectedAccount.value,
182
+ path: path,
183
+ newFile: 1,
184
+ }
185
+ });
186
+ };
187
+ </script>
188
+
189
+ <template>
190
+ <div class="repository-browser w-full flex flex-col p-3 md:p-5 gap-3 md:gap-5 bg-gray-50">
191
+ <RepoHeader :selected-account="selectedAccount" :current-path="currentPath" :accounts="store.accounts"
192
+ @path-click="handleBreadcrumbClick" @root-click="handleRootClick"
193
+ @update:selected-account="handleAccountChange">
194
+ <div class="flex gap-2 items-center">
195
+ <t-button theme="primary" @click="handleNewFile" class="flex items-center gap-1">
196
+ <template #icon>
197
+ <FileIcon />
198
+ </template>
199
+ 新建文件
200
+ </t-button>
201
+ </div>
202
+ </RepoHeader>
203
+
204
+ <div class="content-section flex-1 bg-white rounded-lg shadow-sm">
205
+ <t-table :data="sortedFiles" :loading="loading" :columns="columns" row-key="sha" hover stripe size="medium"
206
+ class="min-w-full" @row-click="handleRowClick" row-class-name="hover:bg-blue-50 cursor-pointer" />
207
+ </div>
208
+ </div>
209
+ </template>
210
+
211
+ <style scoped>
212
+ .repository-browser {
213
+ min-height: 600px;
214
+ }
215
+
216
+ /* Add subtle hover animations */
217
+ .t-table__row:hover {
218
+ transition: all 0.2s ease;
219
+ }
220
+
221
+ /* Additional responsive styles */
222
+ @media (max-width: 640px) {
223
+ .t-table {
224
+ font-size: 0.875rem;
225
+ }
226
+ }
227
+ </style>
src/views/SettingView.vue ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { ref, onMounted } from 'vue';
3
+ import { MessagePlugin } from 'tdesign-vue-next';
4
+ import { settingApi, type Settings } from '../services/settingApi';
5
+
6
+ const settings = ref<Settings>({
7
+ feishu: {
8
+ app_id: '',
9
+ app_secret: '',
10
+ verification_token: '',
11
+ encrypt_key: '',
12
+ receive_id: ''
13
+ }
14
+ });
15
+
16
+ const loading = ref(false);
17
+
18
+ onMounted(async () => {
19
+ try {
20
+ const data = await settingApi.get();
21
+ // 合并默认值和获取的数据
22
+ settings.value = {
23
+ feishu: {
24
+ ...settings.value.feishu,
25
+ ...data.feishu
26
+ }
27
+ };
28
+ } catch (error) {
29
+ MessagePlugin.error('获取设置失败');
30
+ }
31
+ });
32
+
33
+ const handleSave = async () => {
34
+ loading.value = true;
35
+ try {
36
+ if (!settings.value) {
37
+ return
38
+ }
39
+ await settingApi.update(settings.value);
40
+ MessagePlugin.success('保存成功');
41
+ } catch (error) {
42
+ MessagePlugin.error('保存失败');
43
+ } finally {
44
+ loading.value = false;
45
+ }
46
+ };
47
+ </script>
48
+
49
+ <template>
50
+ <div class="setting-container p-2 md:p-5">
51
+
52
+ <t-form :data="settings" @submit="handleSave">
53
+ <t-card bordered>
54
+ <t-divider>飞书配置</t-divider>
55
+ <t-form-item label="应用ID" name="feishu.app_id">
56
+ <t-input v-model="settings.feishu.app_id" placeholder="请输入飞书应用ID" />
57
+ </t-form-item>
58
+ <t-form-item label="应用密钥" name="feishu.app_secret">
59
+ <t-input v-model="settings.feishu.app_secret" type="password" placeholder="请输入飞书应用密钥" />
60
+ </t-form-item>
61
+ <t-form-item label="验证Token" name="feishu.verification_token">
62
+ <t-input v-model="settings.feishu.verification_token" placeholder="请输入飞书应用验证Token" />
63
+ </t-form-item>
64
+ <t-form-item label="加密Key" name="feishu.encrypt_key">
65
+ <t-input v-model="settings.feishu.encrypt_key" placeholder="请输入飞书应用加密Key" />
66
+ </t-form-item>
67
+ <t-form-item label="接收ID" name="feishu.receive_id">
68
+ <t-input v-model="settings.feishu.receive_id" placeholder="请输入飞书机器人接收ID" />
69
+ </t-form-item>
70
+
71
+
72
+ <t-form-item class="flex justify-center">
73
+ <t-button theme="primary" type="submit" :loading="loading">保存设置</t-button>
74
+ </t-form-item>
75
+ </t-card>
76
+
77
+
78
+ </t-form>
79
+ </div>
80
+ </template>
81
+
82
+ <style scoped></style>
src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />
test/index.spec.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // test/index.spec.ts
2
+ import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
3
+ import { describe, it, expect } from 'vitest';
4
+ import worker from '../src/index';
5
+
6
+ // For now, you'll need to do something like this to get a correctly-typed
7
+ // `Request` to pass to `worker.fetch()`.
8
+ const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
9
+
10
+ describe('Hello World worker', () => {
11
+ it('responds with Hello World! (unit style)', async () => {
12
+ const request = new IncomingRequest('http://example.com');
13
+ // Create an empty context to pass to `worker.fetch()`.
14
+ const ctx = createExecutionContext();
15
+ const response = await worker.fetch(request, env, ctx);
16
+ // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
17
+ await waitOnExecutionContext(ctx);
18
+ expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
19
+ });
20
+
21
+ it('responds with Hello World! (integration style)', async () => {
22
+ const response = await SELF.fetch('https://example.com');
23
+ expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
24
+ });
25
+ });
test/tsconfig.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers"]
5
+ },
6
+ "include": ["./**/*.ts", "../worker-configuration.d.ts"],
7
+ "exclude": []
8
+ }
tsconfig.app.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
3
+ "compilerOptions": {
4
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5
+
6
+ /* Linting */
7
+ "strict": true,
8
+ "noUnusedLocals": false,
9
+ "noUnusedParameters": false,
10
+ "noFallthroughCasesInSwitch": true,
11
+ "noUncheckedSideEffectImports": true
12
+ },
13
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
14
+ }
tsconfig.functions.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ /* Visit https://aka.ms/tsconfig.json to read more about this file */
4
+ /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
5
+ "target": "ESNext",
6
+ /* Specify a set of bundled library declaration files that describe the target runtime environment. */
7
+ "lib": [
8
+ "esnext"
9
+ ],
10
+ /* Specify what JSX code is generated. */
11
+ "jsx": "react-jsx",
12
+ /* Specify what module code is generated. */
13
+ "module": "ESNext",
14
+ /* Specify how TypeScript looks up a file from a given module specifier. */
15
+ "moduleResolution": "node",
16
+ /* Specify type package names to be included without being referenced in a source file. */
17
+ "types": [
18
+ //"@cloudflare/workers-types/2023-07-01",
19
+ "node"
20
+ ],
21
+ /* Enable importing .json files */
22
+ "resolveJsonModule": true,
23
+ /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
24
+ "allowJs": true,
25
+ /* Enable error reporting in type-checked JavaScript files. */
26
+ "checkJs": false,
27
+
28
+ /* Ensure that each file can be safely transpiled without relying on other imports. */
29
+ "isolatedModules": true,
30
+ /* Allow 'import x from y' when a module doesn't have a default export. */
31
+ "allowSyntheticDefaultImports": true,
32
+ /* Ensure that casing is correct in imports. */
33
+ "forceConsistentCasingInFileNames": true,
34
+ /* Enable all strict type-checking options. */
35
+ "strict": true,
36
+ /* Skip type checking all .d.ts files. */
37
+ "skipLibCheck": true,
38
+ "outDir": "./dist-server", // 输出到 dist
39
+ "noEmit": false, //允许编译器生成 JavaScript 输出文件
40
+ },
41
+ "exclude": [
42
+ "test"
43
+ ],
44
+ "include": [
45
+ "worker-configuration.d.ts",
46
+ "functions/types.d.ts",
47
+ "index.ts",
48
+ "functions/**/*.ts"
49
+ ]
50
+ }
tsconfig.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" },
6
+ { "path": "./tsconfig.functions.json" }
7
+ ]
8
+ }
tsconfig.node.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "isolatedModules": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedSideEffectImports": true
22
+ },
23
+ "include": ["vite.config.ts"]
24
+ }