基于naive ui重构设置-开发者页面及弹窗

This commit is contained in:
jingrow 2025-12-30 00:49:55 +08:00
parent 34577e321d
commit 8b617d833b
5 changed files with 1240 additions and 485 deletions

View File

@ -1,83 +1,175 @@
<template> <template>
<Dialog <n-modal
:options="{ v-model:show="showDialog"
title: '激活Webhook' preset="card"
}" :title="$t('Activate Webhook')"
:style="modalStyle"
:mask-closable="true"
:close-on-esc="true"
class="activate-webhook-modal"
> >
<template #body-content> <template #header>
<div class="space-y-4"> <span class="text-lg font-semibold">{{ $t('Activate Webhook') }}</span>
<FormControl label="端点" v-model="webhook.endpoint" disabled /> </template>
<div v-if="request"> <n-space vertical :size="20">
<p class="text-xs text-gray-600">请求</p> <n-form-item :label="$t('Endpoint')">
<pre <n-input
class="mt-2 whitespace-pre-wrap rounded bg-gray-50 px-2 py-1.5 text-sm text-gray-600" :value="webhook.endpoint"
>{{ request }}</pre
>
</div>
<FormControl
v-if="response_status_code"
label="响应状态码"
v-model="response_status_code"
disabled disabled
:size="inputSize"
class="w-full"
/> />
</n-form-item>
<div v-if="request">
<p class="text-xs text-gray-600 mb-2">{{ $t('Request') }}</p>
<pre class="mt-2 whitespace-pre-wrap rounded bg-gray-50 px-3 py-2 text-sm text-gray-700 border border-gray-200">
{{ request }}
</pre>
</div>
<n-form-item v-if="response_status_code" :label="$t('Response Status Code')">
<n-input
:value="response_status_code"
disabled
:size="inputSize"
class="w-full"
/>
</n-form-item>
<div v-if="response"> <div v-if="response">
<p class="text-xs text-gray-600">响应</p> <p class="text-xs text-gray-600 mb-2">{{ $t('Response') }}</p>
<pre <pre class="mt-2 max-h-52 overflow-y-auto whitespace-pre-wrap rounded bg-gray-50 px-3 py-2 text-sm text-gray-700 border border-gray-200">
class="mt-2 max-h-52 overflow-y-auto whitespace-pre-wrap rounded bg-gray-50 px-2 py-1.5 text-sm text-gray-600" {{ response }}
>{{ response }}</pre </pre>
>
</div> </div>
<div class="flex items-center" v-if="validated"> <div class="flex items-center" v-if="validated">
<ILucideCheck class="h-4 text-green-600" /> <n-icon class="h-5 w-5 text-green-600 mr-2">
<div class="ml-2 text-sm font-medium text-gray-700"> <CheckIcon />
端点已验证 </n-icon>
<div class="text-sm font-medium text-gray-700">
{{ $t('Endpoint validated') }}
</div> </div>
</div> </div>
<n-alert v-if="errorMessage" type="error" :title="errorMessage" />
<ErrorMessage :message="errorMessage" /> </n-space>
</div> <template #action>
</template> <n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
<template v-slot:actions> <n-button @click="hide" :block="isMobile" :size="buttonSize">
<Button {{ $t('Cancel') }}
class="w-full" </n-button>
theme="gray" <n-button
variant="solid"
@click="$resources.validateEndpoint.submit()"
:loading="$resources.validateEndpoint.loading"
loadingText="正在验证Webhook"
v-if="!validated" v-if="!validated"
>验证Webhook</Button type="primary"
:loading="$resources.validateEndpoint.loading"
@click="$resources.validateEndpoint.submit()"
:block="isMobile"
:size="buttonSize"
> >
<Button {{ $t('Validate Webhook') }}
class="w-full" </n-button>
theme="gray" <n-button
variant="solid"
@click="$resources.activateWebhook.submit()"
:loading="$resources.activateWebhook.loading"
loadingText="正在激活Webhook"
v-if="validated" v-if="validated"
>激活Webhook</Button type="primary"
:loading="$resources.activateWebhook.loading"
@click="$resources.activateWebhook.submit()"
:block="isMobile"
:size="buttonSize"
> >
{{ $t('Activate Webhook') }}
</n-button>
</n-space>
</template> </template>
</Dialog> </n-modal>
</template> </template>
<script> <script>
import {
NModal,
NFormItem,
NInput,
NSpace,
NAlert,
NButton,
NIcon,
} from 'naive-ui';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import CheckIcon from '~icons/lucide/check';
export default { export default {
emits: ['success'], components: {
props: ['webhook'], NModal,
NFormItem,
NInput,
NSpace,
NAlert,
NButton,
NIcon,
CheckIcon,
},
emits: ['success', 'update:modelValue'],
props: {
webhook: {
type: Object,
required: true
},
modelValue: {
type: Boolean,
default: false
}
},
data() { data() {
return { return {
errorMessage: '', errorMessage: '',
validated: false, validated: false,
request: null, request: null,
response: null, response: null,
response_status_code: null response_status_code: null,
windowWidth: window.innerWidth,
}; };
}, },
computed: {
showDialog: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
}
},
isMobile() {
return this.windowWidth <= 768;
},
modalStyle() {
return {
width: this.isMobile ? '95vw' : '800px',
maxWidth: this.isMobile ? '95vw' : '90vw',
};
},
inputSize() {
return this.isMobile ? 'medium' : 'large';
},
buttonSize() {
return this.isMobile ? 'medium' : 'medium';
},
},
mounted() {
this.handleResize();
window.addEventListener('resize', this.handleResize);
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
},
methods: {
handleResize() {
this.windowWidth = window.innerWidth;
},
hide() {
this.showDialog = false;
this.errorMessage = '';
this.validated = false;
this.request = null;
this.response = null;
this.response_status_code = null;
}
},
resources: { resources: {
validateEndpoint() { validateEndpoint() {
return { return {
@ -99,9 +191,7 @@ export default {
this.validated = true; this.validated = true;
} else { } else {
this.validated = false; this.validated = false;
this.errorMessage = this.$t('Endpoint should return a status code between 200 and 300. Please check the endpoint and try again.');
this.errorMessage =
'端点应返回200到300之间的状态\n请检查端点并重试';
} }
}, },
onError: e => { onError: e => {
@ -121,7 +211,8 @@ export default {
}; };
}, },
onSuccess(e) { onSuccess(e) {
toast.success('Webhook激活成功'); toast.success(this.$t('Webhook activated successfully'));
this.hide();
this.$emit('success'); this.$emit('success');
}, },
onError(e) { onError(e) {
@ -133,3 +224,54 @@ export default {
} }
}; };
</script> </script>
<style scoped>
:deep(.activate-webhook-modal .n-card) {
width: 800px;
max-width: 90vw;
}
:deep(.activate-webhook-modal .n-card-body) {
padding: 24px;
}
:deep(.activate-webhook-modal .n-form-item) {
margin-bottom: 0;
}
:deep(.activate-webhook-modal .n-input) {
width: 100%;
}
:deep(.activate-webhook-modal .n-card__action) {
padding: 16px 24px;
}
@media (max-width: 768px) {
:deep(.activate-webhook-modal .n-card) {
width: 95vw !important;
max-width: 95vw !important;
margin: 20px auto;
border-radius: 12px;
}
:deep(.activate-webhook-modal .n-card-body) {
padding: 20px 16px;
}
:deep(.activate-webhook-modal .n-card__header) {
padding: 16px;
font-size: 18px;
}
:deep(.activate-webhook-modal .n-card__action) {
padding: 12px 16px;
}
:deep(.activate-webhook-modal .n-button) {
width: 100%;
font-size: 16px;
height: 44px;
}
}
</style>

View File

@ -1,114 +1,179 @@
<template> <template>
<Dialog <n-modal
:options="{ v-model:show="showDialog"
title: $t('Add New Webhook'), preset="card"
actions: [ :title="$t('Add New Webhook')"
{ :style="modalStyle"
label: $t('Add Webhook'), :mask-closable="true"
variant: 'solid', :close-on-esc="true"
onClick: addWebhook, class="add-webhook-modal"
loading: $resources?.addWebhook?.loading
}
]
}"
> >
<template #body-content> <template #header>
<div class="space-y-4"> <span class="text-lg font-semibold">{{ $t('Add New Webhook') }}</span>
<FormControl :label="$t('Endpoint')" v-model="endpoint" />
<div>
<FormControl :label="$t('Secret Key')" v-model="secret">
<template #suffix>
<FeatherIcon
class="w-4 cursor-pointer"
name="refresh-ccw"
@click="generateRandomSecret"
/>
</template> </template>
</FormControl> <n-space vertical :size="20">
<p class="mt-2 text-sm text-gray-700"> <n-form-item :label="$t('Endpoint')" :required="true">
<strong>{{ $t('Note') }}:</strong> {{ $t('The secret key is optional. View') }} <n-input
<a href="https://jingrow.com/docs/webhook-introduction" class="underline" target="_blank" v-model:value="endpoint"
>{{ $t('documentation') }}</a :placeholder="$t('Enter webhook endpoint URL')"
:size="inputSize"
class="w-full"
/>
</n-form-item>
<n-form-item :label="$t('Secret Key')">
<n-input
v-model:value="secret"
:placeholder="$t('Enter secret key (optional)')"
:size="inputSize"
class="w-full"
> >
<template #suffix>
<n-icon
class="cursor-pointer text-gray-500 hover:text-gray-700"
@click="generateRandomSecret"
>
<RefreshIcon />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-alert type="info" class="mb-2">
<template #header>
<strong>{{ $t('Note') }}:</strong>
</template>
{{ $t('The secret key is optional. View') }}
<a href="https://jingrow.com/docs/webhook-introduction" class="text-primary underline" target="_blank">
{{ $t('documentation') }}
</a>
{{ $t('to learn more') }} {{ $t('to learn more') }}
</p> </n-alert>
</div> <div class="mt-4">
<p class="text-base font-medium text-gray-900"> <p class="text-base font-medium text-gray-900 mb-4">
{{ $t('Select Webhook Events') }} {{ $t('Select Webhook Events') }}
</p> </p>
<n-spin :show="$resources.events.loading">
<n-space vertical :size="12" v-if="!$resources.events.loading">
<div <div
class="text-center text-sm leading-10 text-gray-500"
v-if="$resources.events.loading"
>
{{ $t('Loading...') }}
</div>
<div class="mt-6 flex flex-col gap-4" v-else>
<Switch
v-for="event in localizedEvents" v-for="event in localizedEvents"
:key="event.name" :key="event.name"
:label="event.title || event.name" class="flex items-center justify-between p-3 rounded border border-gray-200 hover:bg-gray-50"
:description="event.description" >
:modelValue="isEventSelected(event.name)" <div class="flex-1">
@update:modelValue="selectEvent(event.name)" <div class="text-sm font-medium text-gray-900">
size="sm" {{ event.title || event.name }}
</div>
<div v-if="event.description" class="text-xs text-gray-500 mt-1">
{{ event.description }}
</div>
</div>
<n-switch
:value="isEventSelected(event.name)"
@update:value="selectEvent(event.name)"
size="medium"
/> />
</div> </div>
<ErrorMessage :message="errorMessage || $resources.addWebhook.error" /> </n-space>
</n-spin>
</div> </div>
<n-alert
v-if="errorMessage || $resources.addWebhook.error"
type="error"
:title="errorMessage || $resources.addWebhook.error"
/>
</n-space>
<template #action>
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
<n-button @click="hide" :block="isMobile" :size="buttonSize">
{{ $t('Cancel') }}
</n-button>
<n-button
type="primary"
:loading="$resources.addWebhook.loading"
@click="addWebhook"
:block="isMobile"
:size="buttonSize"
>
{{ $t('Add Webhook') }}
</n-button>
</n-space>
</template> </template>
</Dialog> </n-modal>
</template> </template>
<script> <script>
import {
NModal,
NFormItem,
NInput,
NSpace,
NAlert,
NButton,
NSwitch,
NIcon,
NSpin,
} from 'naive-ui';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import RefreshIcon from '~icons/lucide/refresh-ccw';
export default { export default {
emits: ['success'], components: {
NModal,
NFormItem,
NInput,
NSpace,
NAlert,
NButton,
NSwitch,
NIcon,
NSpin,
RefreshIcon,
},
emits: ['success', 'update:modelValue'],
props: {
modelValue: {
type: Boolean,
default: false
}
},
data() { data() {
return { return {
endpoint: '', endpoint: '',
secret: '', secret: '',
selectedEvents: [], selectedEvents: [],
errorMessage: '' errorMessage: '',
windowWidth: window.innerWidth,
}; };
}, },
mounted() {
if (this.selectedEvents.length) {
this.selectedEvents = this.selectedEvents.map(event => event.name);
}
},
resources: {
events() {
return {
url: 'jcloud.api.webhook.available_events',
inititalData: [],
auto: true
};
},
addWebhook() {
return {
url: 'jcloud.api.webhook.add',
params: {
endpoint: this.endpoint,
secret: this.secret,
events: this.selectedEvents
},
onSuccess() {
toast.success(this.$t('Webhook added successfully'));
this.$emit('success');
}
};
}
},
computed: { computed: {
showDialog: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
}
},
isMobile() {
return this.windowWidth <= 768;
},
modalStyle() {
return {
width: this.isMobile ? '95vw' : '800px',
maxWidth: this.isMobile ? '95vw' : '90vw',
};
},
inputSize() {
return this.isMobile ? 'medium' : 'large';
},
buttonSize() {
return this.isMobile ? 'medium' : 'medium';
},
localizedEvents() { localizedEvents() {
if (!this.$resources.events.data) return []; if (!this.$resources.events.data) return [];
return this.$resources.events.data.map(event => { return this.$resources.events.data.map(event => {
//
const localizedEvent = { ...event }; const localizedEvent = { ...event };
//
if (localizedEvent.name === 'Site Status Update') { if (localizedEvent.name === 'Site Status Update') {
localizedEvent.title = this.$t('Site Status Update'); localizedEvent.title = this.$t('Site Status Update');
localizedEvent.description = this.$t('Pending, Installing, Updating, Active, Inactive, Abnormal, Archived, Paused'); localizedEvent.description = this.$t('Pending, Installing, Updating, Active, Inactive, Abnormal, Archived, Paused');
@ -116,7 +181,6 @@ export default {
localizedEvent.title = this.$t('Site Plan Change'); localizedEvent.title = this.$t('Site Plan Change');
localizedEvent.description = this.$t('Get notifications for site subscription plan changes'); localizedEvent.description = this.$t('Get notifications for site subscription plan changes');
} else { } else {
// title使namedescription使
if (!localizedEvent.title) { if (!localizedEvent.title) {
localizedEvent.title = localizedEvent.name; localizedEvent.title = localizedEvent.name;
} }
@ -129,7 +193,20 @@ export default {
}); });
} }
}, },
mounted() {
this.handleResize();
window.addEventListener('resize', this.handleResize);
if (this.selectedEvents.length) {
this.selectedEvents = this.selectedEvents.map(event => event.name);
}
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
},
methods: { methods: {
handleResize() {
this.windowWidth = window.innerWidth;
},
generateRandomSecret() { generateRandomSecret() {
this.secret = Array(30) this.secret = Array(30)
.fill(0) .fill(0)
@ -162,7 +239,89 @@ export default {
} }
this.errorMessage = ''; this.errorMessage = '';
this.$resources.addWebhook.submit(); this.$resources.addWebhook.submit();
},
hide() {
this.showDialog = false;
this.endpoint = '';
this.secret = '';
this.selectedEvents = [];
this.errorMessage = '';
}
},
resources: {
events() {
return {
url: 'jcloud.api.webhook.available_events',
inititalData: [],
auto: true
};
},
addWebhook() {
return {
url: 'jcloud.api.webhook.add',
params: {
endpoint: this.endpoint,
secret: this.secret,
events: this.selectedEvents
},
onSuccess() {
toast.success(this.$t('Webhook added successfully'));
this.hide();
this.$emit('success');
}
};
} }
} }
}; };
</script> </script>
<style scoped>
:deep(.add-webhook-modal .n-card) {
width: 800px;
max-width: 90vw;
}
:deep(.add-webhook-modal .n-card-body) {
padding: 24px;
}
:deep(.add-webhook-modal .n-form-item) {
margin-bottom: 0;
}
:deep(.add-webhook-modal .n-input) {
width: 100%;
}
:deep(.add-webhook-modal .n-card__action) {
padding: 16px 24px;
}
@media (max-width: 768px) {
:deep(.add-webhook-modal .n-card) {
width: 95vw !important;
max-width: 95vw !important;
margin: 20px auto;
border-radius: 12px;
}
:deep(.add-webhook-modal .n-card-body) {
padding: 20px 16px;
}
:deep(.add-webhook-modal .n-card__header) {
padding: 16px;
font-size: 18px;
}
:deep(.add-webhook-modal .n-card__action) {
padding: 12px 16px;
}
:deep(.add-webhook-modal .n-button) {
width: 100%;
font-size: 16px;
height: 44px;
}
}
</style>

View File

@ -1,71 +1,181 @@
<template> <template>
<div class="p-5"> <div class="developer-settings-container">
<div class="grid grid-cols-1 gap-5"> <n-space vertical :size="24">
<div <!-- API Access 卡片 -->
class="mx-auto min-w-[48rem] max-w-3xl space-y-6 rounded-md border p-4" <n-card :title="$t('API Access')" class="settings-card">
> <n-space vertical :size="20">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between flex-wrap gap-4">
<div class="text-xl font-semibold">{{ $t('API Access') }}</div> <div class="text-base text-gray-700">
<Button @click="showCreateSecretDialog = true">{{ {{ $t('API key and API secret can be used to access') }}
apiKeyButtonLabel <a href="/docs/api" class="text-primary underline" target="_blank">
}}</Button> {{ $t('Jingrow API') }}
</a>
</div>
<n-button type="primary" @click="showCreateSecretDialog = true" :size="buttonSize">
{{ apiKeyButtonLabel }}
</n-button>
</div> </div>
<div v-if="$team.pg?.user_info?.api_key"> <div v-if="$team.pg?.user_info?.api_key">
<ClickToCopyField <ClickToCopyField :textContent="$team.pg.user_info.api_key" />
v-if="$team.pg?.user_info?.api_key"
:textContent="$team.pg.user_info.api_key"
/>
</div> </div>
<div v-else class="pb-2 text-base text-gray-700"> <div v-else class="text-base text-gray-600">
{{ $t("You don't have an API key yet. Click the button above to create one.") }} {{ $t("You don't have an API key yet. Click the button above to create one.") }}
</div> </div>
<Dialog </n-space>
v-model="showCreateSecretDialog" </n-card>
:options="apiKeyDialogOptions"
<!-- SSH Keys 卡片 -->
<n-card :title="$t('SSH Keys')" class="settings-card">
<ObjectList :options="sshKeyListOptions" />
</n-card>
<!-- Webhooks 卡片 -->
<n-card
v-if="$session.hasWebhookConfigurationAccess"
:title="$t('Webhooks')"
class="settings-card"
> >
<template #body-content> <ObjectList :options="webhookListOptions" />
</n-card>
</n-space>
<!-- API Key 创建/重新生成弹窗 -->
<n-modal
v-model:show="showCreateSecretDialog"
preset="card"
:title="$t('API Access')"
:style="modalStyle"
:mask-closable="true"
:close-on-esc="true"
class="api-key-modal"
>
<template #header>
<span class="text-lg font-semibold">{{ $t('API Access') }}</span>
</template>
<n-space vertical :size="20">
<div v-if="createSecret.data"> <div v-if="createSecret.data">
<p class="text-base"> <n-alert type="warning" class="mb-4">
{{ $t('Please copy the API key immediately. You will not be able to view it again!') }} {{ $t('Please copy the API key immediately. You will not be able to view it again!') }}
</p> </n-alert>
<label class="block pt-2"> <n-form-item :label="$t('API Key')">
<span class="mb-2 block text-sm leading-4 text-gray-700"
>API key</span
>
<ClickToCopyField :textContent="createSecret.data.api_key" /> <ClickToCopyField :textContent="createSecret.data.api_key" />
</label> </n-form-item>
<label class="block pt-2"> <n-form-item :label="$t('API Secret')">
<span class="mb-2 block text-sm leading-4 text-gray-700"
>API secret</span
>
<ClickToCopyField :textContent="createSecret.data.api_secret" /> <ClickToCopyField :textContent="createSecret.data.api_secret" />
</label> </n-form-item>
</div> </div>
<div v-else class="text-base text-gray-700"> <div v-else class="text-base text-gray-700">
{{ $t('API key and API secret can be used to access') }} {{ $t('API key and API secret can be used to access') }}
<a href="/docs/api" class="underline" target="_blank" <a href="/docs/api" class="text-primary underline" target="_blank">
>{{ $t('Jingrow API') }}</a {{ $t('Jingrow API') }}
>. </a>.
</div> </div>
</n-space>
<template #action>
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
<n-button @click="showCreateSecretDialog = false" :block="isMobile" :size="buttonSize">
{{ $t('Cancel') }}
</n-button>
<n-button
type="primary"
:loading="createSecret.loading"
:disabled="!!createSecret.data"
@click="createSecret.submit()"
:block="isMobile"
:size="buttonSize"
>
{{ $team.pg.user_info.api_key ? $t('Regenerate API Key') : $t('Create New API Key') }}
</n-button>
</n-space>
</template> </template>
</Dialog> </n-modal>
</div>
<div <!-- SSH Key 添加弹窗 -->
class="mx-auto min-w-[48rem] max-w-3xl space-y-6 rounded-md border p-4" <n-modal
v-model:show="showAddSSHKeyDialog"
preset="card"
:title="$t('Add New SSH Key')"
:style="modalStyle"
:mask-closable="true"
:close-on-esc="true"
class="ssh-key-modal"
> >
<div class="flex items-center justify-between"> <template #header>
<div class="text-xl font-semibold">{{ $t('SSH Keys') }}</div> <span class="text-lg font-semibold">{{ $t('Add New SSH Key') }}</span>
</div> </template>
<ObjectList :options="sshKeyListOptions" /> <n-space vertical :size="20">
</div> <p class="text-base text-gray-700">{{ $t('Add a new SSH key to your account') }}</p>
<div <n-form-item :label="$t('SSH Key')" :required="true">
v-if="$session.hasWebhookConfigurationAccess" <n-input
class="mx-auto min-w-[48rem] max-w-3xl space-y-6 rounded-md border p-4" v-model:value="sshKeyValue"
type="textarea"
:placeholder="sshKeyPlaceholder"
:rows="6"
:size="inputSize"
class="w-full"
/>
</n-form-item>
<n-alert v-if="sshKeyError" type="error" :title="sshKeyError" />
</n-space>
<template #action>
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
<n-button @click="showAddSSHKeyDialog = false" :block="isMobile" :size="buttonSize">
{{ $t('Cancel') }}
</n-button>
<n-button
type="primary"
:loading="addSSHKey.loading"
@click="handleAddSSHKey"
:block="isMobile"
:size="buttonSize"
> >
<div class="flex items-center justify-between"> {{ $t('Add SSH Key') }}
<div class="text-xl font-semibold">{{ $t('Webhooks') }}</div> </n-button>
</n-space>
</template>
</n-modal>
<!-- Webhook 禁用确认弹窗 -->
<n-modal
v-model:show="showDisableWebhookDialog"
preset="dialog"
:title="$t('Disable Webhook')"
:positive-text="$t('Disable')"
:positive-button-props="{ type: 'error' }"
:loading="disableWebhook.loading"
:mask-closable="true"
:close-on-esc="true"
@positive-click="handleDisableWebhook"
>
<div class="py-4">
<p class="text-base mb-2">
{{ $t('Endpoint') }}: <strong>{{ selectedWebhookForAction?.endpoint }}</strong>
</p>
<p class="text-base">{{ $t('Are you sure you want to disable this webhook?') }}</p>
</div> </div>
<ObjectList :options="webhookListOptions" /> </n-modal>
<!-- Webhook 删除确认弹窗 -->
<n-modal
v-model:show="showDeleteWebhookDialog"
preset="dialog"
:title="$t('Delete Webhook')"
:positive-text="$t('Delete')"
:positive-button-props="{ type: 'error' }"
:loading="deleteWebhook.loading"
:mask-closable="true"
:close-on-esc="true"
@positive-click="handleDeleteWebhook"
>
<div class="py-4">
<p class="text-base mb-2">
{{ $t('Endpoint') }}: <strong>{{ selectedWebhookForAction?.endpoint }}</strong>
</p>
<p class="text-base">{{ $t('Are you sure you want to delete this webhook?') }}</p>
</div>
</n-modal>
<!-- Webhook 相关弹窗 -->
<AddNewWebhookDialog <AddNewWebhookDialog
v-if="showAddWebhookDialog" v-if="showAddWebhookDialog"
v-model="showAddWebhookDialog" v-model="showAddWebhookDialog"
@ -89,15 +199,22 @@
:name="selectedWebhook.name" :name="selectedWebhook.name"
/> />
</div> </div>
</div>
</div>
</template> </template>
<script setup> <script setup>
import {
NCard,
NSpace,
NButton,
NModal,
NFormItem,
NInput,
NAlert,
} from 'naive-ui';
import { Badge, createResource } from 'jingrow-ui'; import { Badge, createResource } from 'jingrow-ui';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { computed, h, onMounted, ref, getCurrentInstance } from 'vue'; import { computed, h, onMounted, ref, getCurrentInstance } from 'vue';
import { confirmDialog, icon } from '../../utils/components'; import { icon } from '../../utils/components';
import ObjectList from '../ObjectList.vue'; import ObjectList from '../ObjectList.vue';
import { getTeam } from '../../data/team'; import { getTeam } from '../../data/team';
import { date } from '../../utils/format'; import { date } from '../../utils/format';
@ -115,11 +232,29 @@ const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
const $team = getTeam(); const $team = getTeam();
const router = useRouter(); const router = useRouter();
let showCreateSecretDialog = ref(false); let showCreateSecretDialog = ref(false);
const showAddSSHKeyDialog = ref(false);
const sshKeyValue = ref('');
const sshKeyError = ref('');
const showAddWebhookDialog = ref(false); const showAddWebhookDialog = ref(false);
const showActivateWebhookDialog = ref(false); const showActivateWebhookDialog = ref(false);
const showEditWebhookDialog = ref(false); const showEditWebhookDialog = ref(false);
const showWebhookAttempts = ref(false); const showWebhookAttempts = ref(false);
const selectedWebhook = ref(null); const selectedWebhook = ref(null);
const selectedWebhookForAction = ref(null);
const showDisableWebhookDialog = ref(false);
const showDeleteWebhookDialog = ref(false);
const windowWidth = ref(window.innerWidth);
const isMobile = computed(() => windowWidth.value <= 768);
const modalStyle = computed(() => ({
width: isMobile.value ? '95vw' : '700px',
maxWidth: isMobile.value ? '95vw' : '90vw',
}));
const inputSize = computed(() => (isMobile.value ? 'medium' : 'large'));
const buttonSize = computed(() => (isMobile.value ? 'medium' : 'medium'));
const sshKeyPlaceholder = computed(() => {
return $t("Starts with 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', 'sk-ecdsa-sha2-nistp256@openssh.com', or 'sk-ssh-ed25519@openssh.com'");
});
const createSecret = createResource({ const createSecret = createResource({
url: 'jcloud.api.account.create_api_secret', url: 'jcloud.api.account.create_api_secret',
@ -129,6 +264,7 @@ const createSecret = createResource({
} else { } else {
toast.success($t('API key created successfully')); toast.success($t('API key created successfully'));
} }
$team.reload();
} }
}); });
@ -136,6 +272,9 @@ const addSSHKey = createResource({
url: 'jcloud.api.client.insert', url: 'jcloud.api.client.insert',
onSuccess() { onSuccess() {
toast.success($t('SSH key added successfully')); toast.success($t('SSH key added successfully'));
showAddSSHKeyDialog.value = false;
sshKeyValue.value = '';
sshKeyError.value = '';
}, },
onError(err) { onError(err) {
toast.error( toast.error(
@ -209,7 +348,10 @@ const sshKeyListOptions = computed(() => ({
return { return {
label: $t('Add SSH Key'), label: $t('Add SSH Key'),
slots: { prefix: icon('plus') }, slots: { prefix: icon('plus') },
onClick: () => renderAddNewKeyDialog(listResource) onClick: () => {
showAddSSHKeyDialog.value = true;
sshKeyListResource = listResource;
}
}; };
}, },
rowActions({ row }) { rowActions({ row }) {
@ -236,41 +378,29 @@ const sshKeyListOptions = computed(() => ({
} }
})); }));
function renderAddNewKeyDialog(listResource) { let sshKeyListResource = null;
confirmDialog({
title: $t('Add New SSH Key'), function handleAddSSHKey() {
message: $t('Add a new SSH key to your account'), if (!sshKeyValue.value) {
fields: [ sshKeyError.value = $t('SSH key is required');
{ return;
label: $t('SSH Key'),
fieldname: 'sshKey',
type: 'textarea',
placeholder: $t("Starts with 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', 'sk-ecdsa-sha2-nistp256@openssh.com', or 'sk-ssh-ed25519@openssh.com'"),
required: true
} }
], sshKeyError.value = '';
primaryAction: {
label: $t('Add SSH Key'),
variant: 'solid',
onClick({ hide, values }) {
if (!values.sshKey) throw new Error($t('SSH key is required'));
addSSHKey addSSHKey
.submit({ .submit({
pg: { pg: {
pagetype: 'User SSH Key', pagetype: 'User SSH Key',
ssh_public_key: values.sshKey, ssh_public_key: sshKeyValue.value,
user: $team.pg.user_info.name user: $team.pg.user_info.name
} }
}) })
.then(() => { .then(() => {
listResource.reload(); if (sshKeyListResource) {
hide(); sshKeyListResource.reload();
}
}) })
.catch(error => { .catch(error => {
toast.error(error.message); sshKeyError.value = error.message;
});
}
}
}); });
} }
@ -289,6 +419,7 @@ const deleteWebhook = createResource({
onSuccess() { onSuccess() {
toast.success($t('Webhook deleted successfully')); toast.success($t('Webhook deleted successfully'));
webhookListResource.reload(); webhookListResource.reload();
showDeleteWebhookDialog.value = false;
}, },
onError(err) { onError(err) {
toast.error( toast.error(
@ -341,25 +472,8 @@ const webhookListOptions = computed(() => ({
label: $t('Disable'), label: $t('Disable'),
condition: () => Boolean(row.enabled), condition: () => Boolean(row.enabled),
onClick: () => { onClick: () => {
confirmDialog({ selectedWebhookForAction.value = row;
title: $t('Disable Webhook'), showDisableWebhookDialog.value = true;
message: $t('Endpoint - {endpoint}<br>Are you sure you want to disable this webhook?<br>', { endpoint: row.endpoint }),
primaryAction: {
label: $t('Disable'),
variant: 'solid',
theme: 'red',
onClick({ hide }) {
disableWebhook
.submit({
dt: 'Jcloud Webhook',
dn: row.name,
method: 'disable'
})
.then(hide);
return disableWebhook.promise;
}
}
});
} }
}, },
{ {
@ -379,24 +493,8 @@ const webhookListOptions = computed(() => ({
{ {
label: $t('Delete'), label: $t('Delete'),
onClick() { onClick() {
confirmDialog({ selectedWebhookForAction.value = row;
title: $t('Delete Webhook'), showDeleteWebhookDialog.value = true;
message: $t('Endpoint - {endpoint}<br>Are you sure you want to delete this webhook?<br>', { endpoint: row.endpoint }),
primaryAction: {
label: $t('Delete'),
variant: 'solid',
theme: 'red',
onClick({ hide }) {
deleteWebhook
.submit({
pagetype: 'Jcloud Webhook',
name: row.name
})
.then(hide);
return deleteWebhook.promise;
}
}
});
} }
} }
]; ];
@ -422,6 +520,7 @@ const disableWebhook = createResource({
onSuccess() { onSuccess() {
toast.success($t('Webhook disabled successfully')); toast.success($t('Webhook disabled successfully'));
webhookListResource.reload(); webhookListResource.reload();
showDisableWebhookDialog.value = false;
}, },
onError(err) { onError(err) {
toast.error( toast.error(
@ -432,6 +531,23 @@ const disableWebhook = createResource({
} }
}); });
function handleDisableWebhook() {
if (!selectedWebhookForAction.value) return;
disableWebhook.submit({
dt: 'Jcloud Webhook',
dn: selectedWebhookForAction.value.name,
method: 'disable'
});
}
function handleDeleteWebhook() {
if (!selectedWebhookForAction.value) return;
deleteWebhook.submit({
pagetype: 'Jcloud Webhook',
name: selectedWebhookForAction.value.name
});
}
const onNewWebhookSuccess = () => { const onNewWebhookSuccess = () => {
webhookListResource.reload(); webhookListResource.reload();
showAddWebhookDialog.value = false; showAddWebhookDialog.value = false;
@ -456,27 +572,104 @@ const apiKeyButtonLabel = computed(() => {
: $t('Create New API Key'); : $t('Create New API Key');
}); });
const apiKeyDialogOptions = computed(() => ({ function handleResize() {
title: $t('API Access'), windowWidth.value = window.innerWidth;
size: 'xl',
actions: [
{
label: $team.pg.user_info.api_key
? $t('Regenerate API Key')
: $t('Create New API Key'),
variant: 'solid',
disabled: !!createSecret.data,
loading: createSecret.loading,
onClick() {
createSecret.submit();
} }
}
]
}));
onMounted(() => { onMounted(() => {
handleResize();
window.addEventListener('resize', handleResize);
if (session.hasWebhookConfigurationAccess) { if (session.hasWebhookConfigurationAccess) {
webhookListResource.fetch(); webhookListResource.fetch();
} }
}); });
</script> </script>
<style scoped>
.developer-settings-container {
width: 100%;
padding: 24px;
}
.settings-card {
border-radius: 12px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
transition: box-shadow 0.2s ease;
}
.settings-card:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
:deep(.api-key-modal .n-card),
:deep(.ssh-key-modal .n-card) {
width: 700px;
max-width: 90vw;
}
:deep(.api-key-modal .n-card-body),
:deep(.ssh-key-modal .n-card-body) {
padding: 24px;
}
:deep(.api-key-modal .n-form-item),
:deep(.ssh-key-modal .n-form-item) {
margin-bottom: 0;
}
:deep(.api-key-modal .n-input),
:deep(.ssh-key-modal .n-input) {
width: 100%;
}
:deep(.api-key-modal .n-card__action),
:deep(.ssh-key-modal .n-card__action) {
padding: 16px 24px;
}
@media (max-width: 768px) {
.developer-settings-container {
padding: 16px;
}
.settings-card {
border-radius: 8px;
}
:deep(.api-key-modal .n-card),
:deep(.ssh-key-modal .n-card) {
width: 95vw !important;
max-width: 95vw !important;
margin: 20px auto;
border-radius: 12px;
}
:deep(.api-key-modal .n-card-body),
:deep(.ssh-key-modal .n-card-body) {
padding: 20px 16px;
}
:deep(.api-key-modal .n-card__header),
:deep(.ssh-key-modal .n-card__header) {
padding: 16px;
font-size: 18px;
}
:deep(.api-key-modal .n-card__action),
:deep(.ssh-key-modal .n-card__action) {
padding: 12px 16px;
}
:deep(.api-key-modal .n-space),
:deep(.ssh-key-modal .n-space) {
width: 100%;
}
:deep(.api-key-modal .n-button),
:deep(.ssh-key-modal .n-button) {
width: 100%;
font-size: 16px;
height: 44px;
}
}
</style>

View File

@ -1,104 +1,243 @@
<template> <template>
<Dialog <n-modal
:options="{ v-model:show="showDialog"
title: '编辑Webhook', preset="card"
actions: [ :title="$t('Edit Webhook')"
{ :style="modalStyle"
label: '保存更改', :mask-closable="true"
variant: 'solid', :close-on-esc="true"
onClick: updateWebhook, class="edit-webhook-modal"
loading: this.$resources.updateWebhook.loading
}
]
}"
> >
<template #body-content> <template #header>
<div class="space-y-4"> <span class="text-lg font-semibold">{{ $t('Edit Webhook') }}</span>
<div> </template>
<FormControl label="端点" v-model="endpoint" /> <n-space vertical :size="20">
<p class="mt-1.5 text-sm text-gray-700"> <n-form-item :label="$t('Endpoint')">
如果您更改了端点请确保重新激活webhook <n-input
</p> v-model:value="endpoint"
</div> :placeholder="$t('Enter webhook endpoint URL')"
:size="inputSize"
class="w-full"
/>
</n-form-item>
<n-alert type="info" class="mb-2">
{{ $t('If you change the endpoint, make sure to reactivate the webhook.') }}
</n-alert>
<div v-if="!updateSecret"> <div v-if="!updateSecret">
<p class="block text-xs text-gray-600">密钥</p> <p class="block text-xs text-gray-600 mb-2">{{ $t('Secret Key') }}</p>
<div <div class="mt-1 flex items-center justify-between text-base text-gray-700">
class="mt-1 flex items-center justify-between text-base text-gray-700" <p>{{ $t('Want to change the secret key?') }}</p>
> <n-button size="small" @click="updateSecret = true">
<p>想要更改密钥吗</p> {{ $t('Edit Secret') }}
<Button @click="updateSecret = true">编辑密钥</Button> </n-button>
</div> </div>
</div> </div>
<div v-else> <div v-else>
<FormControl label="密钥" v-model="secret"> <n-form-item :label="$t('Secret Key')">
<n-input
v-model:value="secret"
:placeholder="$t('Enter secret key (optional)')"
:size="inputSize"
class="w-full"
>
<template #suffix> <template #suffix>
<FeatherIcon <n-icon
class="w-4 cursor-pointer" class="cursor-pointer text-gray-500 hover:text-gray-700"
name="refresh-ccw"
@click="generateRandomSecret" @click="generateRandomSecret"
/> >
<RefreshIcon />
</n-icon>
</template> </template>
</FormControl> </n-input>
<p class="mt-1.5 text-sm text-gray-700"> </n-form-item>
<secret>注意</secret> 密钥是可选的查看 <n-alert type="info" class="mb-2">
<a <template #header>
href="https://jingrow.com/docs/webhook-introduction" <strong>{{ $t('Note') }}:</strong>
class="underline" </template>
target="_blank" {{ $t('The secret key is optional. View') }}
>文档</a <a href="https://jingrow.com/docs/webhook-introduction" class="text-primary underline" target="_blank">
> {{ $t('documentation') }}
了解更多 </a>
</p> {{ $t('to learn more') }}
</n-alert>
</div> </div>
<p class="text-base font-medium text-gray-900"> <div class="mt-4">
选择webhook事件 <p class="text-base font-medium text-gray-900 mb-4">
{{ $t('Select Webhook Events') }}
</p> </p>
<n-spin :show="$resources.events.loading">
<n-space vertical :size="12" v-if="!$resources.events.loading">
<div <div
class="text-center text-sm leading-10 text-gray-500"
v-if="$resources.events.loading"
>
加载中...
</div>
<div class="mt-6 flex flex-col gap-3" v-else>
<Switch
v-for="event in $resources.events.data" v-for="event in $resources.events.data"
:key="event.name" :key="event.name"
:label="event.name" class="flex items-center justify-between p-3 rounded border border-gray-200 hover:bg-gray-50"
:description="event.description" >
:modelValue="isEventSelected(event.name)" <div class="flex-1">
@update:modelValue="selectEvent(event.name)" <div class="text-sm font-medium text-gray-900">
size="sm" {{ event.name }}
</div>
<div v-if="event.description" class="text-xs text-gray-500 mt-1">
{{ event.description }}
</div>
</div>
<n-switch
:value="isEventSelected(event.name)"
@update:value="selectEvent(event.name)"
size="medium"
/> />
</div> </div>
<ErrorMessage </n-space>
:message="errorMessage || $resources.updateWebhook.error" </n-spin>
/>
</div> </div>
<n-alert
v-if="errorMessage || $resources.updateWebhook.error"
type="error"
:title="errorMessage || $resources.updateWebhook.error"
/>
</n-space>
<template #action>
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
<n-button @click="hide" :block="isMobile" :size="buttonSize">
{{ $t('Cancel') }}
</n-button>
<n-button
type="primary"
:loading="$resources.updateWebhook.loading"
@click="updateWebhook"
:block="isMobile"
:size="buttonSize"
>
{{ $t('Save Changes') }}
</n-button>
</n-space>
</template> </template>
</Dialog> </n-modal>
</template> </template>
<script> <script>
import {
NModal,
NFormItem,
NInput,
NSpace,
NAlert,
NButton,
NSwitch,
NIcon,
NSpin,
} from 'naive-ui';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { getToastErrorMessage } from '../../utils/toast'; import { getToastErrorMessage } from '../../utils/toast';
import RefreshIcon from '~icons/lucide/refresh-ccw';
export default { export default {
emits: ['success'], components: {
props: ['webhook'], NModal,
NFormItem,
NInput,
NSpace,
NAlert,
NButton,
NSwitch,
NIcon,
NSpin,
RefreshIcon,
},
emits: ['success', 'update:modelValue'],
props: {
webhook: {
type: Object,
required: true
},
modelValue: {
type: Boolean,
default: false
}
},
data() { data() {
return { return {
endpoint: '', endpoint: '',
secret: '', secret: '',
updateSecret: false, updateSecret: false,
selectedEvents: [], selectedEvents: [],
errorMessage: '' errorMessage: '',
windowWidth: window.innerWidth,
}; };
}, },
computed: {
showDialog: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
}
},
isMobile() {
return this.windowWidth <= 768;
},
modalStyle() {
return {
width: this.isMobile ? '95vw' : '800px',
maxWidth: this.isMobile ? '95vw' : '90vw',
};
},
inputSize() {
return this.isMobile ? 'medium' : 'large';
},
buttonSize() {
return this.isMobile ? 'medium' : 'medium';
},
},
mounted() { mounted() {
this.handleResize();
window.addEventListener('resize', this.handleResize);
if (this.selectedEvents.length) { if (this.selectedEvents.length) {
this.selectedEvents = this.selectedEvents.map(event => event.name); this.selectedEvents = this.selectedEvents.map(event => event.name);
} }
}, },
beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
},
methods: {
handleResize() {
this.windowWidth = window.innerWidth;
},
generateRandomSecret() {
this.secret = Array(30)
.fill(0)
.map(
() =>
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'[
Math.floor(Math.random() * 62)
]
)
.join('');
},
selectEvent(event) {
if (this.selectedEvents.includes(event)) {
this.selectedEvents = this.selectedEvents.filter(e => e !== event);
} else {
this.selectedEvents.push(event);
}
},
isEventSelected(event) {
return this.selectedEvents.includes(event);
},
updateWebhook() {
if (this.selectedEvents.length === 0) {
this.errorMessage = this.$t('Please enable at least one event');
return;
}
this.errorMessage = '';
this.$resources.updateWebhook.submit();
},
hide() {
this.showDialog = false;
this.updateSecret = false;
this.errorMessage = '';
}
},
resources: { resources: {
events() { events() {
return { return {
@ -125,8 +264,8 @@ export default {
return { return {
url: 'jcloud.api.webhook.update', url: 'jcloud.api.webhook.update',
validate: () => { validate: () => {
if (!this.selectedEvents) { if (!this.selectedEvents || this.selectedEvents.length === 0) {
return '请至少启用一个事件'; return this.$t('Please enable at least one event');
} }
}, },
makeParams: () => { makeParams: () => {
@ -138,52 +277,72 @@ export default {
}; };
}, },
onSuccess: () => { onSuccess: () => {
toast.success('Webhook更新成功'); toast.success(this.$t('Webhook updated successfully'));
const activationRequired = this.webhook.endpoint !== this.endpoint; const activationRequired = this.webhook.endpoint !== this.endpoint;
this.hide();
this.$emit('success', activationRequired); this.$emit('success', activationRequired);
}, },
onError: e => { onError: e => {
toast.error( toast.error(
getToastErrorMessage( getToastErrorMessage(
e, e,
'更新webhook失败请重试' this.$t('Failed to update webhook, please try again')
) )
); );
} }
}; };
} }
},
computed: {},
methods: {
generateRandomSecret() {
this.secret = Array(30)
.fill(0)
.map(
() =>
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'[
Math.floor(Math.random() * 62)
]
)
.join('');
},
selectEvent(event) {
if (this.selectedEvents.includes(event)) {
this.selectedEvents = this.selectedEvents.filter(e => e !== event);
} else {
this.selectedEvents.push(event);
}
},
isEventSelected(event) {
return this.selectedEvents.includes(event);
},
updateWebhook() {
if (this.selectedEvents.length === 0) {
this.errorMessage = '请至少选择一个事件来添加';
return;
}
this.errorMessage = '';
this.$resources.updateWebhook.submit();
}
} }
}; };
</script> </script>
<style scoped>
:deep(.edit-webhook-modal .n-card) {
width: 800px;
max-width: 90vw;
}
:deep(.edit-webhook-modal .n-card-body) {
padding: 24px;
}
:deep(.edit-webhook-modal .n-form-item) {
margin-bottom: 0;
}
:deep(.edit-webhook-modal .n-input) {
width: 100%;
}
:deep(.edit-webhook-modal .n-card__action) {
padding: 16px 24px;
}
@media (max-width: 768px) {
:deep(.edit-webhook-modal .n-card) {
width: 95vw !important;
max-width: 95vw !important;
margin: 20px auto;
border-radius: 12px;
}
:deep(.edit-webhook-modal .n-card-body) {
padding: 20px 16px;
}
:deep(.edit-webhook-modal .n-card__header) {
padding: 16px;
font-size: 18px;
}
:deep(.edit-webhook-modal .n-card__action) {
padding: 12px 16px;
}
:deep(.edit-webhook-modal .n-button) {
width: 100%;
font-size: 16px;
height: 44px;
}
}
</style>

View File

@ -1,83 +1,124 @@
<template> <template>
<Dialog <n-modal
:options="{ v-model:show="showDialog"
title: selectedWebhookAttemptId preset="card"
? `Webhook 尝试 - ${selectedWebhookAttemptId}` :title="selectedWebhookAttemptId ? $t('Webhook Attempt - {id}', { id: selectedWebhookAttemptId }) : $t('Webhook Attempts')"
: 'Webhook 尝试', :style="modalStyle"
size: '4xl' :mask-closable="true"
}" :close-on-esc="true"
class="webhook-attempts-modal"
> >
<template #body-content> <template #header>
<p class="text-sm mb-2 text-gray-700" v-if="!selectedWebhookAttemptId"> <span class="text-lg font-semibold">
<strong>注意</strong> 您只能查看过去24小时的日志 {{ selectedWebhookAttemptId ? $t('Webhook Attempt - {id}', { id: selectedWebhookAttemptId }) : $t('Webhook Attempts') }}
</p> </span>
</template>
<n-space vertical :size="20">
<n-alert type="info" v-if="!selectedWebhookAttemptId">
<template #header>
<strong>{{ $t('Note') }}:</strong>
</template>
{{ $t('You can only view logs from the past 24 hours') }}
</n-alert>
<ObjectList :options="listOptions" v-if="!selectedWebhookAttemptId" /> <ObjectList :options="listOptions" v-if="!selectedWebhookAttemptId" />
<Button <n-button
class="mb-2"
iconLeft="arrow-left"
v-if="selectedWebhookAttemptId" v-if="selectedWebhookAttemptId"
@click="selectedWebhookAttemptId = null" @click="selectedWebhookAttemptId = null"
:size="buttonSize"
> >
返回 <template #icon>
</Button> <n-icon><ArrowLeftIcon /></n-icon>
</template>
{{ $t('Back') }}
</n-button>
<WebhookAttemptDetails <WebhookAttemptDetails
:id="selectedWebhookAttemptId" :id="selectedWebhookAttemptId"
v-if="selectedWebhookAttemptId" v-if="selectedWebhookAttemptId"
/> />
</n-space>
</n-modal>
</template> </template>
</Dialog>
</template>
<script> <script>
import { Breadcrumbs, Badge } from 'jingrow-ui'; import {
import Header from '../Header.vue'; NModal,
import ObjectList from '../ObjectList.vue'; NSpace,
NAlert,
NButton,
NIcon,
} from 'naive-ui';
import { Badge } from 'jingrow-ui';
import { h } from 'vue'; import { h } from 'vue';
import ObjectList from '../ObjectList.vue';
import WebhookAttemptDetails from './WebhookAttemptDetails.vue'; import WebhookAttemptDetails from './WebhookAttemptDetails.vue';
import ArrowLeftIcon from '~icons/lucide/arrow-left';
export default { export default {
name: 'WebhookAttempts',
props: ['name'],
components: { components: {
Header, NModal,
Breadcrumbs, NSpace,
NAlert,
NButton,
NIcon,
ObjectList, ObjectList,
WebhookAttemptDetails WebhookAttemptDetails,
ArrowLeftIcon,
},
name: 'WebhookAttempts',
props: {
name: {
type: String,
required: true
},
modelValue: {
type: Boolean,
default: false
}
}, },
data() { data() {
return { return {
selectedWebhookAttemptId: null selectedWebhookAttemptId: null,
windowWidth: window.innerWidth,
}; };
}, },
resources: {
attempts() {
return {
url: 'jcloud.api.webhook.attempts',
params: {
webhook: this.$props.name
},
inititalData: [],
auto: true
};
}
},
computed: { computed: {
showDialog: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
}
},
isMobile() {
return this.windowWidth <= 768;
},
modalStyle() {
return {
width: this.isMobile ? '95vw' : '1200px',
maxWidth: this.isMobile ? '95vw' : '90vw',
};
},
buttonSize() {
return this.isMobile ? 'medium' : 'medium';
},
listOptions() { listOptions() {
return { return {
data: () => this.$resources?.attempts?.data || [], data: () => this.$resources?.attempts?.data || [],
columns: [ columns: [
{ {
label: '事件', label: this.$t('Event'),
fieldname: 'event', fieldname: 'event',
width: 0.25 width: 0.25
}, },
{ {
label: '端点', label: this.$t('Endpoint'),
fieldname: 'endpoint', fieldname: 'endpoint',
width: 0.5, width: 0.5,
format: value => value.substring(0, 50) format: value => value.substring(0, 50)
}, },
{ {
label: '状态', label: this.$t('Status'),
fieldname: 'status', fieldname: 'status',
width: 0.1, width: 0.1,
type: 'Component', type: 'Component',
@ -94,7 +135,7 @@ export default {
} }
}, },
{ {
label: '代码', label: this.$t('Code'),
fieldname: 'response_status_code', fieldname: 'response_status_code',
width: 0.1, width: 0.1,
format: val => { format: val => {
@ -104,7 +145,7 @@ export default {
align: 'center' align: 'center'
}, },
{ {
label: '时间戳', label: this.$t('Timestamp'),
fieldname: 'timestamp', fieldname: 'timestamp',
width: 0.3, width: 0.3,
format(value) { format(value) {
@ -117,6 +158,67 @@ export default {
} }
}; };
} }
},
mounted() {
this.handleResize();
window.addEventListener('resize', this.handleResize);
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
},
methods: {
handleResize() {
this.windowWidth = window.innerWidth;
}
},
resources: {
attempts() {
return {
url: 'jcloud.api.webhook.attempts',
params: {
webhook: this.$props.name
},
inititalData: [],
auto: true
};
}
} }
}; };
</script> </script>
<style scoped>
:deep(.webhook-attempts-modal .n-card) {
width: 1200px;
max-width: 90vw;
}
:deep(.webhook-attempts-modal .n-card-body) {
padding: 24px;
}
:deep(.webhook-attempts-modal .n-card__action) {
padding: 16px 24px;
}
@media (max-width: 768px) {
:deep(.webhook-attempts-modal .n-card) {
width: 95vw !important;
max-width: 95vw !important;
margin: 20px auto;
border-radius: 12px;
}
:deep(.webhook-attempts-modal .n-card-body) {
padding: 20px 16px;
}
:deep(.webhook-attempts-modal .n-card__header) {
padding: 16px;
font-size: 18px;
}
:deep(.webhook-attempts-modal .n-card__action) {
padding: 12px 16px;
}
}
</style>