mirror of
https://github.com/wechat-article/wechat-article-exporter.git
synced 2026-06-02 17:39:48 +08:00
189 lines
5.9 KiB
Vue
189 lines
5.9 KiB
Vue
<script setup lang="ts">
|
||
import type { FormError } from '#ui/types';
|
||
import CodeSegment from '~/components/api/CodeSegment.vue';
|
||
import { apis } from '~/config/public-apis';
|
||
|
||
interface Props {
|
||
initialSelected: string;
|
||
}
|
||
const props = defineProps<Props>();
|
||
|
||
const isOpen = ref(false);
|
||
const selectedApi = ref(apis[0]);
|
||
|
||
const payload: Ref<Record<string, any>> = ref({});
|
||
|
||
const host = window.location.protocol + '//' + window.location.host;
|
||
|
||
function onOpen() {
|
||
isOpen.value = true;
|
||
selectedApi.value = apis.find(api => api.name === props.initialSelected)!;
|
||
}
|
||
|
||
function apiChange() {
|
||
payload.value = {};
|
||
}
|
||
|
||
function isEmpty(key: string, obj: Record<string, any>): boolean {
|
||
if (!obj.hasOwnProperty(key)) {
|
||
return true;
|
||
}
|
||
let value = obj[key];
|
||
if (typeof value === 'string') {
|
||
value = value.trim();
|
||
}
|
||
return value === undefined || value === null || value === '';
|
||
}
|
||
|
||
function validate(state: Record<string, any>): FormError[] {
|
||
const errors: FormError[] = [];
|
||
|
||
selectedApi.value.params.forEach(param => {
|
||
if (param.required && isEmpty(param.name, state)) {
|
||
errors.push({ path: param.name, message: param.name + '不能为空' });
|
||
}
|
||
if (!isEmpty(param.name, state) && param.type === 'Int') {
|
||
if (Number.isNaN(parseInt(state[param.name]))) {
|
||
errors.push({ path: param.name, message: param.name + '格式不正确' });
|
||
} else if (parseInt(state[param.name]) < 0) {
|
||
errors.push({ path: param.name, message: param.name + '不得小于0' });
|
||
}
|
||
}
|
||
});
|
||
|
||
return errors;
|
||
}
|
||
|
||
const resp = ref<Record<string, any> | null | string>(null);
|
||
const hasResponse = computed(() => {
|
||
return typeof resp.value === 'string' || (typeof resp.value === 'object' && resp.value);
|
||
});
|
||
const btnLoading = ref(false);
|
||
function submit() {
|
||
const params = toRaw(payload.value);
|
||
|
||
let url = selectedApi.value.url;
|
||
if (selectedApi.value.method === 'GET') {
|
||
url += '?' + new URLSearchParams(params).toString();
|
||
}
|
||
|
||
btnLoading.value = true;
|
||
resp.value = null;
|
||
fetch(url, {
|
||
method: selectedApi.value.method,
|
||
})
|
||
.then(resp => {
|
||
if (resp.headers.get('content-type') === 'application/json') {
|
||
return resp.json();
|
||
} else {
|
||
return resp.text();
|
||
}
|
||
})
|
||
.then(data => {
|
||
resp.value = data;
|
||
})
|
||
.finally(() => {
|
||
btnLoading.value = false;
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<UTooltip text="在线调试" :popper="{ placement: 'top' }">
|
||
<UButton color="blue" variant="ghost" square @click="onOpen" icon="i-lucide:bug-play"></UButton>
|
||
</UTooltip>
|
||
|
||
<USlideover v-model="isOpen" :ui="{ width: 'max-w-[800px]' }">
|
||
<UCard
|
||
class="flex flex-col flex-1"
|
||
:ui="{
|
||
body: { base: 'overflow-y-scroll h-[calc(100vh-72px)]' },
|
||
ring: '',
|
||
divide: 'divide-y divide-gray-100 dark:divide-gray-800',
|
||
}"
|
||
>
|
||
<template #header>
|
||
<h1 class="font-semibold text-2xl">在线接口调试工具</h1>
|
||
</template>
|
||
|
||
<div class="space-y-5">
|
||
<div class="flex items-center gap-3">
|
||
<span>选择接口: </span>
|
||
<USelectMenu
|
||
class="flex-1"
|
||
v-model="selectedApi"
|
||
:options="apis"
|
||
option-attribute="name"
|
||
@change="apiChange"
|
||
>
|
||
<template #label>
|
||
<span
|
||
:class="[
|
||
selectedApi.method === 'GET' ? 'bg-green-400' : 'bg-fuchsia-400',
|
||
'inline-block size-2 flex-shrink-0 rounded-full',
|
||
]"
|
||
aria-hidden="true"
|
||
/>
|
||
<span class="font-medium">{{ selectedApi.name }}</span>
|
||
</template>
|
||
<template #option="{ option: api }">
|
||
<div>
|
||
<p class="font-medium">
|
||
<span
|
||
:class="[
|
||
api.method === 'GET' ? 'bg-green-400' : 'bg-fuchsia-400',
|
||
'inline-block size-2 mr-2 flex-shrink-0 rounded-full',
|
||
]"
|
||
aria-hidden="true"
|
||
/>
|
||
<span>{{ api.name }}</span>
|
||
</p>
|
||
<p class="text-gray-400 font-mono">{{ api.description }}</p>
|
||
</div>
|
||
</template>
|
||
</USelectMenu>
|
||
</div>
|
||
|
||
<div class="space-y-5">
|
||
<div>
|
||
<p class="font-semibold mb-2">请求URL:</p>
|
||
<p class="font-mono border p-2 rounded-md">
|
||
<span class="text-gray-400">{{ host }}</span>
|
||
<span class="font-semibold">{{ selectedApi.url }}</span>
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p class="font-semibold mb-2">请求方式:</p>
|
||
<p class="font-mono border p-2 rounded-md">{{ selectedApi.method }}</p>
|
||
</div>
|
||
<div>
|
||
<p class="font-semibold mb-2">参数:</p>
|
||
<UForm :state="payload" :validate="validate" @submit="submit" class="space-y-3">
|
||
<UFormGroup
|
||
v-for="p in selectedApi.params"
|
||
:key="p.name"
|
||
:label="p.name"
|
||
:name="p.name"
|
||
:required="p.required"
|
||
>
|
||
<UInput
|
||
v-model="payload[p.name]"
|
||
:type="p.type === 'Int' ? 'number' : 'text'"
|
||
:placeholder="p.label + (p.remark ? ',' + p.remark : '')"
|
||
/>
|
||
</UFormGroup>
|
||
<UButton type="submit" color="black" class="px-5" :loading="btnLoading">提交</UButton>
|
||
</UForm>
|
||
</div>
|
||
</div>
|
||
<div v-if="hasResponse">
|
||
<h3 class="font-bold text-2xl">响应</h3>
|
||
<CodeSegment :code="resp!" :lang="typeof resp === 'object' ? 'json' : 'xml'" />
|
||
</div>
|
||
</div>
|
||
</UCard>
|
||
</USlideover>
|
||
</div>
|
||
</template>
|