first commit
This commit is contained in:
parent
05c5677b65
commit
21bb4a5d85
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
11
.prettierrc
Normal file
11
.prettierrc
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"jsxSingleQuote": false,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
93
README.md
93
README.md
@ -1,2 +1,93 @@
|
||||
# FutureCloud
|
||||
# Onlook 启动模板
|
||||
|
||||
<p align="center">
|
||||
<img src="app/favicon.ico" />
|
||||
</p>
|
||||
|
||||
这是一个使用 [Next.js](https://nextjs.org/)、[TailwindCSS](https://tailwindcss.com/) 与 [shadcn/ui](https://ui.shadcn.com) 搭建的 [Onlook](https://onlook.com/) 项目模板。
|
||||
|
||||
## 快速开始
|
||||
|
||||
1. 安装依赖(PowerShell 示例,四选一):
|
||||
|
||||
```powershell
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
2. 启动开发服务器(PowerShell):
|
||||
|
||||
```powershell
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# bun
|
||||
bun dev
|
||||
```
|
||||
|
||||
打开后在 Onlook 中查看:<http://localhost:3000>
|
||||
|
||||
## 目录结构(节选)
|
||||
|
||||
- `app/`:Next.js App Router 页面与布局
|
||||
- `components/`:通用组件
|
||||
- `hooks/`:自定义 Hooks
|
||||
- `lib/`:工具与内容数据
|
||||
- `public/`:静态资源
|
||||
- `app/globals.css`:全局样式(TailwindCSS)
|
||||
|
||||
## 常用脚本
|
||||
|
||||
```powershell
|
||||
# 开发
|
||||
npm run dev
|
||||
|
||||
# 构建
|
||||
npm run build
|
||||
|
||||
# 预览(如有 start 脚本)
|
||||
npm run start
|
||||
```
|
||||
|
||||
使用 `yarn`/`pnpm`/`bun` 时,将 `npm run XXX` 替换为对应命令即可。
|
||||
|
||||
## 环境变量
|
||||
|
||||
在项目根目录创建 `.env.local`,示例:
|
||||
|
||||
```dotenv
|
||||
# 示例
|
||||
NEXT_PUBLIC_APP_NAME=Onlook Starter
|
||||
```
|
||||
|
||||
Next.js 会在运行时自动加载 `.env.local`。请勿将敏感变量提交到版本库。
|
||||
|
||||
## 部署
|
||||
|
||||
- 推荐:Vercel(零配置支持 Next.js)。
|
||||
- 自托管:
|
||||
|
||||
```powershell
|
||||
npm run build
|
||||
npm run start
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
本模板遵循其依赖库的各自许可证。根据你的项目需要添加 `LICENSE` 文件。
|
||||
|
||||
|
||||
199
app/[lang]/about/page.tsx
Normal file
199
app/[lang]/about/page.tsx
Normal file
@ -0,0 +1,199 @@
|
||||
'use client';
|
||||
|
||||
import Navigation from '../../../components/Navigation';
|
||||
import Footer from '../../../components/Footer';
|
||||
import { useLanguage } from '@/hooks/useLanguage';
|
||||
|
||||
interface AboutPageProps {
|
||||
params: {
|
||||
lang: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function AboutPage({ params }: AboutPageProps) {
|
||||
const { currentLang, setCurrentLang, currentContent, createLocalizedPath } = useLanguage(
|
||||
params.lang,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<Navigation
|
||||
currentLang={currentLang}
|
||||
setCurrentLang={setCurrentLang}
|
||||
content={currentContent}
|
||||
createLocalizedPath={createLocalizedPath}
|
||||
/>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative z-10 px-6 py-20">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-8 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
|
||||
{currentContent.about.title}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-300 mb-12 max-w-3xl mx-auto">
|
||||
{currentContent.about.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* About Content */}
|
||||
<section className="relative z-10 px-6 py-20">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center mb-20">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-6 text-cyan-400">我们的故事</h2>
|
||||
<p className="text-gray-300 leading-relaxed mb-6">
|
||||
{currentContent.about.description}
|
||||
</p>
|
||||
<p className="text-gray-300 leading-relaxed">
|
||||
自成立以来,我们已经为数百家企业提供了专业的云计算服务,帮助他们实现数字化转型,提高业务效率,降低运营成本。
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-8 border border-cyan-500/20">
|
||||
<div className="grid grid-cols-2 gap-6 text-center">
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-cyan-400 mb-2">
|
||||
500+
|
||||
</div>
|
||||
<div className="text-gray-400">服务客户</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-cyan-400 mb-2">
|
||||
99.9%
|
||||
</div>
|
||||
<div className="text-gray-400">服务可用性</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-cyan-400 mb-2">
|
||||
24/7
|
||||
</div>
|
||||
<div className="text-gray-400">技术支持</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-cyan-400 mb-2">
|
||||
5年+
|
||||
</div>
|
||||
<div className="text-gray-400">行业经验</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mission, Vision, Values */}
|
||||
<div className="grid md:grid-cols-3 gap-8 mb-20">
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-8 border border-cyan-500/20 text-center">
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-lg flex items-center justify-center mx-auto mb-6">
|
||||
<svg
|
||||
className="w-8 h-8 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-4 text-cyan-400">
|
||||
{currentContent.about.mission.title}
|
||||
</h3>
|
||||
<p className="text-gray-300 leading-relaxed">
|
||||
{currentContent.about.mission.content}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-8 border border-cyan-500/20 text-center">
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-lg flex items-center justify-center mx-auto mb-6">
|
||||
<svg
|
||||
className="w-8 h-8 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-4 text-cyan-400">
|
||||
{currentContent.about.vision.title}
|
||||
</h3>
|
||||
<p className="text-gray-300 leading-relaxed">
|
||||
{currentContent.about.vision.content}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-8 border border-cyan-500/20 text-center">
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-lg flex items-center justify-center mx-auto mb-6">
|
||||
<svg
|
||||
className="w-8 h-8 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-4 text-cyan-400">
|
||||
{currentContent.about.values.title}
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{currentContent.about.values.items.map((value, index) => (
|
||||
<li key={index} className="text-gray-300">
|
||||
{value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Team Section */}
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
|
||||
{currentContent.about.team.title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{currentContent.about.team.members.map((member, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-8 border border-cyan-500/20 text-center"
|
||||
>
|
||||
<div className="w-20 h-20 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<span className="text-white font-bold text-2xl">
|
||||
{member.name.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-2 text-cyan-400">
|
||||
{member.name}
|
||||
</h3>
|
||||
<p className="text-gray-400 mb-4">{member.position}</p>
|
||||
<p className="text-gray-300 text-sm leading-relaxed">
|
||||
{member.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer content={currentContent} createLocalizedPath={createLocalizedPath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
289
app/[lang]/contact/page.tsx
Normal file
289
app/[lang]/contact/page.tsx
Normal file
@ -0,0 +1,289 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Navigation from '../../../components/Navigation';
|
||||
import Footer from '../../../components/Footer';
|
||||
import { useLanguage } from '../../../hooks/useLanguage';
|
||||
|
||||
interface ContactPageProps {
|
||||
params: {
|
||||
lang: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function ContactPage({ params }: ContactPageProps) {
|
||||
const { currentLang, setCurrentLang, currentContent, createLocalizedPath } = useLanguage(
|
||||
params.lang,
|
||||
);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
company: '',
|
||||
phone: '',
|
||||
message: '',
|
||||
});
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
console.log('Form submitted:', formData);
|
||||
alert('感谢您的留言,我们会尽快与您联系!');
|
||||
setFormData({
|
||||
name: '',
|
||||
email: '',
|
||||
company: '',
|
||||
phone: '',
|
||||
message: '',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<Navigation
|
||||
currentLang={currentLang}
|
||||
setCurrentLang={setCurrentLang}
|
||||
content={currentContent}
|
||||
createLocalizedPath={createLocalizedPath}
|
||||
/>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative z-10 px-6 py-20">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-8 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
|
||||
{currentContent.contact.title}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-300 mb-12 max-w-3xl mx-auto">
|
||||
{currentContent.contact.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact Content */}
|
||||
<section className="relative z-10 px-6 py-20">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid lg:grid-cols-2 gap-12">
|
||||
{/* Contact Form */}
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-8 border border-cyan-500/20">
|
||||
<h2 className="text-2xl font-bold mb-6 text-cyan-400">发送消息</h2>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-gray-300">
|
||||
{currentContent.contact.form.name}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
placeholder={
|
||||
currentContent.contact.form.namePlaceholder
|
||||
}
|
||||
className="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:border-cyan-400 transition-colors"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-gray-300">
|
||||
{currentContent.contact.form.email}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
placeholder={
|
||||
currentContent.contact.form.emailPlaceholder
|
||||
}
|
||||
className="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:border-cyan-400 transition-colors"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-gray-300">
|
||||
{currentContent.contact.form.company}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="company"
|
||||
value={formData.company}
|
||||
onChange={handleInputChange}
|
||||
placeholder={
|
||||
currentContent.contact.form.companyPlaceholder
|
||||
}
|
||||
className="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:border-cyan-400 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-gray-300">
|
||||
{currentContent.contact.form.phone}
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
placeholder={
|
||||
currentContent.contact.form.phonePlaceholder
|
||||
}
|
||||
className="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:border-cyan-400 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-gray-300">
|
||||
{currentContent.contact.form.message}
|
||||
</label>
|
||||
<textarea
|
||||
name="message"
|
||||
value={formData.message}
|
||||
onChange={handleInputChange}
|
||||
placeholder={currentContent.contact.form.messagePlaceholder}
|
||||
rows={6}
|
||||
className="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:border-cyan-400 transition-colors resize-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-4 bg-gradient-to-r from-cyan-500 to-blue-600 rounded-lg font-semibold hover:from-cyan-400 hover:to-blue-500 transition-all duration-300 transform hover:scale-105"
|
||||
>
|
||||
{currentContent.contact.form.submit}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Contact Info */}
|
||||
<div className="space-y-8">
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-8 border border-cyan-500/20">
|
||||
<h2 className="text-2xl font-bold mb-6 text-cyan-400">联系信息</h2>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg
|
||||
className="w-6 h-6 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
||||
/>
|
||||
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white mb-1">地址</h3>
|
||||
<p className="text-gray-400">
|
||||
{currentContent.contact.info.address}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg
|
||||
className="w-6 h-6 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white mb-1">电话</h3>
|
||||
<p className="text-gray-400">
|
||||
{currentContent.contact.info.phone}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg
|
||||
className="w-6 h-6 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white mb-1">邮箱</h3>
|
||||
<p className="text-gray-400">
|
||||
{currentContent.contact.info.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg
|
||||
className="w-6 h-6 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white mb-1">
|
||||
服务时间
|
||||
</h3>
|
||||
<p className="text-gray-400">
|
||||
{currentContent.contact.info.hours}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Map placeholder */}
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-8 border border-cyan-500/20">
|
||||
<h3 className="text-xl font-bold mb-4 text-cyan-400">位置地图</h3>
|
||||
<div className="w-full h-64 bg-gray-700 rounded-lg flex items-center justify-center">
|
||||
<p className="text-gray-400">地图位置</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer content={currentContent} createLocalizedPath={createLocalizedPath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
app/[lang]/layout.tsx
Normal file
33
app/[lang]/layout.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Metadata } from 'next';
|
||||
import { content } from '../../lib/content';
|
||||
|
||||
interface LanguageLayoutProps {
|
||||
children: ReactNode;
|
||||
params: {
|
||||
lang: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { lang: string };
|
||||
}): Promise<Metadata> {
|
||||
const lang = params.lang as keyof typeof content;
|
||||
const currentContent = content[lang] || content['zh-CN'];
|
||||
|
||||
return {
|
||||
title: currentContent.title,
|
||||
description: currentContent.description,
|
||||
keywords: currentContent.keywords,
|
||||
};
|
||||
}
|
||||
|
||||
export default function LanguageLayout({ children, params }: LanguageLayoutProps) {
|
||||
return children;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return [{ lang: 'zh-CN' }, { lang: 'zh-TW' }, { lang: 'en' }];
|
||||
}
|
||||
212
app/[lang]/news/[id]/page.tsx
Normal file
212
app/[lang]/news/[id]/page.tsx
Normal file
@ -0,0 +1,212 @@
|
||||
'use client';
|
||||
|
||||
import Navigation from '../../../../components/Navigation';
|
||||
import Footer from '../../../../components/Footer';
|
||||
import { useLanguage } from '../../../../hooks/useLanguage';
|
||||
import Link from 'next/link';
|
||||
import { LanguageType } from '../../../../lib/content';
|
||||
|
||||
interface ArticlePageProps {
|
||||
params: {
|
||||
lang: string;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function ArticlePage({ params }: ArticlePageProps) {
|
||||
const { currentLang, setCurrentLang, currentContent, createLocalizedPath } = useLanguage(
|
||||
params.lang as LanguageType,
|
||||
);
|
||||
const articleId = parseInt(params.id, 10);
|
||||
|
||||
const article = currentContent.news.articles.find((a) => a.id === articleId);
|
||||
const relatedArticles = currentContent.news.articles
|
||||
.filter((a) => a.id !== articleId && a.category === article?.category)
|
||||
.slice(0, 3);
|
||||
|
||||
if (!article) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<Navigation
|
||||
currentLang={currentLang}
|
||||
setCurrentLang={setCurrentLang}
|
||||
content={currentContent}
|
||||
createLocalizedPath={createLocalizedPath}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4 text-cyan-400">文章未找到</h1>
|
||||
<p className="text-gray-400 mb-8">抱歉,您访问的文章不存在。</p>
|
||||
<Link
|
||||
href={createLocalizedPath('/news')}
|
||||
className="px-6 py-3 bg-gradient-to-r from-cyan-500 to-blue-600 rounded-lg font-semibold hover:from-cyan-400 hover:to-blue-500 transition-all duration-300"
|
||||
>
|
||||
返回资讯列表
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 🔧 完整调用 Footer,添加 createLocalizedPath */}
|
||||
<Footer content={currentContent} createLocalizedPath={createLocalizedPath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
industry: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
||||
technology: 'bg-green-500/20 text-green-400 border-green-500/30',
|
||||
company: 'bg-purple-500/20 text-purple-400 border-purple-500/30',
|
||||
case: 'bg-orange-500/20 text-orange-400 border-orange-500/30',
|
||||
};
|
||||
return colors[category] ?? 'bg-gray-500/20 text-gray-400 border-gray-500/30';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<Navigation
|
||||
currentLang={currentLang}
|
||||
setCurrentLang={setCurrentLang}
|
||||
content={currentContent}
|
||||
createLocalizedPath={createLocalizedPath}
|
||||
/>
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<section className="relative z-10 px-6 py-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<nav className="flex items-center space-x-2 text-sm text-gray-400">
|
||||
<Link
|
||||
href={createLocalizedPath('/')}
|
||||
className="hover:text-cyan-400 transition-colors"
|
||||
>
|
||||
首页
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<Link
|
||||
href={createLocalizedPath('/news')}
|
||||
className="hover:text-cyan-400 transition-colors"
|
||||
>
|
||||
资讯
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-gray-300">{article.title}</span>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Article Header */}
|
||||
<section className="relative z-10 px-6 py-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-sm font-medium border ${getCategoryColor(
|
||||
article.category,
|
||||
)}`}
|
||||
>
|
||||
{currentContent.news.categories[article.category]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl md:text-5xl font-bold mb-6 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent leading-tight">
|
||||
{article.title}
|
||||
</h1>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-4 text-gray-400 mb-8">
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
{article.author}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
{article.date}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{article.readTime}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-2 mb-8">
|
||||
{article.tags.map((tag, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-3 py-1 bg-gray-700/50 text-gray-400 text-sm rounded-full"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Article Content */}
|
||||
<section className="relative z-10 px-6 py-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="bg-gray-800/30 backdrop-blur-sm rounded-xl p-8 border border-cyan-500/20">
|
||||
<div className="prose prose-lg prose-invert max-w-none">
|
||||
<p className="text-xl text-gray-300 leading-relaxed mb-6">
|
||||
{article.summary}
|
||||
</p>
|
||||
<div className="text-gray-300 leading-relaxed space-y-4">
|
||||
{article.content.split('\n').map((paragraph, idx) => (
|
||||
<p key={idx}>{paragraph}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Share Section */}
|
||||
<section className="relative z-10 px-6 py-8">
|
||||
<div className="max-w-4xl mx-auto">{/* ... 分享按钮等省略 */}</div>
|
||||
</section>
|
||||
|
||||
{/* Related Articles */}
|
||||
{relatedArticles.length > 0 && (
|
||||
<section className="relative z-10 px-6 py-20">{/* ... 相关文章省略 */}</section>
|
||||
)}
|
||||
|
||||
{/* 🔧 完整调用 Footer,传入 createLocalizedPath */}
|
||||
<Footer content={currentContent} createLocalizedPath={createLocalizedPath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
195
app/[lang]/news/page.tsx
Normal file
195
app/[lang]/news/page.tsx
Normal file
@ -0,0 +1,195 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Navigation from '../../../components/Navigation';
|
||||
import Footer from '../../../components/Footer';
|
||||
import { useLanguage } from '../../../hooks/useLanguage';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface NewsPageProps {
|
||||
params: {
|
||||
lang: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function NewsPage({ params }: NewsPageProps) {
|
||||
const { currentLang, setCurrentLang, currentContent, createLocalizedPath } = useLanguage(
|
||||
params.lang,
|
||||
);
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
|
||||
const filteredArticles =
|
||||
selectedCategory === 'all'
|
||||
? currentContent.news.articles
|
||||
: currentContent.news.articles.filter(
|
||||
(article) => article.category === selectedCategory,
|
||||
);
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
industry: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
||||
technology: 'bg-green-500/20 text-green-400 border-green-500/30',
|
||||
company: 'bg-purple-500/20 text-purple-400 border-purple-500/30',
|
||||
case: 'bg-orange-500/20 text-orange-400 border-orange-500/30',
|
||||
};
|
||||
return colors[category] ?? 'bg-gray-500/20 text-gray-400 border-gray-500/30';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<Navigation
|
||||
currentLang={currentLang}
|
||||
setCurrentLang={setCurrentLang}
|
||||
content={currentContent}
|
||||
createLocalizedPath={createLocalizedPath}
|
||||
/>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative z-10 px-6 py-20">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-8 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
|
||||
{currentContent.news.title}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-300 mb-12 max-w-3xl mx-auto">
|
||||
{currentContent.news.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Category Filter */}
|
||||
<section className="relative z-10 px-6 py-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex flex-wrap justify-center gap-4 mb-12">
|
||||
{Object.entries(currentContent.news.categories).map(([key, label]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setSelectedCategory(key)}
|
||||
className={`px-6 py-3 rounded-full border transition-all duration-300 ${
|
||||
selectedCategory === key
|
||||
? 'bg-cyan-500/20 text-cyan-400 border-cyan-500/50'
|
||||
: 'bg-gray-800/50 text-gray-400 border-gray-600/50 hover:border-cyan-500/30 hover:text-cyan-400'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Articles Grid */}
|
||||
<section className="relative z-10 px-6 py-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{filteredArticles.map((article) => (
|
||||
<article
|
||||
key={article.id}
|
||||
className="bg-gray-800/50 backdrop-blur-sm rounded-xl overflow-hidden border border-cyan-500/20 hover:border-cyan-400/50 transition-all duration-300 group"
|
||||
>
|
||||
{/* Article Image */}
|
||||
<div className="relative h-48 bg-gradient-to-br from-cyan-500/20 to-blue-500/20 overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-gray-900/50 to-transparent" />
|
||||
|
||||
<div className="absolute top-4 left-4">
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-xs font-medium border ${getCategoryColor(article.category)}`}
|
||||
>
|
||||
{currentContent.news.categories[article.category]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Article Content */}
|
||||
<div className="p-6">
|
||||
<div className="flex items-center text-sm text-gray-400 mb-3">
|
||||
<span>{article.author}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>{article.date}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>{article.readTime}</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold mb-3 text-white group-hover:text-cyan-400 transition-colors line-clamp-2">
|
||||
<Link href={createLocalizedPath(`/news/${article.id}`)}>
|
||||
{article.title}
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
<p className="text-gray-300 mb-4 line-clamp-3 leading-relaxed">
|
||||
{article.summary}
|
||||
</p>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{article.tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-gray-700/50 text-gray-400 text-xs rounded-md"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={createLocalizedPath(`/news/${article.id}`)}
|
||||
className="inline-flex items-center text-cyan-400 hover:text-cyan-300 transition-colors group"
|
||||
>
|
||||
阅读更多
|
||||
<svg
|
||||
className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Load More Button */}
|
||||
{filteredArticles.length > 6 && (
|
||||
<div className="text-center mt-12">
|
||||
<button className="px-8 py-4 bg-gradient-to-r from-cyan-500 to-blue-600 rounded-lg font-semibold hover:from-cyan-400 hover:to-blue-500 transition-all duration-300 transform hover:scale-105">
|
||||
加载更多
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Newsletter Subscription */}
|
||||
<section className="relative z-10 px-6 py-20 bg-gray-800/30">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
|
||||
订阅我们的资讯
|
||||
</h2>
|
||||
<p className="text-xl text-gray-300 mb-8">
|
||||
第一时间获取云计算行业最新动态和技术资讯
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center max-w-md mx-auto">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="输入您的邮箱地址"
|
||||
className="flex-1 px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:border-cyan-400 transition-colors"
|
||||
/>
|
||||
|
||||
<button className="px-6 py-3 bg-gradient-to-r from-cyan-500 to-blue-600 rounded-lg font-semibold hover:from-cyan-400 hover:to-blue-500 transition-all duration-300">
|
||||
订阅
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer content={currentContent} createLocalizedPath={createLocalizedPath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
168
app/[lang]/page.tsx
Normal file
168
app/[lang]/page.tsx
Normal file
@ -0,0 +1,168 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Navigation from '../../components/Navigation';
|
||||
import Footer from '../../components/Footer';
|
||||
import { useLanguage } from '../../hooks/useLanguage';
|
||||
|
||||
interface Particle {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
speed: number;
|
||||
opacity: number;
|
||||
}
|
||||
|
||||
interface HomePageProps {
|
||||
params: {
|
||||
lang: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function HomePage({ params }: HomePageProps) {
|
||||
const { currentLang, setCurrentLang, currentContent, createLocalizedPath } = useLanguage(
|
||||
params.lang,
|
||||
);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [particles, setParticles] = useState<Particle[]>([]);
|
||||
|
||||
// Initialize particles for background effect
|
||||
useEffect(() => {
|
||||
const newParticles = Array.from({ length: 50 }, (_, i) => ({
|
||||
id: i,
|
||||
x: Math.random() * 100,
|
||||
y: Math.random() * 100,
|
||||
size: Math.random() * 3 + 1,
|
||||
speed: Math.random() * 0.5 + 0.1,
|
||||
opacity: Math.random() * 0.5 + 0.2,
|
||||
}));
|
||||
setParticles(newParticles);
|
||||
setIsLoaded(true);
|
||||
}, []);
|
||||
|
||||
// Animate particles
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setParticles((prev) =>
|
||||
prev.map((particle) => ({
|
||||
...particle,
|
||||
y: (particle.y + particle.speed) % 100,
|
||||
})),
|
||||
);
|
||||
}, 50);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen bg-gray-900 text-white relative overflow-hidden">
|
||||
{/* Animated particle background */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
{particles.map((particle) => (
|
||||
<div
|
||||
key={particle.id}
|
||||
className="absolute w-1 h-1 bg-cyan-400 rounded-full animate-pulse"
|
||||
style={{
|
||||
left: `${particle.x}%`,
|
||||
top: `${particle.y}%`,
|
||||
width: `${particle.size}px`,
|
||||
height: `${particle.size}px`,
|
||||
opacity: particle.opacity,
|
||||
boxShadow: `0 0 ${particle.size * 2}px rgba(34, 211, 238, 0.5)`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Gradient overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-900/20 via-purple-900/20 to-cyan-900/20" />
|
||||
|
||||
{/* Navigation */}
|
||||
<Navigation
|
||||
currentLang={currentLang}
|
||||
setCurrentLang={setCurrentLang}
|
||||
content={currentContent}
|
||||
createLocalizedPath={createLocalizedPath}
|
||||
/>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative z-10 px-6 py-20">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<div
|
||||
className={`transition-all duration-1000 ${isLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'}`}
|
||||
>
|
||||
<h1 className="text-6xl md:text-8xl font-bold mb-6 bg-gradient-to-r from-cyan-400 via-blue-500 to-purple-600 bg-clip-text text-transparent">
|
||||
{currentContent.hero.title}
|
||||
</h1>
|
||||
<p className="text-2xl md:text-3xl text-cyan-300 mb-8 font-light">
|
||||
{currentContent.hero.subtitle}
|
||||
</p>
|
||||
<p className="text-xl text-gray-300 mb-12 max-w-3xl mx-auto leading-relaxed">
|
||||
{currentContent.hero.description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-6 justify-center">
|
||||
<button className="px-8 py-4 bg-gradient-to-r from-cyan-500 to-blue-600 rounded-full text-lg font-semibold hover:from-cyan-400 hover:to-blue-500 transition-all duration-300 transform hover:scale-105 shadow-lg shadow-cyan-500/25">
|
||||
{currentContent.hero.cta}
|
||||
</button>
|
||||
<button className="px-8 py-4 border-2 border-cyan-400 rounded-full text-lg font-semibold hover:bg-cyan-400 hover:text-gray-900 transition-all duration-300 transform hover:scale-105">
|
||||
{currentContent.hero.learn}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Services Section */}
|
||||
<section className="relative z-10 px-6 py-20">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
|
||||
{currentContent.services.title}
|
||||
</h2>
|
||||
<p className="text-xl text-gray-300">
|
||||
{currentContent.services.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{currentContent.services.items.map((service, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="group relative p-6 bg-gray-800/50 backdrop-blur-sm rounded-2xl border border-cyan-500/20 hover:border-cyan-400/50 transition-all duration-300 transform hover:scale-105"
|
||||
>
|
||||
{/* 3D Icon effect */}
|
||||
<div className="w-16 h-16 mb-6 mx-auto bg-gradient-to-br from-cyan-400 to-blue-600 rounded-xl flex items-center justify-center transform group-hover:rotate-12 transition-transform duration-300">
|
||||
<div className="w-8 h-8 bg-white/20 rounded-lg" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold mb-4 text-cyan-300 group-hover:text-cyan-200 transition-colors">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="text-gray-400 group-hover:text-gray-300 transition-colors">
|
||||
{service.description}
|
||||
</p>
|
||||
|
||||
{/* Glow effect */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-cyan-500/10 to-blue-500/10 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Decorative lines */}
|
||||
<div className="absolute top-1/2 left-0 w-full h-px bg-gradient-to-r from-transparent via-cyan-500/50 to-transparent" />
|
||||
|
||||
<div className="absolute top-1/3 left-0 w-full h-px bg-gradient-to-r from-transparent via-blue-500/30 to-transparent" />
|
||||
|
||||
<div className="absolute top-2/3 left-0 w-full h-px bg-gradient-to-r from-transparent via-purple-500/30 to-transparent" />
|
||||
|
||||
{/* Footer */}
|
||||
<Footer content={currentContent} createLocalizedPath={createLocalizedPath} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
156
app/[lang]/pricing/page.tsx
Normal file
156
app/[lang]/pricing/page.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
'use client';
|
||||
|
||||
import Navigation from '../../../components/Navigation';
|
||||
import Footer from '../../../components/Footer';
|
||||
import { useLanguage } from '../../../hooks/useLanguage';
|
||||
|
||||
interface PricingPageProps {
|
||||
params: {
|
||||
lang: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function PricingPage({ params }: PricingPageProps) {
|
||||
const { currentLang, setCurrentLang, currentContent, createLocalizedPath } = useLanguage(
|
||||
params.lang,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<Navigation
|
||||
currentLang={currentLang}
|
||||
setCurrentLang={setCurrentLang}
|
||||
content={currentContent}
|
||||
createLocalizedPath={createLocalizedPath}
|
||||
/>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative z-10 px-6 py-20">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-8 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
|
||||
{currentContent.pricing.title}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-300 mb-12 max-w-3xl mx-auto">
|
||||
{currentContent.pricing.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Pricing Cards */}
|
||||
<section className="relative z-10 px-6 py-20">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{currentContent.pricing.plans.map((plan, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`relative bg-gray-800/50 backdrop-blur-sm rounded-xl p-8 border transition-all duration-300 ${
|
||||
plan.popular
|
||||
? 'border-cyan-400 scale-105 shadow-2xl shadow-cyan-500/20'
|
||||
: 'border-cyan-500/20 hover:border-cyan-400/50'
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
||||
<span className="bg-gradient-to-r from-cyan-400 to-blue-500 text-white px-4 py-1 rounded-full text-sm font-semibold">
|
||||
推荐
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<h3 className="text-2xl font-bold mb-2 text-cyan-400">
|
||||
{plan.name}
|
||||
</h3>
|
||||
<div className="mb-4">
|
||||
<span className="text-4xl font-bold">{plan.price}</span>
|
||||
<span className="text-gray-400">{plan.period}</span>
|
||||
</div>
|
||||
<p className="text-gray-300">{plan.description}</p>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-4 mb-8">
|
||||
{plan.features.map((feature, featureIndex) => (
|
||||
<li
|
||||
key={featureIndex}
|
||||
className="flex items-center text-gray-300"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 text-cyan-400 mr-3 flex-shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
className={`w-full py-4 rounded-lg font-semibold transition-all duration-300 ${
|
||||
plan.popular
|
||||
? 'bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white'
|
||||
: 'border border-cyan-500 text-cyan-400 hover:bg-cyan-500/10'
|
||||
}`}
|
||||
>
|
||||
选择方案
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section className="relative z-10 px-6 py-20 bg-gray-800/30">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
|
||||
常见问题
|
||||
</h2>
|
||||
<p className="text-xl text-gray-300">解答您关心的问题</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{
|
||||
question: '如何选择适合的方案?',
|
||||
answer: '我们建议根据您的业务规模、预期流量和技术需求来选择。我们的专业团队可以为您提供免费咨询,帮助您选择最适合的方案。',
|
||||
},
|
||||
{
|
||||
question: '是否支持方案升级?',
|
||||
answer: '是的,您可以随时升级到更高级的方案。升级过程无缝衔接,不会影响您的业务运行。',
|
||||
},
|
||||
{
|
||||
question: '技术支持包含哪些内容?',
|
||||
answer: '我们提供7x24小时技术支持,包括系统监控、故障排除、性能优化、安全更新等全方位服务。',
|
||||
},
|
||||
{
|
||||
question: '数据安全如何保障?',
|
||||
answer: '我们采用AWS企业级安全标准,包括数据加密、访问控制、定期备份、灾难恢复等多重安全保障措施。',
|
||||
},
|
||||
].map((faq: { question: string; answer: string }, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-gray-800/50 rounded-lg p-6 border border-cyan-500/20"
|
||||
>
|
||||
<h3 className="text-xl font-bold mb-3 text-cyan-400">
|
||||
{faq.question}
|
||||
</h3>
|
||||
<p className="text-gray-300 leading-relaxed">{faq.answer}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer content={currentContent} createLocalizedPath={createLocalizedPath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
119
app/[lang]/services/page.tsx
Normal file
119
app/[lang]/services/page.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
'use client';
|
||||
|
||||
import Navigation from '../../../components/Navigation';
|
||||
import Footer from '../../../components/Footer';
|
||||
import { useLanguage } from '../../../hooks/useLanguage';
|
||||
|
||||
interface ServicesPageProps {
|
||||
params: {
|
||||
lang: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function ServicesPage({ params }: ServicesPageProps) {
|
||||
const { currentLang, setCurrentLang, currentContent, createLocalizedPath } = useLanguage(
|
||||
params.lang,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<Navigation
|
||||
currentLang={currentLang}
|
||||
setCurrentLang={setCurrentLang}
|
||||
content={currentContent}
|
||||
createLocalizedPath={createLocalizedPath}
|
||||
/>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative z-10 px-6 py-20">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-8 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
|
||||
{currentContent.services.title}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-300 mb-12 max-w-3xl mx-auto">
|
||||
{currentContent.services.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Services Grid */}
|
||||
<section className="relative z-10 px-6 py-20">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{currentContent.services.items.map((service, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-8 border border-cyan-500/20 hover:border-cyan-400/50 transition-all duration-300 group"
|
||||
>
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-lg flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300">
|
||||
<svg
|
||||
className="w-8 h-8 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold mb-4 text-cyan-400">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="text-gray-300 mb-6 leading-relaxed">
|
||||
{service.description}
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
{service.features.map((feature, featureIndex) => (
|
||||
<li
|
||||
key={featureIndex}
|
||||
className="flex items-center text-gray-400"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 text-cyan-400 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="relative z-10 px-6 py-20">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
|
||||
{currentContent.hero.cta}
|
||||
</h2>
|
||||
<p className="text-xl text-gray-300 mb-8">{currentContent.hero.description}</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<button className="px-8 py-4 bg-gradient-to-r from-cyan-500 to-blue-600 rounded-lg font-semibold hover:from-cyan-400 hover:to-blue-500 transition-all duration-300 transform hover:scale-105">
|
||||
{currentContent.hero.cta}
|
||||
</button>
|
||||
<button className="px-8 py-4 border border-cyan-500 rounded-lg font-semibold hover:bg-cyan-500/10 transition-all duration-300">
|
||||
{currentContent.hero.learn}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer content={currentContent} createLocalizedPath={createLocalizedPath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
128
app/[lang]/solutions/page.tsx
Normal file
128
app/[lang]/solutions/page.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
'use client';
|
||||
|
||||
import Navigation from '../../../components/Navigation';
|
||||
import Footer from '../../../components/Footer';
|
||||
import { useLanguage } from '../../../hooks/useLanguage';
|
||||
|
||||
interface SolutionsPageProps {
|
||||
params: {
|
||||
lang: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function SolutionsPage({ params }: SolutionsPageProps) {
|
||||
const { currentLang, setCurrentLang, currentContent, createLocalizedPath } = useLanguage(
|
||||
params.lang,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<Navigation
|
||||
currentLang={currentLang}
|
||||
setCurrentLang={setCurrentLang}
|
||||
content={currentContent}
|
||||
createLocalizedPath={createLocalizedPath}
|
||||
/>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative z-10 px-6 py-20">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-8 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
|
||||
{currentContent.solutions.title}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-300 mb-12 max-w-3xl mx-auto">
|
||||
{currentContent.solutions.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Solutions Grid */}
|
||||
<section className="relative z-10 px-6 py-20">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid md:grid-cols-2 gap-12">
|
||||
{currentContent.solutions.items.map((solution, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-8 border border-cyan-500/20 hover:border-cyan-400/50 transition-all duration-300 group"
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-lg flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<span className="text-white font-bold text-lg">
|
||||
{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-2xl font-bold mb-4 text-cyan-400">
|
||||
{solution.title}
|
||||
</h3>
|
||||
<p className="text-gray-300 mb-6 leading-relaxed">
|
||||
{solution.description}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{solution.benefits.map((benefit, benefitIndex) => (
|
||||
<div
|
||||
key={benefitIndex}
|
||||
className="flex items-center text-gray-400"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 text-cyan-400 mr-2 flex-shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm">{benefit}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Process Section */}
|
||||
<section className="relative z-10 px-6 py-20 bg-gray-800/30">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
|
||||
实施流程
|
||||
</h2>
|
||||
<p className="text-xl text-gray-300">专业的实施流程,确保项目成功交付</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-4 gap-8">
|
||||
{[
|
||||
{ title: '需求分析', description: '深入了解客户需求和现状' },
|
||||
{ title: '方案设计', description: '制定详细的技术实施方案' },
|
||||
{ title: '项目实施', description: '按计划执行迁移和部署' },
|
||||
{ title: '运维支持', description: '提供持续的运维和优化' },
|
||||
].map((step: { title: string; description: string }, index: number) => (
|
||||
<div key={index} className="text-center">
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<span className="text-white font-bold text-xl">
|
||||
{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-2 text-cyan-400">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-gray-400">{step.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer content={currentContent} createLocalizedPath={createLocalizedPath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 175 KiB |
104
app/globals.css
Normal file
104
app/globals.css
Normal file
@ -0,0 +1,104 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
13
app/layout.tsx
Normal file
13
app/layout.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
export const metadata: Metadata = {
|
||||
title: 'My New App',
|
||||
description: 'Generated by Onlook',
|
||||
};
|
||||
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
34
app/page.tsx
Normal file
34
app/page.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function RootPage() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// 检测浏览器语言并重定向到相应的语言路径
|
||||
const browserLang = navigator.language;
|
||||
let detectedLang = 'zh-CN';
|
||||
|
||||
if (browserLang.startsWith('zh-TW') || browserLang.startsWith('zh-HK')) {
|
||||
detectedLang = 'zh-TW';
|
||||
} else if (browserLang.startsWith('zh')) {
|
||||
detectedLang = 'zh-CN';
|
||||
} else {
|
||||
detectedLang = 'en';
|
||||
}
|
||||
|
||||
router.replace(`/${detectedLang}`);
|
||||
}, [router]);
|
||||
|
||||
// 显示加载状态
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 border-4 border-cyan-400 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
||||
<p className="text-gray-300">正在加载...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
87
app/sitemap.xml/route.ts
Normal file
87
app/sitemap.xml/route.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml-stylesheet type="text/xsl" href="/sitemap.xsl"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<!-- Default language pages -->
|
||||
<url>
|
||||
<loc>http://localhost:3000/zh-CN</loc>
|
||||
<xhtml:link rel="alternate" hreflang="zh-CN" href="http://localhost:3000/zh-CN"/>
|
||||
<xhtml:link rel="alternate" hreflang="zh-TW" href="http://localhost:3000/zh-TW"/>
|
||||
<xhtml:link rel="alternate" hreflang="en" href="http://localhost:3000/en"/>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
|
||||
<!-- About pages -->
|
||||
<url>
|
||||
<loc>http://localhost:3000/zh-CN/about</loc>
|
||||
<xhtml:link rel="alternate" hreflang="zh-CN" href="http://localhost:3000/zh-CN/about"/>
|
||||
<xhtml:link rel="alternate" hreflang="zh-TW" href="http://localhost:3000/zh-TW/about"/>
|
||||
<xhtml:link rel="alternate" hreflang="en" href="http://localhost:3000/en/about"/>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
|
||||
<!-- Services pages -->
|
||||
<url>
|
||||
<loc>http://localhost:3000/zh-CN/services</loc>
|
||||
<xhtml:link rel="alternate" hreflang="zh-CN" href="http://localhost:3000/zh-CN/services"/>
|
||||
<xhtml:link rel="alternate" hreflang="zh-TW" href="http://localhost:3000/zh-TW/services"/>
|
||||
<xhtml:link rel="alternate" hreflang="en" href="http://localhost:3000/en/services"/>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
|
||||
<!-- Solutions pages -->
|
||||
<url>
|
||||
<loc>http://localhost:3000/zh-CN/solutions</loc>
|
||||
<xhtml:link rel="alternate" hreflang="zh-CN" href="http://localhost:3000/zh-CN/solutions"/>
|
||||
<xhtml:link rel="alternate" hreflang="zh-TW" href="http://localhost:3000/zh-TW/solutions"/>
|
||||
<xhtml:link rel="alternate" hreflang="en" href="http://localhost:3000/en/solutions"/>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
|
||||
<!-- Pricing pages -->
|
||||
<url>
|
||||
<loc>http://localhost:3000/zh-CN/pricing</loc>
|
||||
<xhtml:link rel="alternate" hreflang="zh-CN" href="http://localhost:3000/zh-CN/pricing"/>
|
||||
<xhtml:link rel="alternate" hreflang="zh-TW" href="http://localhost:3000/zh-TW/pricing"/>
|
||||
<xhtml:link rel="alternate" hreflang="en" href="http://localhost:3000/en/pricing"/>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
|
||||
<!-- News pages -->
|
||||
<url>
|
||||
<loc>http://localhost:3000/zh-CN/news</loc>
|
||||
<xhtml:link rel="alternate" hreflang="zh-CN" href="http://localhost:3000/zh-CN/news"/>
|
||||
<xhtml:link rel="alternate" hreflang="zh-TW" href="http://localhost:3000/zh-TW/news"/>
|
||||
<xhtml:link rel="alternate" hreflang="en" href="http://localhost:3000/en/news"/>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
|
||||
<!-- Contact pages -->
|
||||
<url>
|
||||
<loc>http://localhost:3000/zh-CN/contact</loc>
|
||||
<xhtml:link rel="alternate" hreflang="zh-CN" href="http://localhost:3000/zh-CN/contact"/>
|
||||
<xhtml:link rel="alternate" hreflang="zh-TW" href="http://localhost:3000/zh-TW/contact"/>
|
||||
<xhtml:link rel="alternate" hreflang="en" href="http://localhost:3000/en/contact"/>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
|
||||
</urlset>`;
|
||||
|
||||
return new NextResponse(sitemap, {
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
'Cache-Control': 'public, max-age=3600, s-maxage=3600',
|
||||
},
|
||||
});
|
||||
}
|
||||
20
components.json
Normal file
20
components.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
277
components/Footer.tsx
Normal file
277
components/Footer.tsx
Normal file
@ -0,0 +1,277 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface FooterProps {
|
||||
content: {
|
||||
footer: {
|
||||
company: {
|
||||
name: string;
|
||||
description: string;
|
||||
slogan: string;
|
||||
};
|
||||
social: {
|
||||
title: string;
|
||||
links: {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: ReactNode;
|
||||
}[];
|
||||
};
|
||||
quickLinks: {
|
||||
title: string;
|
||||
links: {
|
||||
name: string;
|
||||
href: string;
|
||||
}[];
|
||||
};
|
||||
services: {
|
||||
title: string;
|
||||
items: string[];
|
||||
};
|
||||
contact: {
|
||||
title: string;
|
||||
address: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
hours: string;
|
||||
};
|
||||
newsletter: {
|
||||
title: string;
|
||||
description: string;
|
||||
placeholder: string;
|
||||
button: string;
|
||||
};
|
||||
certifications: {
|
||||
title: string;
|
||||
items: string[];
|
||||
};
|
||||
copyright: string;
|
||||
icp: string;
|
||||
legal: {
|
||||
links: {
|
||||
name: string;
|
||||
href: string;
|
||||
}[];
|
||||
};
|
||||
sitemap: string;
|
||||
};
|
||||
};
|
||||
createLocalizedPath: (path: string) => string;
|
||||
}
|
||||
|
||||
export default function Footer({ content, createLocalizedPath }: FooterProps) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [isSubscribed, setIsSubscribed] = useState(false);
|
||||
|
||||
const handleSubscribe = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (email) {
|
||||
setIsSubscribed(true);
|
||||
setEmail('');
|
||||
setTimeout(() => setIsSubscribed(false), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="relative z-10 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white">
|
||||
{/* 主体内容 */}
|
||||
<div className="max-w-7xl mx-auto px-6 py-16">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12">
|
||||
{/* 公司信息 */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-2xl font-bold bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent mb-3">
|
||||
{content.footer.company.name}
|
||||
</h3>
|
||||
<p className="text-gray-300 text-sm leading-relaxed mb-4">
|
||||
{content.footer.company.description}
|
||||
</p>
|
||||
<p className="text-cyan-400 font-medium text-sm italic">
|
||||
{content.footer.company.slogan}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold mb-4 text-gray-200">
|
||||
{content.footer.social.title}
|
||||
</h4>
|
||||
<div className="flex space-x-4 mb-6">
|
||||
{content.footer.social.links.map((social, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
href={social.href}
|
||||
className="w-10 h-10 bg-slate-700 hover:bg-gradient-to-r hover:from-cyan-500 hover:to-blue-500 rounded-lg flex items-center justify-center transition-all duration-300 transform hover:scale-110 hover:shadow-lg"
|
||||
title={social.name}
|
||||
>
|
||||
<span className="text-lg">{social.icon}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 快速链接 */}
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold mb-6 text-gray-200">
|
||||
{content.footer.quickLinks.title}
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
{content.footer.quickLinks.links.map((link, idx) => (
|
||||
<li key={idx}>
|
||||
<a
|
||||
href={createLocalizedPath(link.href)}
|
||||
className="text-gray-400 hover:text-cyan-400 transition-colors duration-200 text-sm flex items-center group"
|
||||
>
|
||||
<span className="w-1 h-1 bg-cyan-400 rounded-full mr-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200" />
|
||||
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 核心服务 */}
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold mb-6 text-gray-200">
|
||||
{content.footer.services.title}
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
{content.footer.services.items.map((service, idx) => (
|
||||
<li key={idx} className="text-gray-400 text-sm flex items-center">
|
||||
<span className="w-1 h-1 bg-blue-400 rounded-full mr-3" />
|
||||
|
||||
{service}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 联系信息 & 订阅 */}
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold mb-6 text-gray-200">
|
||||
{content.footer.contact.title}
|
||||
</h4>
|
||||
<div className="space-y-4 mb-8">
|
||||
<div className="flex items-start space-x-3">
|
||||
<span className="text-cyan-400 mt-1">📍</span>
|
||||
<span className="text-gray-400 text-sm">
|
||||
{content.footer.contact.address}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-cyan-400">📞</span>
|
||||
<a
|
||||
href={`tel:${content.footer.contact.phone}`}
|
||||
className="text-gray-400 hover:text-cyan-400 transition-colors text-sm"
|
||||
>
|
||||
{content.footer.contact.phone}
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-cyan-400">✉️</span>
|
||||
<a
|
||||
href={`mailto:${content.footer.contact.email}`}
|
||||
className="text-gray-400 hover:text-cyan-400 transition-colors text-sm"
|
||||
>
|
||||
{content.footer.contact.email}
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-cyan-400">🕒</span>
|
||||
<span className="text-gray-400 text-sm">
|
||||
{content.footer.contact.hours}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-md font-semibold mb-3 text-gray-200">
|
||||
{content.footer.newsletter.title}
|
||||
</h5>
|
||||
<p className="text-gray-400 text-xs mb-4">
|
||||
{content.footer.newsletter.description}
|
||||
</p>
|
||||
<form onSubmit={handleSubscribe} className="space-y-3">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder={content.footer.newsletter.placeholder}
|
||||
className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-cyan-400 focus:ring-1 focus:ring-cyan-400 text-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-gradient-to-r from-cyan-500 to-blue-500 hover:from-cyan-600 hover:to-blue-600 text-white py-2 px-4 rounded-lg transition-all duration-200 transform hover:scale-105 text-sm font-medium"
|
||||
>
|
||||
{isSubscribed
|
||||
? `✓ ${content.footer.newsletter.button}`
|
||||
: content.footer.newsletter.button}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 资质认证 */}
|
||||
<div className="mt-12 pt-8 border-t border-slate-700">
|
||||
<h4 className="text-lg font-semibold mb-6 text-gray-200 text-center">
|
||||
{content.footer.certifications.title}
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{content.footer.certifications.items.map((cert, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-slate-800 rounded-lg p-4 text-center hover:bg-slate-700 transition-colors duration-200"
|
||||
>
|
||||
<div className="text-2xl mb-2">🏆</div>
|
||||
<span className="text-gray-300 text-xs font-medium">{cert}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部版权 & 法律链接 */}
|
||||
<div className="border-t border-slate-700 bg-slate-900">
|
||||
<div className="max-w-7xl mx-auto px-6 py-6">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
|
||||
<div className="text-center md:text-left">
|
||||
<p className="text-gray-400 text-sm">{content.footer.copyright}</p>
|
||||
<p className="text-gray-500 text-xs mt-1">{content.footer.icp}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-center md:justify-end space-x-6">
|
||||
{content.footer.legal.links.map((link, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
href={link.href}
|
||||
className="text-gray-400 hover:text-cyan-400 transition-colors duration-200 text-xs"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href="/sitemap.xml"
|
||||
className="text-gray-400 hover:text-cyan-400 transition-colors duration-200 text-xs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{content.footer.sitemap}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 装饰背景 */}
|
||||
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-10 left-10 w-32 h-32 bg-cyan-500/5 rounded-full blur-xl" />
|
||||
<div className="absolute bottom-10 right-10 w-40 h-40 bg-blue-500/5 rounded-full blur-xl" />
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-purple-500/3 rounded-full blur-2xl" />
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
156
components/Navigation.tsx
Normal file
156
components/Navigation.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { LanguageType } from '../lib/content';
|
||||
|
||||
interface NavigationProps {
|
||||
/** 🔧 修改 #1:currentLang 从 string 改为 LanguageType */
|
||||
currentLang: LanguageType;
|
||||
|
||||
/** 🔧 修改 #2:setCurrentLang 返回值改为 void(原来写成了 string),签名也保持只接受 LanguageType */
|
||||
setCurrentLang: (lang: LanguageType) => void;
|
||||
|
||||
content: {
|
||||
nav: {
|
||||
home: string;
|
||||
services: string;
|
||||
solutions: string;
|
||||
pricing: string;
|
||||
news: string;
|
||||
contact: string;
|
||||
};
|
||||
};
|
||||
|
||||
/** 🔧 修改 #3:createLocalizedPath 的签名改为 (path: string) => string */
|
||||
createLocalizedPath?: (path: string) => string;
|
||||
}
|
||||
|
||||
export default function Navigation({
|
||||
currentLang,
|
||||
setCurrentLang,
|
||||
content,
|
||||
createLocalizedPath,
|
||||
}: NavigationProps) {
|
||||
const pathname = usePathname();
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
// Helper to build localized URLs
|
||||
const getLocalizedPath = (path: string) => {
|
||||
return createLocalizedPath ? createLocalizedPath(path) : path;
|
||||
};
|
||||
|
||||
const navItems = [
|
||||
{ label: content.nav.home, href: getLocalizedPath('/') },
|
||||
{ label: content.nav.services, href: getLocalizedPath('/services') },
|
||||
{ label: content.nav.solutions, href: getLocalizedPath('/solutions') },
|
||||
{ label: content.nav.pricing, href: getLocalizedPath('/pricing') },
|
||||
{ label: content.nav.news, href: getLocalizedPath('/news') },
|
||||
{ label: content.nav.contact, href: getLocalizedPath('/contact') },
|
||||
];
|
||||
|
||||
return (
|
||||
<nav className="relative z-10 px-6 py-4 border-b border-cyan-500/20 backdrop-blur-sm">
|
||||
<div className="max-w-7xl mx-auto flex justify-between items-center">
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
className="text-2xl font-bold bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent hover:scale-105 transition-transform duration-300"
|
||||
>
|
||||
CloudAgency
|
||||
</Link>
|
||||
|
||||
{/* Desktop Nav */}
|
||||
<div className="hidden md:flex space-x-8">
|
||||
{navItems.map((item, idx) => {
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={idx}
|
||||
href={item.href}
|
||||
className={`relative group transition-colors duration-300 ${
|
||||
isActive ? 'text-cyan-400' : 'text-gray-300 hover:text-cyan-400'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
<span
|
||||
className={`absolute -bottom-1 left-0 h-0.5 bg-cyan-400 transition-all duration-300 ${
|
||||
isActive ? 'w-full' : 'w-0 group-hover:w-full'
|
||||
}`}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Right side */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* 🔧 修改 #4:onChange 时将 value 强制断言为 LanguageType */}
|
||||
<select
|
||||
value={currentLang}
|
||||
onChange={(e) => setCurrentLang(e.target.value as LanguageType)}
|
||||
className="bg-gray-800 border border-cyan-500/30 rounded-lg px-3 py-1 text-sm focus:outline-none focus:border-cyan-400 transition-colors"
|
||||
>
|
||||
<option value="zh-CN">简体中文</option>
|
||||
<option value="zh-TW">繁體中文</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
className="md:hidden text-gray-300 hover:text-cyan-400 transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen((o) => !o)}
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
{isMobileMenuOpen ? (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
) : (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{isMobileMenuOpen && (
|
||||
<div className="md:hidden absolute top-full left-0 right-0 bg-gray-900/95 backdrop-blur-sm border-b border-cyan-500/20">
|
||||
<div className="px-6 py-4 space-y-4">
|
||||
{navItems.map((item, idx) => {
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={idx}
|
||||
href={item.href}
|
||||
className={`block py-2 transition-colors duration-300 ${
|
||||
isActive
|
||||
? 'text-cyan-400'
|
||||
: 'text-gray-300 hover:text-cyan-400'
|
||||
}`}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
49
hooks/useLanguage.ts
Normal file
49
hooks/useLanguage.ts
Normal file
@ -0,0 +1,49 @@
|
||||
// hooks/useLanguage.tsx
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { content, LanguageType, ContentType } from '../lib/content';
|
||||
|
||||
export function useLanguage(
|
||||
initialLang: string
|
||||
): {
|
||||
currentLang: LanguageType;
|
||||
setCurrentLang: (newLang: LanguageType) => void;
|
||||
createLocalizedPath: (path: string) => string;
|
||||
currentContent: ContentType;
|
||||
} {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const SUPPORTED = ['en', 'zh-CN', 'zh-TW'] as const;
|
||||
const normalized = (SUPPORTED.includes(initialLang as any)
|
||||
? initialLang
|
||||
: 'en') as LanguageType;
|
||||
|
||||
const [currentLang, setLang] = useState<LanguageType>(normalized);
|
||||
useEffect(() => {
|
||||
setLang(normalized);
|
||||
}, [normalized]);
|
||||
|
||||
const setCurrentLang = (newLang: LanguageType) => {
|
||||
const parts = pathname.split('/').filter(Boolean);
|
||||
const rest = parts.slice(1).join('/');
|
||||
router.push(`/${newLang}${rest ? '/' + rest : ''}`);
|
||||
};
|
||||
|
||||
const createLocalizedPath = (path: string) =>
|
||||
path.startsWith('/')
|
||||
? `/${currentLang}${path}`
|
||||
: `/${currentLang}/${path}`;
|
||||
|
||||
// —— 关键:从 content 里取出当前语言的数据 ——
|
||||
const currentContent: ContentType = content[currentLang];
|
||||
|
||||
return {
|
||||
currentLang,
|
||||
setCurrentLang,
|
||||
createLocalizedPath,
|
||||
currentContent,
|
||||
};
|
||||
}
|
||||
1326
lib/content copy.ts
Normal file
1326
lib/content copy.ts
Normal file
File diff suppressed because it is too large
Load Diff
1326
lib/content.ts
Normal file
1326
lib/content.ts
Normal file
File diff suppressed because it is too large
Load Diff
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
10
next.config.mjs
Normal file
10
next.config.mjs
Normal file
@ -0,0 +1,10 @@
|
||||
import path from 'path';
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
// 把 .next 目录改成 build/.next
|
||||
distDir: 'build',
|
||||
// 如果你希望所有页面都带尾部斜杠(可选)
|
||||
trailingSlash: true,
|
||||
};
|
||||
export default nextConfig;
|
||||
5582
package-lock.json
generated
Normal file
5582
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@onlook/next-template",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.438.0",
|
||||
"next": "14.2.23",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "^15.1.6",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.3.3",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
8
postcss.config.mjs
Normal file
8
postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
BIN
public/images/logo-text.png
Normal file
BIN
public/images/logo-text.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
348
public/sitemap.xsl
Normal file
348
public/sitemap.xsl
Normal file
@ -0,0 +1,348 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
|
||||
|
||||
<xsl:template match="/">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<title>网站地图 - Sitemap</title>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
||||
color: white;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="75" cy="75" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="50" cy="10" r="0.5" fill="rgba(255,255,255,0.05)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
|
||||
animation: float 20s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateX(0px) translateY(0px); }
|
||||
50% { transform: translateX(-20px) translateY(-10px); }
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.9;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 30px;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||
min-width: 120px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #4f46e5;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #64748b;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.url-list {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.url-item {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.url-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
||||
transform: scaleY(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.url-item:hover {
|
||||
transform: translateX(10px);
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
|
||||
.url-item:hover::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.url-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.url-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 15px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.url-link {
|
||||
color: #4f46e5;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.url-link:hover {
|
||||
color: #7c3aed;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.url-meta {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.priority-high { background: #fef3c7; color: #d97706; }
|
||||
.priority-medium { background: #dbeafe; color: #2563eb; }
|
||||
.priority-low { background: #f3f4f6; color: #6b7280; }
|
||||
|
||||
.lang-links {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lang-link {
|
||||
padding: 6px 12px;
|
||||
background: #e0e7ff;
|
||||
color: #4338ca;
|
||||
text-decoration: none;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.lang-link:hover {
|
||||
background: #4338ca;
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(67, 56, 202, 0.3);
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #60a5fa;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header h1 { font-size: 2rem; }
|
||||
.stats { flex-direction: column; gap: 15px; }
|
||||
.url-meta { flex-direction: column; gap: 10px; }
|
||||
.content { padding: 20px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🗺️ 网站地图</h1>
|
||||
<p>探索我们网站的所有页面和多语言版本</p>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number"><xsl:value-of select="count(//sitemap:url)"/></span>
|
||||
<div class="stat-label">总页面数</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">3</span>
|
||||
<div class="stat-label">支持语言</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number"><xsl:value-of select="count(//sitemap:url[sitemap:priority='1.0'])"/></span>
|
||||
<div class="stat-label">高优先级</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number"><xsl:value-of select="count(//sitemap:url[sitemap:changefreq='daily'])"/></span>
|
||||
<div class="stat-label">每日更新</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="url-list">
|
||||
<xsl:for-each select="sitemap:urlset/sitemap:url">
|
||||
<div class="url-item">
|
||||
<div class="url-main">
|
||||
<div class="url-icon">
|
||||
<xsl:choose>
|
||||
<xsl:when test="contains(sitemap:loc, '/about')">👥</xsl:when>
|
||||
<xsl:when test="contains(sitemap:loc, '/services')">🛠️</xsl:when>
|
||||
<xsl:when test="contains(sitemap:loc, '/solutions')">💡</xsl:when>
|
||||
<xsl:when test="contains(sitemap:loc, '/pricing')">💰</xsl:when>
|
||||
<xsl:when test="contains(sitemap:loc, '/news')">📰</xsl:when>
|
||||
<xsl:when test="contains(sitemap:loc, '/contact')">📞</xsl:when>
|
||||
<xsl:otherwise>🏠</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</div>
|
||||
<a href="{sitemap:loc}" class="url-link" target="_blank">
|
||||
<xsl:value-of select="sitemap:loc"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="url-meta">
|
||||
<div class="meta-item">
|
||||
<span>📅</span>
|
||||
<span>更新频率: <xsl:value-of select="sitemap:changefreq"/></span>
|
||||
</div>
|
||||
<div>
|
||||
<xsl:attribute name="class">
|
||||
meta-item
|
||||
<xsl:choose>
|
||||
<xsl:when test="sitemap:priority >= 0.8"> priority-high</xsl:when>
|
||||
<xsl:when test="sitemap:priority >= 0.7"> priority-medium</xsl:when>
|
||||
<xsl:otherwise> priority-low</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:attribute>
|
||||
<span>⭐</span>
|
||||
<span>优先级: <xsl:value-of select="sitemap:priority"/></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lang-links">
|
||||
<xsl:for-each select="xhtml:link">
|
||||
<a href="{@href}" class="lang-link" target="_blank">
|
||||
<xsl:choose>
|
||||
<xsl:when test="@hreflang='zh-CN'">🇨🇳 简体中文</xsl:when>
|
||||
<xsl:when test="@hreflang='zh-TW'">🇹🇼 繁體中文</xsl:when>
|
||||
<xsl:when test="@hreflang='en'">🇺🇸 English</xsl:when>
|
||||
<xsl:otherwise><xsl:value-of select="@hreflang"/></xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</a>
|
||||
</xsl:for-each>
|
||||
</div>
|
||||
</div>
|
||||
</xsl:for-each>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>此站点地图包含了网站的所有公开页面 | 生成时间: <script>document.write(new Date().toLocaleString('zh-CN'))</script></p>
|
||||
<p>支持多语言访问,点击语言标签可切换到对应版本</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
67
tailwind.config.ts
Normal file
67
tailwind.config.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))'
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
'1': 'hsl(var(--chart-1))',
|
||||
'2': 'hsl(var(--chart-2))',
|
||||
'3': 'hsl(var(--chart-3))',
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
};
|
||||
export default config;
|
||||
40
tsconfig.json
Normal file
40
tsconfig.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"build/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user