-
- {{ 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 2950debdd9b7b77a3202205a85b01f82990670da..7a6d687f3327bd7f0775935f618ef88fb0c1f782 100644
GIT binary patch
delta 1584
zcmb7^e@t6d6vyAY@AlCjw1qKs4q*jK2M@~NwWV}!py(ni+a!iAL`TBw1|e=zrZLfJ
z7()a9n8+yENnny@e;A!4!!Tt*|1icS{Dqw{$Qp&YfHB5k(7705jo&JeiRATe`qdj
zq*`C2Bh7}g{7&C)s#Em*AtPq~iEeGG4(d8=#IjQK>!xa@myZ}Rd7l2-R87?PtPwMP
zNiEeGwT~7uihoMSY7Aq%`kt}RRwOq$RKu8bnYvY@2(dLNJOi7!mug?aGbDUQD%(LD
zJ|k5ep?-fxWs@A>1AByXO#H7X7!CMCK27r;_xo$Te$7|wtqX*LUNx%L1bkX&Pz})e
zcCpGDT_3$IPGJ9;etcIfIcxl2iwZ#RpvW`wFIqhnqOr
zRwad-`UQm&9`0s5OfxHp$Q$ArnhbKc%)8|Gg}-UZrRxZEAMHe@%$uQGDXHjHjTAdA
z?V-k3B!#ZMDwR{Os;BhI8)*E+NvVS3r=(JD4x`hXCHhfIxfEekAL4Fm_=w50ewlgn
z^FAulm~x>(@7hM!HC9D~F;+qCF;>bgYjBm^tJim!=g|853e?9mmPL1Npuc5J_=egH
zr$?a`dM|~mU8&5GLI+du=LqQaCyo1i4(s>4PR9zFf|pp{M)l=-Z|>Lgwb#
zC&YPrCKn!-3*o`G9evHam_k(BIDd~sDlC*gzmwH36SMCE9y
ze(xdOYdm|^6Q4^~#anNBbn|`jGbuwTBR9*+c1sb<7GC?%r9{qI6zd-!(5DoJl#mOy
zf$iWS@G#f`9tHW}F;D<@0T(C&CBO|zfd@2zGEfc_pbyAmPzfFZc_08Hb=YdbZV&={
z!Q)^bCx_E+!Dy#9q;+9F)I|N7+NA}mgQ`uO7~Gh@g2AvB
izA`g4&)P)pxb1A8$xgDA!@ZRjL)K+BV_lZ+_WlbhPLXB+
delta 1254
zcma))T}%{L6vy|@%r5&ib3hPKRtkEd3}v;ueC#3=kwR^3nowGhP(=$Yp{4~)jTMR&
zSaz2fh~Pe4gJngeNQh7fiEEG=TVD)q3E8&PYPYl|Z8Vh!+J+aCwr2)-?k0ZtkmLqDkLds2|R-}ScRwX
zJNyC<;5%4=d+;U9!3~aTtLBT!ahI172u{RyYZb&;T`X7%HF?4ni>$fD`PH0XT4`
zvh<#G7N-_>pG3agU4bm>b|Uw8Z$}pT+K?~#s*(A=LS&wACvvY(K<0YukvZO1k=fpK
zWQNy_O!qV*Wlt$G&0|NVdSZ|%U8j(=s}i}ZYcDdn%c8;kliVMpJm)@!+~F=lTHF+w
z;MO6xbv7X5I}ajbJ2Q}5J0)aHM>BFuM;S7zBMTYX5sMU0Bg|C@Q$9kM$iWqjSsde0
z_CX0OLY(wOx+N}2cIl`%c2??@1o(iz&f6mX)kTvh5fkD7d=}BAzslW!`GDk;V$pwXE;Ag^
z?lAlTSwah!XS@j}QlfuDGjBYvZ89Vpnx(DUFT|(DnZdq1fraBZ3tKtC%dGlmUiOPC
zvXI7?6aUH_B}MGYyS$avPI9up?KUO*SeO!-&BvqLf0LQzICeBWwE7#uDXi@=ztJjP
zrNqI~?~@IWAEDZ8b?D#-C0T6G<;~>Ur&McKlZ;WSwW(jzqm;bJ=9P_KKTc$S;TR=W
z|HX$gvyE~x+p~;EoyHTbAM?j?O43-M6&DSS81MgSbN0%0N+_#6gLB;1xRmwswn<8o
z+4(TuqHYCy5<;YmMOm=%XaLvDi1kA0O-i=0mKLn}{M*fo4ZcN5EMp<8tYnCuGn>hD
z1tnYj!SF`+twS^}Qn^iLoy(SMchp*I@E>?y=F2&jG|r{!(GpdA+`~~LcN5*AdPRaj
zjlwSUok~CJ{hm)zsteS#?Jl7@GKc;GJNG`Ht|Z&lWNnzIMt%@IPW8nB#g+dwTJ_f3
z%08>;lInkpigV~r)wyDx>Ud?}-ORg0(hURn3G0zSsGynn@Y(QC {
+ 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),