mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
feat: migrate to Nuxt 4 (Nitro) and Tailwind CSS v3
This commit is contained in:
parent
f4959eebf8
commit
b139712a91
166 changed files with 19790 additions and 5539 deletions
325
app/components/GroupEditor.vue
Normal file
325
app/components/GroupEditor.vue
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="open" class="overlay" @click.self="close"></div>
|
||||
</Transition>
|
||||
<Transition name="slide">
|
||||
<aside v-if="open" class="editor" role="dialog" aria-modal="true">
|
||||
<div class="editor-head">
|
||||
<h2>{{ isEdit ? t("groupEditor.editTitle") : t("groupEditor.newTitle") }}</h2>
|
||||
<button class="icon-btn" :title="t('groupEditor.close')" @click="close">✕</button>
|
||||
</div>
|
||||
<form class="editor-body" @submit.prevent="save">
|
||||
<div class="row2">
|
||||
<div class="field">
|
||||
<label>{{ t("groupEditor.name") }}</label>
|
||||
<input
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
class="input"
|
||||
:placeholder="t('groupEditor.namePlaceholder')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ t("groupEditor.id") }}</label>
|
||||
<input
|
||||
v-model="form.id"
|
||||
type="text"
|
||||
class="input"
|
||||
:placeholder="t('groupEditor.idPlaceholder')"
|
||||
required
|
||||
/>
|
||||
<div class="hint">
|
||||
{{ isEdit ? t("groupEditor.renameHint") : t("groupEditor.idHint") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row2">
|
||||
<div class="field">
|
||||
<label>{{ t("groupEditor.language") }}</label>
|
||||
<input
|
||||
v-model="form.lang"
|
||||
type="text"
|
||||
class="input"
|
||||
:placeholder="t('groupEditor.langPlaceholder')"
|
||||
/>
|
||||
<div class="hint">{{ t("groupEditor.langHint") }}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label
|
||||
>{{ t("groupEditor.emoji") }}
|
||||
<span class="lbl-note">{{ t("groupEditor.emojiNote") }}</span></label
|
||||
>
|
||||
<label class="inline">
|
||||
<input v-model="form.emoji" type="checkbox" />
|
||||
<span>{{ t("groupEditor.emojiLabel") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label
|
||||
>{{ t("groupEditor.membersNote") }}
|
||||
<span class="lbl-note">{{ t("groupEditor.membersHint") }}</span></label
|
||||
>
|
||||
<p class="hint">
|
||||
{{ t("groupEditor.membersGoPanel") }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="superAdmin" class="field">
|
||||
<label
|
||||
>{{ t("groupEditor.owners") }}
|
||||
<span class="lbl-note">{{ t("groupEditor.ownersNote") }}</span></label
|
||||
>
|
||||
<input
|
||||
v-model="form.owners"
|
||||
type="text"
|
||||
class="input"
|
||||
:placeholder="t('groupEditor.ownersPlaceholder')"
|
||||
/>
|
||||
<div class="hint">{{ t("groupEditor.ownersHint") }}</div>
|
||||
</div>
|
||||
<div v-else class="field">
|
||||
<label
|
||||
>{{ t("groupEditor.owners") }}
|
||||
<span class="lbl-note">{{ t("groupEditor.ownersSuperOnly") }}</span></label
|
||||
>
|
||||
<input v-model="ownersReadonly" type="text" class="input opacity-60" disabled />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label
|
||||
>{{ t("groupEditor.providers") }}
|
||||
<span class="lbl-note">{{ t("groupEditor.providersNote") }}</span></label
|
||||
>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<label class="inline">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="form.providers.includes('github')"
|
||||
@change="toggleProvider('github', $event)"
|
||||
/>
|
||||
<span>GitHub</span>
|
||||
</label>
|
||||
<label class="inline">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="form.providers.includes('gitea')"
|
||||
@change="toggleProvider('gitea', $event)"
|
||||
/>
|
||||
<span>Gitea</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hint">{{ t("groupEditor.providersHint") }}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label
|
||||
>{{ t("groupEditor.installationId") }}
|
||||
<span class="lbl-note">{{ t("groupEditor.installationIdNote") }}</span></label
|
||||
>
|
||||
<input
|
||||
v-model="form.installationId"
|
||||
type="text"
|
||||
class="input"
|
||||
inputmode="numeric"
|
||||
:placeholder="t('groupEditor.installationIdPlaceholder')"
|
||||
/>
|
||||
<div class="hint">{{ t("groupEditor.installationIdHint") }}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label
|
||||
>{{ t("groupEditor.logTarget") }}
|
||||
<span class="lbl-note">{{ t("groupEditor.logTargetNote") }}</span></label
|
||||
>
|
||||
<select v-model="form.logPlatform" class="select">
|
||||
<option value="">{{ t("groupEditor.logDisabled") }}</option>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
</select>
|
||||
<template v-if="form.logPlatform === 'discord'">
|
||||
<input
|
||||
v-model="form.logChannelId"
|
||||
type="text"
|
||||
class="input mt-2"
|
||||
:placeholder="t('routeEditor.channelPlaceholder')"
|
||||
/>
|
||||
<input
|
||||
v-model="form.logThreadId"
|
||||
type="text"
|
||||
class="input mt-2"
|
||||
:placeholder="t('routeEditor.threadPlaceholder')"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="form.logPlatform === 'telegram'">
|
||||
<input
|
||||
v-model="form.logChatId"
|
||||
type="text"
|
||||
class="input mt-2"
|
||||
:placeholder="t('routeEditor.chatPlaceholder')"
|
||||
/>
|
||||
<input
|
||||
v-model="form.logTopicId"
|
||||
type="text"
|
||||
class="input mt-2"
|
||||
:placeholder="t('routeEditor.topicPlaceholder')"
|
||||
/>
|
||||
</template>
|
||||
<div class="hint">{{ t("groupEditor.logTargetHint") }}</div>
|
||||
</div>
|
||||
<div class="err">{{ formError }}</div>
|
||||
</form>
|
||||
<div class="editor-foot">
|
||||
<button class="btn btn-ghost" type="button" @click="close">
|
||||
{{ t("groupEditor.cancel") }}
|
||||
</button>
|
||||
<button class="btn btn-accent" type="button" :disabled="saving" @click="save">
|
||||
{{ t("groupEditor.save") }}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from "vue";
|
||||
import type { Group } from "~/types";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
group: Group | null;
|
||||
saving: boolean;
|
||||
superAdmin?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void;
|
||||
(e: "save", group: Group): void;
|
||||
}>();
|
||||
|
||||
const isEdit = computed(() => props.group != null);
|
||||
const formError = ref("");
|
||||
const ownersReadonly = computed(() => (props.group?.owners ?? []).join(", "));
|
||||
|
||||
const form = reactive({
|
||||
id: "",
|
||||
name: "",
|
||||
owners: "",
|
||||
providers: [] as ("github" | "gitea")[],
|
||||
installationId: "",
|
||||
emoji: true,
|
||||
lang: "",
|
||||
logPlatform: "" as "" | "discord" | "telegram",
|
||||
logChannelId: "",
|
||||
logThreadId: "",
|
||||
logChatId: "",
|
||||
logTopicId: "",
|
||||
});
|
||||
|
||||
function splitList(text: string): string[] {
|
||||
return text
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function toggleProvider(p: "github" | "gitea", e: Event): void {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
form.providers = checked
|
||||
? [...new Set([...form.providers, p])]
|
||||
: form.providers.filter((x) => x !== p);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (!open) return;
|
||||
const g = props.group;
|
||||
const lt = g?.logTarget;
|
||||
form.id = g?.id ?? "";
|
||||
form.name = g?.name ?? "";
|
||||
form.owners = (g?.owners ?? []).join(", ");
|
||||
form.providers = (g?.providers ?? []).filter(
|
||||
(p): p is "github" | "gitea" => p === "github" || p === "gitea",
|
||||
);
|
||||
form.installationId = g?.installationId != null ? String(g.installationId) : "";
|
||||
form.emoji = g?.emoji ?? true;
|
||||
form.lang = g?.lang ?? "";
|
||||
form.logPlatform = lt?.platform ?? "";
|
||||
form.logChannelId = lt?.channelId ?? "";
|
||||
form.logThreadId = lt?.threadId ?? "";
|
||||
form.logChatId = lt?.chatId ?? "";
|
||||
form.logTopicId = lt?.topicId ?? "";
|
||||
formError.value = "";
|
||||
},
|
||||
);
|
||||
|
||||
function close(): void {
|
||||
emit("close");
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
formError.value = "";
|
||||
const id = form.id.trim();
|
||||
const name = form.name.trim();
|
||||
if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) {
|
||||
formError.value = t("groupEditor.errIdFormat");
|
||||
return;
|
||||
}
|
||||
if (!name) {
|
||||
formError.value = t("groupEditor.errName");
|
||||
return;
|
||||
}
|
||||
let logTarget:
|
||||
| { platform: "discord"; channelId: string; threadId?: string }
|
||||
| {
|
||||
platform: "telegram";
|
||||
chatId: string;
|
||||
topicId?: string;
|
||||
}
|
||||
| undefined;
|
||||
if (form.logPlatform === "discord") {
|
||||
const channelId = form.logChannelId.trim();
|
||||
if (!channelId) {
|
||||
formError.value = t("groupEditor.errLogChannel");
|
||||
return;
|
||||
}
|
||||
logTarget = { platform: "discord", channelId, threadId: form.logThreadId.trim() || undefined };
|
||||
} else if (form.logPlatform === "telegram") {
|
||||
const chatId = form.logChatId.trim();
|
||||
if (!chatId) {
|
||||
formError.value = t("groupEditor.errLogChat");
|
||||
return;
|
||||
}
|
||||
logTarget = { platform: "telegram", chatId, topicId: form.logTopicId.trim() || undefined };
|
||||
}
|
||||
const installationText = form.installationId.trim();
|
||||
let installationId: number | undefined;
|
||||
if (installationText) {
|
||||
installationId = Number(installationText);
|
||||
if (!Number.isInteger(installationId) || installationId <= 0) {
|
||||
formError.value = t("groupEditor.errInstallationId");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const owners = splitList(form.owners);
|
||||
const members = props.group?.members
|
||||
? props.group.members.map((m) => ({ ...m }))
|
||||
: props.group?.adminIds?.length
|
||||
? props.group.adminIds.map((login) => ({ login, role: "owner" as const }))
|
||||
: [];
|
||||
emit("save", {
|
||||
id,
|
||||
name,
|
||||
members,
|
||||
adminIds: members.filter((m) => m.role === "owner").map((m) => m.login),
|
||||
owners: props.superAdmin ? (owners.length ? owners : undefined) : props.group?.owners,
|
||||
providers: form.providers.length ? form.providers : undefined,
|
||||
installationId,
|
||||
emoji: form.emoji,
|
||||
lang: form.lang.trim() || undefined,
|
||||
logTarget,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue