Init
This commit is contained in:
29
.eslintrc.cjs
Normal file
29
.eslintrc.cjs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'@typescript-eslint/recommended',
|
||||||
|
'prettier'
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
extraFileExtensions: ['.svelte']
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2017: true,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.svelte'],
|
||||||
|
parser: 'svelte-eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@typescript-eslint/parser'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
.svelte-kit/
|
||||||
|
.vite/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.log
|
||||||
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
|
}
|
||||||
290
README.md
Normal file
290
README.md
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
# My App Store
|
||||||
|
|
||||||
|
一个基于苹果 App Store 源码架构构建的现代化前端应用,使用 SvelteKit + TypeScript 开发。
|
||||||
|
|
||||||
|
## ✨ 特性
|
||||||
|
|
||||||
|
- 🎨 **现代化 UI 设计** - 基于苹果设计语言的组件系统
|
||||||
|
- 🌍 **多语言支持** - 内置国际化系统,支持 8 种语言
|
||||||
|
- 🎭 **主题系统** - 支持亮色/暗色主题,多种颜色预设
|
||||||
|
- ♿ **无障碍优先** - 完整的 ARIA 支持和键盘导航
|
||||||
|
- 📱 **响应式设计** - 适配所有设备尺寸
|
||||||
|
- ⚡ **高性能** - 基于 Svelte 的极致性能优化
|
||||||
|
- 🔧 **TypeScript** - 完整的类型安全
|
||||||
|
- 🧩 **模块化架构** - 可复用的组件和工具库
|
||||||
|
|
||||||
|
## 🏗️ 架构设计
|
||||||
|
|
||||||
|
### 核心系统
|
||||||
|
|
||||||
|
- **路由系统** - 基于 SvelteKit 的文件路由 + 自定义路由状态管理
|
||||||
|
- **状态管理** - Svelte stores + 自定义状态管理器
|
||||||
|
- **主题系统** - CSS 变量 + 动态主题切换
|
||||||
|
- **国际化** - 基于 Intl API 的完整 i18n 解决方案
|
||||||
|
- **组件库** - 从苹果源码提取并现代化改造的组件系统
|
||||||
|
|
||||||
|
### 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── lib/
|
||||||
|
│ ├── components/ # 组件库
|
||||||
|
│ │ └── base/ # 基础组件 (Button, Modal, etc.)
|
||||||
|
│ ├── stores/ # 状态管理
|
||||||
|
│ │ ├── router.ts # 路由状态
|
||||||
|
│ │ └── theme.ts # 主题状态
|
||||||
|
│ ├── i18n/ # 国际化
|
||||||
|
│ ├── styles/ # 样式系统
|
||||||
|
│ │ └── variables.scss # 设计系统变量
|
||||||
|
│ └── utils/ # 工具函数
|
||||||
|
├── routes/ # 页面路由
|
||||||
|
│ ├── +layout.svelte # 全局布局
|
||||||
|
│ └── +page.svelte # 首页
|
||||||
|
└── app.html # HTML 模板
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- npm 或 pnpm
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd my-app-store
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 开发模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
访问 http://localhost:5173
|
||||||
|
|
||||||
|
### 构建生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预览生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 组件系统
|
||||||
|
|
||||||
|
### Button 组件
|
||||||
|
|
||||||
|
支持多种样式变体和尺寸:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<Button variant="primary" size="lg">
|
||||||
|
<span slot="icon-before">🚀</span>
|
||||||
|
Get Started
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**变体类型:**
|
||||||
|
- `primary` - 主要按钮
|
||||||
|
- `secondary` - 次要按钮
|
||||||
|
- `tertiary` - 第三级按钮
|
||||||
|
- `pill` - 药丸按钮
|
||||||
|
- `text` - 文本按钮
|
||||||
|
- `alert` - 警告按钮
|
||||||
|
|
||||||
|
**尺寸:**
|
||||||
|
- `sm` - 小尺寸 (32px)
|
||||||
|
- `md` - 中等尺寸 (40px)
|
||||||
|
- `lg` - 大尺寸 (48px)
|
||||||
|
|
||||||
|
### Modal 组件
|
||||||
|
|
||||||
|
功能完整的模态框组件:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<Modal size="md" ariaLabel="Settings Modal">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
<p>Modal content here...</p>
|
||||||
|
</Modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
**特性:**
|
||||||
|
- 焦点陷阱和键盘导航
|
||||||
|
- 多种尺寸选项
|
||||||
|
- 自动 polyfill 支持
|
||||||
|
- 无障碍友好
|
||||||
|
|
||||||
|
## 🌍 国际化
|
||||||
|
|
||||||
|
### 支持的语言
|
||||||
|
|
||||||
|
- 🇺🇸 English
|
||||||
|
- 🇨🇳 中文
|
||||||
|
- 🇯🇵 日本語
|
||||||
|
- 🇰🇷 한국어
|
||||||
|
- 🇪🇸 Español
|
||||||
|
- 🇫🇷 Français
|
||||||
|
- 🇩🇪 Deutsch
|
||||||
|
- 🇸🇦 العربية
|
||||||
|
|
||||||
|
### 使用翻译
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
import { t } from '~/lib/i18n';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>{$t('navigation.home')}</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新语言
|
||||||
|
|
||||||
|
1. 在 `src/lib/i18n/index.ts` 中添加语言配置
|
||||||
|
2. 添加对应的翻译内容
|
||||||
|
3. 更新 `supportedLocales` 数组
|
||||||
|
|
||||||
|
## 🎭 主题系统
|
||||||
|
|
||||||
|
### 预设主题
|
||||||
|
|
||||||
|
- Apple - 苹果蓝
|
||||||
|
- Google - 谷歌蓝
|
||||||
|
- GitHub - 深色主题
|
||||||
|
- Spotify - 绿色主题
|
||||||
|
|
||||||
|
### 自定义主题
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { theme } from '~/lib/stores/theme';
|
||||||
|
|
||||||
|
// 设置主色调
|
||||||
|
theme.setPrimaryColor('#FF6B6B');
|
||||||
|
|
||||||
|
// 应用预设
|
||||||
|
theme.applyPreset('spotify');
|
||||||
|
|
||||||
|
// 切换主题模式
|
||||||
|
theme.setMode('dark');
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS 变量
|
||||||
|
|
||||||
|
主题系统基于 CSS 变量,支持动态切换:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
.my-component {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: var(--color-gray-900);
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 响应式设计
|
||||||
|
|
||||||
|
使用移动优先的响应式设计:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
.component {
|
||||||
|
// 移动端样式
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
// 平板和桌面样式
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ♿ 无障碍支持
|
||||||
|
|
||||||
|
- 完整的 ARIA 标签
|
||||||
|
- 键盘导航支持
|
||||||
|
- 焦点管理
|
||||||
|
- 屏幕阅读器友好
|
||||||
|
- 高对比度模式
|
||||||
|
- 减少动画选项
|
||||||
|
|
||||||
|
## 🔧 开发工具
|
||||||
|
|
||||||
|
### 代码检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### 类型检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码格式化
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run format
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 部署
|
||||||
|
|
||||||
|
### Vercel
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i -g vercel
|
||||||
|
vercel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Netlify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
# 上传 build 目录
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM node:18-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "build"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 贡献指南
|
||||||
|
|
||||||
|
1. Fork 项目
|
||||||
|
2. 创建特性分支 (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. 提交更改 (`git commit -m 'Add amazing feature'`)
|
||||||
|
4. 推送到分支 (`git push origin feature/amazing-feature`)
|
||||||
|
5. 开启 Pull Request
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
本项目基于 MIT 许可证开源。
|
||||||
|
|
||||||
|
## 🙏 致谢
|
||||||
|
|
||||||
|
- 感谢苹果公司提供的优秀设计灵感
|
||||||
|
- 感谢 Svelte 团队的出色框架
|
||||||
|
- 感谢所有开源贡献者
|
||||||
|
|
||||||
|
## 📞 联系方式
|
||||||
|
|
||||||
|
如有问题或建议,请通过以下方式联系:
|
||||||
|
|
||||||
|
- 创建 Issue
|
||||||
|
- 发送邮件
|
||||||
|
- 社交媒体
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**注意:** 本项目仅用于学习和研究目的,不用于商业用途。所有设计灵感来源于公开可访问的资源。
|
||||||
4462
package-lock.json
generated
Normal file
4462
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "my-app-store",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"dev": "vite dev",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||||
|
"format": "prettier --plugin-search-dir . --write ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "^2.0.0",
|
||||||
|
"@sveltejs/kit": "^1.20.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"eslint": "^8.28.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-svelte": "^2.30.0",
|
||||||
|
"prettier": "^2.8.0",
|
||||||
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
|
"sass": "^1.69.5",
|
||||||
|
"svelte": "^4.0.5",
|
||||||
|
"svelte-check": "^3.4.3",
|
||||||
|
"tslib": "^2.4.1",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^4.4.2"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"svelte-i18n": "^3.7.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/app.d.ts
vendored
Normal file
12
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
12
src/app.html
Normal file
12
src/app.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" %sveltekit.theme%>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover" %sveltekit.theme%>
|
||||||
|
<div style="display: contents" %sveltekit.theme%>%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
309
src/lib/components/base/Button.svelte
Normal file
309
src/lib/components/base/Button.svelte
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
import { makeSafeTick } from '$lib/utils/makeSafeTick';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const handleButtonClick = () => {
|
||||||
|
dispatch('buttonClick');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 按钮类型定义 - 基于苹果设计规范简化
|
||||||
|
type ButtonType =
|
||||||
|
| 'primary' // 原 buttonA
|
||||||
|
| 'secondary' // 原 buttonB
|
||||||
|
| 'tertiary' // 原 buttonD
|
||||||
|
| 'alert' // 原 alertButton
|
||||||
|
| 'alert-secondary' // 原 alertButtonSecondary
|
||||||
|
| 'pill' // 原 pillButton
|
||||||
|
| 'text' // 原 textButton
|
||||||
|
| 'social' // 原 socialProfileButton
|
||||||
|
| null;
|
||||||
|
|
||||||
|
export let variant: ButtonType = 'primary';
|
||||||
|
export let makeFocused = false;
|
||||||
|
export let ariaLabel: string | null = null;
|
||||||
|
export let type: 'button' | 'submit' = 'button';
|
||||||
|
export let disabled = false;
|
||||||
|
export let buttonElement: HTMLButtonElement | null = null;
|
||||||
|
export let size: 'sm' | 'md' | 'lg' = 'md';
|
||||||
|
export let fullWidth = false;
|
||||||
|
|
||||||
|
function handleKeyUp(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter' || e.key === 'Escape') {
|
||||||
|
handleButtonClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeTick = makeSafeTick();
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await safeTick(async (tick) => {
|
||||||
|
await tick();
|
||||||
|
if (makeFocused && buttonElement) {
|
||||||
|
buttonElement.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="button-wrapper"
|
||||||
|
class:primary={variant === 'primary'}
|
||||||
|
class:secondary={variant === 'secondary'}
|
||||||
|
class:tertiary={variant === 'tertiary'}
|
||||||
|
class:alert={variant === 'alert'}
|
||||||
|
class:alert-secondary={variant === 'alert-secondary'}
|
||||||
|
class:pill={variant === 'pill'}
|
||||||
|
class:text={variant === 'text'}
|
||||||
|
class:social={variant === 'social'}
|
||||||
|
class:size-sm={size === 'sm'}
|
||||||
|
class:size-md={size === 'md'}
|
||||||
|
class:size-lg={size === 'lg'}
|
||||||
|
class:full-width={fullWidth}
|
||||||
|
data-testid="button-wrapper"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
on:click={handleButtonClick}
|
||||||
|
data-testid="button"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
bind:this={buttonElement}
|
||||||
|
on:keyup={handleKeyUp}
|
||||||
|
class:link={variant === 'text'}
|
||||||
|
{type}
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
{#if $$slots['icon-before']}
|
||||||
|
<div class="button__icon button__icon--before">
|
||||||
|
<slot name="icon-before" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<slot />
|
||||||
|
{#if $$slots['icon-after']}
|
||||||
|
<div class="button__icon button__icon--after">
|
||||||
|
<slot name="icon-after" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.button-wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
&.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
font-family: var(--font-family-primary);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
opacity: var(--buttonDisabledOpacity, 0.5);
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
opacity: var(--buttonDisabledOpacityDark, 1);
|
||||||
|
background-color: var(--buttonDisabledBGColorDark, rgba(255, 255, 255, 0.5));
|
||||||
|
color: var(--buttonDisabledTextColorDark, var(--color-gray-400));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尺寸变体
|
||||||
|
.size-sm button {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-md button {
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-lg button {
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 24px;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主要按钮样式
|
||||||
|
.primary button {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover:not([disabled]) {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 90%, black);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not([disabled]) {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 80%, black);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 次要按钮样式
|
||||||
|
.secondary button {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--color-primary);
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
|
||||||
|
&:hover:not([disabled]) {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not([disabled]) {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 20%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第三级按钮样式
|
||||||
|
.tertiary button {
|
||||||
|
background-color: var(--color-gray-100);
|
||||||
|
color: var(--color-gray-700);
|
||||||
|
|
||||||
|
&:hover:not([disabled]) {
|
||||||
|
background-color: var(--color-gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not([disabled]) {
|
||||||
|
background-color: var(--color-gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background-color: var(--color-gray-700);
|
||||||
|
color: var(--color-gray-200);
|
||||||
|
|
||||||
|
&:hover:not([disabled]) {
|
||||||
|
background-color: var(--color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not([disabled]) {
|
||||||
|
background-color: var(--color-gray-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 警告按钮样式
|
||||||
|
.alert button {
|
||||||
|
background-color: var(--color-error);
|
||||||
|
color: white;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
|
||||||
|
&:hover:not([disabled]) {
|
||||||
|
background-color: color-mix(in srgb, var(--color-error) 90%, black);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-secondary button {
|
||||||
|
background-color: var(--color-gray-100);
|
||||||
|
color: var(--color-error);
|
||||||
|
|
||||||
|
&:hover:not([disabled]) {
|
||||||
|
background-color: var(--color-gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background-color: var(--color-gray-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 药丸按钮样式
|
||||||
|
.pill button {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-radius: var(--border-radius-full);
|
||||||
|
height: 32px;
|
||||||
|
min-width: 90px;
|
||||||
|
|
||||||
|
&:hover:not([disabled]) {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 20%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文本按钮样式
|
||||||
|
.text button {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--color-primary);
|
||||||
|
padding: 8px 12px;
|
||||||
|
min-width: auto;
|
||||||
|
|
||||||
|
&:hover:not([disabled]) {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 社交按钮样式
|
||||||
|
.social button {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
height: auto;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图标样式
|
||||||
|
.button__icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
fill: currentColor;
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
|
||||||
|
&--before {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--after {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全宽样式
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式调整
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.button-wrapper:not(.full-width) {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
302
src/lib/components/base/Modal.svelte
Normal file
302
src/lib/components/base/Modal.svelte
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let modalTriggerElement: HTMLElement | null = null;
|
||||||
|
export let error: boolean = false;
|
||||||
|
export let dialogId: string = '';
|
||||||
|
export let dialogClassNames: string = '';
|
||||||
|
export let disableScrim: boolean = false;
|
||||||
|
export let showOnMount: boolean = false;
|
||||||
|
export let preventDefaultClose: boolean = false;
|
||||||
|
export let ariaLabelledBy: string | null = null;
|
||||||
|
export let ariaLabel: string | null = null;
|
||||||
|
export let size: 'sm' | 'md' | 'lg' | 'xl' | 'full' = 'md';
|
||||||
|
|
||||||
|
let ariaHidden: boolean = true;
|
||||||
|
let dialogElement: HTMLDialogElement;
|
||||||
|
let needsPolyfill: boolean = false;
|
||||||
|
let isDialogInShadow: boolean = false;
|
||||||
|
|
||||||
|
export function showModal() {
|
||||||
|
// 防止背景滚动
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
|
||||||
|
// 处理 polyfill 情况
|
||||||
|
if (needsPolyfill) {
|
||||||
|
isDialogInShadow = isInShadow(dialogElement);
|
||||||
|
if (!isDialogInShadow) {
|
||||||
|
document.body.appendChild(dialogElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ariaHidden = false;
|
||||||
|
dialogElement.showModal();
|
||||||
|
|
||||||
|
// 焦点管理
|
||||||
|
const firstFocusable = dialogElement.querySelector(
|
||||||
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||||
|
) as HTMLElement;
|
||||||
|
firstFocusable?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function close() {
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
|
||||||
|
// 清理 polyfill 添加的元素
|
||||||
|
if (needsPolyfill && !isDialogInShadow) {
|
||||||
|
try {
|
||||||
|
document.body.removeChild(dialogElement);
|
||||||
|
} catch (e) {
|
||||||
|
// 元素可能已经被移除
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ariaHidden = true;
|
||||||
|
dialogElement.close();
|
||||||
|
|
||||||
|
// 恢复焦点到触发元素
|
||||||
|
modalTriggerElement?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose(e: Event) {
|
||||||
|
if (preventDefaultClose) {
|
||||||
|
e.preventDefault();
|
||||||
|
} else {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
dispatch('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape' && !preventDefaultClose) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 焦点陷阱
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
trapFocus(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function trapFocus(e: KeyboardEvent) {
|
||||||
|
const focusableElements = dialogElement.querySelectorAll(
|
||||||
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||||
|
) as NodeListOf<HTMLElement>;
|
||||||
|
|
||||||
|
const firstElement = focusableElements[0];
|
||||||
|
const lastElement = focusableElements[focusableElements.length - 1];
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
if (document.activeElement === firstElement) {
|
||||||
|
lastElement?.focus();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (document.activeElement === lastElement) {
|
||||||
|
firstElement?.focus();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInShadow(node: HTMLElement | ParentNode | null): boolean {
|
||||||
|
while (node) {
|
||||||
|
if (node.toString() === '[object ShadowRoot]') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
node = node.parentNode;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
// 检查是否需要 dialog polyfill
|
||||||
|
needsPolyfill = !('showModal' in dialogElement);
|
||||||
|
|
||||||
|
if (needsPolyfill) {
|
||||||
|
try {
|
||||||
|
const { default: dialogPolyfill } = await import('dialog-polyfill');
|
||||||
|
dialogPolyfill.registerDialog(dialogElement);
|
||||||
|
dialogElement.classList.add('dialog-polyfill');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Dialog polyfill failed to load:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showOnMount) {
|
||||||
|
showModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
|
<dialog
|
||||||
|
data-testid="modal-dialog"
|
||||||
|
class="modal"
|
||||||
|
class:error
|
||||||
|
class:no-scrim={disableScrim}
|
||||||
|
class:size-sm={size === 'sm'}
|
||||||
|
class:size-md={size === 'md'}
|
||||||
|
class:size-lg={size === 'lg'}
|
||||||
|
class:size-xl={size === 'xl'}
|
||||||
|
class:size-full={size === 'full'}
|
||||||
|
class:needs-polyfill={needsPolyfill}
|
||||||
|
class={dialogClassNames}
|
||||||
|
id={dialogId}
|
||||||
|
bind:this={dialogElement}
|
||||||
|
on:click|self={handleClose}
|
||||||
|
on:close={handleClose}
|
||||||
|
on:cancel={handleClose}
|
||||||
|
aria-labelledby={ariaLabelledBy}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-hidden={ariaHidden}
|
||||||
|
>
|
||||||
|
<div class="modal__content">
|
||||||
|
<slot {handleClose} />
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
:global(body.modal-open) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
|
||||||
|
&::backdrop {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.no-scrim::backdrop {
|
||||||
|
background-color: transparent;
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border: 2px solid var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尺寸变体
|
||||||
|
&.size-sm {
|
||||||
|
width: 320px;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.size-md {
|
||||||
|
width: 480px;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.size-lg {
|
||||||
|
width: 640px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.size-xl {
|
||||||
|
width: 800px;
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.size-full {
|
||||||
|
width: 95vw;
|
||||||
|
height: 95vh;
|
||||||
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式调整
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
&:not(.size-full) {
|
||||||
|
width: 95vw;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__content {
|
||||||
|
background-color: var(--color-gray-50);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background-color: var(--color-gray-800);
|
||||||
|
color: var(--color-gray-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polyfill 样式支持
|
||||||
|
:global(.needs-polyfill) {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: fit-content;
|
||||||
|
height: fit-content;
|
||||||
|
margin: auto;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&:not([open]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&._dialog_overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动画效果
|
||||||
|
.modal {
|
||||||
|
animation: modalFadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 焦点样式
|
||||||
|
.modal :global(*:focus) {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
326
src/lib/i18n/index.ts
Normal file
326
src/lib/i18n/index.ts
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
import { writable, derived } from 'svelte/store';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
export interface Locale {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
flag: string;
|
||||||
|
rtl?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Translation {
|
||||||
|
[key: string]: string | Translation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface I18nState {
|
||||||
|
currentLocale: Locale;
|
||||||
|
translations: Record<string, Translation>;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支持的语言列表
|
||||||
|
export const supportedLocales: Locale[] = [
|
||||||
|
{ code: 'en', name: 'English', flag: '🇺🇸' },
|
||||||
|
{ code: 'zh', name: '中文', flag: '🇨🇳' },
|
||||||
|
{ code: 'ja', name: '日本語', flag: '🇯🇵' },
|
||||||
|
{ code: 'ko', name: '한국어', flag: '🇰🇷' },
|
||||||
|
{ code: 'es', name: 'Español', flag: '🇪🇸' },
|
||||||
|
{ code: 'fr', name: 'Français', flag: '🇫🇷' },
|
||||||
|
{ code: 'de', name: 'Deutsch', flag: '🇩🇪' },
|
||||||
|
{ code: 'ar', name: 'العربية', flag: '🇸🇦', rtl: true }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 默认翻译内容
|
||||||
|
const defaultTranslations: Record<string, Translation> = {
|
||||||
|
en: {
|
||||||
|
common: {
|
||||||
|
loading: 'Loading...',
|
||||||
|
error: 'Error',
|
||||||
|
retry: 'Retry',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
confirm: 'Confirm',
|
||||||
|
save: 'Save',
|
||||||
|
delete: 'Delete',
|
||||||
|
edit: 'Edit',
|
||||||
|
close: 'Close',
|
||||||
|
back: 'Back',
|
||||||
|
next: 'Next',
|
||||||
|
previous: 'Previous',
|
||||||
|
search: 'Search',
|
||||||
|
filter: 'Filter',
|
||||||
|
sort: 'Sort',
|
||||||
|
more: 'More'
|
||||||
|
},
|
||||||
|
navigation: {
|
||||||
|
home: 'Home',
|
||||||
|
apps: 'Apps',
|
||||||
|
games: 'Games',
|
||||||
|
search: 'Search',
|
||||||
|
account: 'Account'
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
download: 'Download',
|
||||||
|
install: 'Install',
|
||||||
|
update: 'Update',
|
||||||
|
open: 'Open',
|
||||||
|
share: 'Share',
|
||||||
|
favorite: 'Favorite',
|
||||||
|
rate: 'Rate'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
common: {
|
||||||
|
loading: '加载中...',
|
||||||
|
error: '错误',
|
||||||
|
retry: '重试',
|
||||||
|
cancel: '取消',
|
||||||
|
confirm: '确认',
|
||||||
|
save: '保存',
|
||||||
|
delete: '删除',
|
||||||
|
edit: '编辑',
|
||||||
|
close: '关闭',
|
||||||
|
back: '返回',
|
||||||
|
next: '下一步',
|
||||||
|
previous: '上一步',
|
||||||
|
search: '搜索',
|
||||||
|
filter: '筛选',
|
||||||
|
sort: '排序',
|
||||||
|
more: '更多'
|
||||||
|
},
|
||||||
|
navigation: {
|
||||||
|
home: '首页',
|
||||||
|
apps: '应用',
|
||||||
|
games: '游戏',
|
||||||
|
search: '搜索',
|
||||||
|
account: '账户'
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
download: '下载',
|
||||||
|
install: '安装',
|
||||||
|
update: '更新',
|
||||||
|
open: '打开',
|
||||||
|
share: '分享',
|
||||||
|
favorite: '收藏',
|
||||||
|
rate: '评分'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检测浏览器语言
|
||||||
|
function detectBrowserLocale(): string {
|
||||||
|
if (!browser) return 'en';
|
||||||
|
|
||||||
|
const browserLang = navigator.language || 'en';
|
||||||
|
const langCode = browserLang.split('-')[0];
|
||||||
|
|
||||||
|
// 检查是否支持该语言
|
||||||
|
const supported = supportedLocales.find(locale => locale.code === langCode);
|
||||||
|
return supported ? langCode : 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 i18n store
|
||||||
|
function createI18n() {
|
||||||
|
const defaultLocale = supportedLocales.find(l => l.code === 'en')!;
|
||||||
|
|
||||||
|
const initialState: I18nState = {
|
||||||
|
currentLocale: defaultLocale,
|
||||||
|
translations: defaultTranslations,
|
||||||
|
isLoading: false,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const { subscribe, set, update } = writable(initialState);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
|
||||||
|
// 初始化 i18n
|
||||||
|
init: async () => {
|
||||||
|
const detectedLocale = detectBrowserLocale();
|
||||||
|
const locale = supportedLocales.find(l => l.code === detectedLocale) || defaultLocale;
|
||||||
|
|
||||||
|
update(state => ({ ...state, isLoading: true }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadLocale(locale.code);
|
||||||
|
update(state => ({
|
||||||
|
...state,
|
||||||
|
currentLocale: locale,
|
||||||
|
isLoading: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 设置 HTML 属性
|
||||||
|
if (browser) {
|
||||||
|
document.documentElement.lang = locale.code;
|
||||||
|
document.documentElement.dir = locale.rtl ? 'rtl' : 'ltr';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
update(state => ({
|
||||||
|
...state,
|
||||||
|
isLoading: false,
|
||||||
|
error: error as Error
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 切换语言
|
||||||
|
setLocale: async (localeCode: string) => {
|
||||||
|
const locale = supportedLocales.find(l => l.code === localeCode);
|
||||||
|
if (!locale) {
|
||||||
|
throw new Error(`Unsupported locale: ${localeCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(state => ({ ...state, isLoading: true, error: null }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadLocale(localeCode);
|
||||||
|
update(state => ({
|
||||||
|
...state,
|
||||||
|
currentLocale: locale,
|
||||||
|
isLoading: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 更新 HTML 属性
|
||||||
|
if (browser) {
|
||||||
|
document.documentElement.lang = locale.code;
|
||||||
|
document.documentElement.dir = locale.rtl ? 'rtl' : 'ltr';
|
||||||
|
|
||||||
|
// 保存到 localStorage
|
||||||
|
localStorage.setItem('preferred-locale', localeCode);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
update(state => ({
|
||||||
|
...state,
|
||||||
|
isLoading: false,
|
||||||
|
error: error as Error
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 添加翻译
|
||||||
|
addTranslations: (localeCode: string, translations: Translation) => {
|
||||||
|
update(state => ({
|
||||||
|
...state,
|
||||||
|
translations: {
|
||||||
|
...state.translations,
|
||||||
|
[localeCode]: {
|
||||||
|
...state.translations[localeCode],
|
||||||
|
...translations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清除错误
|
||||||
|
clearError: () => {
|
||||||
|
update(state => ({ ...state, error: null }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载语言包
|
||||||
|
async function loadLocale(localeCode: string): Promise<void> {
|
||||||
|
// 如果已经有默认翻译,直接返回
|
||||||
|
if (defaultTranslations[localeCode]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟异步加载翻译文件
|
||||||
|
// 在实际应用中,这里会从服务器或本地文件加载翻译
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
// 这里可以添加动态加载翻译文件的逻辑
|
||||||
|
console.log(`Loading translations for ${localeCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 i18n 实例
|
||||||
|
export const i18n = createI18n();
|
||||||
|
|
||||||
|
// 派生状态
|
||||||
|
export const currentLocale = derived(i18n, $i18n => $i18n.currentLocale);
|
||||||
|
export const translations = derived(i18n, $i18n => $i18n.translations);
|
||||||
|
export const isI18nLoading = derived(i18n, $i18n => $i18n.isLoading);
|
||||||
|
|
||||||
|
// 翻译函数
|
||||||
|
export const t = derived(
|
||||||
|
[currentLocale, translations],
|
||||||
|
([$currentLocale, $translations]) => {
|
||||||
|
return (key: string, params?: Record<string, string | number>): string => {
|
||||||
|
const localeTranslations = $translations[$currentLocale.code] || $translations.en;
|
||||||
|
const value = getNestedValue(localeTranslations, key);
|
||||||
|
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
console.warn(`Translation not found for key: ${key}`);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参数替换
|
||||||
|
if (params) {
|
||||||
|
return value.replace(/\{\{(\w+)\}\}/g, (match, paramKey) => {
|
||||||
|
return params[paramKey]?.toString() || match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取嵌套对象的值
|
||||||
|
function getNestedValue(obj: any, path: string): any {
|
||||||
|
return path.split('.').reduce((current, key) => {
|
||||||
|
return current && current[key] !== undefined ? current[key] : undefined;
|
||||||
|
}, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化数字
|
||||||
|
export const formatNumber = derived(currentLocale, $currentLocale => {
|
||||||
|
return (value: number, options?: Intl.NumberFormatOptions): string => {
|
||||||
|
if (!browser) return value.toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new Intl.NumberFormat($currentLocale.code, options).format(value);
|
||||||
|
} catch {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
export const formatDate = derived(currentLocale, $currentLocale => {
|
||||||
|
return (date: Date, options?: Intl.DateTimeFormatOptions): string => {
|
||||||
|
if (!browser) return date.toISOString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new Intl.DateTimeFormat($currentLocale.code, options).format(date);
|
||||||
|
} catch {
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 格式化相对时间
|
||||||
|
export const formatRelativeTime = derived(currentLocale, $currentLocale => {
|
||||||
|
return (value: number, unit: Intl.RelativeTimeFormatUnit): string => {
|
||||||
|
if (!browser) return `${value} ${unit}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rtf = new Intl.RelativeTimeFormat($currentLocale.code, { numeric: 'auto' });
|
||||||
|
return rtf.format(value, unit);
|
||||||
|
} catch {
|
||||||
|
return `${value} ${unit}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化 i18n(在浏览器环境中)
|
||||||
|
if (browser) {
|
||||||
|
// 从 localStorage 恢复用户偏好
|
||||||
|
const savedLocale = localStorage.getItem('preferred-locale');
|
||||||
|
if (savedLocale && supportedLocales.find(l => l.code === savedLocale)) {
|
||||||
|
i18n.setLocale(savedLocale);
|
||||||
|
} else {
|
||||||
|
i18n.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
223
src/lib/stores/router.ts
Normal file
223
src/lib/stores/router.ts
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
import { writable, derived } from 'svelte/store';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
export interface Page {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
data?: any;
|
||||||
|
meta?: {
|
||||||
|
description?: string;
|
||||||
|
keywords?: string[];
|
||||||
|
image?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RouterState {
|
||||||
|
currentPage: Page | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
history: Page[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建路由状态
|
||||||
|
function createRouter() {
|
||||||
|
const initialState: RouterState = {
|
||||||
|
currentPage: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
history: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const { subscribe, set, update } = writable(initialState);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
|
||||||
|
// 导航到新页面
|
||||||
|
navigate: async (url: string, options?: { replace?: boolean; data?: any }) => {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
update(state => ({ ...state, isLoading: true, error: null }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const page = await loadPage(url, options?.data);
|
||||||
|
|
||||||
|
update(state => {
|
||||||
|
const newHistory = options?.replace
|
||||||
|
? state.history
|
||||||
|
: [...state.history, page];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentPage: page,
|
||||||
|
isLoading: false,
|
||||||
|
history: newHistory
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新浏览器历史
|
||||||
|
if (options?.replace) {
|
||||||
|
history.replaceState({ page }, page.title, url);
|
||||||
|
} else {
|
||||||
|
history.pushState({ page }, page.title, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新页面标题
|
||||||
|
document.title = page.title;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
update(state => ({
|
||||||
|
...state,
|
||||||
|
isLoading: false,
|
||||||
|
error: error as Error
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
goBack: () => {
|
||||||
|
if (browser && window.history.length > 1) {
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 前进到下一页
|
||||||
|
goForward: () => {
|
||||||
|
if (browser) {
|
||||||
|
window.history.forward();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重新加载当前页面
|
||||||
|
reload: async () => {
|
||||||
|
update(state => {
|
||||||
|
if (state.currentPage) {
|
||||||
|
return { ...state, isLoading: true, error: null };
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = get(router);
|
||||||
|
if (currentState.currentPage) {
|
||||||
|
try {
|
||||||
|
const page = await loadPage(currentState.currentPage.url);
|
||||||
|
update(state => ({
|
||||||
|
...state,
|
||||||
|
currentPage: page,
|
||||||
|
isLoading: false
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
update(state => ({
|
||||||
|
...state,
|
||||||
|
isLoading: false,
|
||||||
|
error: error as Error
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清除错误状态
|
||||||
|
clearError: () => {
|
||||||
|
update(state => ({ ...state, error: null }));
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置当前页面(用于 SSR)
|
||||||
|
setCurrentPage: (page: Page) => {
|
||||||
|
update(state => ({
|
||||||
|
...state,
|
||||||
|
currentPage: page,
|
||||||
|
isLoading: false,
|
||||||
|
error: null
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载函数
|
||||||
|
async function loadPage(url: string, data?: any): Promise<Page> {
|
||||||
|
// 模拟页面加载延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
// 这里应该根据 URL 加载对应的页面数据
|
||||||
|
// 在实际应用中,这可能涉及 API 调用或路由匹配
|
||||||
|
const pageId = extractPageId(url);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: pageId,
|
||||||
|
title: `Page ${pageId}`,
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
meta: {
|
||||||
|
description: `Description for ${pageId}`,
|
||||||
|
keywords: [pageId, 'app', 'store']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 URL 提取页面 ID
|
||||||
|
function extractPageId(url: string): string {
|
||||||
|
const segments = url.split('/').filter(Boolean);
|
||||||
|
return segments[segments.length - 1] || 'home';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前 store 值的辅助函数
|
||||||
|
function get<T>(store: { subscribe: (fn: (value: T) => void) => () => void }): T {
|
||||||
|
let value: T;
|
||||||
|
const unsubscribe = store.subscribe(v => value = v);
|
||||||
|
unsubscribe();
|
||||||
|
return value!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建路由实例
|
||||||
|
export const router = createRouter();
|
||||||
|
|
||||||
|
// 派生状态
|
||||||
|
export const currentPage = derived(router, $router => $router.currentPage);
|
||||||
|
export const isLoading = derived(router, $router => $router.isLoading);
|
||||||
|
export const routerError = derived(router, $router => $router.error);
|
||||||
|
export const canGoBack = derived(router, $router => $router.history.length > 1);
|
||||||
|
|
||||||
|
// 浏览器历史事件处理
|
||||||
|
if (browser) {
|
||||||
|
window.addEventListener('popstate', (event) => {
|
||||||
|
if (event.state?.page) {
|
||||||
|
router.setCurrentPage(event.state.page);
|
||||||
|
} else {
|
||||||
|
// 如果没有状态,重新加载当前 URL 的页面
|
||||||
|
loadPage(window.location.pathname + window.location.search)
|
||||||
|
.then(page => router.setCurrentPage(page))
|
||||||
|
.catch(error => console.error('Failed to load page on popstate:', error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 路由守卫类型
|
||||||
|
export type RouteGuard = (to: Page, from: Page | null) => boolean | Promise<boolean>;
|
||||||
|
|
||||||
|
// 路由守卫管理
|
||||||
|
class RouteGuardManager {
|
||||||
|
private guards: RouteGuard[] = [];
|
||||||
|
|
||||||
|
addGuard(guard: RouteGuard) {
|
||||||
|
this.guards.push(guard);
|
||||||
|
return () => {
|
||||||
|
const index = this.guards.indexOf(guard);
|
||||||
|
if (index > -1) {
|
||||||
|
this.guards.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkGuards(to: Page, from: Page | null): Promise<boolean> {
|
||||||
|
for (const guard of this.guards) {
|
||||||
|
const result = await guard(to, from);
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routeGuards = new RouteGuardManager();
|
||||||
346
src/lib/stores/theme.ts
Normal file
346
src/lib/stores/theme.ts
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
import { writable, derived } from 'svelte/store';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
export type ThemeMode = 'light' | 'dark' | 'auto';
|
||||||
|
|
||||||
|
export interface ThemeConfig {
|
||||||
|
mode: ThemeMode;
|
||||||
|
primaryColor: string;
|
||||||
|
accentColor: string;
|
||||||
|
borderRadius: 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
fontScale: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
reducedMotion: boolean;
|
||||||
|
highContrast: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeState {
|
||||||
|
config: ThemeConfig;
|
||||||
|
resolvedMode: 'light' | 'dark';
|
||||||
|
isSystemDark: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认主题配置
|
||||||
|
const defaultThemeConfig: ThemeConfig = {
|
||||||
|
mode: 'auto',
|
||||||
|
primaryColor: '#007AFF',
|
||||||
|
accentColor: '#5856D6',
|
||||||
|
borderRadius: 'md',
|
||||||
|
fontScale: 'md',
|
||||||
|
reducedMotion: false,
|
||||||
|
highContrast: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 主题预设
|
||||||
|
export const themePresets = {
|
||||||
|
apple: {
|
||||||
|
primaryColor: '#007AFF',
|
||||||
|
accentColor: '#5856D6'
|
||||||
|
},
|
||||||
|
google: {
|
||||||
|
primaryColor: '#4285F4',
|
||||||
|
accentColor: '#34A853'
|
||||||
|
},
|
||||||
|
github: {
|
||||||
|
primaryColor: '#24292e',
|
||||||
|
accentColor: '#0366d6'
|
||||||
|
},
|
||||||
|
spotify: {
|
||||||
|
primaryColor: '#1DB954',
|
||||||
|
accentColor: '#1ed760'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检测系统主题偏好
|
||||||
|
function detectSystemTheme(): boolean {
|
||||||
|
if (!browser) return false;
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测用户偏好
|
||||||
|
function detectUserPreferences(): Partial<ThemeConfig> {
|
||||||
|
if (!browser) return {};
|
||||||
|
|
||||||
|
const preferences: Partial<ThemeConfig> = {};
|
||||||
|
|
||||||
|
// 检测动画偏好
|
||||||
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||||
|
preferences.reducedMotion = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测对比度偏好
|
||||||
|
if (window.matchMedia('(prefers-contrast: high)').matches) {
|
||||||
|
preferences.highContrast = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return preferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建主题 store
|
||||||
|
function createTheme() {
|
||||||
|
const initialState: ThemeState = {
|
||||||
|
config: defaultThemeConfig,
|
||||||
|
resolvedMode: 'light',
|
||||||
|
isSystemDark: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const { subscribe, set, update } = writable(initialState);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
|
||||||
|
// 初始化主题
|
||||||
|
init: () => {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
// 从 localStorage 恢复配置
|
||||||
|
const savedConfig = localStorage.getItem('theme-config');
|
||||||
|
let config = defaultThemeConfig;
|
||||||
|
|
||||||
|
if (savedConfig) {
|
||||||
|
try {
|
||||||
|
config = { ...defaultThemeConfig, ...JSON.parse(savedConfig) };
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to parse saved theme config:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用用户系统偏好
|
||||||
|
const userPreferences = detectUserPreferences();
|
||||||
|
config = { ...config, ...userPreferences };
|
||||||
|
|
||||||
|
const isSystemDark = detectSystemTheme();
|
||||||
|
const resolvedMode = config.mode === 'auto'
|
||||||
|
? (isSystemDark ? 'dark' : 'light')
|
||||||
|
: config.mode;
|
||||||
|
|
||||||
|
update(() => ({
|
||||||
|
config,
|
||||||
|
resolvedMode,
|
||||||
|
isSystemDark
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 应用主题到 DOM
|
||||||
|
applyTheme(config, resolvedMode);
|
||||||
|
|
||||||
|
// 监听系统主题变化
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
const handleSystemThemeChange = (e: MediaQueryListEvent) => {
|
||||||
|
update(state => {
|
||||||
|
const newIsSystemDark = e.matches;
|
||||||
|
const newResolvedMode = state.config.mode === 'auto'
|
||||||
|
? (newIsSystemDark ? 'dark' : 'light')
|
||||||
|
: state.config.mode;
|
||||||
|
|
||||||
|
if (newResolvedMode !== state.resolvedMode) {
|
||||||
|
applyTheme(state.config, newResolvedMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isSystemDark: newIsSystemDark,
|
||||||
|
resolvedMode: newResolvedMode
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaQuery.addEventListener('change', handleSystemThemeChange);
|
||||||
|
|
||||||
|
// 返回清理函数
|
||||||
|
return () => {
|
||||||
|
mediaQuery.removeEventListener('change', handleSystemThemeChange);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置主题模式
|
||||||
|
setMode: (mode: ThemeMode) => {
|
||||||
|
update(state => {
|
||||||
|
const newConfig = { ...state.config, mode };
|
||||||
|
const newResolvedMode = mode === 'auto'
|
||||||
|
? (state.isSystemDark ? 'dark' : 'light')
|
||||||
|
: mode;
|
||||||
|
|
||||||
|
applyTheme(newConfig, newResolvedMode);
|
||||||
|
saveConfig(newConfig);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
config: newConfig,
|
||||||
|
resolvedMode: newResolvedMode
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置主色调
|
||||||
|
setPrimaryColor: (color: string) => {
|
||||||
|
update(state => {
|
||||||
|
const newConfig = { ...state.config, primaryColor: color };
|
||||||
|
applyTheme(newConfig, state.resolvedMode);
|
||||||
|
saveConfig(newConfig);
|
||||||
|
|
||||||
|
return { ...state, config: newConfig };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置强调色
|
||||||
|
setAccentColor: (color: string) => {
|
||||||
|
update(state => {
|
||||||
|
const newConfig = { ...state.config, accentColor: color };
|
||||||
|
applyTheme(newConfig, state.resolvedMode);
|
||||||
|
saveConfig(newConfig);
|
||||||
|
|
||||||
|
return { ...state, config: newConfig };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 应用主题预设
|
||||||
|
applyPreset: (presetName: keyof typeof themePresets) => {
|
||||||
|
const preset = themePresets[presetName];
|
||||||
|
if (!preset) return;
|
||||||
|
|
||||||
|
update(state => {
|
||||||
|
const newConfig = { ...state.config, ...preset };
|
||||||
|
applyTheme(newConfig, state.resolvedMode);
|
||||||
|
saveConfig(newConfig);
|
||||||
|
|
||||||
|
return { ...state, config: newConfig };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置圆角大小
|
||||||
|
setBorderRadius: (radius: ThemeConfig['borderRadius']) => {
|
||||||
|
update(state => {
|
||||||
|
const newConfig = { ...state.config, borderRadius: radius };
|
||||||
|
applyTheme(newConfig, state.resolvedMode);
|
||||||
|
saveConfig(newConfig);
|
||||||
|
|
||||||
|
return { ...state, config: newConfig };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置字体缩放
|
||||||
|
setFontScale: (scale: ThemeConfig['fontScale']) => {
|
||||||
|
update(state => {
|
||||||
|
const newConfig = { ...state.config, fontScale: scale };
|
||||||
|
applyTheme(newConfig, state.resolvedMode);
|
||||||
|
saveConfig(newConfig);
|
||||||
|
|
||||||
|
return { ...state, config: newConfig };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 切换减少动画
|
||||||
|
toggleReducedMotion: () => {
|
||||||
|
update(state => {
|
||||||
|
const newConfig = { ...state.config, reducedMotion: !state.config.reducedMotion };
|
||||||
|
applyTheme(newConfig, state.resolvedMode);
|
||||||
|
saveConfig(newConfig);
|
||||||
|
|
||||||
|
return { ...state, config: newConfig };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 切换高对比度
|
||||||
|
toggleHighContrast: () => {
|
||||||
|
update(state => {
|
||||||
|
const newConfig = { ...state.config, highContrast: !state.config.highContrast };
|
||||||
|
applyTheme(newConfig, state.resolvedMode);
|
||||||
|
saveConfig(newConfig);
|
||||||
|
|
||||||
|
return { ...state, config: newConfig };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重置主题
|
||||||
|
reset: () => {
|
||||||
|
const isSystemDark = detectSystemTheme();
|
||||||
|
const resolvedMode = defaultThemeConfig.mode === 'auto'
|
||||||
|
? (isSystemDark ? 'dark' : 'light')
|
||||||
|
: defaultThemeConfig.mode;
|
||||||
|
|
||||||
|
update(() => ({
|
||||||
|
config: defaultThemeConfig,
|
||||||
|
resolvedMode,
|
||||||
|
isSystemDark
|
||||||
|
}));
|
||||||
|
|
||||||
|
applyTheme(defaultThemeConfig, resolvedMode);
|
||||||
|
if (browser) {
|
||||||
|
localStorage.removeItem('theme-config');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用主题到 DOM
|
||||||
|
function applyTheme(config: ThemeConfig, resolvedMode: 'light' | 'dark') {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
// 设置主题模式
|
||||||
|
root.setAttribute('data-theme', resolvedMode);
|
||||||
|
root.classList.toggle('dark', resolvedMode === 'dark');
|
||||||
|
|
||||||
|
// 设置颜色
|
||||||
|
root.style.setProperty('--color-primary', config.primaryColor);
|
||||||
|
root.style.setProperty('--color-accent', config.accentColor);
|
||||||
|
|
||||||
|
// 设置圆角
|
||||||
|
const radiusMap = {
|
||||||
|
none: '0px',
|
||||||
|
sm: '4px',
|
||||||
|
md: '8px',
|
||||||
|
lg: '12px',
|
||||||
|
xl: '16px'
|
||||||
|
};
|
||||||
|
root.style.setProperty('--border-radius-base', radiusMap[config.borderRadius]);
|
||||||
|
|
||||||
|
// 设置字体缩放
|
||||||
|
const fontScaleMap = {
|
||||||
|
xs: '0.875',
|
||||||
|
sm: '0.9375',
|
||||||
|
md: '1',
|
||||||
|
lg: '1.125',
|
||||||
|
xl: '1.25'
|
||||||
|
};
|
||||||
|
root.style.setProperty('--font-scale', fontScaleMap[config.fontScale]);
|
||||||
|
|
||||||
|
// 设置动画偏好
|
||||||
|
root.style.setProperty('--transition-duration', config.reducedMotion ? '0ms' : '250ms');
|
||||||
|
|
||||||
|
// 设置对比度
|
||||||
|
root.classList.toggle('high-contrast', config.highContrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置到 localStorage
|
||||||
|
function saveConfig(config: ThemeConfig) {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem('theme-config', JSON.stringify(config));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to save theme config:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建主题实例
|
||||||
|
export const theme = createTheme();
|
||||||
|
|
||||||
|
// 派生状态
|
||||||
|
export const themeMode = derived(theme, $theme => $theme.config.mode);
|
||||||
|
export const resolvedThemeMode = derived(theme, $theme => $theme.resolvedMode);
|
||||||
|
export const primaryColor = derived(theme, $theme => $theme.config.primaryColor);
|
||||||
|
export const accentColor = derived(theme, $theme => $theme.config.accentColor);
|
||||||
|
export const isDark = derived(theme, $theme => $theme.resolvedMode === 'dark');
|
||||||
|
|
||||||
|
// 主题切换辅助函数
|
||||||
|
export const toggleTheme = () => {
|
||||||
|
theme.setMode(resolvedThemeMode.subscribe(mode => {
|
||||||
|
theme.setMode(mode === 'dark' ? 'light' : 'dark');
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化主题(在浏览器环境中)
|
||||||
|
if (browser) {
|
||||||
|
theme.init();
|
||||||
|
}
|
||||||
121
src/lib/styles/variables.scss
Normal file
121
src/lib/styles/variables.scss
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/* 设计系统变量 */
|
||||||
|
:root {
|
||||||
|
/* 颜色系统 - 基于苹果设计语言 */
|
||||||
|
--color-primary: #007AFF;
|
||||||
|
--color-secondary: #5856D6;
|
||||||
|
--color-success: #34C759;
|
||||||
|
--color-warning: #FF9500;
|
||||||
|
--color-error: #FF3B30;
|
||||||
|
|
||||||
|
/* 灰度系统 */
|
||||||
|
--color-gray-50: #F9FAFB;
|
||||||
|
--color-gray-100: #F3F4F6;
|
||||||
|
--color-gray-200: #E5E7EB;
|
||||||
|
--color-gray-300: #D1D5DB;
|
||||||
|
--color-gray-400: #9CA3AF;
|
||||||
|
--color-gray-500: #6B7280;
|
||||||
|
--color-gray-600: #4B5563;
|
||||||
|
--color-gray-700: #374151;
|
||||||
|
--color-gray-800: #1F2937;
|
||||||
|
--color-gray-900: #111827;
|
||||||
|
|
||||||
|
/* 系统颜色 - 适配苹果原有变量 */
|
||||||
|
--systemBlue: var(--color-primary);
|
||||||
|
--systemGray: var(--color-gray-500);
|
||||||
|
--systemQuinary: var(--color-gray-100);
|
||||||
|
--systemTertiary: var(--color-gray-400);
|
||||||
|
--systemTertiary-onLight: var(--color-gray-600);
|
||||||
|
|
||||||
|
/* 按钮相关变量 */
|
||||||
|
--keyColorBG: var(--color-primary);
|
||||||
|
--buttonBackgroundColor: var(--keyColorBG);
|
||||||
|
--buttonTextColor: white;
|
||||||
|
--buttonDisabledOpacity: 0.5;
|
||||||
|
--buttonDisabledBGColor: var(--color-gray-200);
|
||||||
|
--buttonDisabledTextColor: var(--color-gray-400);
|
||||||
|
|
||||||
|
/* 间距系统 */
|
||||||
|
--spacing-xs: 4px;
|
||||||
|
--spacing-sm: 8px;
|
||||||
|
--spacing-md: 16px;
|
||||||
|
--spacing-lg: 24px;
|
||||||
|
--spacing-xl: 32px;
|
||||||
|
--spacing-2xl: 48px;
|
||||||
|
|
||||||
|
/* 字体系统 */
|
||||||
|
--font-family-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
--font-size-xs: 12px;
|
||||||
|
--font-size-sm: 14px;
|
||||||
|
--font-size-md: 16px;
|
||||||
|
--font-size-lg: 18px;
|
||||||
|
--font-size-xl: 20px;
|
||||||
|
--font-size-2xl: 24px;
|
||||||
|
|
||||||
|
/* 字体权重 */
|
||||||
|
--font-weight-normal: 400;
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-semibold: 600;
|
||||||
|
--font-weight-bold: 700;
|
||||||
|
|
||||||
|
/* 行高 */
|
||||||
|
--line-height-tight: 1.25;
|
||||||
|
--line-height-normal: 1.5;
|
||||||
|
--line-height-relaxed: 1.75;
|
||||||
|
|
||||||
|
/* 圆角 */
|
||||||
|
--border-radius-sm: 4px;
|
||||||
|
--border-radius-md: 8px;
|
||||||
|
--border-radius-lg: 12px;
|
||||||
|
--border-radius-xl: 16px;
|
||||||
|
--border-radius-full: 9999px;
|
||||||
|
|
||||||
|
/* 阴影 */
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
/* 过渡动画 */
|
||||||
|
--transition-fast: 150ms ease-in-out;
|
||||||
|
--transition-normal: 250ms ease-in-out;
|
||||||
|
--transition-slow: 350ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-primary: #0A84FF;
|
||||||
|
--color-gray-50: #1F2937;
|
||||||
|
--color-gray-100: #374151;
|
||||||
|
--color-gray-200: #4B5563;
|
||||||
|
--color-gray-300: #6B7280;
|
||||||
|
--color-gray-400: #9CA3AF;
|
||||||
|
--color-gray-500: #D1D5DB;
|
||||||
|
--color-gray-600: #E5E7EB;
|
||||||
|
--color-gray-700: #F3F4F6;
|
||||||
|
--color-gray-800: #F9FAFB;
|
||||||
|
--color-gray-900: #FFFFFF;
|
||||||
|
|
||||||
|
--buttonDisabledOpacityDark: 1;
|
||||||
|
--buttonDisabledBGColorDark: rgba(255, 255, 255, 0.5);
|
||||||
|
--buttonDisabledTextColorDark: var(--systemTertiary-onLight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局重置和基础样式 */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-family-primary);
|
||||||
|
line-height: var(--line-height-normal);
|
||||||
|
color: var(--color-gray-900);
|
||||||
|
background-color: var(--color-gray-50);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SCSS 变量用于断点 */
|
||||||
|
$breakpoint-small: 640px;
|
||||||
|
$breakpoint-medium: 768px;
|
||||||
|
$breakpoint-large: 1024px;
|
||||||
19
src/lib/utils/makeSafeTick.ts
Normal file
19
src/lib/utils/makeSafeTick.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* 创建一个安全的 tick 函数,用于在组件挂载后安全地执行操作
|
||||||
|
* 这是对苹果原有 @amp/web-app-components 中 makeSafeTick 的替代实现
|
||||||
|
*/
|
||||||
|
export function makeSafeTick() {
|
||||||
|
return async (callback: (tick: () => Promise<void>) => Promise<void>) => {
|
||||||
|
// 创建一个 tick 函数,它会在下一个微任务中执行
|
||||||
|
const tick = () => new Promise<void>(resolve => {
|
||||||
|
// 使用 setTimeout 确保在下一个事件循环中执行
|
||||||
|
setTimeout(resolve, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await callback(tick);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SafeTick callback error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
439
src/routes/+layout.svelte
Normal file
439
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
let isDarkMode = false;
|
||||||
|
let currentPath = '/';
|
||||||
|
let currentCategory: string | null = null;
|
||||||
|
|
||||||
|
const primaryNav = [
|
||||||
|
{ label: 'Today', href: '/', icon: 'today' },
|
||||||
|
{ label: 'Projects', href: '/projects', icon: 'projects' },
|
||||||
|
{ label: 'About', href: '/about', icon: 'about' },
|
||||||
|
{ label: 'Contact', href: '/contact', icon: 'contact' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const categoryNav = [
|
||||||
|
{ label: 'Brand Design', href: '/projects?category=Brand Design' },
|
||||||
|
{ label: 'UI/UX Design', href: '/projects?category=UI/UX Design' },
|
||||||
|
{ label: 'Web Design', href: '/projects?category=Web Design' },
|
||||||
|
{ label: 'Graphic Design', href: '/projects?category=Graphic Design' },
|
||||||
|
{ label: 'Photography', href: '/projects?category=Photography' },
|
||||||
|
{ label: 'Print Design', href: '/projects?category=Print Design' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
isDarkMode = !isDarkMode;
|
||||||
|
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateTo(href: string) {
|
||||||
|
goto(href);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: currentPath = $page.url.pathname;
|
||||||
|
$: currentCategory = $page.url.searchParams.get('category');
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// 检测系统主题偏好
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
isDarkMode = prefersDark;
|
||||||
|
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content="LiuBai Design - A modern creative studio experience built with SvelteKit" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="app-shell">
|
||||||
|
<aside class="sidebar">
|
||||||
|
<header class="sidebar__header">
|
||||||
|
<div class="sidebar__store-icon" aria-hidden="true">🎨</div>
|
||||||
|
<div class="sidebar__heading">
|
||||||
|
<span class="sidebar__title">LiuBai Design</span>
|
||||||
|
<span class="sidebar__subtitle">Creative Studio</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="sidebar__search">
|
||||||
|
<span class="sidebar__search-icon" aria-hidden="true">🔍</span>
|
||||||
|
<input type="search" placeholder="Search" aria-label="Search LiuBai Design" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="sidebar__nav">
|
||||||
|
<div class="sidebar__section" aria-label="Primary navigation">
|
||||||
|
{#each primaryNav as item}
|
||||||
|
<button
|
||||||
|
class:active={item.href === '/' ? currentPath === '/' : currentPath.startsWith(item.href)}
|
||||||
|
on:click={() => navigateTo(item.href)}
|
||||||
|
>
|
||||||
|
<span class="sidebar__bullet" aria-hidden="true">•</span>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar__section" aria-label="Design categories">
|
||||||
|
<span class="sidebar__section-label">Design Categories</span>
|
||||||
|
{#each categoryNav as item}
|
||||||
|
<button
|
||||||
|
class:active={currentCategory === item.label}
|
||||||
|
on:click={() => navigateTo(item.href)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<footer class="sidebar__footer">
|
||||||
|
<button on:click={toggleTheme} aria-label="Toggle theme">
|
||||||
|
<span class="sidebar__avatar" aria-hidden="true">{isDarkMode ? '☀️' : '🌙'}</span>
|
||||||
|
{isDarkMode ? 'Light' : 'Dark'}
|
||||||
|
</button>
|
||||||
|
<button aria-label="Account">
|
||||||
|
<span class="sidebar__avatar" aria-hidden="true">🧑💻</span>
|
||||||
|
Account
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="content">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(html) {
|
||||||
|
height: 100%;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body) {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 亮色模式 */
|
||||||
|
:global(body) {
|
||||||
|
background: #f5f5f7;
|
||||||
|
color: #1d1d1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式 */
|
||||||
|
:global(.dark body) {
|
||||||
|
background: radial-gradient(circle at top left, rgba(46, 46, 82, 0.6), transparent 40%),
|
||||||
|
radial-gradient(circle at bottom right, rgba(36, 36, 60, 0.5), transparent 45%),
|
||||||
|
#050506;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px minmax(0, 1fr);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto 1fr auto;
|
||||||
|
gap: 32px;
|
||||||
|
padding: 32px 24px;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border-right: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
backdrop-filter: blur(30px);
|
||||||
|
box-shadow: 12px 0 40px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sidebar {
|
||||||
|
background: rgba(10, 10, 18, 0.9);
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 12px 0 40px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__header {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__store-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, #4f46e5 0%, #9333ea 100%);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__heading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: rgba(0, 0, 0, 0.9);
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sidebar__heading {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__search {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.4);
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sidebar__search-icon {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__search input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px 10px 34px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(0, 0, 0, 0.08);
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sidebar__search input {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: rgba(255, 255, 255, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__search input:focus {
|
||||||
|
outline: none;
|
||||||
|
background: rgba(0, 0, 0, 0.12);
|
||||||
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sidebar__search input:focus {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
box-shadow: 0 0 0 2px rgba(147, 197, 253, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__section {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__section-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: rgba(0, 0, 0, 0.35);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sidebar__section-label {
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__section button {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(0, 0, 0, 0.66);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sidebar__section button {
|
||||||
|
color: rgba(255, 255, 255, 0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__section button:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.9);
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sidebar__section button:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__section button.active {
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(135deg, rgba(88, 88, 255, 0.45), rgba(138, 92, 255, 0.45));
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__bullet {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sidebar__bullet {
|
||||||
|
background: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__section button.active .sidebar__bullet {
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__footer button {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
color: rgba(0, 0, 0, 0.72);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sidebar__footer button {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__footer button:hover {
|
||||||
|
border-color: rgba(0, 0, 0, 0.2);
|
||||||
|
background: rgba(0, 0, 0, 0.08);
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sidebar__footer button:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__avatar {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sidebar__avatar {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 48px 56px 64px;
|
||||||
|
background: linear-gradient(180deg, rgba(245, 245, 247, 0.9) 0%, rgba(240, 240, 242, 0.92) 55%, rgba(235, 235, 237, 0.96) 100%);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .content {
|
||||||
|
background: linear-gradient(180deg, rgba(16, 16, 28, 0.9) 0%, rgba(8, 8, 18, 0.92) 55%, rgba(6, 6, 12, 0.96) 100%);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content::-webkit-scrollbar-track {
|
||||||
|
background: rgba(200, 200, 200, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .content::-webkit-scrollbar-track {
|
||||||
|
background: rgba(12, 12, 18, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(150, 150, 150, 0.45);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .content::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(110, 110, 145, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.app-shell {
|
||||||
|
grid-template-columns: 240px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 36px 32px 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.app-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 20px 24px;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__nav,
|
||||||
|
.sidebar__footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
700
src/routes/+page.svelte
Normal file
700
src/routes/+page.svelte
Normal file
@@ -0,0 +1,700 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
// 特色项目数据
|
||||||
|
const featuredProjects = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Creative Brand Identity',
|
||||||
|
subtitle: 'Complete visual system for modern brands',
|
||||||
|
badge: 'FEATURED PROJECT',
|
||||||
|
category: 'Brand Design',
|
||||||
|
description: 'A comprehensive brand identity system featuring logo design, color palettes, typography, and brand guidelines.',
|
||||||
|
bgColor: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%), radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.15) 0%, transparent 50%)',
|
||||||
|
textColor: 'white',
|
||||||
|
image: '🎨',
|
||||||
|
buttonText: 'View Project'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Mobile App Interface',
|
||||||
|
subtitle: 'Modern UI/UX for digital experiences',
|
||||||
|
badge: 'UI/UX DESIGN',
|
||||||
|
category: 'Digital Design',
|
||||||
|
description: 'Intuitive mobile interface design with focus on user experience and modern aesthetics.',
|
||||||
|
bgColor: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%), radial-gradient(circle at 20% 80%, rgba(240, 147, 251, 0.3) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.15) 0%, transparent 50%)',
|
||||||
|
textColor: 'white',
|
||||||
|
image: '📱',
|
||||||
|
buttonText: 'Explore'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const smallProjects = [
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Web Portfolio',
|
||||||
|
category: 'Web Design',
|
||||||
|
icon: '💻',
|
||||||
|
bgColor: '#4f46e5'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Print Design',
|
||||||
|
category: 'Graphic Design',
|
||||||
|
icon: '📄',
|
||||||
|
bgColor: '#059669'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: 'Photography',
|
||||||
|
category: 'Visual Arts',
|
||||||
|
icon: '📸',
|
||||||
|
bgColor: '#dc2626'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: 'Illustration',
|
||||||
|
category: 'Digital Art',
|
||||||
|
icon: '🖼️',
|
||||||
|
bgColor: '#7c3aed'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function navigateToProject(projectId: number) {
|
||||||
|
goto(`/projects/${projectId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToProjects() {
|
||||||
|
goto('/projects');
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToContact() {
|
||||||
|
goto('/contact');
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
console.log('Home page mounted');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>LiuBai Design - Creative Studio</title>
|
||||||
|
<meta name="description" content="LiuBai Design - Discover amazing creative works and design solutions" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="today-page">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<header class="page-header">
|
||||||
|
<h1 class="page-title">Today</h1>
|
||||||
|
<p class="page-subtitle">Discover our latest creative works and design solutions</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 主要特色项目 -->
|
||||||
|
<section class="hero-section">
|
||||||
|
<div class="hero-card" style="background: {featuredProjects[0].bgColor}" on:click={() => navigateToProject(featuredProjects[0].id)} role="button" tabindex="0">
|
||||||
|
<div class="hero-content">
|
||||||
|
<div class="hero-badge">{featuredProjects[0].badge}</div>
|
||||||
|
<h2 class="hero-title">{featuredProjects[0].title}</h2>
|
||||||
|
<p class="hero-description">{featuredProjects[0].description}</p>
|
||||||
|
<div class="hero-project-info">
|
||||||
|
<div class="project-icon">{featuredProjects[0].image}</div>
|
||||||
|
<div class="project-details">
|
||||||
|
<h3 class="project-name">{featuredProjects[0].subtitle}</h3>
|
||||||
|
<p class="project-category">{featuredProjects[0].category}</p>
|
||||||
|
</div>
|
||||||
|
<button class="project-button" on:click|stopPropagation={() => navigateToProject(featuredProjects[0].id)}>{featuredProjects[0].buttonText}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 项目展示区域 -->
|
||||||
|
<section class="projects-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Featured Works</h2>
|
||||||
|
<p class="section-subtitle">Explore our creative portfolio</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="projects-grid">
|
||||||
|
<!-- 大型项目卡片 -->
|
||||||
|
<div class="large-project-card" style="background: {featuredProjects[1].bgColor}" on:click={() => navigateToProject(featuredProjects[1].id)} role="button" tabindex="0">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-badge">{featuredProjects[1].badge}</div>
|
||||||
|
<h3 class="card-title">{featuredProjects[1].title}</h3>
|
||||||
|
<p class="card-description">{featuredProjects[1].description}</p>
|
||||||
|
<div class="card-project-info">
|
||||||
|
<div class="project-icon small">{featuredProjects[1].image}</div>
|
||||||
|
<div class="project-details">
|
||||||
|
<h4 class="project-name">{featuredProjects[1].subtitle}</h4>
|
||||||
|
<p class="project-category">{featuredProjects[1].category}</p>
|
||||||
|
</div>
|
||||||
|
<button class="project-button small" on:click|stopPropagation={() => navigateToProject(featuredProjects[1].id)}>{featuredProjects[1].buttonText}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 小型项目卡片网格 -->
|
||||||
|
<div class="small-projects-grid">
|
||||||
|
{#each smallProjects as project}
|
||||||
|
<div class="small-project-card" on:click={navigateToProjects} role="button" tabindex="0">
|
||||||
|
<div class="small-card-icon" style="background-color: {project.bgColor}">
|
||||||
|
{project.icon}
|
||||||
|
</div>
|
||||||
|
<h4 class="small-card-title">{project.name}</h4>
|
||||||
|
<p class="small-card-category">{project.category}</p>
|
||||||
|
<button class="small-card-button" on:click|stopPropagation={navigateToProjects}>View</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 服务展示 -->
|
||||||
|
<section class="services-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Our Services</h2>
|
||||||
|
<p class="section-subtitle">What we can create for you</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="services-grid">
|
||||||
|
<div class="service-card" on:click={navigateToContact} role="button" tabindex="0">
|
||||||
|
<div class="service-icon">🎨</div>
|
||||||
|
<h3 class="service-title">Brand Identity</h3>
|
||||||
|
<p class="service-description">Complete visual identity systems that tell your brand's story</p>
|
||||||
|
</div>
|
||||||
|
<div class="service-card" on:click={navigateToContact} role="button" tabindex="0">
|
||||||
|
<div class="service-icon">💻</div>
|
||||||
|
<h3 class="service-title">Web Design</h3>
|
||||||
|
<p class="service-description">Modern, responsive websites that engage and convert</p>
|
||||||
|
</div>
|
||||||
|
<div class="service-card" on:click={navigateToContact} role="button" tabindex="0">
|
||||||
|
<div class="service-icon">📱</div>
|
||||||
|
<h3 class="service-title">UI/UX Design</h3>
|
||||||
|
<p class="service-description">Intuitive interfaces that users love to interact with</p>
|
||||||
|
</div>
|
||||||
|
<div class="service-card" on:click={navigateToContact} role="button" tabindex="0">
|
||||||
|
<div class="service-icon">📸</div>
|
||||||
|
<h3 class="service-title">Photography</h3>
|
||||||
|
<p class="service-description">Professional photography for brands and products</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.today-page {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
animation: fadeIn 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .page-title {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #6B7280;
|
||||||
|
margin: 0;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .page-subtitle {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section */
|
||||||
|
.hero-section {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 48px;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 400px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card:hover {
|
||||||
|
transform: translateY(-8px) scale(1.02);
|
||||||
|
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 50%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
width: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
animation: slideInUp 0.8s ease-out 0.2s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.2;
|
||||||
|
animation: slideInUp 0.8s ease-out 0.4s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
opacity: 0.9;
|
||||||
|
line-height: 1.5;
|
||||||
|
animation: slideInUp 0.8s ease-out 0.6s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-project-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
animation: slideInUp 0.8s ease-out 0.8s both;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-project-info:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-color: rgba(255, 255, 255, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-icon.small {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
font-size: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-category {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-button {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: width 0.6s, height 0.6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-button:hover::before {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-button.small {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Projects Section */
|
||||||
|
.projects-section {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .section-title {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #6B7280;
|
||||||
|
margin: 0;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .section-subtitle {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-project-card {
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 32px;
|
||||||
|
color: white;
|
||||||
|
min-height: 300px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-project-card:hover {
|
||||||
|
transform: translateY(-6px) scale(1.02);
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-project-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 50%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-description {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
opacity: 0.9;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-project-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-projects-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-project-card {
|
||||||
|
background: white;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 16px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
animation: slideInUp 0.6s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-project-card:nth-child(1) { animation-delay: 0.1s; }
|
||||||
|
.small-project-card:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.small-project-card:nth-child(3) { animation-delay: 0.3s; }
|
||||||
|
.small-project-card:nth-child(4) { animation-delay: 0.4s; }
|
||||||
|
|
||||||
|
:global(.dark) .small-project-card {
|
||||||
|
background: rgba(28, 28, 30, 0.8);
|
||||||
|
color: #f5f5f7;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-project-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .small-project-card:hover {
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-card-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-card-category {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .small-card-category {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-card-button {
|
||||||
|
background: #007AFF;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-card-button:hover {
|
||||||
|
background: #0056CC;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Services Section */
|
||||||
|
.services-section {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card {
|
||||||
|
background: white;
|
||||||
|
padding: 32px 24px;
|
||||||
|
border-radius: 16px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
animation: slideInUp 0.6s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card:nth-child(1) { animation-delay: 0.2s; }
|
||||||
|
.service-card:nth-child(2) { animation-delay: 0.3s; }
|
||||||
|
.service-card:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
.service-card:nth-child(4) { animation-delay: 0.5s; }
|
||||||
|
|
||||||
|
:global(.dark) .service-card {
|
||||||
|
background: rgba(28, 28, 30, 0.8);
|
||||||
|
color: #f5f5f7;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .service-card:hover {
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .service-description {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-card {
|
||||||
|
padding: 32px 24px;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-project-info {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-projects-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
587
src/routes/about/+page.svelte
Normal file
587
src/routes/about/+page.svelte
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
const skills = [
|
||||||
|
{ name: 'Brand Identity', level: 95, icon: '🎨' },
|
||||||
|
{ name: 'UI/UX Design', level: 90, icon: '📱' },
|
||||||
|
{ name: 'Web Development', level: 85, icon: '💻' },
|
||||||
|
{ name: 'Print Design', level: 88, icon: '📄' },
|
||||||
|
{ name: 'Photography', level: 80, icon: '📸' },
|
||||||
|
{ name: 'Illustration', level: 75, icon: '🖼️' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const timeline = [
|
||||||
|
{
|
||||||
|
year: '2024',
|
||||||
|
title: 'Founded LiuBai Design',
|
||||||
|
description: 'Established creative studio focusing on modern brand identity and digital experiences.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: '2023',
|
||||||
|
title: 'Senior Designer at Tech Corp',
|
||||||
|
description: 'Led design team for major product launches and brand redesign initiatives.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: '2022',
|
||||||
|
title: 'Freelance Designer',
|
||||||
|
description: 'Worked with various startups and established companies on branding and web design projects.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: '2021',
|
||||||
|
title: 'Design Degree Completed',
|
||||||
|
description: 'Graduated with honors in Visual Communication Design from Design University.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
console.log('About page mounted');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>About - LiuBai Design</title>
|
||||||
|
<meta name="description" content="Learn about LiuBai Design studio and our creative approach" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="about-page">
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="hero-section">
|
||||||
|
<div class="hero-content">
|
||||||
|
<div class="hero-avatar">
|
||||||
|
<div class="avatar-icon">🎨</div>
|
||||||
|
</div>
|
||||||
|
<h1 class="hero-title">About LiuBai Design</h1>
|
||||||
|
<p class="hero-description">
|
||||||
|
We are a creative studio passionate about crafting meaningful brand experiences
|
||||||
|
and digital solutions that connect with people and drive results.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Story Section -->
|
||||||
|
<section class="story-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Our Story</h2>
|
||||||
|
</div>
|
||||||
|
<div class="story-content">
|
||||||
|
<p class="story-text">
|
||||||
|
Founded in 2024, LiuBai Design emerged from a passion for creating visual experiences
|
||||||
|
that not only look beautiful but also serve a purpose. We believe that great design
|
||||||
|
is the perfect balance between aesthetics and functionality.
|
||||||
|
</p>
|
||||||
|
<p class="story-text">
|
||||||
|
Our approach combines strategic thinking with creative execution, ensuring that every
|
||||||
|
project we undertake delivers both visual impact and measurable results for our clients.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Skills Section -->
|
||||||
|
<section class="skills-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">What We Do</h2>
|
||||||
|
<p class="section-subtitle">Our areas of expertise</p>
|
||||||
|
</div>
|
||||||
|
<div class="skills-grid">
|
||||||
|
{#each skills as skill, index}
|
||||||
|
<div class="skill-card" style="animation-delay: {index * 0.1}s">
|
||||||
|
<div class="skill-icon">{skill.icon}</div>
|
||||||
|
<h3 class="skill-name">{skill.name}</h3>
|
||||||
|
<div class="skill-bar">
|
||||||
|
<div class="skill-progress" style="width: {skill.level}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="skill-level">{skill.level}%</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Timeline Section -->
|
||||||
|
<section class="timeline-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Our Journey</h2>
|
||||||
|
<p class="section-subtitle">Key milestones in our creative journey</p>
|
||||||
|
</div>
|
||||||
|
<div class="timeline">
|
||||||
|
{#each timeline as item, index}
|
||||||
|
<div class="timeline-item" style="animation-delay: {index * 0.2}s">
|
||||||
|
<div class="timeline-marker"></div>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<div class="timeline-year">{item.year}</div>
|
||||||
|
<h3 class="timeline-title">{item.title}</h3>
|
||||||
|
<p class="timeline-description">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Values Section -->
|
||||||
|
<section class="values-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Our Values</h2>
|
||||||
|
</div>
|
||||||
|
<div class="values-grid">
|
||||||
|
<div class="value-card">
|
||||||
|
<div class="value-icon">💡</div>
|
||||||
|
<h3 class="value-title">Innovation</h3>
|
||||||
|
<p class="value-description">
|
||||||
|
We constantly explore new design trends and technologies to deliver cutting-edge solutions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="value-card">
|
||||||
|
<div class="value-icon">🤝</div>
|
||||||
|
<h3 class="value-title">Collaboration</h3>
|
||||||
|
<p class="value-description">
|
||||||
|
We work closely with our clients as partners to understand their vision and goals.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="value-card">
|
||||||
|
<div class="value-icon">⭐</div>
|
||||||
|
<h3 class="value-title">Excellence</h3>
|
||||||
|
<p class="value-description">
|
||||||
|
We strive for perfection in every detail and never compromise on quality.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Contact CTA -->
|
||||||
|
<section class="cta-section">
|
||||||
|
<div class="cta-content">
|
||||||
|
<h2 class="cta-title">Ready to Work Together?</h2>
|
||||||
|
<p class="cta-description">
|
||||||
|
Let's create something amazing together. Get in touch to discuss your project.
|
||||||
|
</p>
|
||||||
|
<button class="cta-button" on:click={() => window.location.href = '/contact'}>
|
||||||
|
Get In Touch
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.about-page {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
animation: fadeIn 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section */
|
||||||
|
.hero-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-avatar {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 32px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-avatar::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .hero-title {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .hero-description {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Headers */
|
||||||
|
.section-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .section-title {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #6B7280;
|
||||||
|
margin: 0;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .section-subtitle {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Story Section */
|
||||||
|
.story-section {
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-content {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-text {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #374151;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .story-text {
|
||||||
|
color: #D1D5DB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skills Section */
|
||||||
|
.skills-section {
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card {
|
||||||
|
background: white;
|
||||||
|
padding: 32px 24px;
|
||||||
|
border-radius: 20px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
animation: slideInUp 0.6s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .skill-card {
|
||||||
|
background: rgba(28, 28, 30, 0.8);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .skill-name {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .skill-bar {
|
||||||
|
background: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-progress {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #007AFF 0%, #00C7FF 100%);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 1s ease-out 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-level {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #007AFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Timeline Section */
|
||||||
|
.timeline-section {
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 20px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .timeline::before {
|
||||||
|
background: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 60px;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
animation: slideInLeft 0.6s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-marker {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 8px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #007AFF;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid white;
|
||||||
|
box-shadow: 0 0 0 3px #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .timeline-marker {
|
||||||
|
border-color: rgba(28, 28, 30, 0.8);
|
||||||
|
box-shadow: 0 0 0 3px #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-year {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #007AFF;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .timeline-title {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-description {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .timeline-description {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Values Section */
|
||||||
|
.values-section {
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.values-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-card {
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .value-title {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-description {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .value-description {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CTA Section */
|
||||||
|
.cta-section {
|
||||||
|
text-align: center;
|
||||||
|
padding: 64px 32px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 24px;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-title {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-description {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
color: white;
|
||||||
|
padding: 16px 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInLeft {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.values-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section {
|
||||||
|
padding: 48px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-title {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
667
src/routes/contact/+page.svelte
Normal file
667
src/routes/contact/+page.svelte
Normal file
@@ -0,0 +1,667 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
let formData = {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
company: '',
|
||||||
|
project: '',
|
||||||
|
budget: '',
|
||||||
|
message: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
let isSubmitting = false;
|
||||||
|
let submitMessage = '';
|
||||||
|
|
||||||
|
const budgetOptions = [
|
||||||
|
'Under $5,000',
|
||||||
|
'$5,000 - $10,000',
|
||||||
|
'$10,000 - $25,000',
|
||||||
|
'$25,000 - $50,000',
|
||||||
|
'Over $50,000'
|
||||||
|
];
|
||||||
|
|
||||||
|
const projectTypes = [
|
||||||
|
'Brand Identity',
|
||||||
|
'Web Design',
|
||||||
|
'Mobile App',
|
||||||
|
'Print Design',
|
||||||
|
'Photography',
|
||||||
|
'Other'
|
||||||
|
];
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
isSubmitting = true;
|
||||||
|
|
||||||
|
// 模拟表单提交
|
||||||
|
try {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// 这里应该是实际的表单提交逻辑
|
||||||
|
console.log('Form submitted:', formData);
|
||||||
|
|
||||||
|
submitMessage = 'Thank you for your message! We\'ll get back to you within 24 hours.';
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
formData = {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
company: '',
|
||||||
|
project: '',
|
||||||
|
budget: '',
|
||||||
|
message: ''
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
submitMessage = 'Sorry, there was an error sending your message. Please try again.';
|
||||||
|
} finally {
|
||||||
|
isSubmitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
console.log('Contact page mounted');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Contact - LiuBai Design</title>
|
||||||
|
<meta name="description" content="Get in touch with LiuBai Design for your next creative project" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="contact-page">
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="hero-section">
|
||||||
|
<div class="hero-content">
|
||||||
|
<h1 class="hero-title">Let's Create Something Amazing</h1>
|
||||||
|
<p class="hero-description">
|
||||||
|
Ready to bring your vision to life? We'd love to hear about your project
|
||||||
|
and discuss how we can help you achieve your goals.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="contact-content">
|
||||||
|
<!-- Contact Form -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-header">
|
||||||
|
<h2 class="form-title">Tell Us About Your Project</h2>
|
||||||
|
<p class="form-description">
|
||||||
|
Fill out the form below and we'll get back to you within 24 hours.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if submitMessage}
|
||||||
|
<div class="submit-message" class:success={submitMessage.includes('Thank you')} class:error={submitMessage.includes('error')}>
|
||||||
|
{submitMessage}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form class="contact-form" on:submit|preventDefault={handleSubmit}>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name" class="form-label">Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
bind:value={formData.name}
|
||||||
|
class="form-input"
|
||||||
|
required
|
||||||
|
placeholder="Your full name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email" class="form-label">Email *</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
bind:value={formData.email}
|
||||||
|
class="form-input"
|
||||||
|
required
|
||||||
|
placeholder="your@email.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="company" class="form-label">Company</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="company"
|
||||||
|
bind:value={formData.company}
|
||||||
|
class="form-input"
|
||||||
|
placeholder="Your company name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="project" class="form-label">Project Type</label>
|
||||||
|
<select id="project" bind:value={formData.project} class="form-select">
|
||||||
|
<option value="">Select project type</option>
|
||||||
|
{#each projectTypes as type}
|
||||||
|
<option value={type}>{type}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="budget" class="form-label">Budget Range</label>
|
||||||
|
<select id="budget" bind:value={formData.budget} class="form-select">
|
||||||
|
<option value="">Select budget range</option>
|
||||||
|
{#each budgetOptions as budget}
|
||||||
|
<option value={budget}>{budget}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="message" class="form-label">Project Details *</label>
|
||||||
|
<textarea
|
||||||
|
id="message"
|
||||||
|
bind:value={formData.message}
|
||||||
|
class="form-textarea"
|
||||||
|
required
|
||||||
|
placeholder="Tell us about your project, goals, timeline, and any specific requirements..."
|
||||||
|
rows="6"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="submit-button"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{#if isSubmitting}
|
||||||
|
<span class="loading-spinner">⏳</span>
|
||||||
|
Sending...
|
||||||
|
{:else}
|
||||||
|
Send Message
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Info -->
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="info-card">
|
||||||
|
<h3 class="info-title">Get In Touch</h3>
|
||||||
|
<div class="contact-methods">
|
||||||
|
<div class="contact-method">
|
||||||
|
<div class="method-icon">📧</div>
|
||||||
|
<div class="method-content">
|
||||||
|
<div class="method-label">Email</div>
|
||||||
|
<div class="method-value">hello@liubaidesign.com</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="contact-method">
|
||||||
|
<div class="method-icon">📱</div>
|
||||||
|
<div class="method-content">
|
||||||
|
<div class="method-label">Phone</div>
|
||||||
|
<div class="method-value">+1 (555) 123-4567</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="contact-method">
|
||||||
|
<div class="method-icon">📍</div>
|
||||||
|
<div class="method-content">
|
||||||
|
<div class="method-label">Location</div>
|
||||||
|
<div class="method-value">San Francisco, CA</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-card">
|
||||||
|
<h3 class="info-title">What Happens Next?</h3>
|
||||||
|
<div class="process-steps">
|
||||||
|
<div class="process-step">
|
||||||
|
<div class="step-number">1</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="step-title">We Review</div>
|
||||||
|
<div class="step-description">We'll review your project details and requirements</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="process-step">
|
||||||
|
<div class="step-number">2</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="step-title">We Connect</div>
|
||||||
|
<div class="step-description">Schedule a call to discuss your vision and goals</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="process-step">
|
||||||
|
<div class="step-number">3</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="step-title">We Propose</div>
|
||||||
|
<div class="step-description">Receive a detailed proposal and timeline</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-card">
|
||||||
|
<h3 class="info-title">Follow Us</h3>
|
||||||
|
<div class="social-links">
|
||||||
|
<a href="#" class="social-link">
|
||||||
|
<div class="social-icon">📘</div>
|
||||||
|
<span>Facebook</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="social-link">
|
||||||
|
<div class="social-icon">📷</div>
|
||||||
|
<span>Instagram</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="social-link">
|
||||||
|
<div class="social-icon">🐦</div>
|
||||||
|
<span>Twitter</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="social-link">
|
||||||
|
<div class="social-icon">💼</div>
|
||||||
|
<span>LinkedIn</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.contact-page {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
animation: fadeIn 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section */
|
||||||
|
.hero-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .hero-title {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .hero-description {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contact Content */
|
||||||
|
.contact-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
gap: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Section */
|
||||||
|
.form-section {
|
||||||
|
background: white;
|
||||||
|
padding: 48px;
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .form-section {
|
||||||
|
background: rgba(28, 28, 30, 0.8);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .form-title {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-description {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #6B7280;
|
||||||
|
margin: 0;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .form-description {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-message {
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-message.success {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-message.error {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .submit-message.success {
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
color: #22c55e;
|
||||||
|
border-color: rgba(34, 197, 94, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .submit-message.error {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #ef4444;
|
||||||
|
border-color: rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styles */
|
||||||
|
.contact-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .form-label {
|
||||||
|
color: #D1D5DB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-select,
|
||||||
|
.form-textarea {
|
||||||
|
padding: 16px;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: white;
|
||||||
|
color: #1d1d1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .form-input,
|
||||||
|
:global(.dark) .form-select,
|
||||||
|
:global(.dark) .form-textarea {
|
||||||
|
background: rgba(55, 65, 81, 0.5);
|
||||||
|
border-color: #4B5563;
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus,
|
||||||
|
.form-select:focus,
|
||||||
|
.form-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007AFF;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
background: #007AFF;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 18px 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover:not(:disabled) {
|
||||||
|
background: #0056CC;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info Section */
|
||||||
|
.info-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
background: white;
|
||||||
|
padding: 32px 24px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .info-card {
|
||||||
|
background: rgba(28, 28, 30, 0.8);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .info-title {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contact Methods */
|
||||||
|
.contact-methods {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-method {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #9CA3AF;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-value {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .method-value {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Process Steps */
|
||||||
|
.process-steps {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-step {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: #007AFF;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d1d1f;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .step-title {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.4;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .step-description {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Social Links */
|
||||||
|
.social-links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
color: #1d1d1f;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .social-link {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link:hover {
|
||||||
|
color: #007AFF;
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 32px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.contact-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
padding: 32px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
410
src/routes/projects/+page.svelte
Normal file
410
src/routes/projects/+page.svelte
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
// 项目数据
|
||||||
|
const projects = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Creative Brand Identity',
|
||||||
|
category: 'Brand Design',
|
||||||
|
description: 'Complete visual identity system for modern brands',
|
||||||
|
image: '🎨',
|
||||||
|
tags: ['Logo Design', 'Brand Guidelines', 'Typography'],
|
||||||
|
status: 'Completed',
|
||||||
|
client: 'Tech Startup',
|
||||||
|
year: '2024'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Mobile App Interface',
|
||||||
|
category: 'UI/UX Design',
|
||||||
|
description: 'Modern mobile interface with focus on user experience',
|
||||||
|
image: '📱',
|
||||||
|
tags: ['Mobile Design', 'User Experience', 'Prototyping'],
|
||||||
|
status: 'In Progress',
|
||||||
|
client: 'E-commerce Platform',
|
||||||
|
year: '2024'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Web Portfolio',
|
||||||
|
category: 'Web Design',
|
||||||
|
description: 'Responsive portfolio website with modern aesthetics',
|
||||||
|
image: '💻',
|
||||||
|
tags: ['Web Development', 'Responsive Design', 'Animation'],
|
||||||
|
status: 'Completed',
|
||||||
|
client: 'Creative Agency',
|
||||||
|
year: '2023'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'Print Design Collection',
|
||||||
|
category: 'Graphic Design',
|
||||||
|
description: 'Various print materials including brochures and posters',
|
||||||
|
image: '📄',
|
||||||
|
tags: ['Print Design', 'Layout', 'Typography'],
|
||||||
|
status: 'Completed',
|
||||||
|
client: 'Local Business',
|
||||||
|
year: '2023'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let selectedCategory = 'All';
|
||||||
|
const categories = ['All', 'Brand Design', 'UI/UX Design', 'Web Design', 'Graphic Design'];
|
||||||
|
|
||||||
|
$: filteredProjects = selectedCategory === 'All'
|
||||||
|
? projects
|
||||||
|
: projects.filter(project => project.category === selectedCategory);
|
||||||
|
|
||||||
|
function navigateToProject(projectId: number) {
|
||||||
|
goto(`/projects/${projectId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCategory(category: string) {
|
||||||
|
selectedCategory = category;
|
||||||
|
// 更新URL参数
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
if (category === 'All') {
|
||||||
|
url.searchParams.delete('category');
|
||||||
|
} else {
|
||||||
|
url.searchParams.set('category', category);
|
||||||
|
}
|
||||||
|
goto(url.pathname + url.search, { replaceState: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// 从URL参数读取分类
|
||||||
|
const urlCategory = $page.url.searchParams.get('category');
|
||||||
|
if (urlCategory && categories.includes(urlCategory)) {
|
||||||
|
selectedCategory = urlCategory;
|
||||||
|
}
|
||||||
|
console.log('Projects page mounted');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Projects - LiuBai Design</title>
|
||||||
|
<meta name="description" content="Explore our creative projects and design portfolio" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="projects-page">
|
||||||
|
<header class="page-header">
|
||||||
|
<h1 class="page-title">Projects</h1>
|
||||||
|
<p class="page-subtitle">Explore our creative works and design solutions</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 分类筛选 -->
|
||||||
|
<div class="filter-section">
|
||||||
|
<div class="filter-tabs">
|
||||||
|
{#each categories as category}
|
||||||
|
<button
|
||||||
|
class="filter-tab"
|
||||||
|
class:active={selectedCategory === category}
|
||||||
|
on:click={() => selectCategory(category)}
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 项目网格 -->
|
||||||
|
<div class="projects-grid">
|
||||||
|
{#each filteredProjects as project (project.id)}
|
||||||
|
<div class="project-card" on:click={() => navigateToProject(project.id)} role="button" tabindex="0">
|
||||||
|
<div class="project-image">
|
||||||
|
<div class="project-icon">{project.image}</div>
|
||||||
|
<div class="project-status" class:completed={project.status === 'Completed'}>
|
||||||
|
{project.status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="project-content">
|
||||||
|
<div class="project-category">{project.category}</div>
|
||||||
|
<h3 class="project-title">{project.title}</h3>
|
||||||
|
<p class="project-description">{project.description}</p>
|
||||||
|
<div class="project-meta">
|
||||||
|
<span class="project-client">{project.client}</span>
|
||||||
|
<span class="project-year">{project.year}</span>
|
||||||
|
</div>
|
||||||
|
<div class="project-tags">
|
||||||
|
{#each project.tags as tag}
|
||||||
|
<span class="tag">{tag}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if filteredProjects.length === 0}
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-icon">🔍</div>
|
||||||
|
<h3>No projects found</h3>
|
||||||
|
<p>Try selecting a different category</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.projects-page {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
animation: fadeIn 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .page-title {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #6B7280;
|
||||||
|
margin: 0;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .page-subtitle {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tab {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
background: white;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .filter-tab {
|
||||||
|
background: rgba(28, 28, 30, 0.8);
|
||||||
|
border-color: #374151;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tab:hover {
|
||||||
|
border-color: #007AFF;
|
||||||
|
color: #007AFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tab.active {
|
||||||
|
background: #007AFF;
|
||||||
|
border-color: #007AFF;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
cursor: pointer;
|
||||||
|
animation: slideInUp 0.6s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .project-card {
|
||||||
|
background: rgba(28, 28, 30, 0.8);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .project-card:hover {
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-image {
|
||||||
|
height: 200px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-status {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-status.completed {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-content {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-category {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #007AFF;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .project-title {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .project-description {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .tag {
|
||||||
|
background: #374151;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 64px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .empty-state h3 {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
color: #6B7280;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .empty-state p {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.projects-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tabs {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tab {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
569
src/routes/projects/[id]/+page.svelte
Normal file
569
src/routes/projects/[id]/+page.svelte
Normal file
@@ -0,0 +1,569 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
// 项目数据(实际应用中应该从API获取)
|
||||||
|
const projectsData = {
|
||||||
|
'1': {
|
||||||
|
id: 1,
|
||||||
|
title: 'Creative Brand Identity',
|
||||||
|
category: 'Brand Design',
|
||||||
|
description: 'A comprehensive brand identity system featuring logo design, color palettes, typography, and brand guidelines for a modern tech startup.',
|
||||||
|
image: '🎨',
|
||||||
|
client: 'Tech Startup',
|
||||||
|
year: '2024',
|
||||||
|
duration: '3 months',
|
||||||
|
status: 'Completed',
|
||||||
|
tags: ['Logo Design', 'Brand Guidelines', 'Typography', 'Color Theory'],
|
||||||
|
challenge: 'Create a modern, memorable brand identity that reflects innovation and trustworthiness while appealing to both B2B and B2C audiences.',
|
||||||
|
solution: 'Developed a clean, geometric logo with a vibrant color palette and comprehensive brand guidelines that ensure consistency across all touchpoints.',
|
||||||
|
results: 'Increased brand recognition by 150% and improved customer engagement across digital platforms.',
|
||||||
|
gallery: ['🎨', '🖼️', '📊', '📱', '💻', '📄']
|
||||||
|
},
|
||||||
|
'2': {
|
||||||
|
id: 2,
|
||||||
|
title: 'Mobile App Interface',
|
||||||
|
category: 'UI/UX Design',
|
||||||
|
description: 'Modern mobile interface design with focus on user experience and accessibility for an e-commerce platform.',
|
||||||
|
image: '📱',
|
||||||
|
client: 'E-commerce Platform',
|
||||||
|
year: '2024',
|
||||||
|
duration: '4 months',
|
||||||
|
status: 'In Progress',
|
||||||
|
tags: ['Mobile Design', 'User Experience', 'Prototyping', 'Accessibility'],
|
||||||
|
challenge: 'Design an intuitive shopping experience that reduces cart abandonment and improves conversion rates.',
|
||||||
|
solution: 'Created a streamlined user flow with clear navigation, optimized product discovery, and simplified checkout process.',
|
||||||
|
results: 'Currently in development phase with promising user testing results showing 40% improvement in task completion.',
|
||||||
|
gallery: ['📱', '🛒', '💳', '🔍', '⭐', '📊']
|
||||||
|
},
|
||||||
|
'3': {
|
||||||
|
id: 3,
|
||||||
|
title: 'Web Portfolio',
|
||||||
|
category: 'Web Design',
|
||||||
|
description: 'Responsive portfolio website with modern aesthetics and smooth animations for a creative agency.',
|
||||||
|
image: '💻',
|
||||||
|
client: 'Creative Agency',
|
||||||
|
year: '2023',
|
||||||
|
duration: '2 months',
|
||||||
|
status: 'Completed',
|
||||||
|
tags: ['Web Development', 'Responsive Design', 'Animation', 'Performance'],
|
||||||
|
challenge: 'Create a visually stunning portfolio that showcases work effectively while maintaining fast loading times.',
|
||||||
|
solution: 'Implemented a clean, grid-based layout with optimized images and smooth CSS animations.',
|
||||||
|
results: 'Achieved 95+ PageSpeed score and increased client inquiries by 200%.',
|
||||||
|
gallery: ['💻', '🎨', '📸', '🎬', '📱', '⚡']
|
||||||
|
},
|
||||||
|
'4': {
|
||||||
|
id: 4,
|
||||||
|
title: 'Print Design Collection',
|
||||||
|
category: 'Graphic Design',
|
||||||
|
description: 'Various print materials including brochures, posters, and business cards for local businesses.',
|
||||||
|
image: '📄',
|
||||||
|
client: 'Local Business',
|
||||||
|
year: '2023',
|
||||||
|
duration: '1 month',
|
||||||
|
status: 'Completed',
|
||||||
|
tags: ['Print Design', 'Layout', 'Typography', 'Brand Consistency'],
|
||||||
|
challenge: 'Create cohesive print materials that work across different formats and maintain brand consistency.',
|
||||||
|
solution: 'Developed a flexible design system that adapts to various print formats while maintaining visual hierarchy.',
|
||||||
|
results: 'Improved brand perception and increased foot traffic by 30%.',
|
||||||
|
gallery: ['📄', '📰', '🏷️', '📋', '📊', '🎨']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$: projectId = $page.params.id;
|
||||||
|
$: project = projectsData[projectId];
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!project) {
|
||||||
|
// 如果项目不存在,重定向到项目列表页
|
||||||
|
window.location.href = '/projects';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{project?.title || 'Project'} - LiuBai Design</title>
|
||||||
|
<meta name="description" content={project?.description || 'Project details'} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
{#if project}
|
||||||
|
<div class="project-detail">
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<button class="back-button" on:click={goBack}>
|
||||||
|
← Back to Projects
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 项目头部 -->
|
||||||
|
<header class="project-header">
|
||||||
|
<div class="project-hero">
|
||||||
|
<div class="project-hero-image">
|
||||||
|
<div class="hero-icon">{project.image}</div>
|
||||||
|
</div>
|
||||||
|
<div class="project-hero-content">
|
||||||
|
<div class="project-category">{project.category}</div>
|
||||||
|
<h1 class="project-title">{project.title}</h1>
|
||||||
|
<p class="project-description">{project.description}</p>
|
||||||
|
|
||||||
|
<div class="project-meta-grid">
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">Client</span>
|
||||||
|
<span class="meta-value">{project.client}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">Year</span>
|
||||||
|
<span class="meta-value">{project.year}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">Duration</span>
|
||||||
|
<span class="meta-value">{project.duration}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">Status</span>
|
||||||
|
<span class="meta-value status" class:completed={project.status === 'Completed'}>
|
||||||
|
{project.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="project-tags">
|
||||||
|
{#each project.tags as tag}
|
||||||
|
<span class="tag">{tag}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 项目内容 -->
|
||||||
|
<div class="project-content">
|
||||||
|
<div class="content-section">
|
||||||
|
<h2>Challenge</h2>
|
||||||
|
<p>{project.challenge}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-section">
|
||||||
|
<h2>Solution</h2>
|
||||||
|
<p>{project.solution}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-section">
|
||||||
|
<h2>Results</h2>
|
||||||
|
<p>{project.results}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-section">
|
||||||
|
<h2>Gallery</h2>
|
||||||
|
<div class="gallery-grid">
|
||||||
|
{#each project.gallery as item, index}
|
||||||
|
<div class="gallery-item" style="animation-delay: {index * 0.1}s">
|
||||||
|
<div class="gallery-icon">{item}</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 相关项目 -->
|
||||||
|
<div class="related-projects">
|
||||||
|
<h2>More Projects</h2>
|
||||||
|
<div class="related-grid">
|
||||||
|
{#each Object.values(projectsData).filter(p => p.id !== project.id).slice(0, 3) as relatedProject}
|
||||||
|
<div class="related-card" on:click={() => window.location.href = `/projects/${relatedProject.id}`}>
|
||||||
|
<div class="related-image">
|
||||||
|
<div class="related-icon">{relatedProject.image}</div>
|
||||||
|
</div>
|
||||||
|
<div class="related-content">
|
||||||
|
<div class="related-category">{relatedProject.category}</div>
|
||||||
|
<h3 class="related-title">{relatedProject.title}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="loading">
|
||||||
|
<div class="loading-spinner">🔄</div>
|
||||||
|
<p>Loading project...</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.project-detail {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
animation: fadeIn 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #007AFF;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
color: #0056CC;
|
||||||
|
transform: translateX(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-header {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 300px 1fr;
|
||||||
|
gap: 48px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-hero-image {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-hero-image::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-icon {
|
||||||
|
font-size: 120px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-category {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #007AFF;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
line-height: 1.2;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .project-title {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-description {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .project-description {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-meta-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #9CA3AF;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .meta-value {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value.status {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value.status.completed {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .tag {
|
||||||
|
background: #374151;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-content {
|
||||||
|
margin-bottom: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section h2 {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .content-section h2 {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section p {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.7;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .content-section p {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
animation: slideInUp 0.6s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .gallery-item {
|
||||||
|
background: rgba(28, 28, 30, 0.8);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-projects {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-projects h2 {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .related-projects h2 {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .related-card {
|
||||||
|
background: rgba(28, 28, 30, 0.8);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-image {
|
||||||
|
height: 150px;
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-category {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #007AFF;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d1d1f;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .related-title {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 400px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
font-size: 48px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.project-hero {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-hero-image {
|
||||||
|
max-width: 200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-icon {
|
||||||
|
font-size: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-meta-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
42
start.md
Normal file
42
start.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 🚀 快速启动指南
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 启动开发服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在浏览器中访问 http://localhost:5173
|
||||||
|
|
||||||
|
## 可用的脚本
|
||||||
|
|
||||||
|
- `npm run dev` - 启动开发服务器
|
||||||
|
- `npm run build` - 构建生产版本
|
||||||
|
- `npm run preview` - 预览生产版本
|
||||||
|
- `npm run check` - 类型检查
|
||||||
|
- `npm run lint` - 代码检查
|
||||||
|
- `npm run format` - 代码格式化
|
||||||
|
|
||||||
|
## 项目特性
|
||||||
|
|
||||||
|
✅ 现代化 UI 组件系统
|
||||||
|
✅ 多主题支持(亮色/暗色)
|
||||||
|
✅ 国际化支持(8种语言)
|
||||||
|
✅ 响应式设计
|
||||||
|
✅ TypeScript 支持
|
||||||
|
✅ 无障碍友好
|
||||||
|
|
||||||
|
## 开始开发
|
||||||
|
|
||||||
|
1. 在 `src/routes/` 中添加新页面
|
||||||
|
2. 在 `src/lib/components/` 中创建新组件
|
||||||
|
3. 在 `src/lib/stores/` 中管理状态
|
||||||
|
4. 在 `src/lib/i18n/` 中添加翻译
|
||||||
|
|
||||||
|
享受开发吧!🎉
|
||||||
12
svelte.config.js
Normal file
12
svelte.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
kit: {
|
||||||
|
adapter: adapter()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
vite.config.ts
Normal file
18
vite.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()],
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
additionalData: '@use "src/lib/styles/variables.scss" as *;'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'~': '/src'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user