新增请求测试小工具

This commit is contained in:
Guarp 2025-03-08 21:24:14 +08:00
parent 58eff630d7
commit 40e90fef4f
8 changed files with 571 additions and 21 deletions

View File

@ -60,21 +60,15 @@ editorConfig.MENU_CONF.uploadImage = {
fieldName: "image", // fieldName: "image", //
async customUpload(file, insertFn) { async customUpload(file, insertFn) {
const index = imagesCache.value.length;
const index = imagesCache.value.length; //
//
const imageSize = await getImageSize(file); const imageSize = await getImageSize(file);
const objectURL = URL.createObjectURL(file); // blob URL
//
imagesCache.value.push({ imagesCache.value.push({
file, file,
url: objectURL, // blob URL
originalWidth: imageSize.width, originalWidth: imageSize.width,
originalHeight: imageSize.height originalHeight: imageSize.height,
}); });
// URL
const objectURL = URL.createObjectURL(file);
insertFn(objectURL, `image-${index}`, objectURL); // insertFn(objectURL, `image-${index}`, objectURL); //
} }
}; };
@ -155,10 +149,30 @@ const submitBlog = async () => {
let content = editorRef.value.getHtml(); // HTML let content = editorRef.value.getHtml(); // HTML
const images = [...imagesCache.value]; // let images = [...imagesCache.value]; //
// `<img>` // `<img>` blob URL
const imgTags = content.match(/<img[^>]+>/g) || []; const imgTags = content.match(/<img[^>]+>/g) || [];
const usedUrls = new Set(); // 使 blob URL
imgTags.forEach((imgTag) => {
const srcMatch = imgTag.match(/src=["']([^"']+)["']/);
if (srcMatch) {
usedUrls.add(srcMatch[1]); // 使 blob URL Set
}
});
// imagesCache 使
images = images.filter((img) => {
const isUsed = usedUrls.has(img.url);
if (!isUsed) {
URL.revokeObjectURL(img.url); // 使 blob URL
}
return isUsed; // 使
});
// imagesCache
imagesCache.value = images;
// blob URL // blob URL
const urlToIndexMap = new Map(); const urlToIndexMap = new Map();
@ -185,8 +199,8 @@ const submitBlog = async () => {
const styleMatch = imgTag.match(/style=["']([^"']+)["']/); const styleMatch = imgTag.match(/style=["']([^"']+)["']/);
let width = "", height = ""; let width = "", height = "";
// images blob URL // images url
const imageData = images.find((img) => URL.createObjectURL(img.file) === src); const imageData = images.find((img) => img.url === src); // 使 url
const originalWidth = imageData?.originalWidth || 0; const originalWidth = imageData?.originalWidth || 0;
const originalHeight = imageData?.originalHeight || 0; const originalHeight = imageData?.originalHeight || 0;
@ -217,7 +231,6 @@ const submitBlog = async () => {
} }
}); });
// 2 // 2
const formData = new FormData(); const formData = new FormData();
formData.append("title", title); formData.append("title", title);
@ -225,20 +238,21 @@ const submitBlog = async () => {
formData.append("allow_comments", allowComments); formData.append("allow_comments", allowComments);
formData.append("draft", true); formData.append("draft", true);
images.forEach((imgData, index) => { console.log(images);
formData.append(`images[${index}]`, imgData.file); // images.forEach((imgData) => {
formData.append(`images`, imgData.file); //
}); });
console.log(Object.fromEntries(formData.entries())); console.log(Object.fromEntries(formData.entries()));
// 3
// 3
api.post('/blogs', formData, { api.post('/blogs', formData, {
headers: { headers: {
"Content-Type": "multipart/form-data", "Content-Type": "multipart/form-data",
}, },
}).then(response => { }).then(response => {
if (response.code === 0) { if (response.code === 0) {
swal.window('success', `提交成功, 博客id${response.blogId || '未找到blogId字段'}`); swal.window('success', `提交成功, 博客id${response.blogId || '未找到blogId字段'}`, 'ok', '好的');
return; return;
} }
swal.window('error', '提交失败', `code字段为${response.code}; 错误信息: ${response.message}`, 'ok', '好的'); swal.window('error', '提交失败', `code字段为${response.code}; 错误信息: ${response.message}`, 'ok', '好的');

View File

@ -1,7 +1,12 @@
<script setup> <script setup>
import {ref} from "vue"; import {onMounted, ref} from "vue";
import {blogImage} from "../utils/imageResource.js";
const blogDisplay = ref('<h2>654654654</h2><ul><li><strong>砍砍价考核表计划表</strong></li></ul><ol><li><strong>1第三方代发</strong></li><li><strong>水电费水电费sd f收到f</strong></li><li><strong>收到f sd </strong></li><li><strong>11234</strong></li></ol>') const blogDisplay = ref(null);
onMounted(() => {
})
</script> </script>
<template> <template>

View File

@ -13,6 +13,7 @@ const searchQuery = ref('');
const tools = ref([ const tools = ref([
{ id: 1, title: 'GPA在线计算器', description: '手动输入在线算', image: null, category: ['计算'] }, { id: 1, title: 'GPA在线计算器', description: '手动输入在线算', image: null, category: ['计算'] },
{ id: 2, title: 'PDF页面提取器', description: '提取指定页和封面', image: null, category: ['提取', 'pdf'] }, { id: 2, title: 'PDF页面提取器', description: '提取指定页和封面', image: null, category: ['提取', 'pdf'] },
{ id: 3, title: '请求测试器', description: '自定义请求测试', image: null, category: ['开发', '网络'] },
]); ]);
onMounted(() => { onMounted(() => {

View File

@ -0,0 +1,24 @@
<script setup>
import {onMounted, ref} from "vue";
const blogDisplay = ref(null);
onMounted(() => {
})
</script>
<template>
<div class="container">
<div v-html="blogDisplay"></div>
</div>
</template>
<style scoped>
.container {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
}
</style>

View File

@ -0,0 +1,490 @@
<template>
<div class="request-tester">
<!-- 左侧操作区 -->
<div class="operation-area">
<!-- URL 输入 -->
<div class="form-group">
<label>URL</label>
<input v-model="url" placeholder="请输入目标 URL" />
</div>
<!-- 请求方法选择 -->
<div class="form-group">
<label>请求方法</label>
<select v-model="method">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
</select>
</div>
<!-- 请求头设置 -->
<div class="form-group">
<label>请求头</label>
<div v-for="(header, index) in headers" :key="index" class="key-value-pair">
<input v-model="header.key" placeholder="键" />
<input v-model="header.value" placeholder="值" />
<button @click="removeHeader(index)">删除</button>
</div>
<button @click="addHeader">添加请求头</button>
</div>
<!-- 请求体类型选择 -->
<div class="form-group" v-if="method !== 'GET' && method !== 'DELETE'">
<label>请求体类型</label>
<select v-model="bodyType">
<option value="json">JSON</option>
<option value="formdata">FormData</option>
</select>
</div>
<!-- 请求体设置JSON -->
<div class="form-group" v-if="method !== 'GET' && method !== 'DELETE' && bodyType === 'json'">
<label>请求体 (JSON)</label>
<textarea v-model="body" placeholder="请输入 JSON 文本"></textarea>
</div>
<!-- 请求体设置FormData -->
<div class="form-group" v-if="method !== 'GET' && method !== 'DELETE' && bodyType === 'formdata'">
<label>请求体 (FormData)</label>
<div v-for="(item, index) in formDataItems" :key="index" class="formdata-item">
<!-- 键名 -->
<input v-model="item.key" placeholder="键名" />
<!-- 类型选择 -->
<select v-model="item.type" @change="resetValue(item)">
<option value="pair">普通键值对</option>
<option value="array">数组</option>
</select>
<!-- 普通键值对输入 -->
<div v-if="item.type === 'pair'">
<input v-if="!item.isFile" v-model="item.value" placeholder="值" />
<input v-else type="file" @change="handleFileChange(item, $event)" />
<div style="margin: 10px 0"/>
<button @click="item.isFile = !item.isFile">
{{ item.isFile ? '切换为文本' : '切换为文件' }}
</button>
</div>
<!-- 数组输入 -->
<div v-if="item.type === 'array'">
<div v-for="(val, valIndex) in item.value" :key="valIndex" class="array-item">
<input v-if="!val.isFile" v-model="val.content" placeholder="数组值" />
<input v-else type="file" @change="handleArrayFileChange(item, valIndex, $event)" />
<button @click="val.isFile = !val.isFile">
{{ val.isFile ? '切换为文本' : '切换为文件' }}
</button>
<button @click="removeArrayItem(item, valIndex)">删除</button>
</div>
<button @click="addArrayItem(item)">添加数组项</button>
</div>
<!-- 删除键值对 -->
<button @click="removeFormDataItem(index)">删除键值对</button>
</div>
<button @click="addFormDataItem">添加键值对</button>
</div>
<!-- 查询参数设置 -->
<div class="form-group">
<label>查询参数</label>
<div v-for="(param, index) in params" :key="index" class="key-value-pair">
<input v-model="param.key" placeholder="键" />
<input v-model="param.value" placeholder="值" />
<button @click="removeParam(index)">删除</button>
</div>
<button @click="addParam">添加参数</button>
</div>
<!-- 操作按钮 -->
<div class="actions">
<button @click="sendRequest">发送请求</button>
<button @click="savePreset">保存预设</button>
</div>
<!-- 响应结果 -->
<div class="response" v-if="response">
<h3>响应结果</h3>
<pre>{{ response }}</pre>
</div>
</div>
<!-- 右侧储存栏 -->
<div class="storage-area">
<h3>储存栏</h3>
<div v-if="!requestTesterPresets || Object.keys(requestTesterPresets).length === 0">暂无预设</div>
<div v-else v-for="(preset, name) in requestTesterPresets" :key="name" class="preset-item">
<span @click="loadPreset(name)" class="preset-name">{{ name }}</span>
<button @click="deletePreset(name)">删除</button>
</div>
</div>
</div>
</template>
<script>
import { ref, computed } from 'vue';
import { useStore } from 'vuex';
import axios from 'axios';
import Swal from 'sweetalert2';
import swal from "../../../utils/sweetalert.js";
export default {
name: 'RequestTester',
setup() {
const store = useStore();
//
const url = ref('');
const method = ref('GET');
const headers = ref([{ key: '', value: '' }]);
const bodyType = ref('json'); // json formdata
const body = ref(''); // JSON
const formDataItems = ref([]); // FormData
const params = ref([{ key: '', value: '' }]);
const response = ref(null);
// Vuex requestTester
const requestTesterPresets = computed(() => store.state.demosLocal.requestTester || {});
//
const addHeader = () => headers.value.push({ key: '', value: '' });
const removeHeader = (index) => headers.value.splice(index, 1);
//
const addParam = () => params.value.push({ key: '', value: '' });
const removeParam = (index) => params.value.splice(index, 1);
// FormData
const addFormDataItem = () => {
formDataItems.value.push({ key: '', type: 'pair', value: '', isFile: false });
};
const removeFormDataItem = (index) => {
formDataItems.value.splice(index, 1);
};
const resetValue = (item) => {
if (item.type === 'pair') {
item.value = '';
item.isFile = false;
} else if (item.type === 'array') {
item.value = [{ content: '', isFile: false }];
}
};
const addArrayItem = (item) => {
if (!item.value) item.value = [];
item.value.push({ content: '', isFile: false });
};
const removeArrayItem = (item, index) => {
item.value.splice(index, 1);
};
const handleFileChange = (item, event) => {
item.value = event.target.files[0];
};
const handleArrayFileChange = (item, index, event) => {
item.value[index].content = event.target.files[0];
};
//
const sendRequest = async () => {
response.value = '等待响应...'; //
try {
let finalUrl = url.value;
const headersObj = headers.value.reduce((acc, h) => {
if (h.key && h.value) acc[h.key] = h.value;
return acc;
}, {});
//
const queryParams = params.value
.filter((p) => p.key && p.value)
.reduce((acc, p) => {
acc[p.key] = p.value;
return acc;
}, {});
let axiosConfig = {
method: method.value,
url: finalUrl,
headers: headersObj,
params: queryParams,
};
//
if (method.value !== 'GET' && method.value !== 'DELETE') {
if (bodyType.value === 'json') {
axiosConfig.data = body.value ? JSON.parse(body.value) : {};
if (!headersObj['Content-Type']) {
axiosConfig.headers['Content-Type'] = 'application/json';
}
} else if (bodyType.value === 'formdata') {
const formData = new FormData();
formDataItems.value.forEach((item) => {
if (item.key) {
if (item.type === 'pair' && item.value) {
formData.append(item.key, item.value);
} else if (item.type === 'array' && item.value.length) {
item.value.forEach((val) => {
if (val.content) {
formData.append(item.key + '[]', val.content);
}
});
}
}
});
axiosConfig.data = formData;
}
}
const res = await axios(axiosConfig);
response.value = JSON.stringify(res.data, null, 2);
} catch (error) {
response.value = `错误: ${error.message}`;
}
};
//
const savePreset = async () => {
const result = await Swal.fire({
title: '请输入预设名称',
input: 'text',
inputLabel: '预设名称',
inputPlaceholder: '请输入您的预设名称...',
showCancelButton: true,
cancelButtonText: '取消',
confirmButtonText: '确定',
inputValidator: (value) => !value && '预设名称不能为空!',
});
if (!result.isConfirmed) return;
const presetName = result.value;
const preset = {
url: url.value,
method: method.value,
headers: [...headers.value],
bodyType: bodyType.value,
body: body.value,
formDataItems: formDataItems.value.map((item) => ({
key: item.key,
type: item.type,
value: item.type === 'pair' ? (item.isFile ? 'File' : item.value) : item.value.map((v) => (v.isFile ? 'File' : v.content)),
})), //
params: [...params.value],
};
store.commit('setLocalDemoValue', {
demo: 'requestTester',
value: { [presetName]: preset },
});
};
//
const loadPreset = (name) => {
const preset = requestTesterPresets.value[name];
if (preset) {
url.value = preset.url;
method.value = preset.method;
headers.value = [...preset.headers];
bodyType.value = preset.bodyType;
body.value = preset.body;
formDataItems.value = preset.formDataItems.map((item) => ({
key: item.key,
type: item.type,
value: item.type === 'pair' ? '' : [], //
isFile: item.type === 'pair' && item.value === 'File',
}));
params.value = [...preset.params];
response.value = null;
}
};
//
const deletePreset = (name) => {
swal.window('info', '确定删除?', `删除预设"${name}"`, '确定', '取消').then(result => {
if (result.isConfirmed) {
store.commit('deleteLocalDemoValue', {
demo: 'requestTester',
value: name,
});
}
})
};
return {
url,
method,
headers,
bodyType,
body,
formDataItems,
params,
response,
requestTesterPresets,
addHeader,
removeHeader,
addParam,
removeParam,
addFormDataItem,
removeFormDataItem,
resetValue,
addArrayItem,
removeArrayItem,
handleFileChange,
handleArrayFileChange,
sendRequest,
savePreset,
loadPreset,
deletePreset,
};
},
};
</script>
<style scoped>
/* 默认深色模式 */
:root {
--bg-color: #2c2c2c;
--text-color: #ffffff;
--input-bg: #3c3c3c;
--border-color: #555;
--button-bg: #4a90e2;
--button-hover: #357abd;
}
/* 浅色模式 */
.theme-light {
--bg-color: #ffffff;
--text-color: #333333;
--input-bg: #f0f0f0;
--border-color: #ccc;
--button-bg: #4a90e2;
--button-hover: #357abd;
}
.request-tester {
display: flex;
gap: 20px;
max-width: 1200px;
width: 100%;
height: calc(100vh - 60px);
color: var(--text-color);
}
.operation-area {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.storage-area {
width: 250px;
padding: 20px;
border-left: 1px solid var(--border-color);
overflow-y: auto;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
}
input,
select,
textarea {
width: 100%;
padding: 8px;
background-color: var(--input-bg);
border: 1px solid var(--border-color);
color: var(--text-color);
border-radius: 4px;
}
textarea {
height: 100px;
resize: vertical;
}
.key-value-pair {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.key-value-pair input {
flex: 1;
}
.formdata-item {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
border: 1px solid var(--border-color);
padding: 10px;
border-radius: 4px;
}
.formdata-item input {
width: calc(100% - 18px);
}
.array-item {
display: flex;
gap: 10px;
margin-top: 5px;
}
button {
padding: 6px 12px;
background-color: var(--button-bg);
color: #fff;
border: gray solid 1px;
border-radius: 4px;
cursor: pointer;
}
.theme-light button {
color: black;
}
button:hover {
background-color: var(--button-hover);
}
.actions {
margin-top: 20px;
display: flex;
gap: 10px;
}
.response {
margin-top: 20px;
}
.response pre {
background-color: var(--input-bg);
padding: 10px;
border-radius: 4px;
}
.preset-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 0;
}
.preset-name {
cursor: pointer;
color: var(--button-bg);
}
.preset-name:hover {
text-decoration: underline;
}
</style>

View File

@ -4,6 +4,7 @@ import AuthService from "../../services/auth.js";
import Home from '../pages/Home.vue'; import Home from '../pages/Home.vue';
import Login from "../pages/Login.vue"; import Login from "../pages/Login.vue";
import Blog_home from "../pages/Blog_home.vue"; import Blog_home from "../pages/Blog_home.vue";
import Account from "../pages/accountPages/Account.vue"; import Account from "../pages/accountPages/Account.vue";
import Account_selfpage from "../pages/accountPages/Account_selfpage.vue"; import Account_selfpage from "../pages/accountPages/Account_selfpage.vue";
import Account_worksmanage from "../pages/accountPages/Account_worksmanage.vue"; import Account_worksmanage from "../pages/accountPages/Account_worksmanage.vue";
@ -12,14 +13,19 @@ import Account_draft from "../pages/accountPages/Account_draft.vue";
import Account_userInfo from "../pages/accountPages/Account_userInfo.vue"; import Account_userInfo from "../pages/accountPages/Account_userInfo.vue";
import Account_admin_uploadLog from "../pages/accountPages/Account_admin_uploadLog.vue"; import Account_admin_uploadLog from "../pages/accountPages/Account_admin_uploadLog.vue";
import Account_admin_userManage from "../pages/accountPages/Account_admin_userManage.vue"; import Account_admin_userManage from "../pages/accountPages/Account_admin_userManage.vue";
import Projects from "../pages/Projects_home.vue"; import Projects from "../pages/Projects_home.vue";
import Demos_home from "../pages/Demos_home.vue"; import Demos_home from "../pages/Demos_home.vue";
import Board_page from "../pages/demoPages/messageBoard/Board_page.vue"; import Board_page from "../pages/demoPages/messageBoard/Board_page.vue";
import Pod_page from "../pages/demoPages/podExercise/Pod_page.vue"; import Pod_page from "../pages/demoPages/podExercise/Pod_page.vue";
import Pod_quiz from "../pages/demoPages/podExercise/Quiz.vue"; import Pod_quiz from "../pages/demoPages/podExercise/Quiz.vue";
import Tools_home from "../pages/Tools_home.vue"; import Tools_home from "../pages/Tools_home.vue";
import GpaCalculator_page from "../pages/toolPages/gpaCalculator/gpaCalculator_page.vue"; import GpaCalculator_page from "../pages/toolPages/gpaCalculator/gpaCalculator_page.vue";
import PdfEx_page from "../pages/toolPages/pdfExtractor/pdfEx_page.vue"; import PdfEx_page from "../pages/toolPages/pdfExtractor/pdfEx_page.vue";
import RequestTester_page from "../pages/toolPages/RequestTester/requestTester_page.vue";
import About from "../pages/About.vue"; import About from "../pages/About.vue";
import Editor from "../pages/Editor.vue"; import Editor from "../pages/Editor.vue";
import NotFound from "../pages/errorPages/notFound.vue"; import NotFound from "../pages/errorPages/notFound.vue";
@ -81,6 +87,8 @@ const routes = [
{path: "gpa", component: GpaCalculator_page}, {path: "gpa", component: GpaCalculator_page},
{path: "2", component: PdfEx_page}, {path: "2", component: PdfEx_page},
{path: "pdf-extractor", component: PdfEx_page}, {path: "pdf-extractor", component: PdfEx_page},
{path: "3", component: RequestTester_page},
{path: "request-tester", component: RequestTester_page},
] ]
}, { }, {
path: '/about', path: '/about',

View File

@ -38,6 +38,9 @@ const store = createStore({
setLocalDemoValue(state, obj) { setLocalDemoValue(state, obj) {
state.demosLocal[obj.demo] = {...state.demosLocal[obj.demo], ...obj.value} state.demosLocal[obj.demo] = {...state.demosLocal[obj.demo], ...obj.value}
}, },
deleteLocalDemoValue(state, obj) {
delete state.demosLocal[obj.demo][obj.value];
},
saveEdit(state, obj) { saveEdit(state, obj) {
state.editStore = {...state.editStore, ...obj}; state.editStore = {...state.editStore, ...obj};
}, },

View File

@ -0,0 +1,5 @@
import {getDomain} from "./getDomain.js";
export function blogImage(id) {
return `https://${getDomain()}/data/blog/images/${id}`;
}