github-actions[bot] commited on
Commit ·
7fc5208
1
Parent(s): 37f52e0
Update from GitHub Actions
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .cnb.yml +10 -0
- .editorconfig +12 -0
- .prettierrc +6 -0
- .vscode/settings.json +5 -0
- Dockerfile +59 -0
- auto-imports.d.ts +10 -0
- components.d.ts +39 -0
- functions/api/account.ts +58 -0
- functions/api/login.ts +42 -0
- functions/api/mail/all.ts +66 -0
- functions/api/mail/auth.ts +203 -0
- functions/api/mail/callback.ts +29 -0
- functions/api/mail/new.ts +66 -0
- functions/api/mail/send.ts +70 -0
- functions/api/setting.ts +51 -0
- functions/types.d.ts +43 -0
- functions/utils/auth.ts +41 -0
- functions/utils/cors.ts +37 -0
- functions/utils/emailVerification.ts +41 -0
- functions/utils/jwt.ts +87 -0
- functions/utils/mail.ts +256 -0
- index.html +12 -0
- index.ts +184 -0
- package-lock.json +0 -0
- package.json +50 -0
- public/vite.svg +1 -0
- src/App.vue +222 -0
- src/assets/base.css +3 -0
- src/assets/logo.png +0 -0
- src/assets/main.css +2 -0
- src/assets/vue.svg +1 -0
- src/components/MonacoEditor.vue +134 -0
- src/main.ts +16 -0
- src/router/index.ts +50 -0
- src/services/accountApi.ts +32 -0
- src/services/mailApi.ts +57 -0
- src/services/settingApi.ts +42 -0
- src/services/userApi.ts +22 -0
- src/services/util.ts +29 -0
- src/stores/counter.ts +12 -0
- src/style.css +79 -0
- src/views/AccountView.vue +137 -0
- src/views/LoginView.vue +147 -0
- src/views/MailView.vue +411 -0
- src/views/SettingView.vue +85 -0
- src/vite-env.d.ts +1 -0
- test/index.spec.ts +25 -0
- test/tsconfig.json +8 -0
- tsconfig.app.json +14 -0
- tsconfig.functions.json +50 -0
.cnb.yml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
master:
|
| 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:$GITHUB_TOKEN@github.com/zhezzma/msmail.git HEAD:master
|
.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,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
RouterLink: typeof import('vue-router')['RouterLink']
|
| 13 |
+
RouterView: typeof import('vue-router')['RouterView']
|
| 14 |
+
TAside: typeof import('tdesign-vue-next')['Aside']
|
| 15 |
+
TButton: typeof import('tdesign-vue-next')['Button']
|
| 16 |
+
TCard: typeof import('tdesign-vue-next')['Card']
|
| 17 |
+
TCheckbox: typeof import('tdesign-vue-next')['Checkbox']
|
| 18 |
+
TContent: typeof import('tdesign-vue-next')['Content']
|
| 19 |
+
TDialog: typeof import('tdesign-vue-next')['Dialog']
|
| 20 |
+
TDivider: typeof import('tdesign-vue-next')['Divider']
|
| 21 |
+
TDrawer: typeof import('tdesign-vue-next')['Drawer']
|
| 22 |
+
TDropdown: typeof import('tdesign-vue-next')['Dropdown']
|
| 23 |
+
TFooter: typeof import('tdesign-vue-next')['Footer']
|
| 24 |
+
TForm: typeof import('tdesign-vue-next')['Form']
|
| 25 |
+
TFormItem: typeof import('tdesign-vue-next')['FormItem']
|
| 26 |
+
THeader: typeof import('tdesign-vue-next')['Header']
|
| 27 |
+
TIcon: typeof import('tdesign-vue-next')['Icon']
|
| 28 |
+
TInput: typeof import('tdesign-vue-next')['Input']
|
| 29 |
+
TLayout: typeof import('tdesign-vue-next')['Layout']
|
| 30 |
+
TList: typeof import('tdesign-vue-next')['List']
|
| 31 |
+
TListItem: typeof import('tdesign-vue-next')['ListItem']
|
| 32 |
+
TLoading: typeof import('tdesign-vue-next')['Loading']
|
| 33 |
+
TMenu: typeof import('tdesign-vue-next')['Menu']
|
| 34 |
+
TMenuItem: typeof import('tdesign-vue-next')['MenuItem']
|
| 35 |
+
TPagination: typeof import('tdesign-vue-next')['Pagination']
|
| 36 |
+
TTable: typeof import('tdesign-vue-next')['Table']
|
| 37 |
+
TTextarea: typeof import('tdesign-vue-next')['Textarea']
|
| 38 |
+
}
|
| 39 |
+
}
|
functions/api/account.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { authMiddleware } from "../utils/auth.js";
|
| 2 |
+
import { addCorsHeaders } from "../utils/cors.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 addCorsHeaders(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 addCorsHeaders(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 addCorsHeaders(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 addCorsHeaders(new Response(JSON.stringify({ message: '保存成功' }), {
|
| 41 |
+
status: 200,
|
| 42 |
+
headers: { 'Content-Type': 'application/json' }
|
| 43 |
+
}));
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// 不支持的请求方法
|
| 47 |
+
return addCorsHeaders(new Response(JSON.stringify({ error: '不支持的请求方法' }), {
|
| 48 |
+
status: 405,
|
| 49 |
+
headers: { 'Content-Type': 'application/json' }
|
| 50 |
+
}));
|
| 51 |
+
|
| 52 |
+
} catch (error) {
|
| 53 |
+
return addCorsHeaders(new Response(JSON.stringify({ error: '服务器内部错误' }), {
|
| 54 |
+
status: 500,
|
| 55 |
+
headers: { 'Content-Type': 'application/json' }
|
| 56 |
+
}));
|
| 57 |
+
}
|
| 58 |
+
};
|
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/mail/all.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { authApiToken, authMiddleware } from "../../utils/auth.js";
|
| 2 |
+
import { addCorsHeaders } from "../../utils/cors.js";
|
| 3 |
+
import { get_access_token, getEmails } from "../../utils/mail.js";
|
| 4 |
+
|
| 5 |
+
export const onRequest = async (context: RouteContext): Promise<Response> => {
|
| 6 |
+
const request = context.request;
|
| 7 |
+
const env: Env = context.env;
|
| 8 |
+
|
| 9 |
+
// 验证权限
|
| 10 |
+
const authResponse = await authMiddleware(request, env);
|
| 11 |
+
const apiResponse = await authApiToken(request, env);
|
| 12 |
+
if (authResponse && apiResponse) {
|
| 13 |
+
return addCorsHeaders(authResponse);
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// 获取请求参数
|
| 17 |
+
const url = new URL(request.url);
|
| 18 |
+
const method = request.method;
|
| 19 |
+
const params: any = method === 'GET'
|
| 20 |
+
? Object.fromEntries(url.searchParams)
|
| 21 |
+
: await request.json();
|
| 22 |
+
|
| 23 |
+
const { email, limit = 10 } = params;
|
| 24 |
+
|
| 25 |
+
// 检查必要参数
|
| 26 |
+
if (!email) {
|
| 27 |
+
return new Response(
|
| 28 |
+
JSON.stringify({
|
| 29 |
+
error: 'Missing required parameters: email'
|
| 30 |
+
}),
|
| 31 |
+
{ status: 400 }
|
| 32 |
+
);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
try {
|
| 36 |
+
// 从KV获取刷新令牌
|
| 37 |
+
const tokenInfoStr = await env.KV.get(`refresh_token_${email}`);
|
| 38 |
+
if (!tokenInfoStr) {
|
| 39 |
+
throw new Error("No refresh token found for this email");
|
| 40 |
+
}
|
| 41 |
+
const tokenInfo = JSON.parse(tokenInfoStr);
|
| 42 |
+
const access_token = await get_access_token(tokenInfo, env.ENTRA_CLIENT_ID, env.ENTRA_CLIENT_SECRET);
|
| 43 |
+
const emails = await getEmails(access_token, Number(limit));
|
| 44 |
+
|
| 45 |
+
return new Response(
|
| 46 |
+
JSON.stringify(emails),
|
| 47 |
+
{
|
| 48 |
+
status: 200,
|
| 49 |
+
headers: {
|
| 50 |
+
'Content-Type': 'application/json',
|
| 51 |
+
'Access-Control-Allow-Origin': '*'
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
);
|
| 55 |
+
} catch (error: any) {
|
| 56 |
+
return new Response(
|
| 57 |
+
JSON.stringify({ error: error.message }),
|
| 58 |
+
{
|
| 59 |
+
status: 500, headers: {
|
| 60 |
+
'Content-Type': 'application/json',
|
| 61 |
+
'Access-Control-Allow-Origin': '*'
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
);
|
| 65 |
+
}
|
| 66 |
+
};
|
functions/api/mail/auth.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { authApiToken, authMiddleware } from "../../utils/auth.js";
|
| 2 |
+
import { addCorsHeaders } from "../../utils/cors.js";
|
| 3 |
+
import { chromium } from 'playwright';
|
| 4 |
+
import { getVerificationCode } from '../../utils/emailVerification.js';
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
export const onRequest = async (context: RouteContext): Promise<Response> => {
|
| 8 |
+
const request = context.request;
|
| 9 |
+
const env: Env = context.env;
|
| 10 |
+
|
| 11 |
+
// 验证权限
|
| 12 |
+
const authResponse = await authMiddleware(request, env);
|
| 13 |
+
const apiResponse = await authApiToken(request, env);
|
| 14 |
+
if (authResponse && apiResponse) {
|
| 15 |
+
return addCorsHeaders(authResponse);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
try {
|
| 19 |
+
const { email } = await request.json() as any;
|
| 20 |
+
if (!email) {
|
| 21 |
+
throw new Error("Email is required");
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// 获取账户信息
|
| 25 |
+
const accountsStr = await env.KV.get("accounts");
|
| 26 |
+
const accounts: any[] = accountsStr ? JSON.parse(accountsStr) : [];
|
| 27 |
+
const account = accounts.find(a => a.email === email);
|
| 28 |
+
if (!account) {
|
| 29 |
+
throw new Error("Account not found");
|
| 30 |
+
}
|
| 31 |
+
const clientId = env.ENTRA_CLIENT_ID;
|
| 32 |
+
const clientSecret = env.ENTRA_CLIENT_SECRET;
|
| 33 |
+
const redirectUri = env.AUTH_REDIRECT_URI;
|
| 34 |
+
let browser;
|
| 35 |
+
let context;
|
| 36 |
+
try {
|
| 37 |
+
browser = await chromium.launch({
|
| 38 |
+
headless: true,
|
| 39 |
+
args: [
|
| 40 |
+
'--no-sandbox',
|
| 41 |
+
'--disable-setuid-sandbox',
|
| 42 |
+
'--disable-dev-shm-usage',
|
| 43 |
+
'--disable-blink-features=AutomationControlled', // 禁用自动化特征
|
| 44 |
+
'--disable-infobars',
|
| 45 |
+
'--window-size=1920,1080'
|
| 46 |
+
],
|
| 47 |
+
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, // 使用系统 Chromium
|
| 48 |
+
});
|
| 49 |
+
context = await browser.newContext();
|
| 50 |
+
const page = await context.newPage();
|
| 51 |
+
|
| 52 |
+
const authUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` +
|
| 53 |
+
`client_id=${clientId}` +
|
| 54 |
+
`&response_type=code` +
|
| 55 |
+
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
| 56 |
+
`&response_mode=query` +
|
| 57 |
+
`&scope=offline_access%20IMAP.AccessAsUser.All%20User.Read%20Mail.ReadWrite.Shared%20Mail.Send%20Mail.Read` +
|
| 58 |
+
`&prompt=consent` + //强制显示权限确认窗口
|
| 59 |
+
`&state=${email}`;
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
console.log(authUrl)
|
| 63 |
+
|
| 64 |
+
//开始认证
|
| 65 |
+
await page.goto(authUrl);
|
| 66 |
+
await page.fill('input[type="email"]', account.email);
|
| 67 |
+
await page.click('input[type="submit"]');
|
| 68 |
+
|
| 69 |
+
try {
|
| 70 |
+
await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 3000 });
|
| 71 |
+
await page.click('#idA_PWD_SwitchToPassword');
|
| 72 |
+
} catch (error) {
|
| 73 |
+
console.log(`没有切换到密码登录,继续执行: ${error}`);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
try {
|
| 77 |
+
const timestamp = Math.floor(Date.now() / 1000);
|
| 78 |
+
await page.waitForSelector('#idA_PWD_SwitchToCredPicker', { timeout: 3000 });
|
| 79 |
+
|
| 80 |
+
try {
|
| 81 |
+
//处理email验证
|
| 82 |
+
const proof = [
|
| 83 |
+
{
|
| 84 |
+
"suffix": "godgodgame.com",
|
| 85 |
+
"apiUrl": "http://159.138.99.139:89/api/latest-email",
|
| 86 |
+
"token": env.PROOF_GODGODGAME_TOKEN
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"suffix": "igiven.com",
|
| 90 |
+
"apiUrl": "http://159.138.99.139:90/api/latest-email",
|
| 91 |
+
"token": env.PROOF_IGIVEN_TOKEN
|
| 92 |
+
}
|
| 93 |
+
]
|
| 94 |
+
|
| 95 |
+
const suffix = email.substring(email.indexOf('@') + 1);
|
| 96 |
+
const proofConfig = proof.find(p => p.suffix === suffix)!;
|
| 97 |
+
const verificationCode = await getVerificationCode(proofConfig.apiUrl, proofConfig.token!, account.proofEmail, timestamp);
|
| 98 |
+
|
| 99 |
+
await page.fill('input[type="tel"]', verificationCode);
|
| 100 |
+
await page.click('button[type="submit"]');
|
| 101 |
+
}
|
| 102 |
+
catch (error) {
|
| 103 |
+
throw new Error(`邮箱验证失败:${error}`)
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
await page.click('#idA_PWD_SwitchToCredPicker');
|
| 107 |
+
await page.click('#tileList > div:nth-child(2) Button');
|
| 108 |
+
|
| 109 |
+
} catch (error) {
|
| 110 |
+
console.log(`没有直接发邮件验证,继续执行: ${error}`);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
//输入密码
|
| 114 |
+
await page.waitForURL("https://login.live.com/**")
|
| 115 |
+
await page.fill('input[type="password"]', account.password);
|
| 116 |
+
await page.click('button[type="submit"]');
|
| 117 |
+
|
| 118 |
+
//确认登录
|
| 119 |
+
await page.waitForURL('https://login.live.com/ppsecure/**')
|
| 120 |
+
await page.click('button[type="submit"]#acceptButton'); // 同意按钮
|
| 121 |
+
|
| 122 |
+
//同意授权
|
| 123 |
+
try {
|
| 124 |
+
// 等待同意授权页面,如果超时5秒则跳过
|
| 125 |
+
await page.waitForURL("https://account.live.com/Consent/**", { timeout: 5000 })
|
| 126 |
+
await page.click('button[type="submit"][data-testid="appConsentPrimaryButton"]');
|
| 127 |
+
} catch (error) {
|
| 128 |
+
// 如果超时或页面未出现,直接继续执行
|
| 129 |
+
console.log("Consent page not found or timeout, skipping...");
|
| 130 |
+
}
|
| 131 |
+
await page.waitForURL((url)=>{
|
| 132 |
+
console.log(url)
|
| 133 |
+
return url.href.startsWith(redirectUri);
|
| 134 |
+
})
|
| 135 |
+
} finally {
|
| 136 |
+
if (context) await context.close();
|
| 137 |
+
if (browser) await browser.close();
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
// 等待回调处理完成,检查 KV 中是否有对应的 code
|
| 142 |
+
let code = null;
|
| 143 |
+
const maxRetries = 30;
|
| 144 |
+
let retries = 0;
|
| 145 |
+
|
| 146 |
+
while (!code && retries < maxRetries) {
|
| 147 |
+
const codeKey = `code_${email}`;
|
| 148 |
+
code = await env.KV.get(codeKey);
|
| 149 |
+
|
| 150 |
+
if (!code) {
|
| 151 |
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
| 152 |
+
retries++;
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
if (!code) {
|
| 157 |
+
throw new Error("Authorization timeout");
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// 使用授权码获取刷新令牌
|
| 161 |
+
const tokenResponse = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
| 162 |
+
method: 'POST',
|
| 163 |
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
| 164 |
+
body: new URLSearchParams({
|
| 165 |
+
client_id: clientId,
|
| 166 |
+
client_secret: clientSecret,
|
| 167 |
+
code: code,
|
| 168 |
+
redirect_uri: redirectUri,
|
| 169 |
+
grant_type: 'authorization_code'
|
| 170 |
+
})
|
| 171 |
+
});
|
| 172 |
+
|
| 173 |
+
const tokenData: any = await tokenResponse.json();
|
| 174 |
+
if (!tokenData.refresh_token) {
|
| 175 |
+
throw new Error("Failed to get refresh token");
|
| 176 |
+
}
|
| 177 |
+
if (!tokenData.expires_in) {
|
| 178 |
+
throw new Error("Missing expires_in in token response");
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// 存储刷新令牌和时间戳
|
| 182 |
+
const tokenInfo = {
|
| 183 |
+
...tokenData,
|
| 184 |
+
timestamp: Date.now()
|
| 185 |
+
};
|
| 186 |
+
console.log(tokenInfo);
|
| 187 |
+
await env.KV.put(`refresh_token_${email}`, JSON.stringify(tokenInfo));
|
| 188 |
+
|
| 189 |
+
// 删除临时授权码
|
| 190 |
+
await env.KV.delete(`code_${email}`);
|
| 191 |
+
|
| 192 |
+
return new Response(JSON.stringify({ success: true }), {
|
| 193 |
+
status: 200,
|
| 194 |
+
headers: { 'Content-Type': 'application/json' }
|
| 195 |
+
});
|
| 196 |
+
|
| 197 |
+
} catch (error: any) {
|
| 198 |
+
return new Response(
|
| 199 |
+
JSON.stringify({ error: error.message }),
|
| 200 |
+
{ status: 500 }
|
| 201 |
+
);
|
| 202 |
+
}
|
| 203 |
+
};
|
functions/api/mail/callback.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
export const onRequest = async (context: RouteContext): Promise<Response> => {
|
| 3 |
+
const request = context.request;
|
| 4 |
+
const env: Env = context.env;
|
| 5 |
+
|
| 6 |
+
try {
|
| 7 |
+
const url = new URL(request.url);
|
| 8 |
+
const code = url.searchParams.get('code');
|
| 9 |
+
const email = url.searchParams.get('state'); // 假设我们在state参数中传递了email
|
| 10 |
+
const error_Msg = url.searchParams.get('error_description');
|
| 11 |
+
if (!code || !email) {
|
| 12 |
+
throw new Error(`Missing code or email:${error_Msg}`);
|
| 13 |
+
}
|
| 14 |
+
console.log(code,email)
|
| 15 |
+
// 将授权码存储到KV中
|
| 16 |
+
await env.KV.put(`code_${email}`, code, { expirationTtl: 300 }); // 5分钟过期
|
| 17 |
+
|
| 18 |
+
return new Response("Authorization successful. You can close this window.", {
|
| 19 |
+
status: 200,
|
| 20 |
+
headers: { 'Content-Type': 'text/plain' }
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
} catch (error: any) {
|
| 24 |
+
return new Response(
|
| 25 |
+
JSON.stringify({ error: error.message }),
|
| 26 |
+
{ status: 500 }
|
| 27 |
+
);
|
| 28 |
+
}
|
| 29 |
+
};
|
functions/api/mail/new.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { authApiToken, authMiddleware } from "../../utils/auth.js";
|
| 2 |
+
import { addCorsHeaders } from "../../utils/cors.js";
|
| 3 |
+
import { get_access_token, getEmails } from "../../utils/mail.js";
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
export const onRequest = async (context: RouteContext): Promise<Response> => {
|
| 7 |
+
const request = context.request;
|
| 8 |
+
const env: Env = context.env;
|
| 9 |
+
|
| 10 |
+
// 验证权限
|
| 11 |
+
const authResponse = await authMiddleware(request, env);
|
| 12 |
+
const apiResponse = await authApiToken(request, env);
|
| 13 |
+
if (authResponse && apiResponse) {
|
| 14 |
+
|
| 15 |
+
return addCorsHeaders(authResponse);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// 获取请求参数
|
| 19 |
+
const url = new URL(request.url);
|
| 20 |
+
const method = request.method;
|
| 21 |
+
const params: any = method === 'GET'
|
| 22 |
+
? Object.fromEntries(url.searchParams)
|
| 23 |
+
: await request.json();
|
| 24 |
+
|
| 25 |
+
const { email, mailbox = "INBOX ", response_type = 'json' } = params;
|
| 26 |
+
|
| 27 |
+
// 检查必要参数
|
| 28 |
+
if (!email) {
|
| 29 |
+
return new Response(
|
| 30 |
+
JSON.stringify({
|
| 31 |
+
error: 'Missing required parameters: email'
|
| 32 |
+
}),
|
| 33 |
+
{ status: 400 }
|
| 34 |
+
);
|
| 35 |
+
}
|
| 36 |
+
try {
|
| 37 |
+
// 从KV获取刷新令牌
|
| 38 |
+
const tokenInfoStr = await env.KV.get(`refresh_token_${email}`);
|
| 39 |
+
if (!tokenInfoStr) {
|
| 40 |
+
throw new Error("No refresh token found for this email");
|
| 41 |
+
}
|
| 42 |
+
const tokenInfo = JSON.parse(tokenInfoStr);
|
| 43 |
+
const access_token = await get_access_token(tokenInfo, env.ENTRA_CLIENT_ID, env.ENTRA_CLIENT_SECRET);
|
| 44 |
+
const emails = await getEmails(access_token, 1); // 只获取最新的一封邮件
|
| 45 |
+
return new Response(
|
| 46 |
+
JSON.stringify(emails[0] || null),
|
| 47 |
+
{
|
| 48 |
+
status: 200,
|
| 49 |
+
headers: {
|
| 50 |
+
'Content-Type': 'application/json',
|
| 51 |
+
'Access-Control-Allow-Origin': '*'
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
);
|
| 55 |
+
} catch (error: any) {
|
| 56 |
+
return new Response(
|
| 57 |
+
JSON.stringify({ error: error.message }),
|
| 58 |
+
{
|
| 59 |
+
status: 500, headers: {
|
| 60 |
+
'Content-Type': 'application/json',
|
| 61 |
+
'Access-Control-Allow-Origin': '*'
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
);
|
| 65 |
+
}
|
| 66 |
+
};
|
functions/api/mail/send.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { authApiToken, authMiddleware } from "../../utils/auth.js";
|
| 2 |
+
import { addCorsHeaders } from "../../utils/cors.js";
|
| 3 |
+
import { get_access_token, sendEmail } from "../../utils/mail.js";
|
| 4 |
+
|
| 5 |
+
export const onRequest = async (context: RouteContext): Promise<Response> => {
|
| 6 |
+
const request = context.request;
|
| 7 |
+
const env: Env = context.env;
|
| 8 |
+
|
| 9 |
+
// 验证权限
|
| 10 |
+
const authResponse = await authMiddleware(request, env);
|
| 11 |
+
const apiResponse = await authApiToken(request, env);
|
| 12 |
+
if (authResponse && apiResponse) {
|
| 13 |
+
return addCorsHeaders(authResponse);
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
const method = request.method;
|
| 17 |
+
if (method !== 'POST') {
|
| 18 |
+
return new Response(
|
| 19 |
+
JSON.stringify({ error: 'Method not allowed' }),
|
| 20 |
+
{ status: 405 }
|
| 21 |
+
);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
try {
|
| 25 |
+
const { email, to, subject, body, isHtml = false } = await request.json() as any;
|
| 26 |
+
|
| 27 |
+
// 检查必要参数
|
| 28 |
+
if (!email || !to || !subject || !body) {
|
| 29 |
+
return new Response(
|
| 30 |
+
JSON.stringify({
|
| 31 |
+
error: 'Missing required parameters: email, to, subject, body'
|
| 32 |
+
}),
|
| 33 |
+
{ status: 400 }
|
| 34 |
+
);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// 从KV获取刷新令牌
|
| 38 |
+
const tokenInfoStr = await env.KV.get(`refresh_token_${email}`);
|
| 39 |
+
if (!tokenInfoStr) {
|
| 40 |
+
throw new Error("No refresh token found for this email");
|
| 41 |
+
}
|
| 42 |
+
const tokenInfo = JSON.parse(tokenInfoStr);
|
| 43 |
+
const access_token = await get_access_token(tokenInfo, env.ENTRA_CLIENT_ID, env.ENTRA_CLIENT_SECRET);
|
| 44 |
+
|
| 45 |
+
// 发送邮件
|
| 46 |
+
await sendEmail(access_token, Array.isArray(to) ? to : [to], subject, body, isHtml);
|
| 47 |
+
|
| 48 |
+
return new Response(
|
| 49 |
+
JSON.stringify({ message: 'Email sent successfully' }),
|
| 50 |
+
{
|
| 51 |
+
status: 200,
|
| 52 |
+
headers: {
|
| 53 |
+
'Content-Type': 'application/json',
|
| 54 |
+
'Access-Control-Allow-Origin': '*'
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
);
|
| 58 |
+
} catch (error: any) {
|
| 59 |
+
return new Response(
|
| 60 |
+
JSON.stringify({ error: error.message }),
|
| 61 |
+
{
|
| 62 |
+
status: 500, headers: {
|
| 63 |
+
'Content-Type': 'application/json',
|
| 64 |
+
'Access-Control-Allow-Origin': '*'
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
);
|
| 68 |
+
}
|
| 69 |
+
};
|
| 70 |
+
|
functions/api/setting.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import { authMiddleware } from "../utils/auth.js";
|
| 3 |
+
import { addCorsHeaders } from "../utils/cors.js";
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
export const onRequest = async (context: RouteContext): Promise<Response> => {
|
| 7 |
+
const request = context.request;
|
| 8 |
+
const env = context.env as Env;
|
| 9 |
+
|
| 10 |
+
const authResponse = await authMiddleware(request, env);
|
| 11 |
+
if (authResponse) {
|
| 12 |
+
return addCorsHeaders(authResponse);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const KV_KEY = "settings"
|
| 16 |
+
|
| 17 |
+
try {
|
| 18 |
+
// GET 请求处理
|
| 19 |
+
if (request.method === 'GET') {
|
| 20 |
+
const settings = await env.KV.get(KV_KEY);
|
| 21 |
+
return addCorsHeaders(new Response(settings || '{}', {
|
| 22 |
+
status: 200,
|
| 23 |
+
headers: { 'Content-Type': 'application/json' }
|
| 24 |
+
}));
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// POST 请求处理
|
| 28 |
+
if (request.method === 'POST') {
|
| 29 |
+
const data = await request.json();
|
| 30 |
+
// 存储账号数据
|
| 31 |
+
await env.KV.put(KV_KEY, JSON.stringify(data));
|
| 32 |
+
|
| 33 |
+
return addCorsHeaders(new Response(JSON.stringify({ message: '保存成功' }), {
|
| 34 |
+
status: 200,
|
| 35 |
+
headers: { 'Content-Type': 'application/json' }
|
| 36 |
+
}));
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// 不支持的请求方法
|
| 40 |
+
return addCorsHeaders(new Response(JSON.stringify({ error: '不支持的请求方法' }), {
|
| 41 |
+
status: 405,
|
| 42 |
+
headers: { 'Content-Type': 'application/json' }
|
| 43 |
+
}));
|
| 44 |
+
|
| 45 |
+
} catch (error) {
|
| 46 |
+
return addCorsHeaders(new Response(JSON.stringify({ error: '服务器内部错误' }), {
|
| 47 |
+
status: 500,
|
| 48 |
+
headers: { 'Content-Type': 'application/json' }
|
| 49 |
+
}));
|
| 50 |
+
}
|
| 51 |
+
};
|
functions/types.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
PASSWORD: string; // 密码
|
| 14 |
+
ENTRA_CLIENT_ID:string;
|
| 15 |
+
ENTRA_CLIENT_SECRET:string;
|
| 16 |
+
AUTH_REDIRECT_URI:string;
|
| 17 |
+
PROOF_GODGODGAME_TOKEN:string;
|
| 18 |
+
PROOF_IGIVEN_TOKEN:string;
|
| 19 |
+
KV: KVNamespace;
|
| 20 |
+
ASSETS:any;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
/**
|
| 24 |
+
* 登录凭证接口
|
| 25 |
+
*/
|
| 26 |
+
interface LoginCredentials {
|
| 27 |
+
/** 用户名 */
|
| 28 |
+
username: string;
|
| 29 |
+
/** 密码 */
|
| 30 |
+
password: string;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
interface RouteContext {
|
| 35 |
+
request: Request;
|
| 36 |
+
functionPath: string;
|
| 37 |
+
waitUntil: (promise: Promise<any>) => void;
|
| 38 |
+
passThroughOnException: () => void;
|
| 39 |
+
next: (input?: Request | string, init?: RequestInit) => Promise<Response>;
|
| 40 |
+
env: Env;
|
| 41 |
+
params: any;
|
| 42 |
+
data: any;
|
| 43 |
+
}
|
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/cors.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* CORS 相关的响应头配置
|
| 3 |
+
*/
|
| 4 |
+
export const CORS_HEADERS = {
|
| 5 |
+
'Access-Control-Allow-Origin': '*', // 允许所有来源
|
| 6 |
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', // 允许的HTTP方法
|
| 7 |
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With', // 允许的请求头
|
| 8 |
+
'Access-Control-Max-Age': '86400', // 预检请求的有效期
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* 处理 OPTIONS 预检请求
|
| 13 |
+
*/
|
| 14 |
+
export function handleOptions(): Response {
|
| 15 |
+
return new Response(null, {
|
| 16 |
+
status: 204,
|
| 17 |
+
headers: CORS_HEADERS
|
| 18 |
+
});
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* 为响应添加 CORS 头
|
| 23 |
+
* @param response 原始响应对象
|
| 24 |
+
* @returns 添加了 CORS 头的新响应对象
|
| 25 |
+
*/
|
| 26 |
+
export function addCorsHeaders(response: Response): Response {
|
| 27 |
+
const newHeaders = new Headers(response.headers);
|
| 28 |
+
Object.entries(CORS_HEADERS).forEach(([key, value]) => {
|
| 29 |
+
newHeaders.set(key, value);
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
return new Response(response.body, {
|
| 33 |
+
status: response.status,
|
| 34 |
+
statusText: response.statusText,
|
| 35 |
+
headers: newHeaders
|
| 36 |
+
});
|
| 37 |
+
}
|
functions/utils/emailVerification.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
export async function getVerificationCode(proofApi: string, apiKey: String, proofEmail: string, timestamp: number): Promise<string> {
|
| 4 |
+
const maxRetries = 30;
|
| 5 |
+
console.log(`开始获取验证码${proofApi},${apiKey},${timestamp},${new Date(timestamp * 1000)}`);
|
| 6 |
+
for (let i = 0; i < maxRetries; i++) {
|
| 7 |
+
try {
|
| 8 |
+
const params = new URLSearchParams({
|
| 9 |
+
to: proofEmail,
|
| 10 |
+
from: 'account-security-noreply@accountprotection.microsoft.com',
|
| 11 |
+
timestamp: timestamp.toString()
|
| 12 |
+
});
|
| 13 |
+
|
| 14 |
+
const url = `${proofApi}?${params.toString()}`;
|
| 15 |
+
const response = await fetch(url, {
|
| 16 |
+
headers: {
|
| 17 |
+
'Authorization': `Bearer ${apiKey}`
|
| 18 |
+
},
|
| 19 |
+
method: 'GET'
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
if (response.status === 200) {
|
| 23 |
+
const data: any = await response.json();
|
| 24 |
+
const match = data.text.match(/:\s*(\d+)\n\n/);
|
| 25 |
+
if (match) {
|
| 26 |
+
console.log(proofEmail, `获取验证码成功: ${match[1]}`);
|
| 27 |
+
return match[1];
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
else {
|
| 31 |
+
console.log(proofEmail, `获取验证码失败: ${await response.text()}`);
|
| 32 |
+
}
|
| 33 |
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
| 34 |
+
} catch (error) {
|
| 35 |
+
if (i === maxRetries - 1) {
|
| 36 |
+
throw new Error("Failed to get verification code after maximum retries");
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
throw new Error("Failed to get verification code");
|
| 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 |
+
}
|
functions/utils/mail.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import PostalMime from 'postal-mime';
|
| 2 |
+
|
| 3 |
+
export async function get_access_token(tokenInfo: any, client_id: string, clientSecret: string) {
|
| 4 |
+
// 检查token是否过期(提前5分钟刷新)
|
| 5 |
+
const now = Date.now();
|
| 6 |
+
const expiryTime = tokenInfo.timestamp + (tokenInfo.expires_in * 1000);
|
| 7 |
+
const shouldRefresh = now >= (expiryTime - 300000); // 5分钟 = 300000毫秒
|
| 8 |
+
if (!shouldRefresh) {
|
| 9 |
+
return tokenInfo.access_token;
|
| 10 |
+
}
|
| 11 |
+
const response = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
| 12 |
+
method: 'POST',
|
| 13 |
+
headers: {
|
| 14 |
+
'Content-Type': 'application/x-www-form-urlencoded'
|
| 15 |
+
},
|
| 16 |
+
body: new URLSearchParams({
|
| 17 |
+
'client_id': client_id,
|
| 18 |
+
'client_secret': clientSecret,
|
| 19 |
+
'grant_type': 'refresh_token',
|
| 20 |
+
'refresh_token': tokenInfo.refresh_token
|
| 21 |
+
})
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
if (!response.ok) {
|
| 25 |
+
const errorText = await response.text();
|
| 26 |
+
throw new Error(`HTTP error! status: ${response.status}, response: ${errorText}`);
|
| 27 |
+
}
|
| 28 |
+
const data = await response.json() as any;
|
| 29 |
+
return data.access_token;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
//https://learn.microsoft.com/zh-cn/graph/api/resources/mail-api-overview?view=graph-rest-1.0
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* 获取邮件列表
|
| 36 |
+
*/
|
| 37 |
+
interface ParsedEmail {
|
| 38 |
+
id: string;
|
| 39 |
+
subject: string;
|
| 40 |
+
from: string;
|
| 41 |
+
to: string[];
|
| 42 |
+
date: {
|
| 43 |
+
created: string;
|
| 44 |
+
received: string;
|
| 45 |
+
sent: string;
|
| 46 |
+
modified: string;
|
| 47 |
+
};
|
| 48 |
+
text: string;
|
| 49 |
+
html: string;
|
| 50 |
+
importance: string;
|
| 51 |
+
isRead: boolean;
|
| 52 |
+
isDraft: boolean;
|
| 53 |
+
hasAttachments: boolean;
|
| 54 |
+
webLink: string;
|
| 55 |
+
preview: string;
|
| 56 |
+
categories: string[];
|
| 57 |
+
internetMessageId: string;
|
| 58 |
+
metadata: {
|
| 59 |
+
platform?: string;
|
| 60 |
+
browser?: string;
|
| 61 |
+
ip?: string;
|
| 62 |
+
location?: string;
|
| 63 |
+
};
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
async function parseEmail(email: any): Promise<ParsedEmail> {
|
| 67 |
+
const parser = new PostalMime();
|
| 68 |
+
let content = '';
|
| 69 |
+
|
| 70 |
+
try {
|
| 71 |
+
if (email.body.content) {
|
| 72 |
+
content = email.body.content;
|
| 73 |
+
} else {
|
| 74 |
+
const response = await fetch(email.body.contentUrl);
|
| 75 |
+
content = await response.text();
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
const parsed = await parser.parse(content);
|
| 79 |
+
|
| 80 |
+
// 从HTML内容中提取元数据
|
| 81 |
+
const htmlContent = email.body.content || '';
|
| 82 |
+
const platformMatch = htmlContent.match(/Platform:\s*([^<\r\n]+)/);
|
| 83 |
+
const browserMatch = htmlContent.match(/Browser:\s*([^<\r\n]+)/);
|
| 84 |
+
const ipMatch = htmlContent.match(/IP address:\s*([^<\r\n]+)/);
|
| 85 |
+
const locationMatch = htmlContent.match(/Country\/region:\s*([^<\r\n]+)/);
|
| 86 |
+
|
| 87 |
+
return {
|
| 88 |
+
id: email.id,
|
| 89 |
+
subject: email.subject,
|
| 90 |
+
from: email.from.emailAddress.address,
|
| 91 |
+
to: email.toRecipients.map((r: any) => r.emailAddress.address),
|
| 92 |
+
date: {
|
| 93 |
+
created: email.createdDateTime,
|
| 94 |
+
received: email.receivedDateTime,
|
| 95 |
+
sent: email.sentDateTime,
|
| 96 |
+
modified: email.lastModifiedDateTime
|
| 97 |
+
},
|
| 98 |
+
text: parsed.text || email.bodyPreview || '',
|
| 99 |
+
html: parsed.html || email.body.content || '',
|
| 100 |
+
importance: email.importance,
|
| 101 |
+
isRead: email.isRead,
|
| 102 |
+
isDraft: email.isDraft,
|
| 103 |
+
hasAttachments: email.hasAttachments,
|
| 104 |
+
webLink: email.webLink,
|
| 105 |
+
preview: email.bodyPreview,
|
| 106 |
+
categories: email.categories,
|
| 107 |
+
internetMessageId: email.internetMessageId,
|
| 108 |
+
metadata: {
|
| 109 |
+
platform: platformMatch?.[1]?.trim(),
|
| 110 |
+
browser: browserMatch?.[1]?.trim(),
|
| 111 |
+
ip: ipMatch?.[1]?.trim(),
|
| 112 |
+
location: locationMatch?.[1]?.trim(),
|
| 113 |
+
}
|
| 114 |
+
};
|
| 115 |
+
} catch (error) {
|
| 116 |
+
console.error('解析邮件失败:', error);
|
| 117 |
+
return {
|
| 118 |
+
id: email.id,
|
| 119 |
+
subject: email.subject,
|
| 120 |
+
from: email.from.emailAddress.address,
|
| 121 |
+
to: email.toRecipients.map((r: any) => r.emailAddress.address),
|
| 122 |
+
date: {
|
| 123 |
+
created: email.createdDateTime,
|
| 124 |
+
received: email.receivedDateTime,
|
| 125 |
+
sent: email.sentDateTime,
|
| 126 |
+
modified: email.lastModifiedDateTime
|
| 127 |
+
},
|
| 128 |
+
text: email.bodyPreview || '',
|
| 129 |
+
html: email.body.content || '',
|
| 130 |
+
importance: email.importance,
|
| 131 |
+
isRead: email.isRead,
|
| 132 |
+
isDraft: email.isDraft,
|
| 133 |
+
hasAttachments: email.hasAttachments,
|
| 134 |
+
webLink: email.webLink,
|
| 135 |
+
preview: email.bodyPreview,
|
| 136 |
+
categories: email.categories,
|
| 137 |
+
internetMessageId: email.internetMessageId,
|
| 138 |
+
metadata: {}
|
| 139 |
+
};
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
export async function getEmails(accessToken: string, limit = 50): Promise<ParsedEmail[]> {
|
| 144 |
+
const endpoint = 'https://graph.microsoft.com/v1.0/me/messages';
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
console.log(accessToken)
|
| 148 |
+
const response = await fetch(`${endpoint}?$top=${limit}`, {
|
| 149 |
+
method: 'GET',
|
| 150 |
+
headers: {
|
| 151 |
+
'Authorization': `Bearer ${accessToken}`,
|
| 152 |
+
'Content-Type': 'application/json'
|
| 153 |
+
}
|
| 154 |
+
});
|
| 155 |
+
|
| 156 |
+
const data = await response.json() as any;
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
if (!response.ok) {
|
| 161 |
+
throw new Error(`获取邮件失败: ${data.error?.message}`);
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
const emails = data.value;
|
| 165 |
+
console.log(emails)
|
| 166 |
+
const parsedEmails = await Promise.all(emails.map(parseEmail));
|
| 167 |
+
return parsedEmails;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/**
|
| 171 |
+
* 删除单个邮件
|
| 172 |
+
*/
|
| 173 |
+
export async function deleteEmail(accessToken: string, emailId: string): Promise<void> {
|
| 174 |
+
const endpoint = `https://graph.microsoft.com/v1.0/me/messages/${emailId}`;
|
| 175 |
+
|
| 176 |
+
const response = await fetch(endpoint, {
|
| 177 |
+
method: 'DELETE',
|
| 178 |
+
headers: {
|
| 179 |
+
'Authorization': `Bearer ${accessToken}`
|
| 180 |
+
}
|
| 181 |
+
});
|
| 182 |
+
|
| 183 |
+
if (!response.ok) {
|
| 184 |
+
const errorData = await response.json() as any;
|
| 185 |
+
throw new Error(`删除邮件失败: ${errorData.error?.message}`);
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
/**
|
| 190 |
+
* 清空邮箱 (批量删除邮件)
|
| 191 |
+
*/
|
| 192 |
+
export async function emptyMailbox(accessToken: string, batchSize = 50): Promise<void> {
|
| 193 |
+
let hasMoreEmails = true;
|
| 194 |
+
let totalDeleted = 0;
|
| 195 |
+
|
| 196 |
+
while (hasMoreEmails) {
|
| 197 |
+
// 获取一批邮件
|
| 198 |
+
const emails = await getEmails(accessToken, batchSize);
|
| 199 |
+
|
| 200 |
+
if (emails.length === 0) {
|
| 201 |
+
hasMoreEmails = false;
|
| 202 |
+
break;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
// 删除该批次的每封邮件
|
| 206 |
+
const deletePromises = emails.map(email => deleteEmail(accessToken, email.id));
|
| 207 |
+
await Promise.all(deletePromises);
|
| 208 |
+
|
| 209 |
+
totalDeleted += emails.length;
|
| 210 |
+
console.log(`已删除 ${emails.length} 封邮件,累计: ${totalDeleted}`);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
console.log('邮箱已清空');
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
/**
|
| 217 |
+
* 发送邮件
|
| 218 |
+
*/
|
| 219 |
+
export async function sendEmail(
|
| 220 |
+
accessToken: string,
|
| 221 |
+
to: string[],
|
| 222 |
+
subject: string,
|
| 223 |
+
body: string,
|
| 224 |
+
isHtml = false
|
| 225 |
+
): Promise<void> {
|
| 226 |
+
const endpoint = 'https://graph.microsoft.com/v1.0/me/sendMail';
|
| 227 |
+
|
| 228 |
+
const emailData = {
|
| 229 |
+
message: {
|
| 230 |
+
subject,
|
| 231 |
+
body: {
|
| 232 |
+
contentType: isHtml ? 'HTML' : 'Text',
|
| 233 |
+
content: body
|
| 234 |
+
},
|
| 235 |
+
toRecipients: to.map(recipient => ({
|
| 236 |
+
emailAddress: {
|
| 237 |
+
address: recipient
|
| 238 |
+
}
|
| 239 |
+
}))
|
| 240 |
+
}
|
| 241 |
+
};
|
| 242 |
+
|
| 243 |
+
const response = await fetch(endpoint, {
|
| 244 |
+
method: 'POST',
|
| 245 |
+
headers: {
|
| 246 |
+
'Authorization': `Bearer ${accessToken}`,
|
| 247 |
+
'Content-Type': 'application/json'
|
| 248 |
+
},
|
| 249 |
+
body: JSON.stringify(emailData)
|
| 250 |
+
});
|
| 251 |
+
|
| 252 |
+
if (!response.ok) {
|
| 253 |
+
const errorData = await response.json() as any;
|
| 254 |
+
throw new Error(`发送邮件失败: ${errorData.error?.message}`);
|
| 255 |
+
}
|
| 256 |
+
}
|
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,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
import { onRequest as handleMailAuth } from './functions/api/mail/auth.js'
|
| 21 |
+
import { onRequest as handleMailCallback } from './functions/api/mail/callback.js'
|
| 22 |
+
import { onRequest as handleMailAll } from './functions/api/mail/all.js'
|
| 23 |
+
import { onRequest as handleMailNew } from './functions/api/mail/new.js'
|
| 24 |
+
import { onRequest as handleMailSend } from './functions/api/mail/send.js'
|
| 25 |
+
|
| 26 |
+
dotenv.config({ path: ['.env', '.env.local'], override: true });
|
| 27 |
+
const isDev = process.env.NODE_ENV === 'development'
|
| 28 |
+
|
| 29 |
+
const app = new Hono<{ Bindings: Env }>()
|
| 30 |
+
app.use(compress());
|
| 31 |
+
app.use(prettyJSON());
|
| 32 |
+
app.use(trimTrailingSlash());
|
| 33 |
+
app.use('*', cors({
|
| 34 |
+
origin: '*',
|
| 35 |
+
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
| 36 |
+
allowHeaders: ['Content-Type', 'Authorization'],
|
| 37 |
+
exposeHeaders: ['Content-Length'],
|
| 38 |
+
credentials: true,
|
| 39 |
+
}));
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
const storage = createStorage({
|
| 43 |
+
driver: cloudflareKVHTTPDriver({
|
| 44 |
+
accountId: process.env.CLOUDFLARE_ACCOUNT_ID || "",
|
| 45 |
+
namespaceId: process.env.CLOUDFLARE_NAMESPACE_ID || "",
|
| 46 |
+
apiToken: process.env.CLOUDFLARE_API_TOKEN || "",
|
| 47 |
+
}),
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
var kv: KVNamespace = {
|
| 51 |
+
get: async (key: string) => {
|
| 52 |
+
const value = await storage.getItemRaw(key);
|
| 53 |
+
return value as string;
|
| 54 |
+
},
|
| 55 |
+
put: async (key: string, value: string) => {
|
| 56 |
+
await storage.setItem(key, value);
|
| 57 |
+
},
|
| 58 |
+
delete:async(key:string)=>{
|
| 59 |
+
await storage.removeItem(key);
|
| 60 |
+
}
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
app.use('*', async (c, next) => {
|
| 64 |
+
c.env.KV = kv;
|
| 65 |
+
await next()
|
| 66 |
+
})
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
const scriptPath = fileURLToPath(import.meta.url)
|
| 70 |
+
const scriptDir = dirname(scriptPath)
|
| 71 |
+
const rootDir = isDev ? dirname(scriptPath) : dirname(scriptDir)
|
| 72 |
+
const currentDir = process.cwd();
|
| 73 |
+
let staticPath = path.relative(currentDir, rootDir);
|
| 74 |
+
if (!isDev) {
|
| 75 |
+
staticPath = path.relative(currentDir, path.join(rootDir, "dist"))
|
| 76 |
+
}
|
| 77 |
+
console.log('Script dir:', scriptDir)
|
| 78 |
+
console.log('Root dir:', rootDir)
|
| 79 |
+
console.log('Current dir:', currentDir);
|
| 80 |
+
console.log('Relative path for static files:', staticPath || '.');
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
const createContext = (c: Context) => {
|
| 85 |
+
const eventContext: RouteContext = {
|
| 86 |
+
request: c.req.raw,
|
| 87 |
+
functionPath: c.req.path,
|
| 88 |
+
waitUntil: (promise: Promise<any>) => {
|
| 89 |
+
if (c.executionCtx?.waitUntil) {
|
| 90 |
+
c.executionCtx.waitUntil(promise);
|
| 91 |
+
}
|
| 92 |
+
},
|
| 93 |
+
passThroughOnException: () => {
|
| 94 |
+
if (c.executionCtx?.passThroughOnException) {
|
| 95 |
+
c.executionCtx.passThroughOnException();
|
| 96 |
+
}
|
| 97 |
+
},
|
| 98 |
+
next: async (input?: Request | string, init?: RequestInit) => {
|
| 99 |
+
if (typeof input === 'string') {
|
| 100 |
+
return fetch(input, init);
|
| 101 |
+
} else if (input instanceof Request) {
|
| 102 |
+
return fetch(input);
|
| 103 |
+
}
|
| 104 |
+
return new Response('Not Found', { status: 404 });
|
| 105 |
+
},
|
| 106 |
+
env: {
|
| 107 |
+
...c.env,
|
| 108 |
+
ASSETS: {
|
| 109 |
+
fetch: fetch.bind(globalThis)
|
| 110 |
+
}
|
| 111 |
+
},
|
| 112 |
+
params: c.req.param(),
|
| 113 |
+
// 可以从 c.get() 获取数据,或者传入空对象
|
| 114 |
+
data: c.get('data') || {}
|
| 115 |
+
};
|
| 116 |
+
return eventContext;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
app.all('/api/*', async (c) => {
|
| 120 |
+
try {
|
| 121 |
+
const context = createContext(c);
|
| 122 |
+
const path = c.req.path;
|
| 123 |
+
// 根据路径匹配对应的处理函数
|
| 124 |
+
let response: Response;
|
| 125 |
+
switch (path) {
|
| 126 |
+
case '/api/account':
|
| 127 |
+
response = await handleAccount(context);
|
| 128 |
+
break;
|
| 129 |
+
case '/api/login':
|
| 130 |
+
response = await handleLogin(context);
|
| 131 |
+
break;
|
| 132 |
+
case '/api/setting':
|
| 133 |
+
response = await handleSetting(context);
|
| 134 |
+
break;
|
| 135 |
+
case '/api/mail/auth':
|
| 136 |
+
response = await handleMailAuth(context);
|
| 137 |
+
break;
|
| 138 |
+
case '/api/mail/callback':
|
| 139 |
+
response = await handleMailCallback(context);
|
| 140 |
+
break;
|
| 141 |
+
case '/api/mail/all':
|
| 142 |
+
response = await handleMailAll(context);
|
| 143 |
+
break;
|
| 144 |
+
case '/api/mail/new':
|
| 145 |
+
response = await handleMailNew(context);
|
| 146 |
+
break;
|
| 147 |
+
case '/api/mail/send':
|
| 148 |
+
response = await handleMailSend(context);
|
| 149 |
+
break;
|
| 150 |
+
default:
|
| 151 |
+
return c.json({ error: 'Route not found' }, 404);
|
| 152 |
+
}
|
| 153 |
+
return response;
|
| 154 |
+
} catch (error) {
|
| 155 |
+
return c.json({ error: (error as Error).message }, 500);
|
| 156 |
+
}
|
| 157 |
+
})
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
// 中间件配置
|
| 161 |
+
app.get('/*', serveStatic({
|
| 162 |
+
root: staticPath,
|
| 163 |
+
rewriteRequestPath: (path) => {
|
| 164 |
+
return path === '/' ? '/index.html' : path;
|
| 165 |
+
},
|
| 166 |
+
onFound: async (path, c) => {
|
| 167 |
+
console.log('Found:', path)
|
| 168 |
+
},
|
| 169 |
+
onNotFound: async (path, c) => {
|
| 170 |
+
console.log('Not Found:', path)
|
| 171 |
+
}
|
| 172 |
+
}))
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
// 启动服务器
|
| 176 |
+
const port = parseInt(process.env.PORT || '8788')
|
| 177 |
+
serve({
|
| 178 |
+
fetch: (request: Request, env) => app.fetch(request, { ...env, ...process.env }),
|
| 179 |
+
port
|
| 180 |
+
}, () => {
|
| 181 |
+
console.log(`Server running at http://localhost:${port}`)
|
| 182 |
+
})
|
| 183 |
+
|
| 184 |
+
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,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"playwright": "^1.51.0",
|
| 26 |
+
"postal-mime": "^2.4.3",
|
| 27 |
+
"tailwindcss": "^4.0.14",
|
| 28 |
+
"tdesign-vue-next": "^1.11.4",
|
| 29 |
+
"unstorage": "^1.15.0",
|
| 30 |
+
"vue": "^3.5.13",
|
| 31 |
+
"vue-router": "^4.5.0"
|
| 32 |
+
},
|
| 33 |
+
"devDependencies": {
|
| 34 |
+
"@cloudflare/vitest-pool-workers": "^0.7.5",
|
| 35 |
+
"@cloudflare/workers-types": "^4.20250313.0",
|
| 36 |
+
"@types/node": "^22.10.2",
|
| 37 |
+
"@vitejs/plugin-vue": "^5.2.1",
|
| 38 |
+
"@vue/tsconfig": "^0.7.0",
|
| 39 |
+
"concurrently": "^8.2.2",
|
| 40 |
+
"cross-env": "^7.0.3",
|
| 41 |
+
"tsx": "^4.7.1",
|
| 42 |
+
"typescript": "^5.5.2",
|
| 43 |
+
"unplugin-auto-import": "^19.0.0",
|
| 44 |
+
"unplugin-vue-components": "^28.0.0",
|
| 45 |
+
"vite": "^6.2.0",
|
| 46 |
+
"vitest": "~3.0.7",
|
| 47 |
+
"vue-tsc": "^2.2.4",
|
| 48 |
+
"wrangler": "^4.0.0"
|
| 49 |
+
}
|
| 50 |
+
}
|
public/vite.svg
ADDED
|
|
src/App.vue
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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: '/mail',
|
| 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 |
+
<component :is="Component" />
|
| 128 |
+
</transition>
|
| 129 |
+
</router-view>
|
| 130 |
+
</t-content>
|
| 131 |
+
|
| 132 |
+
<!-- 页脚 -->
|
| 133 |
+
<t-footer class="footer backdrop-blur-sm py-4 text-center text-sm text-gray-600">
|
| 134 |
+
<span class="opacity-75">© 微软邮箱管理系统</span>
|
| 135 |
+
</t-footer>
|
| 136 |
+
</t-layout>
|
| 137 |
+
|
| 138 |
+
</t-layout>
|
| 139 |
+
</template>
|
| 140 |
+
|
| 141 |
+
<style scoped>
|
| 142 |
+
@reference "./assets/base.css";
|
| 143 |
+
|
| 144 |
+
.sidebar {
|
| 145 |
+
@apply bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-800;
|
| 146 |
+
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.header {
|
| 150 |
+
@apply bg-white/80 sticky top-0 z-10;
|
| 151 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.menu-item {
|
| 155 |
+
@apply transition-all duration-300 hover:bg-white/15 active:scale-95;
|
| 156 |
+
@apply flex items-center gap-3 px-4 py-3;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.logout-btn {
|
| 160 |
+
@apply transition-transform hover:scale-105 active:scale-95;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.fade-slide-enter-active,
|
| 164 |
+
.fade-slide-leave-active {
|
| 165 |
+
transition: all 0.3s ease;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.fade-slide-enter-from {
|
| 169 |
+
opacity: 0;
|
| 170 |
+
transform: translateY(20px);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.fade-slide-leave-to {
|
| 174 |
+
opacity: 0;
|
| 175 |
+
transform: translateY(-20px);
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
/* :deep(.t-default-menu.t-menu--dark){
|
| 179 |
+
@apply bg-transparent;
|
| 180 |
+
} */
|
| 181 |
+
|
| 182 |
+
:deep(.t-drawer__body) {
|
| 183 |
+
@apply p-0;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
:deep(.t-drawer__body) {
|
| 187 |
+
@apply p-0;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
:deep(::-webkit-scrollbar) {
|
| 191 |
+
@apply w-2;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
:deep(::-webkit-scrollbar-thumb) {
|
| 195 |
+
@apply bg-gray-400/30 rounded-full transition-colors hover:bg-gray-400/50;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
:deep(::-webkit-scrollbar-track) {
|
| 199 |
+
@apply bg-transparent;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.content {
|
| 203 |
+
background-image: radial-gradient(circle at 50% 50%,
|
| 204 |
+
rgba(255, 255, 255, 0.8) 0%,
|
| 205 |
+
rgba(240, 240, 250, 0.6) 100%);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.footer {
|
| 209 |
+
@apply bg-white/60;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
/* 添加移动端响应式样式 */
|
| 213 |
+
@media (max-width: 1024px) {
|
| 214 |
+
.sidebar {
|
| 215 |
+
display: none;
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.drawer-menu {
|
| 220 |
+
@apply bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-800;
|
| 221 |
+
}
|
| 222 |
+
</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,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 的变化
|
| 89 |
+
watch([() => props.value, () => props.originalValue], ([newVal, newOriginalVal], [oldVal, oldOriginalVal]) => {
|
| 90 |
+
// 判断是否需要重建编辑器
|
| 91 |
+
const modeChanged =
|
| 92 |
+
(oldOriginalVal === undefined && newOriginalVal !== undefined) ||
|
| 93 |
+
(oldOriginalVal !== undefined && newOriginalVal === undefined);
|
| 94 |
+
|
| 95 |
+
if (modeChanged) {
|
| 96 |
+
// 编辑器模式变化,重新创建
|
| 97 |
+
createEditor();
|
| 98 |
+
} else if (editorInstance) {
|
| 99 |
+
if (props.originalValue !== undefined) {
|
| 100 |
+
// 差异模式下更新内容
|
| 101 |
+
const model = editorInstance.getModel();
|
| 102 |
+
if (model && model.original && model.original.getValue() !== newOriginalVal) {
|
| 103 |
+
model.original.setValue(newOriginalVal || '');
|
| 104 |
+
}
|
| 105 |
+
if (model && model.modified && model.modified.getValue() !== newVal) {
|
| 106 |
+
model.modified.setValue(newVal || '');
|
| 107 |
+
}
|
| 108 |
+
} else {
|
| 109 |
+
// 普通模式下更新内容
|
| 110 |
+
if (editorInstance.getValue() !== newVal) {
|
| 111 |
+
editorInstance.setValue(newVal || '');
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}, { deep: true });
|
| 116 |
+
|
| 117 |
+
onBeforeUnmount(() => {
|
| 118 |
+
if (editorInstance) {
|
| 119 |
+
editorInstance.dispose();
|
| 120 |
+
}
|
| 121 |
+
if (editorModel) {
|
| 122 |
+
editorModel.dispose();
|
| 123 |
+
}
|
| 124 |
+
editorInstance = null;
|
| 125 |
+
editorModel = null;
|
| 126 |
+
});
|
| 127 |
+
</script>
|
| 128 |
+
|
| 129 |
+
<style scoped>
|
| 130 |
+
:deep(.monaco-diff-editor),
|
| 131 |
+
:deep(.monaco-editor) {
|
| 132 |
+
height: 100%;
|
| 133 |
+
}
|
| 134 |
+
</style>
|
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,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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: '/mail',
|
| 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: '/mail',
|
| 20 |
+
name: 'Mail',
|
| 21 |
+
component: () => import('../views/MailView.vue'),
|
| 22 |
+
meta: { requiresAuth: true }
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
path: '/setting',
|
| 26 |
+
name: 'Setting',
|
| 27 |
+
component: () => import('../views/SettingView.vue'),
|
| 28 |
+
meta: { requiresAuth: true }
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
path: '/account',
|
| 32 |
+
name: 'Account',
|
| 33 |
+
component: () => import('../views/AccountView.vue'),
|
| 34 |
+
meta: { requiresAuth: true }
|
| 35 |
+
},
|
| 36 |
+
],
|
| 37 |
+
})
|
| 38 |
+
// 添加路由守卫
|
| 39 |
+
router.beforeEach((to, from, next) => {
|
| 40 |
+
const isAuthenticated = localStorage.getItem('isAuthenticated') === 'true'
|
| 41 |
+
if (to.meta.requiresAuth && !isAuthenticated) {
|
| 42 |
+
next('/login')
|
| 43 |
+
} else if (to.path === '/login' && isAuthenticated) {
|
| 44 |
+
next('/')
|
| 45 |
+
} else {
|
| 46 |
+
next()
|
| 47 |
+
}
|
| 48 |
+
})
|
| 49 |
+
|
| 50 |
+
export default router
|
src/services/accountApi.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { API_BASE_URL, getHeaders, handleResponse } from './util';
|
| 2 |
+
|
| 3 |
+
export interface Account {
|
| 4 |
+
email: string;
|
| 5 |
+
password: string;
|
| 6 |
+
proofEmail: string;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export const accountApi = {
|
| 10 |
+
async post(accounts: Account[]) {
|
| 11 |
+
const response = await fetch(
|
| 12 |
+
`${API_BASE_URL}/api/account`,
|
| 13 |
+
{
|
| 14 |
+
headers: getHeaders(),
|
| 15 |
+
method: 'POST',
|
| 16 |
+
body: JSON.stringify(accounts)
|
| 17 |
+
}
|
| 18 |
+
);
|
| 19 |
+
return handleResponse(response);
|
| 20 |
+
},
|
| 21 |
+
|
| 22 |
+
async get(): Promise<Account[]> {
|
| 23 |
+
const response = await fetch(
|
| 24 |
+
`${API_BASE_URL}/api/account`,
|
| 25 |
+
{
|
| 26 |
+
headers: getHeaders()
|
| 27 |
+
}
|
| 28 |
+
);
|
| 29 |
+
return handleResponse(response);
|
| 30 |
+
},
|
| 31 |
+
|
| 32 |
+
}
|
src/services/mailApi.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { API_BASE_URL, getHeaders, handleResponse } from './util';
|
| 2 |
+
|
| 3 |
+
export const mailApi = {
|
| 4 |
+
async refreshAuth(email: string) {
|
| 5 |
+
const response = await fetch(
|
| 6 |
+
`${API_BASE_URL}/api/mail/auth`,
|
| 7 |
+
{
|
| 8 |
+
headers: getHeaders(),
|
| 9 |
+
method: 'POST',
|
| 10 |
+
body: JSON.stringify({ email })
|
| 11 |
+
}
|
| 12 |
+
);
|
| 13 |
+
return handleResponse(response);
|
| 14 |
+
},
|
| 15 |
+
|
| 16 |
+
async getLatestMails(email: string) {
|
| 17 |
+
const response = await fetch(
|
| 18 |
+
`${API_BASE_URL}/api/mail/new`,
|
| 19 |
+
{
|
| 20 |
+
headers: getHeaders(),
|
| 21 |
+
method: 'POST',
|
| 22 |
+
body: JSON.stringify({ email })
|
| 23 |
+
}
|
| 24 |
+
);
|
| 25 |
+
return handleResponse(response);
|
| 26 |
+
},
|
| 27 |
+
|
| 28 |
+
async getAllMails(email: string) {
|
| 29 |
+
const response = await fetch(
|
| 30 |
+
`${API_BASE_URL}/api/mail/all`,
|
| 31 |
+
{
|
| 32 |
+
headers: getHeaders(),
|
| 33 |
+
method: 'POST',
|
| 34 |
+
body: JSON.stringify({ email })
|
| 35 |
+
}
|
| 36 |
+
);
|
| 37 |
+
return handleResponse(response);
|
| 38 |
+
},
|
| 39 |
+
|
| 40 |
+
async sendMail(params: {
|
| 41 |
+
email: string;
|
| 42 |
+
to: string[];
|
| 43 |
+
subject: string;
|
| 44 |
+
body: string;
|
| 45 |
+
isHtml?: boolean;
|
| 46 |
+
}) {
|
| 47 |
+
const response = await fetch(
|
| 48 |
+
`${API_BASE_URL}/api/mail/send`,
|
| 49 |
+
{
|
| 50 |
+
headers: getHeaders(),
|
| 51 |
+
method: 'POST',
|
| 52 |
+
body: JSON.stringify(params)
|
| 53 |
+
}
|
| 54 |
+
);
|
| 55 |
+
return handleResponse(response);
|
| 56 |
+
}
|
| 57 |
+
};
|
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/counter.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ref, computed } from 'vue'
|
| 2 |
+
import { defineStore } from 'pinia'
|
| 3 |
+
|
| 4 |
+
export const useCounterStore = defineStore('counter', () => {
|
| 5 |
+
const count = ref(0)
|
| 6 |
+
const doubleCount = computed(() => count.value * 2)
|
| 7 |
+
function increment() {
|
| 8 |
+
count.value++
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
return { count, doubleCount, increment }
|
| 12 |
+
})
|
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,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 6 |
+
import MonacoEditor from '../components/MonacoEditor.vue';
|
| 7 |
+
|
| 8 |
+
const accountsText = ref<string>('');
|
| 9 |
+
const loading = ref(false);
|
| 10 |
+
const showDiff = ref(false);
|
| 11 |
+
const originalText = ref('');
|
| 12 |
+
|
| 13 |
+
const fetchAccounts = async () => {
|
| 14 |
+
try {
|
| 15 |
+
loading.value = true;
|
| 16 |
+
accountsText.value = "";
|
| 17 |
+
let data = await accountApi.get();
|
| 18 |
+
const formattedData = JSON.stringify(data, 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 |
+
await fetchAccounts();
|
| 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 |
+
// 定义按键事件处理函数
|
| 60 |
+
const handleKeyDown = (e: KeyboardEvent) => {
|
| 61 |
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
|
| 62 |
+
e.preventDefault();
|
| 63 |
+
handleSave();
|
| 64 |
+
}
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
// 在显示差异按钮点击处理中添加
|
| 69 |
+
const toggleDiff = () => {
|
| 70 |
+
showDiff.value = !showDiff.value;
|
| 71 |
+
|
| 72 |
+
if (showDiff.value) {
|
| 73 |
+
// 进入差异模式时,确保有原始文本作为比较
|
| 74 |
+
if (originalText.value === '') {
|
| 75 |
+
originalText.value = accountsText.value;
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
};
|
| 79 |
+
// 在 AccountView.vue 中添加
|
| 80 |
+
watch(showDiff, (newVal) => {
|
| 81 |
+
if (newVal && originalText.value === accountsText.value) {
|
| 82 |
+
// 如果开启差异模式但两个文本相同,可以考虑提示用户
|
| 83 |
+
MessagePlugin.info('当前没有差异可以显示');
|
| 84 |
+
}
|
| 85 |
+
}, { immediate: true });
|
| 86 |
+
|
| 87 |
+
onMounted(() => {
|
| 88 |
+
// 注册全局按键监听
|
| 89 |
+
window.addEventListener('keydown', handleKeyDown);
|
| 90 |
+
fetchAccounts();
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
onUnmounted(() => {
|
| 94 |
+
// 注销全局按键监听
|
| 95 |
+
window.removeEventListener('keydown', handleKeyDown);
|
| 96 |
+
});
|
| 97 |
+
</script>
|
| 98 |
+
|
| 99 |
+
<template>
|
| 100 |
+
<div class="account-container h-full p-2 md:p-5">
|
| 101 |
+
<t-card bordered class="h-full">
|
| 102 |
+
<template #content>
|
| 103 |
+
<div class=" flex flex-col h-full">
|
| 104 |
+
<div class="flex justify-between items-center mb-4 gap-4">
|
| 105 |
+
<div class="flex gap-2">
|
| 106 |
+
<t-button variant="outline" @click="toggleDiff">
|
| 107 |
+
{{ showDiff ? '隐藏对比' : '显示对比' }}
|
| 108 |
+
</t-button>
|
| 109 |
+
<t-button theme="primary" @click="handleSave" :loading="loading">
|
| 110 |
+
保存账号
|
| 111 |
+
</t-button>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<div class="editor-container flex-1">
|
| 116 |
+
<MonacoEditor v-model:value="accountsText" :original-value="showDiff ? originalText : undefined"
|
| 117 |
+
language="json" :options="{ tabSize: 2 }" />
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
</template>
|
| 121 |
+
</t-card>
|
| 122 |
+
</div>
|
| 123 |
+
</template>
|
| 124 |
+
|
| 125 |
+
<style scoped>
|
| 126 |
+
.account-container {
|
| 127 |
+
width: 100%;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
:deep(.t-card__body) {
|
| 131 |
+
height: 100%;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.editor-container {
|
| 135 |
+
border: 1px solid var(--td-component-border);
|
| 136 |
+
}
|
| 137 |
+
</style>
|
src/views/LoginView.vue
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
const handleLogin = async () => {
|
| 17 |
+
if (!username.value || !password.value) {
|
| 18 |
+
MessagePlugin.warning('请输入用户名和密码')
|
| 19 |
+
return
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
loading.value = true
|
| 23 |
+
try {
|
| 24 |
+
const data = await userApi.login(username.value, password.value)
|
| 25 |
+
|
| 26 |
+
if (data.success) {
|
| 27 |
+
localStorage.setItem('token', data.token)
|
| 28 |
+
localStorage.setItem('isAuthenticated', 'true')
|
| 29 |
+
|
| 30 |
+
MessagePlugin.success('登录成功')
|
| 31 |
+
router.push('/')
|
| 32 |
+
} else {
|
| 33 |
+
MessagePlugin.error(data.message || '登录失败')
|
| 34 |
+
}
|
| 35 |
+
} catch (error) {
|
| 36 |
+
MessagePlugin.error('网络错误,请稍后重试')
|
| 37 |
+
console.error('Login error:', error)
|
| 38 |
+
} finally {
|
| 39 |
+
loading.value = false
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
</script>
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
<template>
|
| 46 |
+
<div class="min-h-screen w-full flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50">
|
| 47 |
+
<div class="relative w-full max-w-md">
|
| 48 |
+
<!-- 装饰背景 -->
|
| 49 |
+
<div 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"></div>
|
| 50 |
+
<div 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"></div>
|
| 51 |
+
<div 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"></div>
|
| 52 |
+
|
| 53 |
+
<!-- 登录卡片 -->
|
| 54 |
+
<div class="relative bg-white/80 backdrop-blur-lg rounded-2xl shadow-xl p-8 m-4">
|
| 55 |
+
<!-- Logo和标题 -->
|
| 56 |
+
<div class="flex flex-col items-center mb-8">
|
| 57 |
+
<img :src="logo" alt="logo" class="w-16 h-16 mb-4" />
|
| 58 |
+
<h2 class="text-2xl font-bold text-gray-800">微软账号管理</h2>
|
| 59 |
+
<!-- <p class="text-gray-500 mt-2">登录以继续使用</p> -->
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<!-- 登录表单 -->
|
| 63 |
+
<form @submit.prevent="handleLogin" class="space-y-6">
|
| 64 |
+
<div class="space-y-2">
|
| 65 |
+
<t-input
|
| 66 |
+
v-model="username"
|
| 67 |
+
size="large"
|
| 68 |
+
placeholder="请输入用户名"
|
| 69 |
+
:autofocus="true"
|
| 70 |
+
class="w-full"
|
| 71 |
+
>
|
| 72 |
+
<template #prefix-icon>
|
| 73 |
+
<t-icon name="user" />
|
| 74 |
+
</template>
|
| 75 |
+
</t-input>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
<div class="space-y-2">
|
| 79 |
+
<t-input
|
| 80 |
+
v-model="password"
|
| 81 |
+
type="password"
|
| 82 |
+
size="large"
|
| 83 |
+
placeholder="请输入密码"
|
| 84 |
+
class="w-full"
|
| 85 |
+
>
|
| 86 |
+
<template #prefix-icon>
|
| 87 |
+
<t-icon name="lock-on" />
|
| 88 |
+
</template>
|
| 89 |
+
</t-input>
|
| 90 |
+
</div>
|
| 91 |
+
|
| 92 |
+
<!-- <div class="flex items-center justify-between text-sm">
|
| 93 |
+
<t-checkbox>记住我</t-checkbox>
|
| 94 |
+
<a href="#" class="text-blue-600 hover:text-blue-700 transition-colors">
|
| 95 |
+
忘记密码?
|
| 96 |
+
</a>
|
| 97 |
+
</div> -->
|
| 98 |
+
|
| 99 |
+
<t-button
|
| 100 |
+
type="submit"
|
| 101 |
+
theme="primary"
|
| 102 |
+
:loading="loading"
|
| 103 |
+
size="large"
|
| 104 |
+
class="w-full"
|
| 105 |
+
:disabled="loading"
|
| 106 |
+
>
|
| 107 |
+
{{ loading ? '登录中...' : '登录' }}
|
| 108 |
+
</t-button>
|
| 109 |
+
</form>
|
| 110 |
+
|
| 111 |
+
<!-- 额外信息 -->
|
| 112 |
+
<div class="mt-6 text-center text-sm text-gray-500">
|
| 113 |
+
测试账号: admin / password
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
</template>
|
| 119 |
+
|
| 120 |
+
<style scoped>
|
| 121 |
+
.animate-blob {
|
| 122 |
+
animation: blob 7s infinite;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.animation-delay-2000 {
|
| 126 |
+
animation-delay: 2s;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.animation-delay-4000 {
|
| 130 |
+
animation-delay: 4s;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
@keyframes blob {
|
| 134 |
+
0% {
|
| 135 |
+
transform: translate(0px, 0px) scale(1);
|
| 136 |
+
}
|
| 137 |
+
33% {
|
| 138 |
+
transform: translate(30px, -50px) scale(1.1);
|
| 139 |
+
}
|
| 140 |
+
66% {
|
| 141 |
+
transform: translate(-20px, 20px) scale(0.9);
|
| 142 |
+
}
|
| 143 |
+
100% {
|
| 144 |
+
transform: translate(0px, 0px) scale(1);
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
</style>
|
src/views/MailView.vue
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script setup lang="ts">
|
| 2 |
+
import { computed, ref, watch, onMounted } from 'vue';
|
| 3 |
+
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next';
|
| 4 |
+
import { accountApi, type Account } from '../services/accountApi';
|
| 5 |
+
import { mailApi } from '../services/mailApi';
|
| 6 |
+
const loading = ref(false);
|
| 7 |
+
const pagination = ref({
|
| 8 |
+
current: 1,
|
| 9 |
+
total: 0,
|
| 10 |
+
pageSize: 18
|
| 11 |
+
});
|
| 12 |
+
|
| 13 |
+
const allData = ref<Account[]>([]);
|
| 14 |
+
const userSearch = ref(''); // 添加用户搜索字段
|
| 15 |
+
const sort = ref<{ sortBy: string; descending: boolean }>({
|
| 16 |
+
sortBy: '',
|
| 17 |
+
descending: true
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
const columns = [
|
| 23 |
+
{ colKey: 'email', title: '用户', width: 200, sorter: true },
|
| 24 |
+
{
|
| 25 |
+
colKey: 'action',
|
| 26 |
+
title: '操作',
|
| 27 |
+
width: 400, // 增加宽度以容纳多个按钮
|
| 28 |
+
}
|
| 29 |
+
];
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
// 添加下拉菜单选项函数
|
| 33 |
+
const actionOptions = (row: Account) => [
|
| 34 |
+
{ content: '刷新认证', value: 'refresh', onClick: () => handleRefreshAuth(row) },
|
| 35 |
+
{ content: '最新邮件', value: 'latest', onClick: () => handleLatestMails(row) },
|
| 36 |
+
{ content: '所有邮件', value: 'all', onClick: () => handleAllMails(row) },
|
| 37 |
+
{ content: '发送邮件', value: 'send', onClick: () => handleSendMail(row) },
|
| 38 |
+
];
|
| 39 |
+
|
| 40 |
+
// 实现处理函数
|
| 41 |
+
const handleRefreshAuth = async (row: Account) => {
|
| 42 |
+
try {
|
| 43 |
+
loading.value = true;
|
| 44 |
+
await mailApi.refreshAuth(row.email); // 假设此API存在
|
| 45 |
+
MessagePlugin.success('认证已刷新');
|
| 46 |
+
|
| 47 |
+
} catch (error) {
|
| 48 |
+
MessagePlugin.error('刷新认证失败');
|
| 49 |
+
} finally {
|
| 50 |
+
loading.value = false;
|
| 51 |
+
}
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
interface ParsedEmail {
|
| 55 |
+
subject: string;
|
| 56 |
+
from: string;
|
| 57 |
+
to: string[];
|
| 58 |
+
date: string;
|
| 59 |
+
text: string;
|
| 60 |
+
html: string;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
const showEmailDialog = ref(false);
|
| 64 |
+
const currentEmail = ref<any>(null);
|
| 65 |
+
|
| 66 |
+
const handleLatestMails = async (row: Account) => {
|
| 67 |
+
try {
|
| 68 |
+
loading.value = true;
|
| 69 |
+
const result = await mailApi.getLatestMails(row.email);
|
| 70 |
+
if (result) {
|
| 71 |
+
currentEmail.value = result;
|
| 72 |
+
showEmailDialog.value = true;
|
| 73 |
+
} else {
|
| 74 |
+
MessagePlugin.info('没有找到邮件');
|
| 75 |
+
}
|
| 76 |
+
} catch (error:any) {
|
| 77 |
+
MessagePlugin.error(`${error.message}`);
|
| 78 |
+
} finally {
|
| 79 |
+
loading.value = false;
|
| 80 |
+
}
|
| 81 |
+
};
|
| 82 |
+
|
| 83 |
+
// 添加所有邮件列表相关的状态
|
| 84 |
+
const showAllMailsDialog = ref(false);
|
| 85 |
+
const allMailsList = ref<any[]>([]);
|
| 86 |
+
const mailsLoading = ref(false);
|
| 87 |
+
|
| 88 |
+
const handleAllMails = async (row: Account) => {
|
| 89 |
+
try {
|
| 90 |
+
mailsLoading.value = true;
|
| 91 |
+
showAllMailsDialog.value = true;
|
| 92 |
+
allMailsList.value= []
|
| 93 |
+
const emails = await mailApi.getAllMails(row.email);
|
| 94 |
+
allMailsList.value = emails;
|
| 95 |
+
} catch (error: any) {
|
| 96 |
+
MessagePlugin.error(`获取邮件失败: ${error.message}`);
|
| 97 |
+
} finally {
|
| 98 |
+
mailsLoading.value = false;
|
| 99 |
+
}
|
| 100 |
+
};
|
| 101 |
+
|
| 102 |
+
// 用于打开指定邮件详情
|
| 103 |
+
const viewMailDetail = (email: any) => {
|
| 104 |
+
currentEmail.value = email;
|
| 105 |
+
showEmailDialog.value = true;
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
// 添加发送邮件相关的状态
|
| 111 |
+
const showSendDialog = ref(false);
|
| 112 |
+
const sendMailForm = ref({
|
| 113 |
+
to: '',
|
| 114 |
+
subject: '',
|
| 115 |
+
body: '',
|
| 116 |
+
isHtml: false
|
| 117 |
+
});
|
| 118 |
+
const currentMailAccount = ref<Account | null>(null);
|
| 119 |
+
|
| 120 |
+
// 修改发送邮件处理函数
|
| 121 |
+
const handleSendMail = async (row: Account) => {
|
| 122 |
+
currentMailAccount.value = row;
|
| 123 |
+
sendMailForm.value = {
|
| 124 |
+
to: '',
|
| 125 |
+
subject: '',
|
| 126 |
+
body: '',
|
| 127 |
+
isHtml: false
|
| 128 |
+
};
|
| 129 |
+
showSendDialog.value = true;
|
| 130 |
+
};
|
| 131 |
+
|
| 132 |
+
// 添加发送邮件提交函数
|
| 133 |
+
const submitSendMail = async () => {
|
| 134 |
+
if (!currentMailAccount.value) return;
|
| 135 |
+
|
| 136 |
+
try {
|
| 137 |
+
loading.value = true;
|
| 138 |
+
await mailApi.sendMail({
|
| 139 |
+
email: currentMailAccount.value.email,
|
| 140 |
+
to: sendMailForm.value.to.split(',').map(e => e.trim()),
|
| 141 |
+
subject: sendMailForm.value.subject,
|
| 142 |
+
body: sendMailForm.value.body,
|
| 143 |
+
isHtml: sendMailForm.value.isHtml
|
| 144 |
+
});
|
| 145 |
+
MessagePlugin.success('邮件发送成功');
|
| 146 |
+
showSendDialog.value = false;
|
| 147 |
+
} catch (error: any) {
|
| 148 |
+
MessagePlugin.error(`发送失败: ${error.message}`);
|
| 149 |
+
} finally {
|
| 150 |
+
loading.value = false;
|
| 151 |
+
}
|
| 152 |
+
};
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
const handleSort = (sortInfo: { sortBy: string; descending: boolean }) => {
|
| 156 |
+
sort.value = sortInfo;
|
| 157 |
+
pagination.value.current = 1;
|
| 158 |
+
};
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
// 使用计算属性处理排序和分页
|
| 162 |
+
const processedData = computed(() => {
|
| 163 |
+
let result = [...allData.value];
|
| 164 |
+
|
| 165 |
+
// 用户搜索过滤
|
| 166 |
+
if (userSearch.value) {
|
| 167 |
+
result = result.filter(log =>
|
| 168 |
+
log.email.toLowerCase().includes(userSearch.value.toLowerCase())
|
| 169 |
+
);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
pagination.value.total = result.length; // 更新总数以反映过滤后的结果
|
| 173 |
+
|
| 174 |
+
// 分页处理
|
| 175 |
+
const start = (pagination.value.current - 1) * pagination.value.pageSize;
|
| 176 |
+
const end = start + pagination.value.pageSize;
|
| 177 |
+
return result.slice(start, end);
|
| 178 |
+
});
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
const fetchData = async () => {
|
| 182 |
+
loading.value = true;
|
| 183 |
+
try {
|
| 184 |
+
const data = await accountApi.get();
|
| 185 |
+
allData.value = data;
|
| 186 |
+
pagination.value.total = data.length;
|
| 187 |
+
} catch (error) {
|
| 188 |
+
MessagePlugin.error('获取数据失败');
|
| 189 |
+
} finally {
|
| 190 |
+
loading.value = false;
|
| 191 |
+
}
|
| 192 |
+
};
|
| 193 |
+
|
| 194 |
+
// 添加 onMounted 钩子
|
| 195 |
+
onMounted(() => {
|
| 196 |
+
fetchData();
|
| 197 |
+
});
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
</script>
|
| 201 |
+
|
| 202 |
+
<template>
|
| 203 |
+
<div class="w-full h-full flex flex-col p-2 md:p-5 gap-2 md:gap-5">
|
| 204 |
+
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
| 205 |
+
<div class="flex flex-col md:flex-row md:items-center gap-4">
|
| 206 |
+
<t-input v-model="userSearch" class="w-full md:w-40" placeholder="搜索用户" clearable />
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
+
|
| 210 |
+
<div class="overflow-x-auto flex-1 overflow-y-auto">
|
| 211 |
+
<t-table :data="processedData" :loading="loading" :columns="columns" row-key="datetime" hover
|
| 212 |
+
:sort="sort" @sort-change="handleSort" size="small" class="min-w-full">
|
| 213 |
+
<template #action="{ row }">
|
| 214 |
+
<!-- 桌面端显示 -->
|
| 215 |
+
<div class="hidden md:flex flex-wrap gap-2">
|
| 216 |
+
<t-button size="small" variant="outline" @click="handleRefreshAuth(row)">刷新认证</t-button>
|
| 217 |
+
<t-button size="small" variant="outline" @click="handleLatestMails(row)">最新邮件</t-button>
|
| 218 |
+
<t-button size="small" variant="outline" @click="handleAllMails(row)">所有邮件</t-button>
|
| 219 |
+
<t-button size="small" variant="outline" @click="handleSendMail(row)">发送邮件</t-button>
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
<!-- 移动端显示 -->
|
| 223 |
+
<div class="md:hidden">
|
| 224 |
+
<t-dropdown :options="actionOptions(row)">
|
| 225 |
+
<t-button variant="outline" size="small">操作 <t-icon name="chevron-down" /></t-button>
|
| 226 |
+
</t-dropdown>
|
| 227 |
+
</div>
|
| 228 |
+
</template>
|
| 229 |
+
</t-table>
|
| 230 |
+
</div>
|
| 231 |
+
|
| 232 |
+
<div class="flex justify-end">
|
| 233 |
+
<t-pagination v-model="pagination.current" :total="pagination.total" :page-size="pagination.pageSize"
|
| 234 |
+
size="small" />
|
| 235 |
+
</div>
|
| 236 |
+
|
| 237 |
+
<!-- 邮件详情对话框 -->
|
| 238 |
+
<t-dialog
|
| 239 |
+
:visible="showEmailDialog"
|
| 240 |
+
:header="currentEmail?.subject"
|
| 241 |
+
@close="showEmailDialog = false"
|
| 242 |
+
:width="800"
|
| 243 |
+
:footer="false"
|
| 244 |
+
class="email-detail-dialog"
|
| 245 |
+
>
|
| 246 |
+
<template v-if="currentEmail">
|
| 247 |
+
<div class="email-details">
|
| 248 |
+
<div class="email-meta">
|
| 249 |
+
<p><strong>发件人:</strong> {{ currentEmail.from }}</p>
|
| 250 |
+
<p><strong>收件人:</strong> {{ currentEmail.to?.join(', ') }}</p>
|
| 251 |
+
<p><strong>时间:</strong> {{ new Date(currentEmail.date.received).toLocaleString() }}</p>
|
| 252 |
+
<p v-if="currentEmail.metadata?.location"><strong>位置:</strong> {{ currentEmail.metadata.location }}</p>
|
| 253 |
+
<p v-if="currentEmail.metadata?.ip"><strong>IP:</strong> {{ currentEmail.metadata.ip }}</p>
|
| 254 |
+
<p v-if="currentEmail.metadata?.platform"><strong>平台:</strong> {{ currentEmail.metadata.platform }}</p>
|
| 255 |
+
<p v-if="currentEmail.metadata?.browser"><strong>浏览器:</strong> {{ currentEmail.metadata.browser }}</p>
|
| 256 |
+
<p><strong>重要性:</strong> {{ currentEmail.importance }}</p>
|
| 257 |
+
<p><strong>状态:</strong> {{ currentEmail.isRead ? '已读' : '未读' }}</p>
|
| 258 |
+
</div>
|
| 259 |
+
<div class="email-content" v-html="currentEmail.html || currentEmail.text"></div>
|
| 260 |
+
<div v-if="currentEmail.hasAttachments" class="email-attachments">包含附件</div>
|
| 261 |
+
<div class="email-actions">
|
| 262 |
+
<a :href="currentEmail.webLink" target="_blank">在Outlook中查看</a>
|
| 263 |
+
</div>
|
| 264 |
+
</div>
|
| 265 |
+
</template>
|
| 266 |
+
</t-dialog>
|
| 267 |
+
|
| 268 |
+
<!-- 所有邮件列表对话框 -->
|
| 269 |
+
<t-dialog
|
| 270 |
+
:visible="showAllMailsDialog"
|
| 271 |
+
header="所有邮件"
|
| 272 |
+
@close="showAllMailsDialog = false"
|
| 273 |
+
:width="900"
|
| 274 |
+
:footer="false"
|
| 275 |
+
>
|
| 276 |
+
<t-loading :loading="mailsLoading">
|
| 277 |
+
<div class="emails-list">
|
| 278 |
+
<t-list>
|
| 279 |
+
<t-list-item
|
| 280 |
+
v-for="email in allMailsList"
|
| 281 |
+
:key="email.id"
|
| 282 |
+
@click="viewMailDetail(email)"
|
| 283 |
+
class="email-list-item"
|
| 284 |
+
>
|
| 285 |
+
<div class="flex flex-col gap-1 w-full cursor-pointer hover:bg-gray-50 p-2">
|
| 286 |
+
<div class="flex justify-between">
|
| 287 |
+
<span class="font-medium">{{ email.subject || '(无主题)' }}</span>
|
| 288 |
+
<span class="text-gray-500 text-sm">
|
| 289 |
+
{{ new Date(email.date.received).toLocaleString() }}
|
| 290 |
+
</span>
|
| 291 |
+
</div>
|
| 292 |
+
<div class="flex justify-between text-sm">
|
| 293 |
+
<span class="text-gray-600">发件人: {{ email.from }}</span>
|
| 294 |
+
<span :class="{'text-green-600': email.isRead, 'text-red-600': !email.isRead}">
|
| 295 |
+
{{ email.isRead ? '已读' : '未读' }}
|
| 296 |
+
</span>
|
| 297 |
+
</div>
|
| 298 |
+
</div>
|
| 299 |
+
</t-list-item>
|
| 300 |
+
</t-list>
|
| 301 |
+
</div>
|
| 302 |
+
</t-loading>
|
| 303 |
+
</t-dialog>
|
| 304 |
+
|
| 305 |
+
<!-- 发送邮件对话框 -->
|
| 306 |
+
<t-dialog
|
| 307 |
+
:visible="showSendDialog"
|
| 308 |
+
header="发送邮件"
|
| 309 |
+
@close="showSendDialog = false"
|
| 310 |
+
:width="600"
|
| 311 |
+
:footer="false"
|
| 312 |
+
>
|
| 313 |
+
<template v-if="currentMailAccount">
|
| 314 |
+
<div class="send-mail-form">
|
| 315 |
+
<t-form>
|
| 316 |
+
<t-form-item label="发件人">
|
| 317 |
+
<t-input disabled :value="currentMailAccount.email" />
|
| 318 |
+
</t-form-item>
|
| 319 |
+
<t-form-item label="收件人">
|
| 320 |
+
<t-input v-model="sendMailForm.to" placeholder="多个收件人请用逗号分隔" />
|
| 321 |
+
</t-form-item>
|
| 322 |
+
<t-form-item label="主题">
|
| 323 |
+
<t-input v-model="sendMailForm.subject" />
|
| 324 |
+
</t-form-item>
|
| 325 |
+
<t-form-item label="内容">
|
| 326 |
+
<t-textarea v-model="sendMailForm.body" :rows="6" />
|
| 327 |
+
</t-form-item>
|
| 328 |
+
<t-form-item>
|
| 329 |
+
<t-checkbox v-model="sendMailForm.isHtml">HTML 格式</t-checkbox>
|
| 330 |
+
</t-form-item>
|
| 331 |
+
<t-form-item>
|
| 332 |
+
<div class="flex justify-end gap-2">
|
| 333 |
+
<t-button theme="default" @click="showSendDialog = false">取消</t-button>
|
| 334 |
+
<t-button theme="primary" @click="submitSendMail" :loading="loading">发送</t-button>
|
| 335 |
+
</div>
|
| 336 |
+
</t-form-item>
|
| 337 |
+
</t-form>
|
| 338 |
+
</div>
|
| 339 |
+
</template>
|
| 340 |
+
</t-dialog>
|
| 341 |
+
</div>
|
| 342 |
+
</template>
|
| 343 |
+
|
| 344 |
+
<style scoped>
|
| 345 |
+
@media (max-width: 768px) {
|
| 346 |
+
:deep(.t-table__header) {
|
| 347 |
+
font-size: 14px;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
:deep(.t-table td) {
|
| 351 |
+
font-size: 13px;
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.email-details {
|
| 356 |
+
max-height: 60vh;
|
| 357 |
+
overflow-y: auto;
|
| 358 |
+
padding: 16px;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.email-content {
|
| 362 |
+
margin-top: 16px;
|
| 363 |
+
border-top: 1px solid #eee;
|
| 364 |
+
padding-top: 16px;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.email-meta {
|
| 368 |
+
background: #f5f5f5;
|
| 369 |
+
padding: 12px;
|
| 370 |
+
border-radius: 4px;
|
| 371 |
+
margin-bottom: 16px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.email-actions {
|
| 375 |
+
margin-top: 16px;
|
| 376 |
+
text-align: right;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.email-attachments {
|
| 380 |
+
margin-top: 16px;
|
| 381 |
+
color: #666;
|
| 382 |
+
font-style: italic;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.send-mail-form {
|
| 386 |
+
padding: 16px;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.emails-list {
|
| 390 |
+
max-height: 60vh;
|
| 391 |
+
overflow-y: auto;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.email-list-item {
|
| 395 |
+
border-bottom: 1px solid #eee;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
.email-list-item:last-child {
|
| 399 |
+
border-bottom: none;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
/* 添加以下CSS确保邮件详情弹窗总是显示在最上层 */
|
| 403 |
+
:deep(.email-detail-dialog) {
|
| 404 |
+
z-index: 3000 !important; /* 确保高于其他弹窗 */
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
/* 可能需要调整其他弹窗的z-index */
|
| 408 |
+
:deep(.t-dialog) {
|
| 409 |
+
z-index: 2000;
|
| 410 |
+
}
|
| 411 |
+
</style>
|
src/views/SettingView.vue
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
{
|
| 38 |
+
return
|
| 39 |
+
}
|
| 40 |
+
await settingApi.update(settings.value);
|
| 41 |
+
MessagePlugin.success('保存成功');
|
| 42 |
+
} catch (error) {
|
| 43 |
+
MessagePlugin.error('保存失败');
|
| 44 |
+
} finally {
|
| 45 |
+
loading.value = false;
|
| 46 |
+
}
|
| 47 |
+
};
|
| 48 |
+
</script>
|
| 49 |
+
|
| 50 |
+
<template>
|
| 51 |
+
<div class="setting-container p-2 md:p-5">
|
| 52 |
+
|
| 53 |
+
<t-form :data="settings" @submit="handleSave">
|
| 54 |
+
<t-card bordered>
|
| 55 |
+
<t-divider>飞书配置</t-divider>
|
| 56 |
+
<t-form-item label="应用ID" name="feishu.app_id">
|
| 57 |
+
<t-input v-model="settings.feishu.app_id" placeholder="请输入飞书应用ID" />
|
| 58 |
+
</t-form-item>
|
| 59 |
+
<t-form-item label="应用密钥" name="feishu.app_secret">
|
| 60 |
+
<t-input v-model="settings.feishu.app_secret" type="password" placeholder="请输入飞书应用密钥" />
|
| 61 |
+
</t-form-item>
|
| 62 |
+
<t-form-item label="验证Token" name="feishu.verification_token">
|
| 63 |
+
<t-input v-model="settings.feishu.verification_token" placeholder="请输入飞书应用验证Token" />
|
| 64 |
+
</t-form-item>
|
| 65 |
+
<t-form-item label="加密Key" name="feishu.encrypt_key">
|
| 66 |
+
<t-input v-model="settings.feishu.encrypt_key" placeholder="请输入飞书应用加密Key" />
|
| 67 |
+
</t-form-item>
|
| 68 |
+
<t-form-item label="接收ID" name="feishu.receive_id">
|
| 69 |
+
<t-input v-model="settings.feishu.receive_id" placeholder="请输入飞书机器人接收ID" />
|
| 70 |
+
</t-form-item>
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
<t-form-item class="flex justify-center">
|
| 74 |
+
<t-button theme="primary" type="submit" :loading="loading">保存设置</t-button>
|
| 75 |
+
</t-form-item>
|
| 76 |
+
</t-card>
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
</t-form>
|
| 80 |
+
</div>
|
| 81 |
+
</template>
|
| 82 |
+
|
| 83 |
+
<style scoped>
|
| 84 |
+
|
| 85 |
+
</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 |
+
}
|