-
- {{ user?.username?.charAt(0).toUpperCase() || 'U' }}
+
+ {{ user?.username?.charAt(0).toUpperCase() || 'U' }}
-
{{ user?.username }}
-
管理员
+
{{ user?.nickname || user?.username }}
+
{{ user?.role === 'admin' ? '管理员' : '用户' }}
@@ -52,14 +54,26 @@ async function handleSave() {
{{ error }}
保存成功
-
@@ -330,6 +352,34 @@ function timeAgo(ts: number | string): string {
margin-bottom: 32px;
}
+.exec-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+
+.exec-header .section-title {
+ margin: 0;
+}
+
+.btn-clear-all {
+ padding: 6px 14px;
+ background: transparent;
+ color: var(--color-muted);
+ border: 1px solid var(--color-hairline);
+ border-radius: 6px;
+ font-size: 12px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.btn-clear-all:hover {
+ color: var(--color-error);
+ border-color: var(--color-error);
+}
+
.exec-card {
background: var(--color-surface-card);
border-radius: 12px;
@@ -401,6 +451,32 @@ function timeAgo(ts: number | string): string {
flex: 1;
}
+.btn-delete-log {
+ padding: 4px;
+ background: transparent;
+ color: var(--color-muted-soft);
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ opacity: 0;
+ transition: all 0.15s ease;
+}
+
+.exec-item:hover .btn-delete-log {
+ opacity: 1;
+}
+
+.btn-delete-log:hover {
+ color: var(--color-error);
+ background: rgba(198, 69, 69, 0.1);
+}
+
+.btn-delete-log svg {
+ width: 14px;
+ height: 14px;
+ display: block;
+}
+
.not-found {
margin-top: 40px;
font-size: 15px;
diff --git a/app/plugins/vue3-toastify.client.ts b/app/plugins/vue3-toastify.client.ts
index ebc2070..ee230bc 100644
--- a/app/plugins/vue3-toastify.client.ts
+++ b/app/plugins/vue3-toastify.client.ts
@@ -9,7 +9,7 @@ export default defineNuxtPlugin((nuxtApp) => {
clearOnUrlChange: false,
theme: "colored",
transition: "slide",
- position: "top-right",
+ position: "bottom-right",
});
return {
diff --git a/app/utils/http/factory.ts b/app/utils/http/factory.ts
index 36d9ba9..57f99c7 100644
--- a/app/utils/http/factory.ts
+++ b/app/utils/http/factory.ts
@@ -21,13 +21,13 @@ export function unwrapApiBody
(payload: ApiResponse): T {
}
-export const request = import.meta.server ? undefined : $fetch.create({
+export const request = $fetch.create({
credentials: 'include',
})
const httpFetchDefaults = {
retry: 0,
- $fetch: request,
+ $fetch: import.meta.server ? undefined :request,
transform: unwrapApiBody,
}
diff --git a/package.json b/package.json
index 169a1b0..28164ca 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,6 @@
"tsconfig": "workspace:*",
"tsx": "4.21.0",
"typescript": "6.0.2",
- "vue3-toastify": "^0.2.9"
+ "vue3-toastify": "0.2.9"
}
}
\ No newline at end of file
diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite
index 2950deb..7a6d687 100644
Binary files a/packages/drizzle-pkg/db.sqlite and b/packages/drizzle-pkg/db.sqlite differ
diff --git a/server/api/auth/profile.put.ts b/server/api/auth/profile.put.ts
new file mode 100644
index 0000000..f00464b
--- /dev/null
+++ b/server/api/auth/profile.put.ts
@@ -0,0 +1,85 @@
+import { dbGlobal } from "drizzle-pkg/lib/db";
+import { users } from "drizzle-pkg/lib/schema/auth";
+import { eq } from "drizzle-orm";
+import { UNAUTHORIZED_MESSAGE } from "#server/constants/auth";
+import { toPublicAuthError } from "#server/service/auth/errors";
+
+export default defineWrappedResponseHandler(async (event) => {
+ try {
+ const user = await event.context.auth.requireUser();
+ if (!user) {
+ throw createError({
+ statusCode: 401,
+ statusMessage: UNAUTHORIZED_MESSAGE,
+ });
+ }
+
+ const body = await readBody<{
+ username?: string;
+ email?: string;
+ nickname?: string;
+ }>(event);
+
+ if (!body || Object.keys(body).length === 0) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: "请提供要更新的字段",
+ });
+ }
+
+ const updateData: Partial<{
+ username: string;
+ email: string | null;
+ nickname: string | null;
+ }> = {};
+
+ if (body.username !== undefined) {
+ if (typeof body.username !== "string" || body.username.trim().length === 0) {
+ throw createError({ statusCode: 400, statusMessage: "用户名不能为空" });
+ }
+ if (body.username.length < 2 || body.username.length > 50) {
+ throw createError({ statusCode: 400, statusMessage: "用户名长度需在2-50字符之间" });
+ }
+ updateData.username = body.username.trim();
+ }
+
+ if (body.email !== undefined) {
+ if (body.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) {
+ throw createError({ statusCode: 400, statusMessage: "邮箱格式不正确" });
+ }
+ updateData.email = body.email?.trim() || null;
+ }
+
+ if (body.nickname !== undefined) {
+ updateData.nickname = body.nickname?.trim() || null;
+ }
+
+ if (Object.keys(updateData).length > 0) {
+ await dbGlobal
+ .update(users)
+ .set(updateData)
+ .where(eq(users.id, user.id));
+
+ // Invalidate me cache
+ await event.context.cache.del(`auth:me:${user.id}`);
+ }
+
+ // Fetch updated user data
+ const [row] = await dbGlobal
+ .select({
+ id: users.id,
+ username: users.username,
+ email: users.email,
+ role: users.role,
+ nickname: users.nickname,
+ avatar: users.avatar,
+ })
+ .from(users)
+ .where(eq(users.id, user.id))
+ .limit(1);
+
+ return R.success({ user: row });
+ } catch (err) {
+ throw toPublicAuthError(err);
+ }
+});
\ No newline at end of file
diff --git a/server/api/scheduler/executions/[id].delete.ts b/server/api/scheduler/executions/[id].delete.ts
new file mode 100644
index 0000000..0e5e108
--- /dev/null
+++ b/server/api/scheduler/executions/[id].delete.ts
@@ -0,0 +1,13 @@
+import { deleteExecution } from "#server/service/scheduler";
+
+export default defineWrappedResponseHandler(async (event) => {
+ const id = getRouterParam(event, "id");
+ if (!id) return R.throwError(400, "Missing id", null);
+
+ const deleted = await deleteExecution(id);
+
+ await event.context.cache.del('scheduler:executions:1:20:all:all')
+ await event.context.cache.del('scheduler:stats')
+
+ return R.success({ deleted });
+});
\ No newline at end of file
diff --git a/server/api/scheduler/executions/delete-all.post.ts b/server/api/scheduler/executions/delete-all.post.ts
new file mode 100644
index 0000000..c9d3c65
--- /dev/null
+++ b/server/api/scheduler/executions/delete-all.post.ts
@@ -0,0 +1,13 @@
+import { deleteAllExecutions } from "#server/service/scheduler";
+
+export default defineWrappedResponseHandler(async (event) => {
+ const query = getQuery(event);
+ const taskId = query.taskId as string | undefined;
+
+ const deleted = await deleteAllExecutions(taskId);
+
+ await event.context.cache.del('scheduler:executions:1:20:all:all')
+ await event.context.cache.del('scheduler:stats')
+
+ return R.success({ deleted });
+});
\ No newline at end of file
diff --git a/server/service/scheduler/index.ts b/server/service/scheduler/index.ts
index 83dccc5..5424eec 100644
--- a/server/service/scheduler/index.ts
+++ b/server/service/scheduler/index.ts
@@ -216,6 +216,21 @@ export async function cleanupOldLogs(retentionDays: number) {
}
}
+export async function deleteExecution(id: string) {
+ const result = await dbGlobal
+ .delete(taskExecutionLogs)
+ .where(eq(taskExecutionLogs.id, id));
+ return result.rowsAffected > 0;
+}
+
+export async function deleteAllExecutions(taskId?: string) {
+ const condition = taskId ? eq(taskExecutionLogs.taskId, taskId) : undefined;
+ const result = await dbGlobal
+ .delete(taskExecutionLogs)
+ .where(condition);
+ return result.rowsAffected;
+}
+
export async function getStats() {
const [totalTasks, enabledTasks, recentExecutions] = await Promise.all([
dbGlobal.select({ count: sql`count(*)` }).from(scheduledTasks),