diff --git a/src/pages/Editor.vue b/src/pages/Editor.vue index fae9a74..10605f4 100644 --- a/src/pages/Editor.vue +++ b/src/pages/Editor.vue @@ -60,21 +60,15 @@ editorConfig.MENU_CONF.uploadImage = { fieldName: "image", // 后端接收字段名 async customUpload(file, insertFn) { - - const index = imagesCache.value.length; // 当前图片索引 - - // 读取图片尺寸 + const index = imagesCache.value.length; const imageSize = await getImageSize(file); - - // 存入缓存 + const objectURL = URL.createObjectURL(file); // 生成 blob URL imagesCache.value.push({ file, + url: objectURL, // 保存 blob URL originalWidth: imageSize.width, - originalHeight: imageSize.height + originalHeight: imageSize.height, }); - - // 生成临时预览 URL - const objectURL = URL.createObjectURL(file); insertFn(objectURL, `image-${index}`, objectURL); // 插入编辑器 } }; @@ -155,10 +149,30 @@ const submitBlog = async () => { 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 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 到索引的映射 const urlToIndexMap = new Map(); @@ -185,8 +199,8 @@ const submitBlog = async () => { const styleMatch = imgTag.match(/style=["']([^"']+)["']/); let width = "", height = ""; - // 获取图片的原始尺寸(从 images 中根据 blob URL 匹配) - const imageData = images.find((img) => URL.createObjectURL(img.file) === src); + // 从 images 中根据保存的 url 匹配图片数据 + const imageData = images.find((img) => img.url === src); // 使用保存的 url 比较 const originalWidth = imageData?.originalWidth || 0; const originalHeight = imageData?.originalHeight || 0; @@ -217,7 +231,6 @@ const submitBlog = async () => { } }); - // 2️⃣ 构造表单数据 const formData = new FormData(); formData.append("title", title); @@ -225,20 +238,21 @@ const submitBlog = async () => { formData.append("allow_comments", allowComments); formData.append("draft", true); - images.forEach((imgData, index) => { - formData.append(`images[${index}]`, imgData.file); // 按索引存储图片 + console.log(images); + images.forEach((imgData) => { + formData.append(`images`, imgData.file); // 按索引存储图片 }); console.log(Object.fromEntries(formData.entries())); - // 3️⃣ 发送请求 + // 3️⃣ 发送请求 api.post('/blogs', formData, { headers: { "Content-Type": "multipart/form-data", }, }).then(response => { if (response.code === 0) { - swal.window('success', `提交成功, 博客id${response.blogId || '未找到blogId字段'}`); + swal.window('success', `提交成功, 博客id${response.blogId || '未找到blogId字段'}`, 'ok', '好的'); return; } swal.window('error', '提交失败', `code字段为${response.code}; 错误信息: ${response.message}`, 'ok', '好的'); diff --git a/src/pages/Test_page.vue b/src/pages/Test_page.vue index 9a77c14..77cc9f5 100644 --- a/src/pages/Test_page.vue +++ b/src/pages/Test_page.vue @@ -1,7 +1,12 @@ <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> <template> diff --git a/src/pages/Tools_home.vue b/src/pages/Tools_home.vue index 686b51b..39994c6 100644 --- a/src/pages/Tools_home.vue +++ b/src/pages/Tools_home.vue @@ -13,6 +13,7 @@ const searchQuery = ref(''); const tools = ref([ { id: 1, title: 'GPA在线计算器', description: '手动输入在线算', image: null, category: ['计算'] }, { id: 2, title: 'PDF页面提取器', description: '提取指定页和封面', image: null, category: ['提取', 'pdf'] }, + { id: 3, title: '请求测试器', description: '自定义请求测试', image: null, category: ['开发', '网络'] }, ]); onMounted(() => { diff --git a/src/pages/blogPages/SingleBlog_page.vue b/src/pages/blogPages/SingleBlog_page.vue new file mode 100644 index 0000000..6830045 --- /dev/null +++ b/src/pages/blogPages/SingleBlog_page.vue @@ -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> \ No newline at end of file diff --git a/src/pages/toolPages/RequestTester/requestTester_page.vue b/src/pages/toolPages/RequestTester/requestTester_page.vue new file mode 100644 index 0000000..520d713 --- /dev/null +++ b/src/pages/toolPages/RequestTester/requestTester_page.vue @@ -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> \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js index 5dff1d4..cc26c64 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -4,6 +4,7 @@ import AuthService from "../../services/auth.js"; import Home from '../pages/Home.vue'; import Login from "../pages/Login.vue"; import Blog_home from "../pages/Blog_home.vue"; + import Account from "../pages/accountPages/Account.vue"; import Account_selfpage from "../pages/accountPages/Account_selfpage.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_admin_uploadLog from "../pages/accountPages/Account_admin_uploadLog.vue"; import Account_admin_userManage from "../pages/accountPages/Account_admin_userManage.vue"; + import Projects from "../pages/Projects_home.vue"; + import Demos_home from "../pages/Demos_home.vue"; import Board_page from "../pages/demoPages/messageBoard/Board_page.vue"; import Pod_page from "../pages/demoPages/podExercise/Pod_page.vue"; import Pod_quiz from "../pages/demoPages/podExercise/Quiz.vue"; + import Tools_home from "../pages/Tools_home.vue"; import GpaCalculator_page from "../pages/toolPages/gpaCalculator/gpaCalculator_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 Editor from "../pages/Editor.vue"; import NotFound from "../pages/errorPages/notFound.vue"; @@ -81,6 +87,8 @@ const routes = [ {path: "gpa", component: GpaCalculator_page}, {path: "2", component: PdfEx_page}, {path: "pdf-extractor", component: PdfEx_page}, + {path: "3", component: RequestTester_page}, + {path: "request-tester", component: RequestTester_page}, ] }, { path: '/about', diff --git a/src/store/index.js b/src/store/index.js index df81829..862e090 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -38,6 +38,9 @@ const store = createStore({ setLocalDemoValue(state, obj) { state.demosLocal[obj.demo] = {...state.demosLocal[obj.demo], ...obj.value} }, + deleteLocalDemoValue(state, obj) { + delete state.demosLocal[obj.demo][obj.value]; + }, saveEdit(state, obj) { state.editStore = {...state.editStore, ...obj}; }, diff --git a/src/utils/imageResource.js b/src/utils/imageResource.js new file mode 100644 index 0000000..36f8184 --- /dev/null +++ b/src/utils/imageResource.js @@ -0,0 +1,5 @@ +import {getDomain} from "./getDomain.js"; + +export function blogImage(id) { + return `https://${getDomain()}/data/blog/images/${id}`; +} \ No newline at end of file