新增文件共享;

修改主页
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-for-vue": "^5.1.12",
"axios": "^1.7.9",
"diff": "^7.0.0",
"element-plus": "^2.9.6",
"jquery": "^3.7.1",
"js-cookie": "^3.0.5",
@ -2479,6 +2480,14 @@
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/dom7/-/dom7-3.0.0.tgz",

View File

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

Binary file not shown.

View File

@ -2,10 +2,11 @@
<html lang="zh-CN">
<head>
<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>
<style>
body { margin: 0; overflow: hidden; }
canvas { touch-action: none; }
#crosshair {
position: absolute;
top: 50%;
@ -15,76 +16,223 @@
height: 10px;
background-color: red;
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>
</head>
<body>
<!-- 准星 -->
<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="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;">
<div id="modeSelectModal">
<h2>选择游戏模式</h2>
<p>请选择您的设备类型:</p>
<button id="desktopMode">桌面模式</button>
<button id="mobileMode">移动模式</button>
</div>
<div id="gameOverModal">
<h2>游戏结束</h2>
<p id="finalScore"></p>
<button id="restartButton" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">重新开始</button>
<button id="restartButton">重新开始</button>
</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/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>
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 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 });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 添加光照
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 视角控制(重新设计)
let yaw = 0; // 水平旋转角度(左右)
let pitch = 0; // 垂直旋转角度(上下)
const sensitivity = 0.002; // 鼠标灵敏度
document.addEventListener('click', () => {
renderer.domElement.requestPointerLock();
});
// Camera Control
let yaw = 0, pitch = 0, sensitivity = 0.005;
const keys = {};
function setupDesktopControls() {
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垂直
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);
}
});
// WASD移动控制
const keys = {};
document.addEventListener('keydown', (event) => keys[event.key.toLowerCase()] = true);
document.addEventListener('keyup', (event) => keys[event.key.toLowerCase()] = false);
}
// 子弹管理
let bullets = 10;
const maxBullets = 10;
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');
bulletDisplay.className = 'ui';
bulletDisplay.style.bottom = '10px';
@ -92,8 +240,6 @@
bulletDisplay.innerText = `子弹: ${bullets}`;
document.body.appendChild(bulletDisplay);
// 生命值
let health = 100;
const healthDisplay = document.createElement('div');
healthDisplay.className = 'ui';
healthDisplay.style.bottom = '10px';
@ -102,29 +248,17 @@
document.body.appendChild(healthDisplay);
function deductHealth(amount) {
if (isGamePaused) return; // 如果已暂停,不再扣血
if (isGamePaused) return;
health -= amount;
healthDisplay.innerText = `生命值: ${health}`;
// 触发红色闪烁效果
playDamageSound();
const damageOverlay = document.getElementById('damageOverlay');
gsap.fromTo(
damageOverlay,
{ backgroundColor: 'rgba(255, 0, 0, 0.5)' },
{
backgroundColor: 'rgba(255, 0, 0, 0)',
duration: 0.5,
ease: 'power1.out'
}
);
if (health <= 0) {
endGame();
}
gsap.fromTo(damageOverlay, { backgroundColor: 'rgba(255, 0, 0, 0.5)' }, { backgroundColor: 'rgba(255, 0, 0, 0)', duration: 0.5, ease: 'power1.out' });
if (health <= 0) endGame();
}
// 枪械展示
const gunTexture = new THREE.TextureLoader().load('./image/gun.png'); // 替换为实际枪械图片URL
// Gun
const gunTexture = new THREE.TextureLoader().load('./image/gun.png');
const gunMaterial = new THREE.MeshBasicMaterial({ map: gunTexture, transparent: true });
const gunGeometry = new THREE.PlaneGeometry(1, 1);
const gun = new THREE.Mesh(gunGeometry, gunMaterial);
@ -132,147 +266,111 @@
camera.add(gun);
scene.add(camera);
// 开枪函数
// Shoot Function
function shoot() {
if (bullets > 0) {
if (bullets > 0 && !isGamePaused) {
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();
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
const intersects = raycaster.intersectObjects(enemies.map(e => e.sprite));
if (intersects.length > 0) {
const enemy = intersects[0].object;
const hitDirection = new THREE.Vector3()
.subVectors(enemy.position, camera.position)
.normalize();
// 添加击飞效果
const hitDirection = new THREE.Vector3().subVectors(enemy.position, camera.position).normalize();
gsap.to(enemy.position, {
x: enemy.position.x + hitDirection.x * 5, // 沿射击方向飞5个单位
y: enemy.position.y + 2, // 向上飞2个单位
x: enemy.position.x + hitDirection.x * 5,
y: enemy.position.y + 2,
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,
onComplete: () => {
enemy.material.opacity = 1; // 重置透明度以备复用
}
ease: "power2.out",
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;
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
}
}
}
document.addEventListener('click', shoot);
document.addEventListener('keydown', (event) => {
if (event.key === 'r' && bullets < maxBullets) {
// 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 terrainSize = 100;
const terrainDetail = 0.1;
const terrainHeight = 1; // 平缓地形
const terrainSize = 100, terrainDetail = 0.1, terrainHeight = 1;
const geometry = new THREE.PlaneGeometry(terrainSize, terrainSize, 100, 100);
geometry.rotateX(-Math.PI / 2);
const vertices = geometry.attributes.position.array;
for (let i = 0; i < vertices.length; i += 3) {
const x = vertices[i];
const z = vertices[i + 2];
const y = noise.noise2D(x * terrainDetail, z * terrainDetail) * terrainHeight;
vertices[i + 1] = y;
const x = vertices[i], z = vertices[i + 2];
vertices[i + 1] = noise.noise2D(x * terrainDetail, z * terrainDetail) * terrainHeight;
}
geometry.computeVertexNormals();
const terrainMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });
const terrain = new THREE.Mesh(geometry, terrainMaterial);
scene.add(terrain);
// 射线投射器
const raycaster = new THREE.Raycaster();
raycaster.ray.direction.set(0, -1, 0);
// 敌人生成(高度贴合地表)
// Enemies
let enemies = [];
class Enemy {
constructor() {
const enemyTextures = [
'./image/1.png',
'./image/2.png',
];
const texture = new THREE.TextureLoader().load(enemyTextures[Math.floor(Math.random() * enemyTextures.length)]);
const enemyTextures = ['./image/1.png', './image/2.png'];
const texture = new THREE.TextureLoader().load(enemyTextures[Math.floor(Math.random() * 2)]);
const material = new THREE.SpriteMaterial({ map: texture });
this.sprite = new THREE.Sprite(material);
this.sprite.scale.set(2, 2, 1);
// 随机选择地图边缘0: 上, 1: 下, 2: 左, 3: 右)
const edge = Math.floor(Math.random() * 4);
const halfSize = terrainSize / 2; // terrainSize = 100, halfSize = 50
const edge = Math.floor(Math.random() * 4), halfSize = terrainSize / 2;
switch (edge) {
case 0: // 上边缘 (z = 50)
this.sprite.position.set(
Math.random() * terrainSize - halfSize, // x: [-50, 50]
10, // y: 10
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;
case 0: this.sprite.position.set(Math.random() * terrainSize - halfSize, 10, halfSize); break;
case 1: this.sprite.position.set(Math.random() * terrainSize - halfSize, 10, -halfSize); break;
case 2: this.sprite.position.set(-halfSize, 10, Math.random() * terrainSize - halfSize); break;
case 3: this.sprite.position.set(halfSize, 10, Math.random() * terrainSize - halfSize); break;
}
scene.add(this.sprite);
this.sprite.material.opacity = 0;
gsap.to(this.sprite.material, { opacity: 1, duration: 1 });
}
update() {
this.sprite.lookAt(camera.position);
const direction = new THREE.Vector3().subVectors(camera.position, this.sprite.position).normalize();
this.sprite.position.add(direction.multiplyScalar(0.05));
// 贴合地表高度
raycaster.ray.origin.copy(this.sprite.position);
raycaster.ray.origin.y = 10; // 从上方检测
raycaster.ray.origin.y = 10;
const intersects = raycaster.intersectObject(terrain);
if (intersects.length > 0) {
this.sprite.position.y = intersects[0].point.y + 1; // 高度为地表+1
}
if (intersects.length > 0) this.sprite.position.y = intersects[0].point.y + 1;
if (this.sprite.position.distanceTo(camera.position) < 1) {
deductHealth(15);
scene.remove(this.sprite);
@ -280,111 +378,98 @@
}
}
}
setInterval(() => { if (enemies.length < 10 && !isGamePaused) enemies.push(new Enemy()); }, 2000);
setInterval(() => {
if (enemies.length < 10) {
const newEnemy = new Enemy();
enemies.push(newEnemy);
}
}, 2000); // 每2秒生成一个敌人
// 计时与得分
let timeLeft = 150;
let score = 0;
// Timer and Score
let timeLeft = 150, score = 0;
const timerDisplay = document.createElement('div');
timerDisplay.className = 'ui';
timerDisplay.style.top = '10px';
timerDisplay.style.left = '10px';
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
document.body.appendChild(timerDisplay);
setInterval(() => {
if (isGamePaused) return; // 如果暂停,不更新计时器
if (!isGamePaused) {
timeLeft--;
score++;
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
if (timeLeft <= 0) {
endGame();
if (timeLeft <= 0) endGame();
}
}, 1000);
// 新增游戏结束函数
function endGame() {
isGamePaused = true;
document.exitPointerLock();
// 显示弹窗
if (mode === 'desktop') document.exitPointerLock();
const modal = document.getElementById('gameOverModal');
const finalScore = document.getElementById('finalScore');
finalScore.innerText = `最终得分: ${score}`;
document.getElementById('finalScore').innerText = `最终得分: ${score}`;
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}`;
bulletDisplay.innerText = `子弹: ${bullets}`;
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
// 隐藏弹窗
document.getElementById('gameOverModal').style.display = 'none';
// 重新启动动画循环
animate();
}
// 为重新开始按钮添加事件监听
document.getElementById('restartButton').addEventListener('click', restartGame);
// 天空贴图
// Skybox
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/py.jpg', 'https://i.imgur.com/ny.jpg',
'https://i.imgur.com/pz.jpg', 'https://i.imgur.com/nz.jpg'
]); // 替换为实际天空贴图URL
scene.background = skyboxTexture;
]);
// 动画循环
// Animation Loop
function animate() {
if (isGamePaused) return; // 如果暂停则跳出循环
if (isGamePaused) return;
requestAnimationFrame(animate);
// WASD移动
const speed = 0.1;
const frontVector = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
if (mode === 'desktop') {
if (keys['w']) camera.position.add(frontVector.multiplyScalar(speed));
if (keys['s']) camera.position.add(frontVector.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.y += 1.6;
const intersects = raycaster.intersectObject(terrain);
if (intersects.length > 0) {
camera.position.y = intersects[0].point.y + 1.6;
}
if (intersects.length > 0) camera.position.y = intersects[0].point.y + 1.6;
// 更新敌人(仅在游戏未暂停时)
if (!isGamePaused) {
enemies.forEach(enemy => enemy.update());
}
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', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();

View File

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

View File

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

View File

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

View File

@ -28,6 +28,14 @@ const demos = ref([
author: ["Louis Zhou"],
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";
const source = ref(0);
const viewCountDisplay = ref(true);
const viewCountDisplay = ref(false);
const outputValue = useTransition(source, {
duration: 3000,
transition: TransitionPresets.easeOutExpo,
@ -15,8 +15,7 @@ onMounted(async () => {
const response = await api.get('/view');
if (response.code === 0) {
source.value = response.views;
} else {
viewCountDisplay.value = false;
viewCountDisplay.value = true;
}
})
@ -28,13 +27,32 @@ onMounted(async () => {
<el-container>
<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-footer>
<!-- <div class="text-bg"></div>-->
<Transition name="fade">
<el-col v-if="viewCountDisplay">
<el-statistic title="网站累计访问次数" :value="outputValue"/>
</el-col>
</Transition>
</el-footer>
</el-container>
@ -48,6 +66,7 @@ img {
width: 100%;
height: 100%;
}
.el-container {
display: flex;
align-items: center;
@ -56,15 +75,86 @@ img {
overflow: hidden;
}
.el-main {
display: flex;
justify-content: center;
justify-content: space-between;
align-items: center;
padding: 0;
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 {
position: relative;
}
.el-col {
//position: absolute;
@ -74,9 +164,137 @@ img {
//align-items: center;
//justify-content: center;
}
.el-statistic {
display: flex;
flex-direction: column-reverse;
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>

View File

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

View File

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

View File

@ -194,7 +194,7 @@ const changeSort = () => {
</div>
<el-divider/>
<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"/>
<Transition name="fade">
<div class="interact-bar" v-if="interactInfo.complete" :class="{outside: windowWidth>1050}">
@ -257,7 +257,7 @@ const changeSort = () => {
:sort-mode="sortMode"
:blog-author-uid="blog.poster"
/>
<p v-else>不允许评论</p>
<div v-else class="loading">不允许评论</div>
</div>

View File

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

View File

@ -43,8 +43,13 @@ const nonNullKeysCount = computed(() => {
const submit = async () => {
const formData = new FormData();
if (infoForm.value.allowComment !== null) {
formData.append("allow_comments", infoForm.value.allowComment);
}
if (infoForm.value.tagSelect !== null) {
formData.append("category", infoForm.value.tagSelect);
}
formData.append("draft", 0);
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";
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 pageLoading = ref(false);
@ -30,22 +30,8 @@ async function refreshBoard(page, pageSize) {
PAGE: page,
PAGE_SIZE: pageSize
}).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;
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: {amount: amount.value}});
})
@ -79,7 +65,7 @@ async function sendMessage() {
swal.tip('info', '不得为空')
return;
}
if (userInput.value.trim().length > 500) {
if (userInput.value.trim().length > 400) {
swal.tip('error', '太长了!')
return;
}
@ -146,7 +132,8 @@ onMounted(async () => {
<div class="board-body">
<div class="message-container" :class="{loading: pageLoading}">
<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>
<paging-controller :current-page="currentPage" :amount="amount" :go-page-func="goPage" :loading="pageLoading"/>
</div>
@ -204,6 +191,7 @@ onMounted(async () => {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 15px;
transition: opacity 0.2s ease;
}

View File

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

View File

@ -1,28 +1,10 @@
<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([
{
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 podList = ref([])
const uniqueCats = computed(() => {
const keys = new Set();
@ -31,10 +13,15 @@ const uniqueCats = computed(() => {
});
return Array.from(keys);
});
onMounted(async () => {
const response = await api.get(demoResourceURL('pod', "index.json"));
podList.value = response;
})
</script>
<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>
<div class="pod-container" v-for="cat in uniqueCats">
<div class="folder">

View File

@ -23,9 +23,12 @@
</div>
</div>
<!-- 提交按钮或下一题按钮 -->
<!-- 提交或下一题按钮 -->
<button v-if="!submitted" @click="submitAnswer" class="submit-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">
@ -47,7 +50,7 @@
<h4>-- 题目 {{ index + 1 }} --</h4>
<QuestionText :text="similar.text" :medias="data.medias"/>
<p>正确答案
<general-renderer :content-input="processMedia(similar.correctAnswer, data.medias)"></general-renderer>
<general-renderer :content-input="processMedia(similar.answers, data.medias)"></general-renderer>
</p>
<div v-if="similar.explanation" class="explanation">
<h4>解题思路</h4>
@ -86,6 +89,9 @@ import {demoResourceURL} from "../../../utils/demoResource.js";
const route = useRoute()
const isLocked = ref(false); //
const lockedSimilarQuestions = ref([]); //
// JSON
const data = reactive({
itemtypes: [],
@ -268,17 +274,18 @@ function generateNewExam() {
function findSimilarQuestions() {
const currentText = currentQuestion.value.text;
const similar = [];
for (let i = 1; i < data.itemtypes.length; i++) {
for (let i = 1; i < data.texts.length; i++) {
if (
calculateStringSimilarity(data.texts[i], currentText) > 50 && //
data.texts[i] !== currentQuestion.value.text//
// similar.length < 2 // 2
calculateStringSimilarity(data.texts[i], currentText) > 50 &&
data.texts[i] !== currentText //
) {
const correctIdx = data.answerkeys[i].findIndex((key, idx) => idx > 0 && key === 1) - 1;
similar.push({
text: data.texts[i],
correctAnswer: data.answers[i][correctIdx + 1],
explanation: data.explanations[i]
answers: data.answers[i],
answerkeys: data.answerkeys[i],
explanation: data.explanations[i],
type: data.itemtypes[i]
});
}
}
@ -286,12 +293,44 @@ function findSimilarQuestions() {
}
function selectCurrentQuestion() {
if (isLocked.value) {
//
if (lockedSimilarQuestions.value.length > 0) {
const randomIndex = Math.floor(Math.random() * lockedSimilarQuestions.value.length);
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() {
if (isLocked.value) {
//
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 {
//
if (questionCount.value < totalQuestions.value) {
questionCount.value++;
selectCurrentQuestion();
} else {
swal.tip('success', '所有题目已完成!将生成新试卷。')
generateNewExam(); //
swal.tip('success', '所有题目已完成!将生成新试卷。');
generateNewExam();
}
}
}
</script>
@ -462,4 +516,23 @@ function nextQuestion() {
.similar-question h4 {
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>

View File

@ -49,16 +49,11 @@
<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)" />
@ -67,8 +62,6 @@
{{ 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="数组值" />
@ -80,8 +73,6 @@
</div>
<button @click="addArrayItem(item)">添加数组项</button>
</div>
<!-- 删除键值对 -->
<button @click="removeFormDataItem(index)">删除键值对</button>
</div>
<button @click="addFormDataItem">添加键值对</button>
@ -114,13 +105,21 @@
<!-- 右侧储存栏 -->
<div class="storage-area">
<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-else v-for="(preset, name) in requestTesterPresets" :key="name" class="preset-item">
<span @click="loadPreset(name)" class="preset-name">{{ name }}</span>
<div class="preset-buttons">
<button @click="exportPreset(name)">导出</button>
<button @click="renamePreset(name)">重命名</button>
<button @click="deletePreset(name)">删除</button>
</div>
</div>
</div>
</div>
</template>
<script>
@ -128,7 +127,7 @@ import { ref, computed } from 'vue';
import {useStore} from 'vuex';
import axios from 'axios';
import Swal from 'sweetalert2';
import swal from "../../../utils/sweetalert.js";
import swal from '../../../utils/sweetalert.js';
export default {
name: 'RequestTester',
@ -139,13 +138,14 @@ export default {
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 bodyType = ref('json');
const body = ref('');
const formDataItems = ref([]);
const params = ref([{key: '', value: ''}]);
const response = ref(null);
const importPresetString = ref('');
// Vuex requestTester
// Vuex
const requestTesterPresets = computed(() => store.state.demosLocal.requestTester || {});
//
@ -156,13 +156,11 @@ export default {
const addParam = () => params.value.push({key: '', value: ''});
const removeParam = (index) => params.value.splice(index, 1);
// FormData
// FormData
const addFormDataItem = () => {
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) => {
if (item.type === 'pair') {
item.value = '';
@ -175,9 +173,7 @@ export default {
if (!item.value) item.value = [];
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) => {
item.value = event.target.files[0];
};
@ -187,16 +183,13 @@ export default {
//
const sendRequest = async () => {
response.value = '等待响应...'; //
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) => {
@ -211,7 +204,6 @@ export default {
params: queryParams,
};
//
if (method.value !== 'GET' && method.value !== 'DELETE') {
if (bodyType.value === 'json') {
axiosConfig.data = body.value ? JSON.parse(body.value) : {};
@ -226,9 +218,7 @@ export default {
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);
}
if (val.content) formData.append(item.key + '[]', val.content);
});
}
}
@ -259,7 +249,7 @@ export default {
if (!result.isConfirmed) return;
const presetName = result.value;
let presetName = result.value;
const preset = {
url: url.value,
method: method.value,
@ -269,15 +259,58 @@ export default {
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)),
})), //
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],
};
//
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', {
demo: 'requestTester',
value: {[presetName]: preset},
});
swal.tip('success', '预设已保存');
};
//
@ -292,24 +325,158 @@ export default {
formDataItems.value = preset.formDataItems.map((item) => ({
key: item.key,
type: item.type,
value: item.type === 'pair' ? '' : [], //
isFile: item.type === 'pair' && item.value === 'File',
value: item.type === 'pair'
? (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];
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) => {
swal.window('info', '确定删除?', `删除预设"${name}"`, '确定', '取消').then(result => {
swal.window('info', '确定删除?', `删除预设"${name}"`, '确定', '取消').then((result) => {
if (result.isConfirmed) {
store.commit('deleteLocalDemoValue', {
demo: 'requestTester',
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 {
@ -322,6 +489,7 @@ export default {
params,
response,
requestTesterPresets,
importPresetString,
addHeader,
removeHeader,
addParam,
@ -337,13 +505,15 @@ export default {
savePreset,
loadPreset,
deletePreset,
exportPreset,
importPreset,
renamePreset,
};
},
};
</script>
<style scoped>
/* 默认深色模式 */
:root {
--bg-color: #2c2c2c;
--text-color: #ffffff;
@ -353,7 +523,6 @@ export default {
--button-hover: #357abd;
}
/* 浅色模式 */
.theme-light {
--bg-color: #ffffff;
--text-color: #333333;
@ -365,7 +534,6 @@ export default {
.request-tester {
display: flex;
max-width: 1200px;
width: 100%;
height: calc(100vh - 60px);
@ -401,7 +569,7 @@ textarea {
width: 100%;
padding: 8px;
background-color: var(--input-bg);
border: 1px solid var(--border-color);
border: 1px solid gray;
color: var(--text-color);
border-radius: 4px;
}
@ -449,6 +617,7 @@ button {
border-radius: 4px;
cursor: pointer;
}
.theme-light button {
color: black;
}
@ -491,4 +660,15 @@ button:hover {
.preset-name:hover {
text-decoration: underline;
}
.preset-actions {
margin-bottom: 15px;
display: flex;
gap: 10px;
}
.preset-buttons {
display: flex;
gap: 5px;
}
</style>

View File

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