feat: add three js
This commit is contained in:
6
bun.lock
6
bun.lock
@@ -12,6 +12,7 @@
|
|||||||
"daisyui": "^5.0.50",
|
"daisyui": "^5.0.50",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.13.2",
|
"element-plus": "^2.13.2",
|
||||||
|
"gsap": "^3.14.2",
|
||||||
"lenis": "^1.3.17",
|
"lenis": "^1.3.17",
|
||||||
"lucide-vue-next": "^0.563.0",
|
"lucide-vue-next": "^0.563.0",
|
||||||
"motion-v": "^1.6.1",
|
"motion-v": "^1.6.1",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"radash": "^12.1.1",
|
"radash": "^12.1.1",
|
||||||
"reka-ui": "^2.8.0",
|
"reka-ui": "^2.8.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"three": "^0.183.1",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-echarts": "^8.0.1",
|
"vue-echarts": "^8.0.1",
|
||||||
"vue-i18n": "11",
|
"vue-i18n": "11",
|
||||||
@@ -719,6 +721,8 @@
|
|||||||
|
|
||||||
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||||
|
|
||||||
|
"gsap": ["gsap@3.14.2", "", {}, "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA=="],
|
||||||
|
|
||||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||||
|
|
||||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
@@ -1123,6 +1127,8 @@
|
|||||||
|
|
||||||
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||||
|
|
||||||
|
"three": ["three@0.183.1", "", {}, "sha512-Psv6bbd3d/M/01MT2zZ+VmD0Vj2dbWTNhfe4CuSg7w5TuW96M3NOyCVuh9SZQ05CpGmD7NEcJhZw4GVjhCYxfQ=="],
|
||||||
|
|
||||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|||||||
6
components.d.ts
vendored
6
components.d.ts
vendored
@@ -15,6 +15,7 @@ declare module 'vue' {
|
|||||||
AnimatePresence: typeof import('motion-v')['AnimatePresence']
|
AnimatePresence: typeof import('motion-v')['AnimatePresence']
|
||||||
AnimText: typeof import('./src/components/special/AnimText.vue')['default']
|
AnimText: typeof import('./src/components/special/AnimText.vue')['default']
|
||||||
AuthDialog: typeof import('./src/components/dialog/AuthDialog.vue')['default']
|
AuthDialog: typeof import('./src/components/dialog/AuthDialog.vue')['default']
|
||||||
|
BasicIntroCard: typeof import('./src/components/card/BasicIntroCard.vue')['default']
|
||||||
BiliBiliIcon: typeof import('./src/components/icon/BiliBiliIcon.vue')['default']
|
BiliBiliIcon: typeof import('./src/components/icon/BiliBiliIcon.vue')['default']
|
||||||
Button: typeof import('./src/components/ui/button/Button.vue')['default']
|
Button: typeof import('./src/components/ui/button/Button.vue')['default']
|
||||||
Calendar: typeof import('./src/components/ui/calendar/Calendar.vue')['default']
|
Calendar: typeof import('./src/components/ui/calendar/Calendar.vue')['default']
|
||||||
@@ -43,6 +44,8 @@ declare module 'vue' {
|
|||||||
FooterBarV2Placeholder: typeof import('./src/components/layout/FooterBarV2Placeholder.vue')['default']
|
FooterBarV2Placeholder: typeof import('./src/components/layout/FooterBarV2Placeholder.vue')['default']
|
||||||
FooterBarV2Space: typeof import('./src/components/layout/FooterBarV2Space.vue')['default']
|
FooterBarV2Space: typeof import('./src/components/layout/FooterBarV2Space.vue')['default']
|
||||||
LogoIcon: typeof import('./src/components/icon/LogoIcon.vue')['default']
|
LogoIcon: typeof import('./src/components/icon/LogoIcon.vue')['default']
|
||||||
|
LogoModel: typeof import('./src/components/three/LogoModel.vue')['default']
|
||||||
|
ModelViewer: typeof import('./src/components/three/ModelViewer.vue')['default']
|
||||||
NativeSelect: typeof import('./src/components/ui/native-select/NativeSelect.vue')['default']
|
NativeSelect: typeof import('./src/components/ui/native-select/NativeSelect.vue')['default']
|
||||||
NativeSelectOptGroup: typeof import('./src/components/ui/native-select/NativeSelectOptGroup.vue')['default']
|
NativeSelectOptGroup: typeof import('./src/components/ui/native-select/NativeSelectOptGroup.vue')['default']
|
||||||
NativeSelectOption: typeof import('./src/components/ui/native-select/NativeSelectOption.vue')['default']
|
NativeSelectOption: typeof import('./src/components/ui/native-select/NativeSelectOption.vue')['default']
|
||||||
@@ -63,6 +66,7 @@ declare global {
|
|||||||
const AnimatePresence: typeof import('motion-v')['AnimatePresence']
|
const AnimatePresence: typeof import('motion-v')['AnimatePresence']
|
||||||
const AnimText: typeof import('./src/components/special/AnimText.vue')['default']
|
const AnimText: typeof import('./src/components/special/AnimText.vue')['default']
|
||||||
const AuthDialog: typeof import('./src/components/dialog/AuthDialog.vue')['default']
|
const AuthDialog: typeof import('./src/components/dialog/AuthDialog.vue')['default']
|
||||||
|
const BasicIntroCard: typeof import('./src/components/card/BasicIntroCard.vue')['default']
|
||||||
const BiliBiliIcon: typeof import('./src/components/icon/BiliBiliIcon.vue')['default']
|
const BiliBiliIcon: typeof import('./src/components/icon/BiliBiliIcon.vue')['default']
|
||||||
const Button: typeof import('./src/components/ui/button/Button.vue')['default']
|
const Button: typeof import('./src/components/ui/button/Button.vue')['default']
|
||||||
const Calendar: typeof import('./src/components/ui/calendar/Calendar.vue')['default']
|
const Calendar: typeof import('./src/components/ui/calendar/Calendar.vue')['default']
|
||||||
@@ -91,6 +95,8 @@ declare global {
|
|||||||
const FooterBarV2Placeholder: typeof import('./src/components/layout/FooterBarV2Placeholder.vue')['default']
|
const FooterBarV2Placeholder: typeof import('./src/components/layout/FooterBarV2Placeholder.vue')['default']
|
||||||
const FooterBarV2Space: typeof import('./src/components/layout/FooterBarV2Space.vue')['default']
|
const FooterBarV2Space: typeof import('./src/components/layout/FooterBarV2Space.vue')['default']
|
||||||
const LogoIcon: typeof import('./src/components/icon/LogoIcon.vue')['default']
|
const LogoIcon: typeof import('./src/components/icon/LogoIcon.vue')['default']
|
||||||
|
const LogoModel: typeof import('./src/components/three/LogoModel.vue')['default']
|
||||||
|
const ModelViewer: typeof import('./src/components/three/ModelViewer.vue')['default']
|
||||||
const NativeSelect: typeof import('./src/components/ui/native-select/NativeSelect.vue')['default']
|
const NativeSelect: typeof import('./src/components/ui/native-select/NativeSelect.vue')['default']
|
||||||
const NativeSelectOptGroup: typeof import('./src/components/ui/native-select/NativeSelectOptGroup.vue')['default']
|
const NativeSelectOptGroup: typeof import('./src/components/ui/native-select/NativeSelectOptGroup.vue')['default']
|
||||||
const NativeSelectOption: typeof import('./src/components/ui/native-select/NativeSelectOption.vue')['default']
|
const NativeSelectOption: typeof import('./src/components/ui/native-select/NativeSelectOption.vue')['default']
|
||||||
|
|||||||
@@ -31,3 +31,7 @@ Hucky 安装了 [motion-v](https://motion.net.cn/docs/vue) 库,您可以在组
|
|||||||
:::tip 提示
|
:::tip 提示
|
||||||
exit 会自动为元素的进入也添加动画效果。
|
exit 会自动为元素的进入也添加动画效果。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
## 3D 动画
|
||||||
|
|
||||||
|
Hucky 选用 [GSAP](https://gsap.com/docs/v3/) 作为 3D 动画库,您可以学习相关知识
|
||||||
|
|||||||
19
docs/cli-feature/three.md
Normal file
19
docs/cli-feature/three.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 3D
|
||||||
|
|
||||||
|
## Three.js
|
||||||
|
|
||||||
|
Hucky 选用 [three.js](https://threejs.org/manual/#zh) 作为 3D 框架,您可以学习相关知识
|
||||||
|
|
||||||
|
## 首页
|
||||||
|
|
||||||
|
Hucky 的首页层叠关系非常复杂,在这里做解释
|
||||||
|
|
||||||
|
- z-10 导航栏 fixed 位于所有元素的最上方
|
||||||
|
- z-8 悬浮文字 仅次于导航栏,要求用户最先看到
|
||||||
|
- z-7 Three.js Canvas 位于悬浮文字下方,要求用户在看到悬浮文字后立即看到 3D 模型,同时将 canvas 背景透明,让用户可以看到 canvas 下方的主内容
|
||||||
|
- z-5 主内容区域 位于 Three.js Canvas 下方,主要提供了背景颜色
|
||||||
|
- z-1 页脚 位于所有元素的最下方且 fixed,完成了类似幕布拉开的效果
|
||||||
|
|
||||||
|
## 3D 动画
|
||||||
|
|
||||||
|
Hucky 选用 [GSAP](https://gsap.com/docs/v3/) 作为 3D 动画库,您可以学习相关知识
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
"daisyui": "^5.0.50",
|
"daisyui": "^5.0.50",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.13.2",
|
"element-plus": "^2.13.2",
|
||||||
|
"gsap": "^3.14.2",
|
||||||
"lenis": "^1.3.17",
|
"lenis": "^1.3.17",
|
||||||
"lucide-vue-next": "^0.563.0",
|
"lucide-vue-next": "^0.563.0",
|
||||||
"motion-v": "^1.6.1",
|
"motion-v": "^1.6.1",
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"radash": "^12.1.1",
|
"radash": "^12.1.1",
|
||||||
"reka-ui": "^2.8.0",
|
"reka-ui": "^2.8.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"three": "^0.183.1",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-echarts": "^8.0.1",
|
"vue-echarts": "^8.0.1",
|
||||||
"vue-i18n": "11",
|
"vue-i18n": "11",
|
||||||
|
|||||||
BIN
public/model/christmas_ball.glb
Normal file
BIN
public/model/christmas_ball.glb
Normal file
Binary file not shown.
29
src/components/card/BasicIntroCard.vue
Normal file
29
src/components/card/BasicIntroCard.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import AnimText from "@/components/special/AnimText.vue";
|
||||||
|
import { navigateTo } from "@/utils/navigator";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="hero flex-1">
|
||||||
|
<div class="hero-content text-center">
|
||||||
|
<div class="max-w-md">
|
||||||
|
<h1 class="text-5xl font-bold">
|
||||||
|
<AnimText text="home.welcome" />
|
||||||
|
</h1>
|
||||||
|
<div class="py-6">
|
||||||
|
<AnimText text="home.intro_line1" /><br />
|
||||||
|
<AnimText text="home.intro_line2" /><br />
|
||||||
|
<AnimText text="home.intro_line3" />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
@click="navigateTo('http://localhost:5174')"
|
||||||
|
>
|
||||||
|
<AnimText text="home.read_doc" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -7,7 +7,7 @@ import RedBookIcon from "../icon/RedBookIcon.vue";
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<footer
|
<footer
|
||||||
class="fixed bottom-0 left-0 w-full z-1 pt-16 pb-10 px-6 md:px-12 lg:px-20 text-sm font-[LXGW] lg:h-102 md:h-122 h-154"
|
class="w-full pt-16 pb-10 px-6 md:px-12 lg:px-20 text-sm font-[LXGW] lg:h-102 md:h-122 h-154"
|
||||||
>
|
>
|
||||||
<!-- Background overlay -->
|
<!-- Background overlay -->
|
||||||
<div
|
<div
|
||||||
|
|||||||
130
src/components/three/LogoModel.vue
Normal file
130
src/components/three/LogoModel.vue
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { gsap } from "gsap";
|
||||||
|
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
||||||
|
import { M } from "motion-v";
|
||||||
|
|
||||||
|
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||||
|
|
||||||
|
let scene: THREE.Scene;
|
||||||
|
let camera: THREE.PerspectiveCamera;
|
||||||
|
let renderer: THREE.WebGLRenderer;
|
||||||
|
let cube: THREE.Mesh;
|
||||||
|
|
||||||
|
// 动画循环的 raf id,用于销毁时清理
|
||||||
|
let animationId: number;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (!canvasRef.value) return; // 防御性编程
|
||||||
|
|
||||||
|
scene = new THREE.Scene();
|
||||||
|
|
||||||
|
camera = new THREE.PerspectiveCamera(
|
||||||
|
75,
|
||||||
|
window.innerWidth / window.innerHeight,
|
||||||
|
0.1,
|
||||||
|
1000,
|
||||||
|
);
|
||||||
|
camera.position.z = 5;
|
||||||
|
|
||||||
|
renderer = new THREE.WebGLRenderer({
|
||||||
|
canvas: canvasRef.value,
|
||||||
|
alpha: true,
|
||||||
|
antialias: true,
|
||||||
|
});
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
// 透明
|
||||||
|
renderer.setClearColor(0x000000, 0);
|
||||||
|
|
||||||
|
// ------------------- 几何体 -------------------
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
loader.load(
|
||||||
|
"/model/christmas_ball.glb",
|
||||||
|
(gltf) => {
|
||||||
|
cube = gltf.scene;
|
||||||
|
|
||||||
|
// 可选:调整模型大小、位置
|
||||||
|
cube.scale.set(0.01, 0.01, 0.01);
|
||||||
|
cube.position.set(4, 0, 0);
|
||||||
|
|
||||||
|
scene.add(cube);
|
||||||
|
|
||||||
|
console.log("模型加载成功!", cube);
|
||||||
|
},
|
||||||
|
(xhr) => {
|
||||||
|
console.log((xhr.loaded / xhr.total) * 100 + "% 已加载");
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error("模型加载失败:", error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 加一点环境光 + 点光(让边缘更亮)
|
||||||
|
scene.add(new THREE.AmbientLight(0xffffff, 1.2));
|
||||||
|
const dirLight = new THREE.DirectionalLight(0xffffff, 2);
|
||||||
|
dirLight.position.set(5, 10, 7.5);
|
||||||
|
scene.add(dirLight);
|
||||||
|
|
||||||
|
animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
animationId = requestAnimationFrame(animate);
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init();
|
||||||
|
|
||||||
|
// 监听滚动事件
|
||||||
|
window.addEventListener("scroll", () => {
|
||||||
|
// 计算滚动进度
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
const maxScroll =
|
||||||
|
document.documentElement.scrollHeight - window.innerHeight;
|
||||||
|
const progress = scrollY / maxScroll;
|
||||||
|
|
||||||
|
cube.position.set(4 + progress, 0, 0);
|
||||||
|
cube.rotation.y = -progress * Math.PI * 1;
|
||||||
|
cube.scale.set(
|
||||||
|
0.01 + progress * 0.001,
|
||||||
|
0.01 + progress * 0.001,
|
||||||
|
0.01 + progress * 0.001,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 接近页的脚时候淡出
|
||||||
|
// if (progress > 0.8) {
|
||||||
|
// gsap.to(cube.material, {
|
||||||
|
// opacity: 0,
|
||||||
|
// duration: 0.5,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// if (progress < 0.8) {
|
||||||
|
// gsap.to(cube.material, {
|
||||||
|
// opacity: 1,
|
||||||
|
// duration: 0.5,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听窗口大小变化
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
if (!camera || !renderer) return;
|
||||||
|
camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cancelAnimationFrame(animationId);
|
||||||
|
window.removeEventListener("resize", onResize); // 如果加了
|
||||||
|
renderer?.dispose(); // 释放资源(推荐)
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<canvas ref="canvasRef" class="pointer-events-none" />
|
||||||
|
</template>
|
||||||
120
src/components/three/ModelViewer.vue
Normal file
120
src/components/three/ModelViewer.vue
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="container" class="three-container" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, onUnmounted, ref } from "vue";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
||||||
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
|
||||||
|
|
||||||
|
const container = ref(null);
|
||||||
|
let renderer = null;
|
||||||
|
let scene = null;
|
||||||
|
let camera = null;
|
||||||
|
let controls = null;
|
||||||
|
let animationFrameId = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!container.value) return;
|
||||||
|
|
||||||
|
// ================== 初始化 ==================
|
||||||
|
scene = new THREE.Scene();
|
||||||
|
scene.background = new THREE.Color(0x0a0a1f);
|
||||||
|
|
||||||
|
camera = new THREE.PerspectiveCamera(
|
||||||
|
60,
|
||||||
|
container.value.clientWidth / container.value.clientHeight,
|
||||||
|
0.1,
|
||||||
|
1000,
|
||||||
|
);
|
||||||
|
camera.position.set(0, 1.5, 5);
|
||||||
|
|
||||||
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
|
renderer.setSize(container.value.clientWidth, container.value.clientHeight);
|
||||||
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
container.value.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
// 控制器
|
||||||
|
controls = new OrbitControls(camera, renderer.domElement);
|
||||||
|
controls.enableDamping = true;
|
||||||
|
controls.dampingFactor = 0.05;
|
||||||
|
controls.enablePan = true;
|
||||||
|
|
||||||
|
// 灯光(很重要,否则模型可能很暗)
|
||||||
|
scene.add(new THREE.AmbientLight(0xffffff, 1.5));
|
||||||
|
const dirLight = new THREE.DirectionalLight(0xffffff, 3);
|
||||||
|
dirLight.position.set(5, 10, 7.5);
|
||||||
|
scene.add(dirLight);
|
||||||
|
|
||||||
|
// ================== 加载模型 ==================
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
const modelUrl = "/model/christmas_ball.glb"; // ← 修改成你自己的模型路径
|
||||||
|
|
||||||
|
loader.load(
|
||||||
|
modelUrl,
|
||||||
|
(gltf) => {
|
||||||
|
const model = gltf.scene;
|
||||||
|
|
||||||
|
// 可选调整
|
||||||
|
model.scale.set(0.01, 0.01, 0.01); // 放大倍数,根据模型大小调整
|
||||||
|
model.position.y = -0.5; // 稍微下移居中
|
||||||
|
// model.rotation.y = Math.PI / 4 // 初始旋转(可选)
|
||||||
|
|
||||||
|
scene.add(model);
|
||||||
|
console.log("模型加载成功", model);
|
||||||
|
},
|
||||||
|
(xhr) => {
|
||||||
|
console.log(`加载进度: ${((xhr.loaded / xhr.total) * 100).toFixed(1)}%`);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error("模型加载失败:", error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ================== 动画循环 ==================
|
||||||
|
function animate() {
|
||||||
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
|
controls.update();
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
}
|
||||||
|
animate();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (animationFrameId) cancelAnimationFrame(animationFrameId);
|
||||||
|
if (controls) controls.dispose();
|
||||||
|
if (renderer) {
|
||||||
|
renderer.dispose();
|
||||||
|
renderer.forceContextLoss();
|
||||||
|
}
|
||||||
|
scene = null;
|
||||||
|
camera = null;
|
||||||
|
renderer = null;
|
||||||
|
controls = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 窗口 resize 处理
|
||||||
|
const onResize = () => {
|
||||||
|
if (!container.value || !camera || !renderer) return;
|
||||||
|
|
||||||
|
const width = container.value.clientWidth;
|
||||||
|
const height = container.value.clientHeight;
|
||||||
|
|
||||||
|
camera.aspect = width / height;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(width, height);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", onResize);
|
||||||
|
onUnmounted(() => window.removeEventListener("resize", onResize));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.three-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh; /* 或你想要的高度,例如 600px */
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import NavBar from "@/components/menu/NavBar.vue";
|
import NavBar from "@/components/menu/NavBar.vue";
|
||||||
import FooterBarV2 from "@/components/layout/FooterBarV2.vue";
|
import FooterBarV2 from "@/components/layout/FooterBarV2.vue";
|
||||||
|
import ModelViewer from "@/components/three/ModelViewer.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -11,9 +12,10 @@ import FooterBarV2 from "@/components/layout/FooterBarV2.vue";
|
|||||||
<NavBar class="fixed top-0 left-0 z-10" />
|
<NavBar class="fixed top-0 left-0 z-10" />
|
||||||
<!-- 同高度占位颜色叠加 -->
|
<!-- 同高度占位颜色叠加 -->
|
||||||
<div class="h-16 bg-base-300" />
|
<div class="h-16 bg-base-300" />
|
||||||
|
<ModelViewer />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FooterBarV2 />
|
<FooterBarV2 class="fixed bottom-0 left-0 z-1" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import BasicIntroCard from "@/components/card/BasicIntroCard.vue";
|
||||||
import DatePickerDisplayCard from "@/components/card/DatePickerDisplayCard.vue";
|
import DatePickerDisplayCard from "@/components/card/DatePickerDisplayCard.vue";
|
||||||
import DevelopProgressCard from "@/components/card/DevelopProgressCard.vue";
|
import DevelopProgressCard from "@/components/card/DevelopProgressCard.vue";
|
||||||
import FooterBarV2 from "@/components/layout/FooterBarV2.vue";
|
import FooterBarV2 from "@/components/layout/FooterBarV2.vue";
|
||||||
import NavBar from "@/components/menu/NavBar.vue";
|
import NavBar from "@/components/menu/NavBar.vue";
|
||||||
import AnimText from "@/components/special/AnimText.vue";
|
import LogoModel from "@/components/three/LogoModel.vue";
|
||||||
import { navigateTo } from "@/utils/navigator";
|
|
||||||
|
|
||||||
const progress = ref([
|
const progress = ref([
|
||||||
{
|
{
|
||||||
@@ -40,38 +40,20 @@ const progress = ref([
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="lg:pb-102 md:pb-122 pb-154">
|
<div class="lg:pb-102 md:pb-122 pb-154">
|
||||||
<!-- 这里开 relative 形成 stack context -->
|
<!-- 主内容区域,在 footer 上方 -->
|
||||||
<div class="bg-base-200 relative z-2">
|
<div class="bg-base-200 relative z-5">
|
||||||
|
<!-- 顶部与首页内容 -->
|
||||||
<div class="h-screen flex flex-col">
|
<div class="h-screen flex flex-col">
|
||||||
<NavBar class="fixed top-0 left-0 z-10" />
|
<NavBar class="fixed top-0 left-0 z-10" />
|
||||||
<!-- 同高度占位颜色叠加 -->
|
<!-- 同高度占位颜色叠加 -->
|
||||||
<div class="h-16 bg-base-300" />
|
<div class="h-16 bg-base-300" />
|
||||||
<div class="hero flex-1">
|
<BasicIntroCard class="isolate z-8" />
|
||||||
<div class="hero-content text-center">
|
|
||||||
<div class="max-w-md">
|
|
||||||
<!-- <h1 class="text-5xl font-bold">{{ $t("home.welcome") }}</h1> -->
|
|
||||||
<h1 class="text-5xl font-bold">
|
|
||||||
<AnimText text="home.welcome" />
|
|
||||||
</h1>
|
|
||||||
<div class="py-6">
|
|
||||||
<!-- {{ $t("home.intro_line1") }}<br />
|
|
||||||
{{ $t("home.intro_line2") }}<br />
|
|
||||||
{{ $t("home.intro_line3") }} -->
|
|
||||||
<AnimText text="home.intro_line1" /><br />
|
|
||||||
<AnimText text="home.intro_line2" /><br />
|
|
||||||
<AnimText text="home.intro_line3" />
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="btn btn-primary"
|
|
||||||
@click="navigateTo('http://localhost:5174')"
|
|
||||||
>
|
|
||||||
<!-- {{ $t("home.read_doc") }} -->
|
|
||||||
<AnimText text="home.read_doc" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- fixed three js 背景 -->
|
||||||
|
<LogoModel class="fixed left-0 top-0 h-screen w-full z-7" />
|
||||||
|
|
||||||
|
<!-- 脚手架开发进度 -->
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<h1 class="text-4xl font-bold mb-12 ml-10">脚手架开发进度</h1>
|
<h1 class="text-4xl font-bold mb-12 ml-10">脚手架开发进度</h1>
|
||||||
<div class="w-full grid grid-cols-3 gap-4 justify-items-center">
|
<div class="w-full grid grid-cols-3 gap-4 justify-items-center">
|
||||||
@@ -85,6 +67,8 @@ const progress = ref([
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 组件演示 -->
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<h1 class="text-4xl font-bold mb-12 ml-10">组件演示</h1>
|
<h1 class="text-4xl font-bold mb-12 ml-10">组件演示</h1>
|
||||||
<div class="w-full grid grid-cols-3 gap-4 justify-items-center">
|
<div class="w-full grid grid-cols-3 gap-4 justify-items-center">
|
||||||
@@ -92,7 +76,9 @@ const progress = ref([
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FooterBarV2 />
|
|
||||||
|
<!-- fixed footer -->
|
||||||
|
<FooterBarV2 class="fixed bottom-0 left-0 z-1" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user