新增文件共享;

修改主页
This commit is contained in:
Guarp 2025-03-29 10:39:20 +08:00
parent 890a5b7e76
commit 2cdc8c99d1
23 changed files with 1291 additions and 479 deletions

9
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12", "@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.7.9", "axios": "^1.7.9",
"diff": "^7.0.0",
"element-plus": "^2.9.6", "element-plus": "^2.9.6",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
@ -2479,6 +2480,14 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/diff": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/dom7": { "node_modules/dom7": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/dom7/-/dom7-3.0.0.tgz", "resolved": "https://registry.npmjs.org/dom7/-/dom7-3.0.0.tgz",

View File

@ -14,6 +14,7 @@
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12", "@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.7.9", "axios": "^1.7.9",
"diff": "^7.0.0",
"element-plus": "^2.9.6", "element-plus": "^2.9.6",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",

Binary file not shown.

View File

@ -2,10 +2,11 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>3D第一人称射击游戏</title> <title>3D第一人称射击游戏</title>
<style> <style>
body { margin: 0; overflow: hidden; } body { margin: 0; overflow: hidden; }
canvas { touch-action: none; }
#crosshair { #crosshair {
position: absolute; position: absolute;
top: 50%; top: 50%;
@ -15,76 +16,223 @@
height: 10px; height: 10px;
background-color: red; background-color: red;
border-radius: 50%; border-radius: 50%;
pointer-events: none;
} }
.ui { font-family: Arial, sans-serif; color: white; font-size: 20px; position: absolute; } .ui { font-family: Arial, sans-serif; color: white; font-size: 16px; position: absolute; pointer-events: none; }
#joystick {
position: absolute;
bottom: 20px;
left: 20px;
width: 100px;
height: 100px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
z-index: 10;
display: none; /* Hidden by default */
}
#joystickKnob {
position: absolute;
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
}
#shootButton, #reloadButton {
display: none; /* Hidden by default */
}
#shootButton {
position: absolute;
bottom: 50px;
right: 20px;
width: 80px;
height: 80px;
background: rgba(255, 0, 0, 0.7);
border-radius: 50%;
border: none;
color: white;
font-size: 20px;
z-index: 10;
}
#reloadButton {
position: absolute;
bottom: 150px;
right: 40px;
width: 40px;
height: 40px;
background: rgba(0, 255, 0, 0.7);
border-radius: 50%;
border: none;
color: white;
font-size: 16px;
z-index: 10;
}
#modeSelectModal, #gameOverModal {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
z-index: 20;
}
#modeSelectModal button, #gameOverModal button {
padding: 10px 20px;
font-size: 16px;
margin: 5px;
cursor: pointer;
}
#gameOverModal { display: none; }
</style> </style>
</head> </head>
<body> <body>
<!-- 准星 -->
<div id="crosshair"></div> <div id="crosshair"></div>
<!-- 受伤闪烁效果 -->
<div id="damageOverlay" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(255, 0, 0, 0); pointer-events: none;"></div> <div id="damageOverlay" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(255, 0, 0, 0); pointer-events: none;"></div>
<!-- 游戏结束弹窗 --> <div id="modeSelectModal">
<div id="gameOverModal" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.8); color: white; padding: 20px; border-radius: 10px; text-align: center; display: none;"> <h2>选择游戏模式</h2>
<p>请选择您的设备类型:</p>
<button id="desktopMode">桌面模式</button>
<button id="mobileMode">移动模式</button>
</div>
<div id="gameOverModal">
<h2>游戏结束</h2> <h2>游戏结束</h2>
<p id="finalScore"></p> <p id="finalScore"></p>
<button id="restartButton" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">重新开始</button> <button id="restartButton">重新开始</button>
</div> </div>
<div id="joystick"><div id="joystickKnob"></div></div>
<button id="shootButton">射击</button>
<button id="reloadButton">换弹</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>
<script> <script>
let isGamePaused = false; let isGamePaused = true; // Start paused until mode is selected
// 初始化场景 let mode = null; // 'desktop' or 'mobile'
// Audio Functions
function playShootSound() { const audio = new Audio('./audio/手枪开枪.mp3'); audio.play(); }
function playReloadSound() { const audio = new Audio('./audio/手枪换弹.mp3'); audio.play(); }
function playDamageSound() { const audio = new Audio('./audio/受击.mp3'); audio.play(); }
// Scene Setup
const scene = new THREE.Scene(); const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.6, 0); // 玩家高度1.6米 camera.position.set(0, 1.6, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true }); const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight); renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement); document.body.appendChild(renderer.domElement);
// 添加光照
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight); scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(1, 1, 1); directionalLight.position.set(1, 1, 1);
scene.add(directionalLight); scene.add(directionalLight);
// 视角控制(重新设计) // Camera Control
let yaw = 0; // 水平旋转角度(左右) let yaw = 0, pitch = 0, sensitivity = 0.005;
let pitch = 0; // 垂直旋转角度(上下)
const sensitivity = 0.002; // 鼠标灵敏度
document.addEventListener('click', () => {
renderer.domElement.requestPointerLock();
});
document.addEventListener('mousemove', (event) => {
if (document.pointerLockElement === renderer.domElement) {
const movementX = event.movementX || 0;
const movementY = event.movementY || 0;
// 水平旋转(向左移动鼠标镜头向左)
yaw -= movementX * sensitivity;
// 垂直旋转(向上移动鼠标镜头向上)
pitch -= movementY * sensitivity;
pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch)); // 限制垂直角度
// 更新相机旋转
camera.rotation.order = 'YXZ'; // 先Y水平后X垂直
camera.rotation.set(pitch, yaw, 0);
}
});
// WASD移动控制
const keys = {}; const keys = {};
document.addEventListener('keydown', (event) => keys[event.key.toLowerCase()] = true);
document.addEventListener('keyup', (event) => keys[event.key.toLowerCase()] = false);
// 子弹管理 function setupDesktopControls() {
let bullets = 10; document.addEventListener('click', () => renderer.domElement.requestPointerLock());
const maxBullets = 10; document.addEventListener('mousemove', (event) => {
if (document.pointerLockElement === renderer.domElement) {
yaw -= (event.movementX || 0) * sensitivity;
pitch -= (event.movementY || 0) * sensitivity;
pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch));
camera.rotation.order = 'YXZ';
camera.rotation.set(pitch, yaw, 0);
}
});
document.addEventListener('keydown', (event) => keys[event.key.toLowerCase()] = true);
document.addEventListener('keyup', (event) => keys[event.key.toLowerCase()] = false);
}
let touchStartX = 0, touchStartY = 0, isSwiping = false;
function setupMobileControls() {
document.addEventListener('touchstart', (e) => {
if (e.target.tagName !== 'BUTTON' && e.target.id !== 'joystick' && e.target.id !== 'joystickKnob') {
isSwiping = true;
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
});
document.addEventListener('touchmove', (e) => {
if (isSwiping) {
const deltaX = e.touches[0].clientX - touchStartX;
const deltaY = e.touches[0].clientY - touchStartY;
yaw -= deltaX * sensitivity;
pitch -= deltaY * sensitivity;
pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch));
camera.rotation.order = 'YXZ';
camera.rotation.set(pitch, yaw, 0);
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
});
document.addEventListener('touchend', () => { isSwiping = false; });
}
// Joystick Control
const joystick = document.getElementById('joystick');
const knob = document.getElementById('joystickKnob');
let joystickActive = false, joystickX = 0, joystickY = 0, joystickTouchId = null;
function setupJoystick() {
joystick.style.display = 'block';
joystick.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
joystickTouchId = touch.identifier;
joystickActive = true;
updateJoystick(touch);
});
document.addEventListener('touchmove', (e) => {
if (joystickActive) {
for (let touch of e.touches) {
if (touch.identifier === joystickTouchId) {
updateJoystick(touch);
break;
}
}
}
});
document.addEventListener('touchend', (e) => {
for (let touch of e.changedTouches) {
if (touch.identifier === joystickTouchId) {
joystickActive = false;
knob.style.left = '50%';
knob.style.top = '50%';
joystickX = 0;
joystickY = 0;
joystickTouchId = null;
break;
}
}
});
}
function updateJoystick(touch) {
const rect = joystick.getBoundingClientRect();
let dx = touch.clientX - (rect.left + rect.width / 2);
let dy = touch.clientY - (rect.top + rect.height / 2);
const distance = Math.sqrt(dx * dx + dy * dy);
const maxDistance = rect.width / 2 - 20;
if (distance > maxDistance) {
dx = dx * maxDistance / distance;
dy = dy * maxDistance / distance;
}
knob.style.left = `${50 + (dx / maxDistance) * 50}%`;
knob.style.top = `${50 + (dy / maxDistance) * 50}%`;
joystickX = dx / maxDistance;
joystickY = dy / maxDistance;
}
// UI Elements
let bullets = 10, maxBullets = 10, health = 100;
const bulletDisplay = document.createElement('div'); const bulletDisplay = document.createElement('div');
bulletDisplay.className = 'ui'; bulletDisplay.className = 'ui';
bulletDisplay.style.bottom = '10px'; bulletDisplay.style.bottom = '10px';
@ -92,8 +240,6 @@
bulletDisplay.innerText = `子弹: ${bullets}`; bulletDisplay.innerText = `子弹: ${bullets}`;
document.body.appendChild(bulletDisplay); document.body.appendChild(bulletDisplay);
// 生命值
let health = 100;
const healthDisplay = document.createElement('div'); const healthDisplay = document.createElement('div');
healthDisplay.className = 'ui'; healthDisplay.className = 'ui';
healthDisplay.style.bottom = '10px'; healthDisplay.style.bottom = '10px';
@ -102,29 +248,17 @@
document.body.appendChild(healthDisplay); document.body.appendChild(healthDisplay);
function deductHealth(amount) { function deductHealth(amount) {
if (isGamePaused) return; // 如果已暂停,不再扣血 if (isGamePaused) return;
health -= amount; health -= amount;
healthDisplay.innerText = `生命值: ${health}`; healthDisplay.innerText = `生命值: ${health}`;
playDamageSound();
// 触发红色闪烁效果
const damageOverlay = document.getElementById('damageOverlay'); const damageOverlay = document.getElementById('damageOverlay');
gsap.fromTo( gsap.fromTo(damageOverlay, { backgroundColor: 'rgba(255, 0, 0, 0.5)' }, { backgroundColor: 'rgba(255, 0, 0, 0)', duration: 0.5, ease: 'power1.out' });
damageOverlay, if (health <= 0) endGame();
{ backgroundColor: 'rgba(255, 0, 0, 0.5)' },
{
backgroundColor: 'rgba(255, 0, 0, 0)',
duration: 0.5,
ease: 'power1.out'
}
);
if (health <= 0) {
endGame();
}
} }
// 枪械展示 // Gun
const gunTexture = new THREE.TextureLoader().load('./image/gun.png'); // 替换为实际枪械图片URL const gunTexture = new THREE.TextureLoader().load('./image/gun.png');
const gunMaterial = new THREE.MeshBasicMaterial({ map: gunTexture, transparent: true }); const gunMaterial = new THREE.MeshBasicMaterial({ map: gunTexture, transparent: true });
const gunGeometry = new THREE.PlaneGeometry(1, 1); const gunGeometry = new THREE.PlaneGeometry(1, 1);
const gun = new THREE.Mesh(gunGeometry, gunMaterial); const gun = new THREE.Mesh(gunGeometry, gunMaterial);
@ -132,147 +266,111 @@
camera.add(gun); camera.add(gun);
scene.add(camera); scene.add(camera);
// 开枪函数 // Shoot Function
function shoot() { function shoot() {
if (bullets > 0) { if (bullets > 0 && !isGamePaused) {
bullets--; bullets--;
bulletDisplay.innerText = `子弹: ${bullets}`; bulletDisplay.innerText = `子弹: ${bullets}`;
gsap.to(gun.position, { z: -1.2, duration: 0.05, yoyo: true, repeat: 1 }); playShootSound();
const tl = gsap.timeline();
tl.to(gun.position, { z: -0.8, duration: 0.02, ease: "power1.in" })
.to(gun.position, { z: -1, duration: 0.3, ease: "power2.out" });
const raycaster = new THREE.Raycaster(); const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera); raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
const intersects = raycaster.intersectObjects(enemies.map(e => e.sprite)); const intersects = raycaster.intersectObjects(enemies.map(e => e.sprite));
if (intersects.length > 0) { if (intersects.length > 0) {
const enemy = intersects[0].object; const enemy = intersects[0].object;
const hitDirection = new THREE.Vector3() const hitDirection = new THREE.Vector3().subVectors(enemy.position, camera.position).normalize();
.subVectors(enemy.position, camera.position)
.normalize();
// 添加击飞效果
gsap.to(enemy.position, { gsap.to(enemy.position, {
x: enemy.position.x + hitDirection.x * 5, // 沿射击方向飞5个单位 x: enemy.position.x + hitDirection.x * 5,
y: enemy.position.y + 2, // 向上飞2个单位 y: enemy.position.y + 2,
z: enemy.position.z + hitDirection.z * 5, z: enemy.position.z + hitDirection.z * 5,
duration: 0.3, // 飞行时间0.3秒
ease: "power2.out", // 减速效果
onComplete: () => {
scene.remove(enemy); // 动画完成后移除敌人
enemies = enemies.filter(e => e.sprite !== enemy);
}
});
// 同时淡出敌人
gsap.to(enemy.material, {
opacity: 0,
duration: 0.3, duration: 0.3,
onComplete: () => { ease: "power2.out",
enemy.material.opacity = 1; // 重置透明度以备复用 onComplete: () => { scene.remove(enemy); enemies = enemies.filter(e => e.sprite !== enemy); }
}
}); });
gsap.to(enemy.material, { opacity: 0, duration: 0.3, onComplete: () => { enemy.material.opacity = 1; } });
score += 20; score += 20;
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`; timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
} }
} }
} }
document.addEventListener('click', shoot);
document.addEventListener('keydown', (event) => {
if (event.key === 'r' && bullets < maxBullets) {
bullets = maxBullets;
bulletDisplay.innerText = `子弹: ${bullets}`;
}
});
// 地形生成 // Reload Function
function reload() {
if (bullets < maxBullets && !isGamePaused) {
playReloadSound();
setTimeout(() => {
bullets = maxBullets;
bulletDisplay.innerText = `子弹: ${bullets}`;
}, 1500);
}
}
// Desktop/Mobile Shoot/Reload Setup
function setupControls() {
if (mode === 'desktop') {
document.addEventListener('click', shoot);
document.addEventListener('keydown', (event) => { if (event.key === 'r') reload(); });
setupDesktopControls();
} else if (mode === 'mobile') {
const shootButton = document.getElementById('shootButton');
const reloadButton = document.getElementById('reloadButton');
shootButton.style.display = 'block';
reloadButton.style.display = 'block';
shootButton.addEventListener('touchstart', (e) => { e.preventDefault(); shoot(); });
reloadButton.addEventListener('touchstart', (e) => { e.preventDefault(); reload(); });
setupMobileControls();
setupJoystick();
}
}
// Terrain
const noise = new SimplexNoise(); const noise = new SimplexNoise();
const terrainSize = 100; const terrainSize = 100, terrainDetail = 0.1, terrainHeight = 1;
const terrainDetail = 0.1;
const terrainHeight = 1; // 平缓地形
const geometry = new THREE.PlaneGeometry(terrainSize, terrainSize, 100, 100); const geometry = new THREE.PlaneGeometry(terrainSize, terrainSize, 100, 100);
geometry.rotateX(-Math.PI / 2); geometry.rotateX(-Math.PI / 2);
const vertices = geometry.attributes.position.array; const vertices = geometry.attributes.position.array;
for (let i = 0; i < vertices.length; i += 3) { for (let i = 0; i < vertices.length; i += 3) {
const x = vertices[i]; const x = vertices[i], z = vertices[i + 2];
const z = vertices[i + 2]; vertices[i + 1] = noise.noise2D(x * terrainDetail, z * terrainDetail) * terrainHeight;
const y = noise.noise2D(x * terrainDetail, z * terrainDetail) * terrainHeight;
vertices[i + 1] = y;
} }
geometry.computeVertexNormals(); geometry.computeVertexNormals();
const terrainMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 }); const terrainMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });
const terrain = new THREE.Mesh(geometry, terrainMaterial); const terrain = new THREE.Mesh(geometry, terrainMaterial);
scene.add(terrain); scene.add(terrain);
// 射线投射器
const raycaster = new THREE.Raycaster(); const raycaster = new THREE.Raycaster();
raycaster.ray.direction.set(0, -1, 0); raycaster.ray.direction.set(0, -1, 0);
// 敌人生成(高度贴合地表) // Enemies
let enemies = []; let enemies = [];
class Enemy { class Enemy {
constructor() { constructor() {
const enemyTextures = [ const enemyTextures = ['./image/1.png', './image/2.png'];
'./image/1.png', const texture = new THREE.TextureLoader().load(enemyTextures[Math.floor(Math.random() * 2)]);
'./image/2.png',
];
const texture = new THREE.TextureLoader().load(enemyTextures[Math.floor(Math.random() * enemyTextures.length)]);
const material = new THREE.SpriteMaterial({ map: texture }); const material = new THREE.SpriteMaterial({ map: texture });
this.sprite = new THREE.Sprite(material); this.sprite = new THREE.Sprite(material);
this.sprite.scale.set(2, 2, 1); this.sprite.scale.set(2, 2, 1);
const edge = Math.floor(Math.random() * 4), halfSize = terrainSize / 2;
// 随机选择地图边缘0: 上, 1: 下, 2: 左, 3: 右)
const edge = Math.floor(Math.random() * 4);
const halfSize = terrainSize / 2; // terrainSize = 100, halfSize = 50
switch (edge) { switch (edge) {
case 0: // 上边缘 (z = 50) case 0: this.sprite.position.set(Math.random() * terrainSize - halfSize, 10, halfSize); break;
this.sprite.position.set( case 1: this.sprite.position.set(Math.random() * terrainSize - halfSize, 10, -halfSize); break;
Math.random() * terrainSize - halfSize, // x: [-50, 50] case 2: this.sprite.position.set(-halfSize, 10, Math.random() * terrainSize - halfSize); break;
10, // y: 10 case 3: this.sprite.position.set(halfSize, 10, Math.random() * terrainSize - halfSize); break;
halfSize // z: 50
);
break;
case 1: // 下边缘 (z = -50)
this.sprite.position.set(
Math.random() * terrainSize - halfSize, // x: [-50, 50]
10, // y: 10
-halfSize // z: -50
);
break;
case 2: // 左边缘 (x = -50)
this.sprite.position.set(
-halfSize, // x: -50
10, // y: 10
Math.random() * terrainSize - halfSize // z: [-50, 50]
);
break;
case 3: // 右边缘 (x = 50)
this.sprite.position.set(
halfSize, // x: 50
10, // y: 10
Math.random() * terrainSize - halfSize // z: [-50, 50]
);
break;
} }
scene.add(this.sprite); scene.add(this.sprite);
this.sprite.material.opacity = 0;
gsap.to(this.sprite.material, { opacity: 1, duration: 1 }); gsap.to(this.sprite.material, { opacity: 1, duration: 1 });
} }
update() { update() {
this.sprite.lookAt(camera.position); this.sprite.lookAt(camera.position);
const direction = new THREE.Vector3().subVectors(camera.position, this.sprite.position).normalize(); const direction = new THREE.Vector3().subVectors(camera.position, this.sprite.position).normalize();
this.sprite.position.add(direction.multiplyScalar(0.05)); this.sprite.position.add(direction.multiplyScalar(0.05));
// 贴合地表高度
raycaster.ray.origin.copy(this.sprite.position); raycaster.ray.origin.copy(this.sprite.position);
raycaster.ray.origin.y = 10; // 从上方检测 raycaster.ray.origin.y = 10;
const intersects = raycaster.intersectObject(terrain); const intersects = raycaster.intersectObject(terrain);
if (intersects.length > 0) { if (intersects.length > 0) this.sprite.position.y = intersects[0].point.y + 1;
this.sprite.position.y = intersects[0].point.y + 1; // 高度为地表+1
}
if (this.sprite.position.distanceTo(camera.position) < 1) { if (this.sprite.position.distanceTo(camera.position) < 1) {
deductHealth(15); deductHealth(15);
scene.remove(this.sprite); scene.remove(this.sprite);
@ -280,111 +378,98 @@
} }
} }
} }
setInterval(() => { if (enemies.length < 10 && !isGamePaused) enemies.push(new Enemy()); }, 2000);
setInterval(() => { // Timer and Score
if (enemies.length < 10) { let timeLeft = 150, score = 0;
const newEnemy = new Enemy();
enemies.push(newEnemy);
}
}, 2000); // 每2秒生成一个敌人
// 计时与得分
let timeLeft = 150;
let score = 0;
const timerDisplay = document.createElement('div'); const timerDisplay = document.createElement('div');
timerDisplay.className = 'ui'; timerDisplay.className = 'ui';
timerDisplay.style.top = '10px'; timerDisplay.style.top = '10px';
timerDisplay.style.left = '10px'; timerDisplay.style.left = '10px';
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`; timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
document.body.appendChild(timerDisplay); document.body.appendChild(timerDisplay);
setInterval(() => { setInterval(() => {
if (isGamePaused) return; // 如果暂停,不更新计时器 if (!isGamePaused) {
timeLeft--; timeLeft--;
score++; score++;
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`; timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
if (timeLeft <= 0) { if (timeLeft <= 0) endGame();
endGame();
} }
}, 1000); }, 1000);
// 新增游戏结束函数
function endGame() { function endGame() {
isGamePaused = true; isGamePaused = true;
document.exitPointerLock(); if (mode === 'desktop') document.exitPointerLock();
// 显示弹窗
const modal = document.getElementById('gameOverModal'); const modal = document.getElementById('gameOverModal');
const finalScore = document.getElementById('finalScore'); document.getElementById('finalScore').innerText = `最终得分: ${score}`;
finalScore.innerText = `最终得分: ${score}`;
modal.style.display = 'block'; modal.style.display = 'block';
} }
// 新增重新开始函数
function restartGame() {
// 重置游戏状态
isGamePaused = false;
health = 100;
bullets = maxBullets;
timeLeft = 150;
score = 0;
enemies.forEach(enemy => scene.remove(enemy.sprite)); // 移除所有敌人
enemies = [];
// 更新UI function restartGame() {
isGamePaused = false;
health = 100; bullets = maxBullets; timeLeft = 150; score = 0;
enemies.forEach(enemy => scene.remove(enemy.sprite));
enemies = [];
healthDisplay.innerText = `生命值: ${health}`; healthDisplay.innerText = `生命值: ${health}`;
bulletDisplay.innerText = `子弹: ${bullets}`; bulletDisplay.innerText = `子弹: ${bullets}`;
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`; timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
// 隐藏弹窗
document.getElementById('gameOverModal').style.display = 'none'; document.getElementById('gameOverModal').style.display = 'none';
// 重新启动动画循环
animate(); animate();
} }
// 为重新开始按钮添加事件监听
document.getElementById('restartButton').addEventListener('click', restartGame); document.getElementById('restartButton').addEventListener('click', restartGame);
// 天空贴图 // Skybox
const skyboxLoader = new THREE.CubeTextureLoader(); const skyboxLoader = new THREE.CubeTextureLoader();
const skyboxTexture = skyboxLoader.load([ scene.background = skyboxLoader.load([
'https://i.imgur.com/px.jpg', 'https://i.imgur.com/nx.jpg', 'https://i.imgur.com/px.jpg', 'https://i.imgur.com/nx.jpg',
'https://i.imgur.com/py.jpg', 'https://i.imgur.com/ny.jpg', 'https://i.imgur.com/py.jpg', 'https://i.imgur.com/ny.jpg',
'https://i.imgur.com/pz.jpg', 'https://i.imgur.com/nz.jpg' 'https://i.imgur.com/pz.jpg', 'https://i.imgur.com/nz.jpg'
]); // 替换为实际天空贴图URL ]);
scene.background = skyboxTexture;
// 动画循环 // Animation Loop
function animate() { function animate() {
if (isGamePaused) return; // 如果暂停则跳出循环 if (isGamePaused) return;
requestAnimationFrame(animate); requestAnimationFrame(animate);
// WASD移动
const speed = 0.1; const speed = 0.1;
const frontVector = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion); const frontVector = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion); const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
if (keys['w']) camera.position.add(frontVector.multiplyScalar(speed)); if (mode === 'desktop') {
if (keys['s']) camera.position.add(frontVector.multiplyScalar(-speed)); if (keys['w']) camera.position.add(frontVector.multiplyScalar(speed));
if (keys['a']) camera.position.add(rightVector.multiplyScalar(-speed)); if (keys['s']) camera.position.add(frontVector.multiplyScalar(-speed));
if (keys['d']) camera.position.add(rightVector.multiplyScalar(speed)); if (keys['a']) camera.position.add(rightVector.multiplyScalar(-speed));
if (keys['d']) camera.position.add(rightVector.multiplyScalar(speed));
} else if (mode === 'mobile') {
camera.position.add(frontVector.multiplyScalar(-joystickY * speed));
camera.position.add(rightVector.multiplyScalar(joystickX * speed));
}
// 贴合地形高度
raycaster.ray.origin.copy(camera.position); raycaster.ray.origin.copy(camera.position);
raycaster.ray.origin.y += 1.6; raycaster.ray.origin.y += 1.6;
const intersects = raycaster.intersectObject(terrain); const intersects = raycaster.intersectObject(terrain);
if (intersects.length > 0) { if (intersects.length > 0) camera.position.y = intersects[0].point.y + 1.6;
camera.position.y = intersects[0].point.y + 1.6;
}
// 更新敌人(仅在游戏未暂停时)
if (!isGamePaused) {
enemies.forEach(enemy => enemy.update());
}
enemies.forEach(enemy => enemy.update());
renderer.render(scene, camera); renderer.render(scene, camera);
} }
animate();
// 自适应窗口大小 // Mode Selection
const modeSelectModal = document.getElementById('modeSelectModal');
document.getElementById('desktopMode').addEventListener('click', () => {
mode = 'desktop';
modeSelectModal.style.display = 'none';
isGamePaused = false;
setupControls();
animate();
});
document.getElementById('mobileMode').addEventListener('click', () => {
mode = 'mobile';
modeSelectModal.style.display = 'none';
isGamePaused = false;
setupControls();
animate();
});
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight; camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();

View File

@ -6,6 +6,7 @@ import store from "../store/index.js";
import {blogImage} from "../utils/imageResource.js"; import {blogImage} from "../utils/imageResource.js";
import swal from "../utils/sweetalert.js"; import swal from "../utils/sweetalert.js";
import api from "../utils/axios.js"; import api from "../utils/axios.js";
import {formatGMTToLocal} from "../utils/formatTime.js";
const props = defineProps({ const props = defineProps({
cover: { cover: {
@ -67,7 +68,7 @@ function onPermission() {
<!-- 左侧博客封面 + 标题 --> <!-- 左侧博客封面 + 标题 -->
<div class="piece-left"> <div class="piece-left">
<!-- <img :src="cover" alt="封面" class="cover-image" />--> <!-- <img :src="cover" alt="封面" class="cover-image" />-->
<DefaultCover :imageSrc="blogImage(cover)" :text="title" size="60" class="cover-image"/> <DefaultCover :imageSrc="blogImage(cover)" :text="title" size="60px" class="cover-image"/>
<div class="title-text">{{ title }}</div> <div class="title-text">{{ title }}</div>
</div> </div>
@ -75,8 +76,8 @@ function onPermission() {
<div class="piece-right"> <div class="piece-right">
<!-- 上方时间信息 --> <!-- 上方时间信息 -->
<div class="times"> <div class="times">
<div>创建时间{{ createdTime }}</div> <div>创建时间{{ formatGMTToLocal(createdTime) }}</div>
<div>最后修改{{ lastModifiedTime }}</div> <div>最后修改{{ formatGMTToLocal(lastModifiedTime) }}</div>
</div> </div>
<!-- 下方操作 --> <!-- 下方操作 -->
<div class="actions"> <div class="actions">

View File

@ -30,7 +30,7 @@ function processFormat(text) {
.replaceAll('/lt', '<') .replaceAll('/lt', '<')
.replaceAll('/gt', '>') .replaceAll('/gt', '>')
.replaceAll(/(?<!<)\/(\w+)/g, '\\$1') .replaceAll(/(?<!<)\/(\w+)/g, '\\$1')
.replaceAll('m\\s', 'm/s') .replaceAll('\\n', '<br>')
; ;
} }
@ -75,8 +75,6 @@ onMounted(async () => {
.renderer-container p { .renderer-container p {
margin-bottom: 1rem; margin-bottom: 1rem;
display: flex;
justify-content: center;
} }
.renderer-container code { .renderer-container code {

View File

@ -66,6 +66,7 @@ const navItems = [
// {name: '', link: '/demos'}, // {name: '', link: '/demos'},
{name: '小工具', link: '/tools'}, {name: '小工具', link: '/tools'},
{name: '留言板', link: '/demos/board'}, {name: '留言板', link: '/demos/board'},
{name: '文件共享', link: '/demos/file-sharing'},
'divider', 'divider',
{name: '日志', link: '/about'} {name: '日志', link: '/about'}
] ]

View File

@ -28,6 +28,14 @@ const demos = ref([
author: ["Louis Zhou"], author: ["Louis Zhou"],
tags: ['游戏'], tags: ['游戏'],
}, },
{
id: 'file-sharing',
name: '文件共享',
description: '云盘',
date: '2025-3-28',
author: ["Mike Rong"],
tags: ['功能'],
},
]); ]);

View File

@ -4,7 +4,7 @@ import {TransitionPresets, useTransition} from '@vueuse/core';
import api from "../utils/axios.js"; import api from "../utils/axios.js";
const source = ref(0); const source = ref(0);
const viewCountDisplay = ref(true); const viewCountDisplay = ref(false);
const outputValue = useTransition(source, { const outputValue = useTransition(source, {
duration: 3000, duration: 3000,
transition: TransitionPresets.easeOutExpo, transition: TransitionPresets.easeOutExpo,
@ -15,8 +15,7 @@ onMounted(async () => {
const response = await api.get('/view'); const response = await api.get('/view');
if (response.code === 0) { if (response.code === 0) {
source.value = response.views; source.value = response.views;
} else { viewCountDisplay.value = true;
viewCountDisplay.value = false;
} }
}) })
@ -28,13 +27,32 @@ onMounted(async () => {
<el-container> <el-container>
<el-main> <el-main>
<div style="font-size: 50px; font-weight: inherit; padding: 20px">欢迎来到 MVA-CYBER !</div> <div class="left">
<div class="sun">
<div class="earth">
<div class="moon"></div>
</div>
</div>
<!-- <div class="ball"/>-->
</div>
<div class="right">
<div class="logo">CYBER</div>
<div class="text">CODING YOUTH BOT AND ENGINEERING REVOLUTION</div>
<div class="link">
<a>ABOUT US</a>
<a>ABOUT THIS WEBSITE</a>
<a>CONTACT US</a>
</div>
</div>
</el-main> </el-main>
<el-footer> <el-footer>
<!-- <div class="text-bg"></div>--> <!-- <div class="text-bg"></div>-->
<el-col v-if="viewCountDisplay"> <Transition name="fade">
<el-statistic title="网站累计访问次数" :value="outputValue"/> <el-col v-if="viewCountDisplay">
</el-col> <el-statistic title="网站累计访问次数" :value="outputValue"/>
</el-col>
</Transition>
</el-footer> </el-footer>
</el-container> </el-container>
@ -48,6 +66,7 @@ img {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.el-container { .el-container {
display: flex; display: flex;
align-items: center; align-items: center;
@ -56,15 +75,86 @@ img {
overflow: hidden; overflow: hidden;
} }
.el-main { .el-main {
display: flex; display: flex;
justify-content: center; justify-content: space-between;
align-items: center;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%;
overflow: hidden;
} }
.left {
height: 100%;
width: 100%;
flex: 1;
position: relative;
//overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.ball {
background: #00a8a8;
width: 25vw;
height: 25vw;
border-radius: 50%;
animation: pulse 5s ease infinite;
}
.theme-light .ball {
background: #ffff3c;
}
@keyframes pulse {
0% {
opacity: 0.7;
filter: blur(50px);
}
50% {
opacity: 0.4;
filter: blur(70px);
}
100% {
opacity: 0.7;
filter: blur(50px);
}
}
.right {
margin-right: 5vw;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 18vh;
}
.logo {
font-family: 'Netron', sans-serif;
font-size: 10vw;
}
.right .text {
font-family: 'Netron', sans-serif;
font-size: 1.27vw;
}
.right .link {
display: flex;
gap: 1.5vw;
font-family: sans-serif;
padding-top: 5.5vh;
font-size: 0.9vw;
}
.link a {
color: white;
cursor: pointer;
}
.theme-light a {
color: #000;
}
.el-footer { .el-footer {
position: relative; position: relative;
} }
.el-col { .el-col {
//position: absolute; //position: absolute;
@ -74,9 +164,137 @@ img {
//align-items: center; //align-items: center;
//justify-content: center; //justify-content: center;
} }
.el-statistic { .el-statistic {
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column-reverse;
align-items: center; align-items: center;
} }
@media (max-width: 900px) {
.el-main {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
position: relative;
}
.left {
position: absolute;
top: 50%;
justify-content: flex-start;
flex-direction: column;
width: 100%;
z-index: -1;
}
.ball {
width: 10rem;
height: 10rem;
}
.right {
margin: 0;
padding-bottom: 0;
}
.logo {
font-size: 5rem;
}
.right .text {
font-size: 0.65rem;
}
.right .link {
display: flex;
gap: 0.5rem;
padding-top: 1.7rem;
font-size: 0.5rem;
}
}
*{
/* 初始化 */
margin: 0;
padding: 0;
}
.left{
/* 自定义属性,--s为太阳的颜色--e为地球的颜色--m为月球的颜色可通过var函数对其调用 */
--s: #f39c12;
--e: #3498db;
--m: #1abc9c;
}
/* 太阳 */
.sun{
/* 绝对定位 */
position: absolute;
width: 100px;
height: 100px;
border-radius: 50%;
/* 通过var函数调用自定义属性--s设置太阳的颜色 */
background-color: var(--s);
/* 通过设置阴影,实现发光的效果 */
box-shadow: 0 0 10px var(--s),
0 0 20px var(--s),
0 0 30px var(--s),
0 0 40px var(--s);
/* 执行动画:动画名 时长 线性的 无限次播放 */
animation: rotate 36.5s linear infinite;
}
/* 太阳外圈(地球轨道) */
.sun::after{
content: "";
width: 330px;
height: 330px;
/* 绝对定位 居中 */
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
border: 1px solid gray;
border-radius: 50%;
z-index: -1;
}
/* 地球 */
.earth{
position: absolute;
left: 200px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: var(--e);
box-shadow: 0 0 10px var(--e),
0 0 20px var(--e),
0 0 30px var(--e),
0 0 40px var(--e);
/* 执行动画:动画名 时长 线性的 无限次播放 */
animation: rotate 3s linear infinite;
}
/* 地球外圈(月球轨道) */
.earth::after{
content: "";
width: 84px;
height: 84px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
border: 1px solid gray;
border-radius: 50%;
}
/* 月球 */
.moon{
position: absolute;
left: 50px;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: var(--m);
box-shadow: 0 0 5px var(--m),
0 0 10px var(--m),
0 0 20px var(--m);
}
/* 定义动画 */
@keyframes rotate {
to{
transform: rotateZ(360deg);
}
}
</style> </style>

View File

@ -5,6 +5,7 @@ import MarkdownViewer from "../../components/mdRenderer.vue";
import swal from "../../utils/sweetalert.js"; import swal from "../../utils/sweetalert.js";
import store from "../../store/index.js"; import store from "../../store/index.js";
import api from "../../utils/axios.js"; import api from "../../utils/axios.js";
import getCurrentTime from "../../utils/getCurrentTime.js";
const mdInput = ref(store.state.editStore.log || ''); const mdInput = ref(store.state.editStore.log || '');
const version = ref(store.state.editStore.logVersion || ''); const version = ref(store.state.editStore.logVersion || '');
@ -66,20 +67,19 @@ const postLog = async () => {
swal.tip('info', '内容为必填项'); swal.tip('info', '内容为必填项');
return; return;
} }
swal.window('info', '提交吗? ', '提交前请检查内容, 确保格式正确', '确定', '取消').then(async (result) => { const result = await swal.window('info', '提交吗? ', '提交前请检查内容, 确保格式正确', '确定', '取消');
if (result.isConfirmed) { if (result.isConfirmed) {
const response = await api.post('/postweblog', { const response = await api.post('/postweblog', {
version: version.value, version: version.value,
date: getFormattedTime(), date: getCurrentTime(),
content: mdInput.value content: mdInput.value
}); });
if (response.code === 0) { if (response.code === 0) {
swal.tip('success', '提交成功'); swal.tip('success', '提交成功');
return; return;
}
swal.tip('error', '提交失败...')
} }
}) swal.tip('error', '提交失败...')
}
} }
const shiftNailed = () => { const shiftNailed = () => {

View File

@ -143,13 +143,13 @@ watch(autoSaveInterval, () => {
/> />
<!-- 改名 --> <!-- 改名 -->
<span <!-- <span-->
v-if="!isEditingUsername" <!-- v-if="!isEditingUsername"-->
class="edit-emoji" <!-- class="edit-emoji"-->
@click="editUsername" <!-- @click="editUsername"-->
> <!-- >-->
🖊 <!-- 🖊-->
</span> <!-- </span>-->
<div v-if="isEditingUsername"> <div v-if="isEditingUsername">
<button <button
class="confirm-btn" class="confirm-btn"

View File

@ -194,7 +194,7 @@ const changeSort = () => {
</div> </div>
<el-divider/> <el-divider/>
<div class="blog-content"> <div class="blog-content">
<div style="padding: 0 56px 40px" v-html="processedContent"/> <div :style="{padding: windowWidth<870 ? '0 5px 40px' : '0 56px 40px'}" v-html="processedContent"/>
<el-divider v-if="windowWidth<1050"/> <el-divider v-if="windowWidth<1050"/>
<Transition name="fade"> <Transition name="fade">
<div class="interact-bar" v-if="interactInfo.complete" :class="{outside: windowWidth>1050}"> <div class="interact-bar" v-if="interactInfo.complete" :class="{outside: windowWidth>1050}">
@ -257,7 +257,7 @@ const changeSort = () => {
:sort-mode="sortMode" :sort-mode="sortMode"
:blog-author-uid="blog.poster" :blog-author-uid="blog.poster"
/> />
<p v-else>不允许评论</p> <div v-else class="loading">不允许评论</div>
</div> </div>

View File

@ -47,15 +47,15 @@
<div class="title">{{ titleInput }}</div> <div class="title">{{ titleInput }}</div>
<div class="blog-meta"> <div class="blog-meta">
<div class="avatar"> <div class="avatar">
<img <Profile_display v-if="store.getters.profileImage" :id="userProfile(store.state.userInfo.profile)"/>
v-if="store.getters.profileImage" </div>
:src="userProfile(store.state.userInfo.profile)" <div class="info-text">
alt="Poster Avatar" <span class="username-text">{{ store.state.userInfo.username || '' }}</span>
/> <span class="date-text">最后更新: {{ getCurrentTime() }}</span>
</div> </div>
<span>{{ store.state.userInfo.username || '' }}</span>
</div> </div>
<div class="viewer" v-html="editorRef.getHtml()"></div> <el-divider/>
<div :style="{padding: windowWidth<870 ? '0 5px 40px' : '0 56px 40px'}" class="viewer" v-html="editorRef.getHtml()"></div>
</el-container> </el-container>
</el-container> </el-container>
</div> </div>
@ -73,6 +73,9 @@ import mobileTest from "../../utils/mobileTest.js";
import {blogImage, userProfile} from "../../utils/imageResource.js"; import {blogImage, userProfile} from "../../utils/imageResource.js";
import store from "../../store/index.js"; import store from "../../store/index.js";
import router from "../../router/index.js"; import router from "../../router/index.js";
import getCurrentTime from "../../utils/getCurrentTime.js";
import {formatGMTToLocal} from "../../utils/formatTime.js";
import Profile_display from "../../components/Profile_display.vue";
// shallowRef // shallowRef
@ -82,11 +85,16 @@ const blogStatus = ref(null);
const titleInput = ref(''); const titleInput = ref('');
const viewing = ref(false); const viewing = ref(false);
const windowWidth = ref(0);
// HTML // HTML
const valueHtml = ref(''); const valueHtml = ref('');
const imagesCache = ref([]); const imagesCache = ref([]);
const checkWindowSize = () => {
windowWidth.value = window.innerWidth;
};
// //
const toolbarConfig = {}; const toolbarConfig = {};
const editorConfig = { const editorConfig = {
@ -126,12 +134,14 @@ const getImageSize = (file) => {
const img = new Image(); const img = new Image();
img.src = URL.createObjectURL(file); img.src = URL.createObjectURL(file);
img.onload = () => { img.onload = () => {
resolve({width: img.width, height: img.height}); resolve({width: img.width, height: img.height})
}; };
}); });
}; };
onMounted(() => { onMounted(() => {
checkWindowSize();
window.addEventListener('resize', checkWindowSize);
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
document.querySelectorAll('[data-menu-key="editImage"]').forEach(btn => { document.querySelectorAll('[data-menu-key="editImage"]').forEach(btn => {
btn.remove(); btn.remove();
@ -535,9 +545,9 @@ const disable = () => {
.editor-container { .editor-container {
max-width: 802px; max-width: 802px;
min-height: 100px;
width: calc(100% - 5px); width: calc(100% - 5px);
height: calc(100% - 10px); height: calc(100% - 10px);
min-height: 500px;
margin-top: 5px; margin-top: 5px;
display: flex; display: flex;
word-break: break-word; word-break: break-word;
@ -566,17 +576,26 @@ const disable = () => {
.blog-meta { .blog-meta {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px;
width: auto; width: auto;
padding: 0 10px; padding: 0 10px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.avatar, .avatar img { .info-text {
width: 40px; padding-left: 20px;
height: 40px; display: flex;
border-radius: 50%; flex-direction: column;
overflow: hidden; }
.username-text {
//cursor: pointer;
font-size: 17px;
font-weight: 600;
}
.date-text {
font-size: 13px;
opacity: 0.6;
} }
.viewer { .viewer {
@ -585,7 +604,6 @@ const disable = () => {
min-height: 100px; min-height: 100px;
height: calc(100vh - 213px); height: calc(100vh - 213px);
word-break: break-word; word-break: break-word;
border: 1px solid rgba(93, 93, 93, 0.34);
padding: 0 10px; padding: 0 10px;
overflow: auto; overflow: auto;
} }

View File

@ -43,8 +43,13 @@ const nonNullKeysCount = computed(() => {
const submit = async () => { const submit = async () => {
const formData = new FormData(); const formData = new FormData();
formData.append("allow_comments", infoForm.value.allowComment); if (infoForm.value.allowComment !== null) {
formData.append("category", infoForm.value.tagSelect); formData.append("allow_comments", infoForm.value.allowComment);
}
if (infoForm.value.tagSelect !== null) {
formData.append("category", infoForm.value.tagSelect);
}
formData.append("draft", 0); formData.append("draft", 0);
console.log(Object.fromEntries(formData.entries())); console.log(Object.fromEntries(formData.entries()));

View File

@ -0,0 +1,249 @@
<template>
<div class="share-page" :class="{ 'theme-light': isLightTheme }">
<div v-if="isLoading" class="loading">正在检测文件共享服务...</div>
<!-- 错误提示 -->
<transition name="fade">
<div v-if="!isLoading && error" class="error">{{ error }}</div>
</transition>
<!-- 主要内容 -->
<transition name="fade">
<div v-if="!isLoading && !error" class="content">
<div class="card">
<div class="title">文件共享</div>
<p class="subtitle">
<span v-if="isVisitor">游客模式使用更多功能请登录</span>
<span v-else-if="!verified">提示可以联系管理员进行认证认证后可使用文件共享账号</span>
</p>
<button @click="handleButtonClick" class="share-button">进入文件共享页</button>
</div>
</div>
</transition>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import store from '../../../store';
import swal from '../../../utils/sweetalert.js';
import api from '../../../utils/axios.js';
export default {
computed: {
store() {
return store;
},
},
setup() {
const isLoading = ref(true);
const error = ref(null);
const ip = ref(null);
const verified = ref();
const account = ref(false);
const isVisitor = ref(false);
const isLightTheme = ref(false);
const startLoading = () => {
isLoading.value = true;
};
const stopLoading = () => {
isLoading.value = false;
};
const fetchIp = async () => {
try {
const response = await api.get('/share/ip');
if (response.code === 0) {
const response1 = await api.get('/share/ping');
const isIpAlive = response1.code === 0;
if (!isIpAlive) {
swal.tip('error', '暂时无法连接文件共享服务,请稍后再试');
throw new Error('连接失败');
}
ip.value = response.ip;
} else {
throw new Error('获取 IP 失败');
}
} catch (err) {
throw err;
}
};
const fetchStatus = async () => {
try {
const response = await api.get('/share/status');
if (response.code === 0) {
verified.value = response.verified;
account.value = response.account;
} else if (response.code === 1) {
isVisitor.value = true;
} else {
throw new Error('获取状态失败');
}
} catch (err) {
throw err;
}
};
const handleButtonClick = async () => {
if (verified.value && !account.value) {
store.commit('startLoading', '正在加载...');
try {
const response = await api.post('/share/signup');
if (response.code === 0) {
store.commit('stopLoading');
window.location.href = `http://${ip.value}:5244`;
} else {
swal.tip('error', '注册失败');
}
} catch (err) {
swal.tip('error', '注册失败');
}
store.commit('stopLoading');
} else {
window.location.href = `http://${ip.value}:5244`;
}
};
onMounted(async () => {
startLoading();
try {
await fetchIp();
await fetchStatus();
} catch (err) {
error.value = '加载失败,请稍后重试';
swal.tip('error', err);
} finally {
stopLoading();
}
});
return {
isLoading,
error,
verified,
account,
isVisitor,
isLightTheme,
handleButtonClick,
};
},
};
</script>
<style scoped>
.share-page {
width: 100%;
min-height: calc(100vh - 60px);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
background: linear-gradient(45deg, var(--bg-color), var(--accent-color));
background-size: 400% 400%;
animation: gradientAnimation 15s ease infinite, bgDisplay 0.5s forwards;
padding: 2rem 2rem 5rem;
box-sizing: border-box;
}
@keyframes bgDisplay {
to {
opacity: 1;
}
}
@keyframes gradientAnimation {
0% {
background-position: 0 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
.loading {
font-size: 1.5rem;
font-weight: bold;
animation: pulse 1.5s infinite;
padding-bottom: 3rem;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.error {
font-size: 1.2rem;
color: #ff4d4f;
}
.content {
text-align: center;
}
.card {
background: var(--card-bg);
color: var(--text-color);
padding: 2rem;
border-radius: 1rem;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
display: inline-block;
min-width: 300px;
max-width: 500px;
}
.title {
font-size: 1.8rem;
font-weight: bold;
margin-bottom: 1rem;
}
.subtitle {
font-size: 1rem;
margin-bottom: 1.5rem;
color: var(--input-text);
}
.share-button {
padding: 0.8rem 2rem;
font-size: 1.1rem;
font-weight: 500;
background: #4caf50;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
}
.theme-light .share-button {
background: #66bb6a;
}
.share-button:hover {
background: #388e3c;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -9,7 +9,7 @@ import AuthService from "../../../../services/auth.js";
import PagingController from "../../../components/PagingController.vue"; import PagingController from "../../../components/PagingController.vue";
const messages = ref(store.state.demosLocal.board?.messages || []); const messages = ref(store.state.demosLocal.board?.messages || []);
const amount = ref(store.state.demosLocal.board?.amount || 0); const amount = ref(store.state.demosLocal.board?.amount || 1);
const currentPage = ref(store.state.demosLocal.board?.currentPage || 1); const currentPage = ref(store.state.demosLocal.board?.currentPage || 1);
const pageLoading = ref(false); const pageLoading = ref(false);
@ -30,22 +30,8 @@ async function refreshBoard(page, pageSize) {
PAGE: page, PAGE: page,
PAGE_SIZE: pageSize PAGE_SIZE: pageSize
}).then(res => { }).then(res => {
res.data = res.data.map((msg)=>{
function stringToFloat(str) {
// 使
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i); //
hash = hash & hash; // 32
}
// 0~1
return Math.abs(hash) / (Math.pow(2, 32) - 1);
}
msg.likes = Math.round(stringToFloat('random'+ msg.content)*10000);
return msg;})
messages.value = res.data; messages.value = res.data;
amount.value = Math.ceil(res.amount / pageSize); amount.value = Math.ceil(res.amount / pageSize) || 1;
store.commit('setLocalDemoValue', {demo: 'board', value: {messages: messages.value}}); store.commit('setLocalDemoValue', {demo: 'board', value: {messages: messages.value}});
store.commit('setLocalDemoValue', {demo: 'board', value: {amount: amount.value}}); store.commit('setLocalDemoValue', {demo: 'board', value: {amount: amount.value}});
}) })
@ -79,7 +65,7 @@ async function sendMessage() {
swal.tip('info', '不得为空') swal.tip('info', '不得为空')
return; return;
} }
if (userInput.value.trim().length > 500) { if (userInput.value.trim().length > 400) {
swal.tip('error', '太长了!') swal.tip('error', '太长了!')
return; return;
} }
@ -146,7 +132,8 @@ onMounted(async () => {
<div class="board-body"> <div class="board-body">
<div class="message-container" :class="{loading: pageLoading}"> <div class="message-container" :class="{loading: pageLoading}">
<Message v-for="msg in messages" :message="msg" @refresh-board="refreshBoard"/> <Message v-for="msg in messages" :message="msg" @refresh-board="refreshBoard"/>
<div v-if="messages?.length === 0">正在加载...</div> <div v-if="pageLoading && messages?.length !== 0">正在加载...</div>
<div v-else-if="messages?.length === 0">暂无留言</div>
</div> </div>
<paging-controller :current-page="currentPage" :amount="amount" :go-page-func="goPage" :loading="pageLoading"/> <paging-controller :current-page="currentPage" :amount="amount" :go-page-func="goPage" :loading="pageLoading"/>
</div> </div>
@ -204,6 +191,7 @@ onMounted(async () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center;
gap: 15px; gap: 15px;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
} }

View File

@ -62,6 +62,7 @@ async function deleteMessage(id, message) {
<style scoped> <style scoped>
.message { .message {
width: calc(100% - 25px);
display: flex; display: flex;
background: #333333; background: #333333;
border-radius: 15px; border-radius: 15px;

View File

@ -1,28 +1,10 @@
<script setup> <script setup>
import {computed, ref} from "vue"; import {computed, ref, onMounted} from "vue";
import api from "../../../utils/axios.js";
import {demoResourceURL} from "../../../utils/demoResource.js";
const podList = ref([ const podList = ref([])
{
title: 'MCQ 1',
categories: 'AP phy 1',
file: '250227',
describe: '',
date: '2025-2-26'
},{
title: 'MCQ 2',
categories: 'AP phy 1',
file: '250306',
describe: '',
date: '2025-2-27'
},{
title: 'MCQ 3',
categories: 'AP phy 1',
file: '250313',
describe: '',
date: '2025-3-6'
}
])
const uniqueCats = computed(() => { const uniqueCats = computed(() => {
const keys = new Set(); const keys = new Set();
@ -31,22 +13,27 @@ const uniqueCats = computed(() => {
}); });
return Array.from(keys); return Array.from(keys);
}); });
onMounted(async () => {
const response = await api.get(demoResourceURL('pod', "index.json"));
podList.value = response;
})
</script> </script>
<template> <template>
<div class="container" v-if="/^\/demos\/pod\/?$/.test($route.path)"> <div class="container" v-if="/^\/demos\/pod\/?$/.test($route.path)" v-loading="podList.length === 0" element-loading-background="rgba(0, 0, 0, 0.5)">
<h1>无限制做题大赛</h1> <h1>无限制做题大赛</h1>
<div class="pod-container" v-for="cat in uniqueCats"> <div class="pod-container" v-for="cat in uniqueCats">
<div class="folder"> <div class="folder">
<div class="folder-title">{{ cat }}</div> <div class="folder-title">{{ cat }}</div>
<div class="pods-display"> <div class="pods-display">
<router-link class="pods" v-for="pod in podList" :to="`/demos/pod/quiz?cat=${pod.categories.toLowerCase().replaceAll(' ', '')}&id=${pod.file}`"> <router-link class="pods" v-for="pod in podList" :to="`/demos/pod/quiz?cat=${pod.categories.toLowerCase().replaceAll(' ', '')}&id=${pod.file}`">
<div class="title">{{ pod.title }}</div> <div class="title">{{ pod.title }}</div>
<div class="date">{{ pod.date }}</div> <div class="date">{{ pod.date }}</div>
</router-link> </router-link>
</div>
</div> </div>
</div> </div>
</div>
</div> </div>
<router-view v-else /> <router-view v-else />
</template> </template>

View File

@ -23,9 +23,12 @@
</div> </div>
</div> </div>
<!-- 提交按钮或下一题按钮 --> <!-- 提交或下一题按钮 -->
<button v-if="!submitted" @click="submitAnswer" class="submit-btn">提交答案</button> <button v-if="!submitted" @click="submitAnswer" class="submit-btn">提交答案</button>
<button v-else @click="nextQuestion" class="next-btn">下一题</button> <button v-else @click="nextQuestion" class="next-btn">下一题</button>
<span> </span>
<button v-if="!isLocked" @click="lockQuestion" class="lock-btn">锁定当前题目</button>
<button v-else @click="unlockQuestion" class="unlock-btn">解锁题目</button>
<!-- 答题结果和解析 --> <!-- 答题结果和解析 -->
<div v-if="submitted" class="result"> <div v-if="submitted" class="result">
@ -47,7 +50,7 @@
<h4>-- 题目 {{ index + 1 }} --</h4> <h4>-- 题目 {{ index + 1 }} --</h4>
<QuestionText :text="similar.text" :medias="data.medias"/> <QuestionText :text="similar.text" :medias="data.medias"/>
<p>正确答案 <p>正确答案
<general-renderer :content-input="processMedia(similar.correctAnswer, data.medias)"></general-renderer> <general-renderer :content-input="processMedia(similar.answers, data.medias)"></general-renderer>
</p> </p>
<div v-if="similar.explanation" class="explanation"> <div v-if="similar.explanation" class="explanation">
<h4>解题思路</h4> <h4>解题思路</h4>
@ -86,6 +89,9 @@ import {demoResourceURL} from "../../../utils/demoResource.js";
const route = useRoute() const route = useRoute()
const isLocked = ref(false); //
const lockedSimilarQuestions = ref([]); //
// JSON // JSON
const data = reactive({ const data = reactive({
itemtypes: [], itemtypes: [],
@ -268,17 +274,18 @@ function generateNewExam() {
function findSimilarQuestions() { function findSimilarQuestions() {
const currentText = currentQuestion.value.text; const currentText = currentQuestion.value.text;
const similar = []; const similar = [];
for (let i = 1; i < data.itemtypes.length; i++) { for (let i = 1; i < data.texts.length; i++) {
if ( if (
calculateStringSimilarity(data.texts[i], currentText) > 50 && // calculateStringSimilarity(data.texts[i], currentText) > 50 &&
data.texts[i] !== currentQuestion.value.text// data.texts[i] !== currentText //
// similar.length < 2 // 2
) { ) {
const correctIdx = data.answerkeys[i].findIndex((key, idx) => idx > 0 && key === 1) - 1; const correctIdx = data.answerkeys[i].findIndex((key, idx) => idx > 0 && key === 1) - 1;
similar.push({ similar.push({
text: data.texts[i], text: data.texts[i],
correctAnswer: data.answers[i][correctIdx + 1], answers: data.answers[i],
explanation: data.explanations[i] answerkeys: data.answerkeys[i],
explanation: data.explanations[i],
type: data.itemtypes[i]
}); });
} }
} }
@ -286,12 +293,44 @@ function findSimilarQuestions() {
} }
function selectCurrentQuestion() { function selectCurrentQuestion() {
if (questionCount.value <= totalQuestions.value) { if (isLocked.value) {
currentQuestion.value = examQuestions.value[questionCount.value - 1]; //
userAnswer.value = null; if (lockedSimilarQuestions.value.length > 0) {
submitted.value = false; const randomIndex = Math.floor(Math.random() * lockedSimilarQuestions.value.length);
similarQuestions.value = []; currentQuestion.value = lockedSimilarQuestions.value[randomIndex];
} else {
swal.tip('info', '没有相似的题目可供选择,已自动解锁。');
isLocked.value = false;
selectCurrentQuestion(); //
}
} else {
//
if (questionCount.value <= totalQuestions.value) {
currentQuestion.value = examQuestions.value[questionCount.value - 1];
}
} }
userAnswer.value = null;
submitted.value = false;
similarQuestions.value = [];
}
function lockQuestion() {
if (currentQuestion.value) {
findSimilarQuestions(); //
if (similarQuestions.value.length > 0) {
lockedSimilarQuestions.value = similarQuestions.value;
isLocked.value = true;
swal.tip('success', '题目已锁定,将显示相似题目。');
} else {
swal.tip('info', '没有找到相似题目,无法锁定。');
}
}
}
function unlockQuestion() {
isLocked.value = false;
lockedSimilarQuestions.value = [];
swal.tip('success', '已解锁,现在将显示正常题目。');
selectCurrentQuestion(); //
} }
// //
@ -322,12 +361,27 @@ function submitAnswer() {
// //
function nextQuestion() { function nextQuestion() {
if (questionCount.value < totalQuestions.value) { if (isLocked.value) {
questionCount.value++; //
selectCurrentQuestion(); if (lockedSimilarQuestions.value.length > 0) {
const randomIndex = Math.floor(Math.random() * lockedSimilarQuestions.value.length);
currentQuestion.value = lockedSimilarQuestions.value[randomIndex];
userAnswer.value = null;
submitted.value = false;
similarQuestions.value = [];
} else {
swal.tip('info', '没有更多相似的题目,已解锁。');
unlockQuestion();
}
} else { } else {
swal.tip('success', '所有题目已完成!将生成新试卷。') //
generateNewExam(); // if (questionCount.value < totalQuestions.value) {
questionCount.value++;
selectCurrentQuestion();
} else {
swal.tip('success', '所有题目已完成!将生成新试卷。');
generateNewExam();
}
} }
} }
</script> </script>
@ -462,4 +516,23 @@ function nextQuestion() {
.similar-question h4 { .similar-question h4 {
margin-bottom: 10px; margin-bottom: 10px;
} }
.lock-controls {
margin-top: 20px;
}
.lock-btn,
.unlock-btn {
padding: 10px 20px;
background-color: #28a745;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.lock-btn:hover,
.unlock-btn:hover {
background-color: #218838;
}
</style> </style>

View File

@ -49,26 +49,19 @@
<div class="form-group" v-if="method !== 'GET' && method !== 'DELETE' && bodyType === 'formdata'"> <div class="form-group" v-if="method !== 'GET' && method !== 'DELETE' && bodyType === 'formdata'">
<label>请求体 (FormData)</label> <label>请求体 (FormData)</label>
<div v-for="(item, index) in formDataItems" :key="index" class="formdata-item"> <div v-for="(item, index) in formDataItems" :key="index" class="formdata-item">
<!-- 键名 -->
<input v-model="item.key" placeholder="键名" /> <input v-model="item.key" placeholder="键名" />
<!-- 类型选择 -->
<select v-model="item.type" @change="resetValue(item)"> <select v-model="item.type" @change="resetValue(item)">
<option value="pair">普通键值对</option> <option value="pair">普通键值对</option>
<option value="array">数组</option> <option value="array">数组</option>
</select> </select>
<!-- 普通键值对输入 -->
<div v-if="item.type === 'pair'"> <div v-if="item.type === 'pair'">
<input v-if="!item.isFile" v-model="item.value" placeholder="值" /> <input v-if="!item.isFile" v-model="item.value" placeholder="值" />
<input v-else type="file" @change="handleFileChange(item, $event)" /> <input v-else type="file" @change="handleFileChange(item, $event)" />
<div style="margin: 10px 0"/> <div style="margin: 10px 0" />
<button @click="item.isFile = !item.isFile"> <button @click="item.isFile = !item.isFile">
{{ item.isFile ? '切换为文本' : '切换为文件' }} {{ item.isFile ? '切换为文本' : '切换为文件' }}
</button> </button>
</div> </div>
<!-- 数组输入 -->
<div v-if="item.type === 'array'"> <div v-if="item.type === 'array'">
<div v-for="(val, valIndex) in item.value" :key="valIndex" class="array-item"> <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-if="!val.isFile" v-model="val.content" placeholder="数组值" />
@ -80,8 +73,6 @@
</div> </div>
<button @click="addArrayItem(item)">添加数组项</button> <button @click="addArrayItem(item)">添加数组项</button>
</div> </div>
<!-- 删除键值对 -->
<button @click="removeFormDataItem(index)">删除键值对</button> <button @click="removeFormDataItem(index)">删除键值对</button>
</div> </div>
<button @click="addFormDataItem">添加键值对</button> <button @click="addFormDataItem">添加键值对</button>
@ -114,21 +105,29 @@
<!-- 右侧储存栏 --> <!-- 右侧储存栏 -->
<div class="storage-area"> <div class="storage-area">
<h3>储存栏</h3> <h3>储存栏</h3>
<div class="preset-actions">
<input v-model="importPresetString" placeholder="粘贴Base64预设字符串以导入" />
<button @click="importPreset" style="width: 100px">导入预设</button>
</div>
<div v-if="!requestTesterPresets || Object.keys(requestTesterPresets).length === 0">暂无预设</div> <div v-if="!requestTesterPresets || Object.keys(requestTesterPresets).length === 0">暂无预设</div>
<div v-else v-for="(preset, name) in requestTesterPresets" :key="name" class="preset-item"> <div v-else v-for="(preset, name) in requestTesterPresets" :key="name" class="preset-item">
<span @click="loadPreset(name)" class="preset-name">{{ name }}</span> <span @click="loadPreset(name)" class="preset-name">{{ name }}</span>
<button @click="deletePreset(name)">删除</button> <div class="preset-buttons">
<button @click="exportPreset(name)">导出</button>
<button @click="renamePreset(name)">重命名</button>
<button @click="deletePreset(name)">删除</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { ref, computed } from 'vue'; import {ref, computed} from 'vue';
import { useStore } from 'vuex'; import {useStore} from 'vuex';
import axios from 'axios'; import axios from 'axios';
import Swal from 'sweetalert2'; import Swal from 'sweetalert2';
import swal from "../../../utils/sweetalert.js"; import swal from '../../../utils/sweetalert.js';
export default { export default {
name: 'RequestTester', name: 'RequestTester',
@ -138,46 +137,43 @@ export default {
// //
const url = ref(''); const url = ref('');
const method = ref('GET'); const method = ref('GET');
const headers = ref([{ key: '', value: '' }]); const headers = ref([{key: '', value: ''}]);
const bodyType = ref('json'); // json formdata const bodyType = ref('json');
const body = ref(''); // JSON const body = ref('');
const formDataItems = ref([]); // FormData const formDataItems = ref([]);
const params = ref([{ key: '', value: '' }]); const params = ref([{key: '', value: ''}]);
const response = ref(null); const response = ref(null);
const importPresetString = ref('');
// Vuex requestTester // Vuex
const requestTesterPresets = computed(() => store.state.demosLocal.requestTester || {}); const requestTesterPresets = computed(() => store.state.demosLocal.requestTester || {});
// //
const addHeader = () => headers.value.push({ key: '', value: '' }); const addHeader = () => headers.value.push({key: '', value: ''});
const removeHeader = (index) => headers.value.splice(index, 1); const removeHeader = (index) => headers.value.splice(index, 1);
// //
const addParam = () => params.value.push({ key: '', value: '' }); const addParam = () => params.value.push({key: '', value: ''});
const removeParam = (index) => params.value.splice(index, 1); const removeParam = (index) => params.value.splice(index, 1);
// FormData // FormData
const addFormDataItem = () => { const addFormDataItem = () => {
formDataItems.value.push({ key: '', type: 'pair', value: '', isFile: false }); formDataItems.value.push({key: '', type: 'pair', value: '', isFile: false});
};
const removeFormDataItem = (index) => {
formDataItems.value.splice(index, 1);
}; };
const removeFormDataItem = (index) => formDataItems.value.splice(index, 1);
const resetValue = (item) => { const resetValue = (item) => {
if (item.type === 'pair') { if (item.type === 'pair') {
item.value = ''; item.value = '';
item.isFile = false; item.isFile = false;
} else if (item.type === 'array') { } else if (item.type === 'array') {
item.value = [{ content: '', isFile: false }]; item.value = [{content: '', isFile: false}];
} }
}; };
const addArrayItem = (item) => { const addArrayItem = (item) => {
if (!item.value) item.value = []; if (!item.value) item.value = [];
item.value.push({ content: '', isFile: false }); item.value.push({content: '', isFile: false});
};
const removeArrayItem = (item, index) => {
item.value.splice(index, 1);
}; };
const removeArrayItem = (item, index) => item.value.splice(index, 1);
const handleFileChange = (item, event) => { const handleFileChange = (item, event) => {
item.value = event.target.files[0]; item.value = event.target.files[0];
}; };
@ -187,16 +183,13 @@ export default {
// //
const sendRequest = async () => { const sendRequest = async () => {
response.value = '等待响应...'; // response.value = '等待响应...';
try { try {
let finalUrl = url.value; let finalUrl = url.value;
const headersObj = headers.value.reduce((acc, h) => { const headersObj = headers.value.reduce((acc, h) => {
if (h.key && h.value) acc[h.key] = h.value; if (h.key && h.value) acc[h.key] = h.value;
return acc; return acc;
}, {}); }, {});
//
const queryParams = params.value const queryParams = params.value
.filter((p) => p.key && p.value) .filter((p) => p.key && p.value)
.reduce((acc, p) => { .reduce((acc, p) => {
@ -211,7 +204,6 @@ export default {
params: queryParams, params: queryParams,
}; };
//
if (method.value !== 'GET' && method.value !== 'DELETE') { if (method.value !== 'GET' && method.value !== 'DELETE') {
if (bodyType.value === 'json') { if (bodyType.value === 'json') {
axiosConfig.data = body.value ? JSON.parse(body.value) : {}; axiosConfig.data = body.value ? JSON.parse(body.value) : {};
@ -226,9 +218,7 @@ export default {
formData.append(item.key, item.value); formData.append(item.key, item.value);
} else if (item.type === 'array' && item.value.length) { } else if (item.type === 'array' && item.value.length) {
item.value.forEach((val) => { item.value.forEach((val) => {
if (val.content) { if (val.content) formData.append(item.key + '[]', val.content);
formData.append(item.key + '[]', val.content);
}
}); });
} }
} }
@ -259,7 +249,7 @@ export default {
if (!result.isConfirmed) return; if (!result.isConfirmed) return;
const presetName = result.value; let presetName = result.value;
const preset = { const preset = {
url: url.value, url: url.value,
method: method.value, method: method.value,
@ -269,15 +259,58 @@ export default {
formDataItems: formDataItems.value.map((item) => ({ formDataItems: formDataItems.value.map((item) => ({
key: item.key, key: item.key,
type: item.type, type: item.type,
value: item.type === 'pair' ? (item.isFile ? 'File' : item.value) : item.value.map((v) => (v.isFile ? 'File' : v.content)), value: item.type === 'pair'
})), // ? (item.isFile ? '[[FILE]]' : item.value)
: item.value.map((v) => (v.isFile ? '[[FILE]]' : v.content)),
isFile: item.type === 'pair' ? item.isFile : undefined,
})),
params: [...params.value], params: [...params.value],
}; };
//
if (requestTesterPresets.value[presetName]) {
const overwriteResult = await Swal.fire({
title: '预设名称已存在',
text: `预设 "${presetName}" 已存在,您想覆盖它还是保存为新名称?`,
showCancelButton: true,
showDenyButton: true,
confirmButtonText: '覆盖',
denyButtonText: '保存为新名称',
cancelButtonText: '取消',
});
if (overwriteResult.isDismissed) return; //
if (overwriteResult.isDenied) {
//
const newNameResult = await Swal.fire({
title: '请输入新预设名称',
input: 'text',
inputValue: presetName,
inputLabel: '新预设名称',
inputPlaceholder: '请输入新的预设名称...',
showCancelButton: true,
cancelButtonText: '取消',
confirmButtonText: '确定',
inputValidator: (value) => {
if (!value) return '预设名称不能为空!';
if (value !== presetName && requestTesterPresets.value[value]) {
return '此名称已被使用,请选择其他名称!';
}
},
});
if (!newNameResult.isConfirmed) return;
presetName = newNameResult.value;
}
// presetName
}
store.commit('setLocalDemoValue', { store.commit('setLocalDemoValue', {
demo: 'requestTester', demo: 'requestTester',
value: { [presetName]: preset }, value: {[presetName]: preset},
}); });
swal.tip('success', '预设已保存');
}; };
// //
@ -292,24 +325,158 @@ export default {
formDataItems.value = preset.formDataItems.map((item) => ({ formDataItems.value = preset.formDataItems.map((item) => ({
key: item.key, key: item.key,
type: item.type, type: item.type,
value: item.type === 'pair' ? '' : [], // value: item.type === 'pair'
isFile: item.type === 'pair' && item.value === 'File', ? (item.value === '[[FILE]]' ? '' : item.value)
: item.value.map((v) => ({content: v === '[[FILE]]' ? '' : v, isFile: v === '[[FILE]]'})),
isFile: item.type === 'pair' ? item.isFile || item.value === '[[FILE]]' : undefined,
})); }));
params.value = [...preset.params]; params.value = [...preset.params];
response.value = null; response.value = null;
if (preset.formDataItems.some((item) =>
(item.type === 'pair' && item.value === '[[FILE]]') ||
(item.type === 'array' && item.value.includes('[[FILE]]'))
)) {
swal.tip('info', '此预设包含文件,请重新选择文件');
}
} }
}; };
// //
const deletePreset = (name) => { const deletePreset = (name) => {
swal.window('info', '确定删除?', `删除预设"${name}"`, '确定', '取消').then(result => { swal.window('info', '确定删除?', `删除预设"${name}"`, '确定', '取消').then((result) => {
if (result.isConfirmed) { if (result.isConfirmed) {
store.commit('deleteLocalDemoValue', { store.commit('deleteLocalDemoValue', {
demo: 'requestTester', demo: 'requestTester',
value: name, value: name,
}); });
swal.tip('success', '预设已删除');
} }
}) });
};
//
const exportPreset = (name) => {
const preset = requestTesterPresets.value[name];
if (preset) {
const exportData = { [name]: preset };
const jsonString = JSON.stringify(exportData);
const base64String = btoa(unescape(encodeURIComponent(jsonString)));
navigator.clipboard.writeText(base64String).then(() => {
Swal.fire({
title: '导出成功',
html: `<div style="max-height: 300px; overflow: auto; background: #f5f5f5; padding: 10px; border-radius: 4px;">${base64String}</div>`,
confirmButtonText: '关闭',
}).then(() => {
swal.tip('success', '预设已复制到剪切板');
});
}).catch(() => {
Swal.fire({
title: '导出成功(复制失败)',
html: `<div style="max-height: 300px; overflow: auto; background: #f5f5f5; padding: 10px; border-radius: 4px;">${base64String}</div>`,
confirmButtonText: '关闭',
});
});
}
};
//
const importPreset = async () => {
try {
const decodedString = decodeURIComponent(escape(atob(importPresetString.value)));
const importedData = JSON.parse(decodedString);
const presetName = Object.keys(importedData)[0];
if (!presetName) throw new Error('无效的预设格式');
//
if (requestTesterPresets.value[presetName]) {
const overwriteResult = await Swal.fire({
title: '预设名称已存在',
text: `预设 "${presetName}" 已存在,您想覆盖它还是导入为新名称?`,
showCancelButton: true,
showDenyButton: true,
confirmButtonText: '覆盖',
denyButtonText: '导入为新名称',
cancelButtonText: '取消',
});
if (overwriteResult.isDismissed) return; //
if (overwriteResult.isDenied) {
//
const newNameResult = await Swal.fire({
title: '请输入新预设名称',
input: 'text',
inputValue: presetName,
inputLabel: '新预设名称',
inputPlaceholder: '请输入新的预设名称...',
showCancelButton: true,
cancelButtonText: '取消',
confirmButtonText: '确定',
inputValidator: (value) => {
if (!value) return '预设名称不能为空!';
if (value !== presetName && requestTesterPresets.value[value]) {
return '此名称已被使用,请选择其他名称!';
}
},
});
if (!newNameResult.isConfirmed) return;
const newPresetName = newNameResult.value;
const newImportedData = { [newPresetName]: importedData[presetName] };
store.commit('setLocalDemoValue', {
demo: 'requestTester',
value: newImportedData,
});
} else if (overwriteResult.isConfirmed) {
//
store.commit('setLocalDemoValue', {
demo: 'requestTester',
value: importedData,
});
}
} else {
//
store.commit('setLocalDemoValue', {
demo: 'requestTester',
value: importedData,
});
}
importPresetString.value = '';
swal.tip('success', '预设已导入');
} catch (error) {
swal.tip('error', '导入失败请检查Base64预设字符串格式');
}
};
//
const renamePreset = async (oldName) => {
const result = await Swal.fire({
title: '重命名预设',
input: 'text',
inputLabel: '新预设名称',
inputValue: oldName,
showCancelButton: true,
cancelButtonText: '取消',
confirmButtonText: '确定',
inputValidator: (value) => !value && '预设名称不能为空!',
});
if (!result.isConfirmed) return;
const newName = result.value;
if (newName === oldName) return;
const preset = requestTesterPresets.value[oldName];
store.commit('setLocalDemoValue', {
demo: 'requestTester',
value: {[newName]: preset},
});
store.commit('deleteLocalDemoValue', {
demo: 'requestTester',
value: oldName,
});
swal.tip('success', '预设已重命名');
}; };
return { return {
@ -322,6 +489,7 @@ export default {
params, params,
response, response,
requestTesterPresets, requestTesterPresets,
importPresetString,
addHeader, addHeader,
removeHeader, removeHeader,
addParam, addParam,
@ -337,13 +505,15 @@ export default {
savePreset, savePreset,
loadPreset, loadPreset,
deletePreset, deletePreset,
exportPreset,
importPreset,
renamePreset,
}; };
}, },
}; };
</script> </script>
<style scoped> <style scoped>
/* 默认深色模式 */
:root { :root {
--bg-color: #2c2c2c; --bg-color: #2c2c2c;
--text-color: #ffffff; --text-color: #ffffff;
@ -353,7 +523,6 @@ export default {
--button-hover: #357abd; --button-hover: #357abd;
} }
/* 浅色模式 */
.theme-light { .theme-light {
--bg-color: #ffffff; --bg-color: #ffffff;
--text-color: #333333; --text-color: #333333;
@ -365,7 +534,6 @@ export default {
.request-tester { .request-tester {
display: flex; display: flex;
max-width: 1200px; max-width: 1200px;
width: 100%; width: 100%;
height: calc(100vh - 60px); height: calc(100vh - 60px);
@ -401,7 +569,7 @@ textarea {
width: 100%; width: 100%;
padding: 8px; padding: 8px;
background-color: var(--input-bg); background-color: var(--input-bg);
border: 1px solid var(--border-color); border: 1px solid gray;
color: var(--text-color); color: var(--text-color);
border-radius: 4px; border-radius: 4px;
} }
@ -449,6 +617,7 @@ button {
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
} }
.theme-light button { .theme-light button {
color: black; color: black;
} }
@ -491,4 +660,15 @@ button:hover {
.preset-name:hover { .preset-name:hover {
text-decoration: underline; text-decoration: underline;
} }
.preset-actions {
margin-bottom: 15px;
display: flex;
gap: 10px;
}
.preset-buttons {
display: flex;
gap: 5px;
}
</style> </style>

View File

@ -1,159 +1,153 @@
import {createRouter, createWebHistory} from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import store from '../store'; import store from '../store';
import AuthService from "../../services/auth.js"; 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";
import Account_setting from "../pages/accountPages/Account_setting.vue";
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/blogPages/blogEditor.vue";
import NotFound from "../pages/errorPages/notFound.vue";
import Test_page from "../pages/Test_page.vue";
import SingleBlog_page from "../pages/blogPages/SingleBlog_page.vue";
import SubmitBlog_page from "../pages/blogPages/submitBlogPages/SubmitBlog_page.vue";
import swal from "../utils/sweetalert.js"; import swal from "../utils/sweetalert.js";
import GunGame_page from "../pages/demoPages/gunGame/gunGame_page.vue";
import api from "../utils/axios.js"; import api from "../utils/axios.js";
import EmailVerify from "../pages/EmailVerify.vue";
// 直接引入组件
import NotFound from '../pages/errorPages/notFound.vue';
import Home from '../pages/Home.vue';
import Login from '../pages/Login.vue';
import EmailVerify from '../pages/EmailVerify.vue';
import BlogHome from '../pages/Blog_home.vue';
import SingleBlog from '../pages/blogPages/SingleBlog_page.vue';
import SubmitBlog from '../pages/blogPages/submitBlogPages/SubmitBlog_page.vue';
import ProjectsHome from '../pages/Projects_home.vue';
import DemosHome from '../pages/Demos_home.vue';
import About from '../pages/About.vue';
import Account from '../pages/accountPages/Account.vue';
import AccountSelfPage from '../pages/accountPages/Account_selfpage.vue';
import AccountWorksManage from '../pages/accountPages/Account_worksmanage.vue';
import AccountDraft from '../pages/accountPages/Account_draft.vue';
import AccountSetting from '../pages/accountPages/Account_setting.vue';
import AccountUserInfo from '../pages/accountPages/Account_userInfo.vue';
import AccountAdminUploadLog from '../pages/accountPages/Account_admin_uploadLog.vue';
import AccountAdminUserManage from '../pages/accountPages/Account_admin_userManage.vue';
import BlogEditor from '../pages/blogPages/blogEditor.vue';
import TestPage from '../pages/Test_page.vue';
const routes = [ const routes = [
{ {
path: '/404', path: '/404',
name: '404', name: '404',
component: NotFound, component: NotFound,
meta: {title: '404'} meta: { title: '404' }
}, { }, {
path: '/', path: '/',
name: 'Home', name: 'Home',
component: Home, component: Home,
meta: {title: '首页'} meta: { title: '首页' }
}, { }, {
path: '/login', path: '/login',
name: 'Login', name: 'Login',
component: Login, component: Login,
meta: {title: '登录'} meta: { title: '登录' }
}, { }, {
path: '/verify', path: '/verify',
name: 'EmailVerify', name: 'EmailVerify',
component: EmailVerify, component: EmailVerify,
meta: {title: '注册成功'} meta: { title: '注册成功' }
}, { }, {
path: '/blog', path: '/blog',
name: 'Blog', name: 'Blog',
component: Blog_home, component: BlogHome,
meta: {title: '博客'} meta: { title: '博客' }
}, { }, {
path: '/blog/:id', path: '/blog/:id',
name: 'Blogs', name: 'Blogs',
component: SingleBlog_page, component: SingleBlog,
meta: {title: '博客'} meta: { title: '博客' }
}, { }, {
path: '/blog/submit', path: '/blog/submit',
name: 'SubmitBlog', name: 'SubmitBlog',
component: SubmitBlog_page, component: SubmitBlog,
meta: {title: '发布博客'} meta: { title: '发布博客' }
},{ }, {
path: '/projects', path: '/projects',
name: 'Projects', name: 'Projects',
component: Projects, component: ProjectsHome,
meta: {title: '项目'} meta: { title: '项目' }
}, { }, {
path: '/demos', path: '/demos',
name: 'Demos', name: 'Demos',
component: Demos_home, component: DemosHome,
meta: {title: '实例'}, meta: { title: '实例' },
children: [ children: [
{ {
path: "board", path: "board",
component: Board_page, component: () => import('../pages/demoPages/messageBoard/Board_page.vue'),
meta: {title: '留言板'}, meta: { title: '留言板' },
}, },
{ {
path: "pod", path: "pod",
component: Pod_page, component: () => import('../pages/demoPages/podExercise/Pod_page.vue'),
meta: {title: '做题'}, meta: { title: '做题' },
children: [ children: [
{path: "quiz", component: Pod_quiz} { path: "quiz", component: () => import('../pages/demoPages/podExercise/Quiz.vue') }
] ]
}, },
{ {
path: "gungame3d", path: "gungame3d",
component: GunGame_page, component: () => import('../pages/demoPages/gunGame/gunGame_page.vue'),
meta: {title: '打枪 '}, meta: { title: '打枪' },
},
{
path: "file-sharing",
component: () => import('../pages/demoPages/fileSharing/Sharing_page.vue'),
meta: { title: '文件共享' },
}, },
] ]
}, { }, {
path: '/tools', path: '/tools',
name: 'Tools', name: 'Tools',
component: Tools_home, component: () => import('../pages/Tools_home.vue'),
meta: {title: '小工具'}, meta: { title: '小工具' },
children: [ children: [
{path: "1", component: GpaCalculator_page}, { path: "1", component: () => import('../pages/toolPages/gpaCalculator/gpaCalculator_page.vue') },
{path: "gpa", component: GpaCalculator_page}, { path: "gpa", component: () => import('../pages/toolPages/gpaCalculator/gpaCalculator_page.vue') },
{path: "2", component: PdfEx_page}, { path: "2", component: () => import('../pages/toolPages/pdfExtractor/pdfEx_page.vue') },
{path: "pdf-extractor", component: PdfEx_page}, { path: "pdf-extractor", component: () => import('../pages/toolPages/pdfExtractor/pdfEx_page.vue') },
{path: "3", component: RequestTester_page}, { path: "3", component: () => import('../pages/toolPages/RequestTester/requestTester_page.vue') },
{path: "request-tester", component: RequestTester_page}, { path: "request-tester", component: () => import('../pages/toolPages/RequestTester/requestTester_page.vue') },
] ]
}, { }, {
path: '/about', path: '/about',
name: 'About', name: 'About',
component: About, component: About,
meta: {title: '关于'}, meta: { title: '关于' },
}, { }, {
path: '/account', path: '/account',
component: Account, component: Account,
meta: {title: '账户'}, meta: { title: '账户' },
children: [ children: [
{path: 'self-page', component: Account_selfpage}, { path: 'self-page', component: AccountSelfPage },
{path: 'works-manage', component: Account_worksmanage}, { path: 'works-manage', component: AccountWorksManage },
{path: 'draft', component: Account_draft}, { path: 'draft', component: AccountDraft },
{path: 'setting', component: Account_setting}, { path: 'setting', component: AccountSetting },
{path: '', component: Account_userInfo}, { path: '', component: AccountUserInfo },
{path: 'upload-log', component: Account_admin_uploadLog}, { path: 'upload-log', component: AccountAdminUploadLog },
{path: 'user-management', component: Account_admin_userManage} { path: 'user-management', component: AccountAdminUserManage }
] ]
}, { }, {
path: '/editor', path: '/editor',
name: 'Editor', name: 'Editor',
component: Editor, component: BlogEditor,
meta: {title: '编辑器'}, meta: { title: '编辑器' },
},{ }, {
path: '/test_page', path: '/test_page',
name: 'Test', name: 'Test',
component: Test_page, component: TestPage,
meta: {title: '测试页'}, meta: { title: '测试页' },
}, },
]; ];
// 其余代码保持不变
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes routes
}); });
let previousRoute = null let previousRoute = null;
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
previousRoute = from; previousRoute = from;
@ -162,11 +156,6 @@ router.beforeEach(async (to, from, next) => {
store.state.isViewCounted = true; store.state.isViewCounted = true;
} }
if (/^\/demos\/?$/.test(to.path)) {
next('/404');
}
if (!store.state.userInfo.uid && store.state.token) { if (!store.state.userInfo.uid && store.state.token) {
AuthService.setSelfInfo(); AuthService.setSelfInfo();
} }
@ -198,13 +187,14 @@ router.beforeEach(async (to, from, next) => {
const title = to.meta?.title; const title = to.meta?.title;
if (title) { if (title) {
document.title = title + ' CYBER'; // 设置页面标题 document.title = title + ' CYBER';
} }
next(); next();
}); });
export function getPreviousRoute() { export function getPreviousRoute() {
return previousRoute return previousRoute;
} }
export default router; export default router;