Files
tootaio.com/app/pages/index.vue
xiaomai cc0cb01d28 fix(security): apply security hardening recommendations from audit
This commit implements several security enhancements based on the findings of a new security audit report, which has also been added to the documentation.

- **Security Headers:** Adds a strict Content-Security-Policy (CSP) and other security headers (X-Content-Type-Options, Referrer-Policy) via Nuxt route rules.
- **Production Hardening:** Disables Nuxt DevTools in production environments to reduce the attack surface.
- **Mixed Content:** All image assets are now loaded over HTTPS to resolve mixed content issues.
- **Tabnabbing:** Secures `window.open` calls by adding `noopener,noreferrer`.
- **Configuration:** Updates `.gitignore` to ignore all `.env.*` files.
- **Docs:** Adds the full security audit report for reference.
- **Build:** Corrects a case-sensitive import path to ensure cross-platform build compatibility.
2025-11-07 11:15:02 +08:00

179 lines
4.8 KiB
Vue

<template>
<div>
<!-- 全幅 Hero - Page 布局之外 -->
<UPageHero
:title="page?.title"
:description="page?.description"
:ui="{
root: 'relative before:absolute before:inset-0 before:bg-[image:var(--bg-image)] before:bg-cover before:bg-center before:-z-10 before:opacity-40',
}"
:style="{
'--bg-image': `url('${currentBgImage}')`,
}"
/>
<UPageSection
:title="page?.capabilities.title"
:features="page?.capabilities.features"
/>
<UPageSection :title="page?.featuredProjects.title">
<UCarousel
v-slot="{ item }"
:items="page?.featuredProjects.projects"
:ui="{ item: 'basis-full sm:basis-1/2 lg:basis-1/3' }"
>
<UPageCard
class="my-2"
:title="item.title"
:description="item.description"
:highlight="item.highlight"
:spotlight="item.spotlight"
>
<img :src="item.image" :alt="item.title" />
<UButton
v-if="item.demoLink"
:href="item.demoLink"
target="_blank"
rel="noopener"
size="sm"
>
{{ $t("index.featuredProjects.viewDemo") }}
</UButton>
</UPageCard>
</UCarousel>
</UPageSection>
<UPageSection :title="page?.techStack.title">
<UMarquee>
<UIcon
v-for="icon in techIcons"
:key="icon"
:name="icon"
class="size-16"
/>
</UMarquee>
<UMarquee reverse>
<UIcon
v-for="icon in toolsIcons"
:key="icon"
:name="icon"
class="size-16"
/>
</UMarquee>
</UPageSection>
<UPageSection
:title="page?.whyChooseUs.title"
:features="page?.whyChooseUs.features"
/>
</div>
</template>
<script lang="ts" setup>
const { data: page } = await useLocalizedCollection("index");
useSeoMeta({
title: page.value?.seo.title,
});
const colorMode = useColorMode();
const backgroundImages = [
"https://img.tootaio.com/i/2025/11/05/avc5ld.png",
"https://img.tootaio.com/i/2025/11/05/avcaff.png",
"https://img.tootaio.com/i/2025/11/05/avcjbw.png",
"https://img.tootaio.com/i/2025/11/05/avcp16.png",
"https://img.tootaio.com/i/2025/11/05/avcv1q.png",
"https://img.tootaio.com/i/2025/11/05/avd47a.png",
"https://img.tootaio.com/i/2025/11/05/avdx6a.png",
"https://img.tootaio.com/i/2025/11/05/avegxy.png",
"https://img.tootaio.com/i/2025/11/05/avemgn.png",
"https://img.tootaio.com/i/2025/11/05/avf3wl.png",
];
const currentBgImage = ref<string | undefined>("");
// 随机选择背景
const randomBg = () => {
const randomIndex = Math.floor(Math.random() * backgroundImages.length);
currentBgImage.value = backgroundImages[randomIndex];
};
// 轮播背景
onMounted(() => {
randomBg();
});
const techIcons = computed(() => [
"skill-icons:html",
"skill-icons:css",
"skill-icons:javascript",
"skill-icons:typescript",
"skill-icons:docker",
colorMode.value === "dark"
? "skill-icons:vuejs-dark"
: "skill-icons:vuejs-light",
colorMode.value === "dark"
? "skill-icons:nuxtjs-dark"
: "skill-icons:nuxtjs-light",
colorMode.value === "dark"
? "skill-icons:tailwindcss-dark"
: "skill-icons:tailwindcss-light",
colorMode.value === "dark"
? "skill-icons:nodejs-dark"
: "skill-icons:nodejs-light",
"skill-icons:cs",
colorMode.value === "dark"
? "skill-icons:python-dark"
: "skill-icons:python-light",
]);
const toolsIcons = ref([
"skill-icons:photoshop",
"skill-icons:illustrator",
"skill-icons:git",
colorMode.value === "dark"
? "skill-icons:vscode-dark"
: "skill-icons:vscode-light",
colorMode.value === "dark"
? "skill-icons:visualstudio-dark"
: "skill-icons:visualstudio-light",
colorMode.value === "dark"
? "skill-icons:github-dark"
: "skill-icons:github-light",
colorMode.value === "dark"
? "skill-icons:godot-dark"
: "skill-icons:godot-light",
colorMode.value === "dark"
? "skill-icons:unity-dark"
: "skill-icons:unity-light",
colorMode.value === "dark"
? "skill-icons:blender-dark"
: "skill-icons:blender-light",
colorMode.value === "dark"
? "skill-icons:androidstudio-dark"
: "skill-icons:androidstudio-light",
colorMode.value === "dark"
? "skill-icons:windows-dark"
: "skill-icons:windows-light",
colorMode.value === "dark"
? "skill-icons:linux-dark"
: "skill-icons:linux-light",
colorMode.value === "dark"
? "skill-icons:apple-dark"
: "skill-icons:apple-light",
colorMode.value === "dark"
? "skill-icons:idea-dark"
: "skill-icons:idea-light",
colorMode.value === "dark"
? "skill-icons:pycharm-dark"
: "skill-icons:pycharm-light",
colorMode.value === "dark"
? "skill-icons:rider-dark"
: "skill-icons:rider-light",
]);
</script>
<style></style>