Initial commit: 入舍智能家装前端项目
This commit is contained in:
commit
0551fb9e20
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Production
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Trae
|
||||||
|
.trae
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
|
||||||
|
// Remove tseslint.configs.recommended and replace with this
|
||||||
|
tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
tseslint.configs.stylisticTypeChecked,
|
||||||
|
|
||||||
|
// Other configs...
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
// Enable lint rules for React
|
||||||
|
reactX.configs['recommended-typescript'],
|
||||||
|
// Enable lint rules for React DOM
|
||||||
|
reactDom.configs.recommended,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<meta name="theme-color" content="#f97316" />
|
||||||
|
<title>入舍 - 智能家装</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "rushe-ui",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.22.0",
|
||||||
|
"zustand": "^4.5.0",
|
||||||
|
"framer-motion": "^11.0.0",
|
||||||
|
"lucide-react": "^0.323.0",
|
||||||
|
"clsx": "^2.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.55",
|
||||||
|
"@types/react-dom": "^18.2.19",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"autoprefixer": "^10.4.17",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||||
|
import AdminLayout from './layouts/AdminLayout'
|
||||||
|
import UserLayout from './layouts/UserLayout'
|
||||||
|
import AdminMinePage from './pages/admin/mine/MinePage'
|
||||||
|
import ToolboxPage from './pages/admin/toolbox/ToolboxPage'
|
||||||
|
import WorkspacePage from './pages/admin/workspace/WorkspacePage'
|
||||||
|
import AIBudgetPage from './pages/user/ai/AIBudgetPage'
|
||||||
|
import AIPage from './pages/user/ai/AIPage'
|
||||||
|
import BudgetDetailPage from './pages/user/ai/BudgetDetailPage'
|
||||||
|
import ExplorePage from './pages/user/explore/ExplorePage'
|
||||||
|
import BusBookingPage from './pages/user/group/BusBookingPage'
|
||||||
|
import GroupBuyPage from './pages/user/group/GroupBuyPage'
|
||||||
|
import GroupStorePage from './pages/user/group/GroupStorePage'
|
||||||
|
import PackageDetailPage from './pages/user/group/PackageDetailPage'
|
||||||
|
import BalconyPage from './pages/user/home/BalconyPage'
|
||||||
|
import DesignerPage from './pages/user/home/DesignerPage'
|
||||||
|
import HomePage from './pages/user/home/HomePage'
|
||||||
|
import HomePage2 from './pages/user/home/HomePage2'
|
||||||
|
import DataAssetsPage from './pages/user/mine/DataAssetsPage'
|
||||||
|
import DesignSchemesPage from './pages/user/mine/DesignSchemesPage'
|
||||||
|
import MinePage from './pages/user/mine/MinePage'
|
||||||
|
import PartnerCenterPage from './pages/user/mine/PartnerCenterPage'
|
||||||
|
import SettingsPage from './pages/user/mine/SettingsPage'
|
||||||
|
import ProgressPage from './pages/user/progress/ProgressPage'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Navigate to="/user/home" replace />} />
|
||||||
|
|
||||||
|
<Route path="/user" element={<UserLayout />}>
|
||||||
|
<Route path="home" element={<HomePage />} />
|
||||||
|
<Route path="home2" element={<HomePage2 />} />
|
||||||
|
<Route path="designer" element={<DesignerPage />} />
|
||||||
|
<Route path="balcony" element={<BalconyPage />} />
|
||||||
|
<Route path="ai" element={<AIPage />} />
|
||||||
|
<Route path="ai/budget" element={<AIBudgetPage />} />
|
||||||
|
<Route path="ai/budget/:category" element={<BudgetDetailPage />} />
|
||||||
|
<Route path="explore" element={<ExplorePage />} />
|
||||||
|
<Route path="group" element={<GroupBuyPage />} />
|
||||||
|
<Route path="group/store" element={<GroupStorePage />} />
|
||||||
|
<Route path="group/package/:id" element={<PackageDetailPage />} />
|
||||||
|
<Route path="group/bus-booking" element={<BusBookingPage />} />
|
||||||
|
<Route path="progress" element={<ProgressPage />} />
|
||||||
|
<Route path="mine" element={<MinePage />} />
|
||||||
|
<Route path="mine/data" element={<DataAssetsPage />} />
|
||||||
|
<Route path="mine/designs" element={<DesignSchemesPage />} />
|
||||||
|
<Route path="mine/partner" element={<PartnerCenterPage />} />
|
||||||
|
<Route path="mine/settings" element={<SettingsPage />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/admin" element={<AdminLayout />}>
|
||||||
|
<Route path="workspace" element={<WorkspacePage />} />
|
||||||
|
<Route path="mine" element={<AdminMinePage />} />
|
||||||
|
<Route path="toolbox" element={<ToolboxPage />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
import { Calculator, Send, Sparkles, X } from 'lucide-react'
|
||||||
|
import { useRef, useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
id: number
|
||||||
|
type: 'ai' | 'user'
|
||||||
|
content: string
|
||||||
|
time: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AIChatModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialMessages: Message[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 'ai',
|
||||||
|
content: '您好!我是入舍智能家装助手,可以为您提供装修咨询、预算估算、风格推荐等服务。有什么可以帮助您的吗?',
|
||||||
|
time: '刚刚',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const quickQuestions = [
|
||||||
|
'装修大概需要多少钱?',
|
||||||
|
'现代简约风格特点',
|
||||||
|
'如何选择装修材料?',
|
||||||
|
'小户型怎么设计?',
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function AIChatModal({ isOpen, onClose }: AIChatModalProps) {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [messages, setMessages] = useState<Message[]>(initialMessages)
|
||||||
|
const [inputValue, setInputValue] = useState('')
|
||||||
|
const [isTyping, setIsTyping] = useState(false)
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSend = () => {
|
||||||
|
if (!inputValue.trim()) return
|
||||||
|
|
||||||
|
const userMessage: Message = {
|
||||||
|
id: Date.now(),
|
||||||
|
type: 'user',
|
||||||
|
content: inputValue,
|
||||||
|
time: '刚刚',
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, userMessage])
|
||||||
|
setInputValue('')
|
||||||
|
setIsTyping(true)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const aiResponses = [
|
||||||
|
'根据您的需求,我建议您可以先了解一下我们的装修套餐,从基础装修到全屋定制,价格区间在5万到30万不等。您想了解哪个档次的装修方案呢?',
|
||||||
|
'这是一个很好的问题!现代简约风格注重简洁的线条和功能性,通常使用中性色调,搭配少量亮色点缀。家具选择以实用为主,避免过多装饰。',
|
||||||
|
'选择装修材料时,建议关注以下几点:1. 环保等级(E0/E1级)2. 耐用性 3. 性价比 4. 售后服务。我们可以为您提供一线品牌材料,品质有保障。',
|
||||||
|
'小户型设计的关键是"轻装修、重装饰",建议:1. 选择浅色系墙面 2. 利用镜面增加空间感 3. 定制收纳家具 4. 选择多功能家具。',
|
||||||
|
]
|
||||||
|
|
||||||
|
const aiMessage: Message = {
|
||||||
|
id: Date.now(),
|
||||||
|
type: 'ai',
|
||||||
|
content: aiResponses[Math.floor(Math.random() * aiResponses.length)],
|
||||||
|
time: '刚刚',
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, aiMessage])
|
||||||
|
setIsTyping(false)
|
||||||
|
setTimeout(scrollToBottom, 100)
|
||||||
|
}, 1500)
|
||||||
|
|
||||||
|
setTimeout(scrollToBottom, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleQuickQuestion = (question: string) => {
|
||||||
|
setInputValue(question)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBudgetClick = () => {
|
||||||
|
onClose()
|
||||||
|
navigate('/user/ai/budget')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-50"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
|
||||||
|
className="fixed bottom-0 left-0 right-0 h-[75vh] z-50 mx-4 rounded-t-3xl shadow-2xl overflow-hidden flex flex-col"
|
||||||
|
style={{ backgroundColor: 'var(--bg-primary)' }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-b" style={{ borderColor: 'var(--border-color)' }}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center">
|
||||||
|
<Sparkles size={16} className="text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-sm" style={{ color: 'var(--text-primary)' }}>智能管家</h3>
|
||||||
|
<p className="text-xs" style={{ color: 'var(--text-tertiary)' }}>在线</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleBudgetClick}
|
||||||
|
className="flex items-center gap-1 px-3 py-1.5 rounded-full bg-gradient-to-r from-orange-400 to-orange-500 text-white text-xs font-medium shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<Calculator size={14} />
|
||||||
|
装修预算
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-8 h-8 rounded-full flex items-center justify-center hover:bg-gray-100 transition-colors"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto px-4 py-3">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<motion.div
|
||||||
|
key={message.id}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className={`flex mb-3 ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
{message.type === 'ai' && (
|
||||||
|
<div className="w-7 h-7 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center mr-2 flex-shrink-0">
|
||||||
|
<Sparkles size={12} className="text-white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`max-w-[75%] px-3 py-2 rounded-2xl text-sm ${
|
||||||
|
message.type === 'user'
|
||||||
|
? 'bg-primary-500 text-white rounded-br-md'
|
||||||
|
: 'rounded-bl-md'
|
||||||
|
}`}
|
||||||
|
style={message.type === 'ai' ? { backgroundColor: 'var(--bg-secondary)', color: 'var(--text-primary)' } : {}}
|
||||||
|
>
|
||||||
|
<p className="leading-relaxed">{message.content}</p>
|
||||||
|
<p className={`text-xs mt-1 ${message.type === 'user' ? 'text-white/70' : ''}`} style={message.type === 'ai' ? { color: 'var(--text-tertiary)' } : {}}>
|
||||||
|
{message.time}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isTyping && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="flex mb-3 justify-start"
|
||||||
|
>
|
||||||
|
<div className="w-7 h-7 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center mr-2 flex-shrink-0">
|
||||||
|
<Sparkles size={12} className="text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="px-3 py-2 rounded-2xl rounded-bl-md" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-primary-400 animate-bounce" style={{ animationDelay: '0ms' }} />
|
||||||
|
<span className="w-2 h-2 rounded-full bg-primary-400 animate-bounce" style={{ animationDelay: '150ms' }} />
|
||||||
|
<span className="w-2 h-2 rounded-full bg-primary-400 animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 py-2 border-t" style={{ borderColor: 'var(--border-color)' }}>
|
||||||
|
<div className="flex gap-2 mb-2 overflow-x-auto pb-1">
|
||||||
|
{quickQuestions.map((question, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => handleQuickQuestion(question)}
|
||||||
|
className="px-3 py-1 rounded-full text-xs whitespace-nowrap border transition-colors hover:bg-primary-50 hover:border-primary-300"
|
||||||
|
style={{ borderColor: 'var(--border-color)', color: 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
|
{question}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
|
||||||
|
placeholder="输入您的问题..."
|
||||||
|
className="flex-1 px-4 py-2 rounded-full text-sm outline-none"
|
||||||
|
style={{ backgroundColor: 'var(--bg-secondary)', color: 'var(--text-primary)' }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={!inputValue.trim()}
|
||||||
|
className="w-10 h-10 rounded-full bg-primary-500 text-white flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
|
||||||
|
>
|
||||||
|
<Send size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
import { Bell, Check, Filter, Package, Percent, Settings, Trash2, X } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { notifications as initialNotifications } from '@/mock/homeData'
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
id: number
|
||||||
|
type: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
time: string
|
||||||
|
isRead: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotificationModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeConfig: Record<string, { icon: React.ReactNode; color: string; label: string }> = {
|
||||||
|
order: { icon: <Package size={16} />, color: 'bg-blue-500', label: '订单' },
|
||||||
|
promotion: { icon: <Percent size={16} />, color: 'bg-orange-500', label: '优惠' },
|
||||||
|
system: { icon: <Settings size={16} />, color: 'bg-gray-500', label: '系统' },
|
||||||
|
design: { icon: <Bell size={16} />, color: 'bg-purple-500', label: '设计' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NotificationModal({ isOpen, onClose }: NotificationModalProps) {
|
||||||
|
const [notifications, setNotifications] = useState<Notification[]>(initialNotifications)
|
||||||
|
const [activeFilter, setActiveFilter] = useState<string>('all')
|
||||||
|
|
||||||
|
const filters = [
|
||||||
|
{ key: 'all', label: '全部' },
|
||||||
|
{ key: 'unread', label: '未读' },
|
||||||
|
{ key: 'order', label: '订单' },
|
||||||
|
{ key: 'promotion', label: '优惠' },
|
||||||
|
{ key: 'system', label: '系统' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const filteredNotifications = notifications.filter((n) => {
|
||||||
|
if (activeFilter === 'all') return true
|
||||||
|
if (activeFilter === 'unread') return !n.isRead
|
||||||
|
return n.type === activeFilter
|
||||||
|
})
|
||||||
|
|
||||||
|
const unreadCount = notifications.filter((n) => !n.isRead).length
|
||||||
|
|
||||||
|
const markAsRead = (id: number) => {
|
||||||
|
setNotifications((prev) =>
|
||||||
|
prev.map((n) => (n.id === id ? { ...n, isRead: true } : n))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const markAllAsRead = () => {
|
||||||
|
setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true })))
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteNotification = (id: number) => {
|
||||||
|
setNotifications((prev) => prev.filter((n) => n.id !== id))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-50"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: -20, scale: 0.95 }}
|
||||||
|
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
|
||||||
|
className="fixed top-0 left-0 right-0 max-h-[85vh] z-50 mx-4 mt-16 rounded-2xl shadow-2xl overflow-hidden"
|
||||||
|
style={{ backgroundColor: 'var(--bg-primary)' }}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col h-full max-h-[85vh]">
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-b" style={{ borderColor: 'var(--border-color)' }}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-bold text-base" style={{ color: 'var(--text-primary)' }}>通知中心</h3>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<span className="bg-primary-500 text-white text-xs font-bold px-2 py-0.5 rounded-full">
|
||||||
|
{unreadCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={markAllAsRead}
|
||||||
|
className="text-xs text-primary-500 font-medium flex items-center gap-1 px-2 py-1 rounded-lg hover:bg-primary-50 transition-colors"
|
||||||
|
>
|
||||||
|
<Check size={14} />
|
||||||
|
全部已读
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-8 h-8 rounded-full flex items-center justify-center hover:bg-gray-100 transition-colors"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 px-4 py-2 overflow-x-auto border-b" style={{ borderColor: 'var(--border-color)' }}>
|
||||||
|
<Filter size={14} style={{ color: 'var(--text-tertiary)' }} />
|
||||||
|
{filters.map((filter) => (
|
||||||
|
<button
|
||||||
|
key={filter.key}
|
||||||
|
onClick={() => setActiveFilter(filter.key)}
|
||||||
|
className={`px-3 py-1 rounded-full text-xs font-medium whitespace-nowrap transition-all ${
|
||||||
|
activeFilter === filter.key
|
||||||
|
? 'bg-primary-500 text-white'
|
||||||
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{filter.label}
|
||||||
|
{filter.key === 'unread' && unreadCount > 0 && (
|
||||||
|
<span className="ml-1">({unreadCount})</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{filteredNotifications.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12" style={{ color: 'var(--text-tertiary)' }}>
|
||||||
|
<Bell size={48} className="opacity-30 mb-3" />
|
||||||
|
<p className="text-sm">暂无通知</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y" style={{ borderColor: 'var(--border-color)' }}>
|
||||||
|
{filteredNotifications.map((notification, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={notification.id}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
className={`relative px-4 py-3 hover:bg-gray-50 transition-colors ${
|
||||||
|
!notification.isRead ? 'bg-primary-50/30' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{!notification.isRead && (
|
||||||
|
<div className="absolute left-2 top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-primary-500" />
|
||||||
|
)}
|
||||||
|
<div className="flex items-start gap-3 ml-2">
|
||||||
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-white flex-shrink-0 ${typeConfig[notification.type]?.color || 'bg-gray-500'}`}>
|
||||||
|
{typeConfig[notification.type]?.icon || <Bell size={16} />}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<h4 className="font-medium text-sm truncate" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
{notification.title}
|
||||||
|
</h4>
|
||||||
|
<span className="text-xs flex-shrink-0" style={{ color: 'var(--text-tertiary)' }}>
|
||||||
|
{notification.time.split(' ')[1]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs mt-1 line-clamp-2" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
{notification.content}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
{!notification.isRead && (
|
||||||
|
<button
|
||||||
|
onClick={() => markAsRead(notification.id)}
|
||||||
|
className="text-xs text-primary-500 font-medium flex items-center gap-1 hover:underline"
|
||||||
|
>
|
||||||
|
<Check size={12} />
|
||||||
|
标为已读
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => deleteNotification(notification.id)}
|
||||||
|
className="text-xs text-red-500 font-medium flex items-center gap-1 hover:underline"
|
||||||
|
>
|
||||||
|
<Trash2 size={12} />
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useThemeStore, ThemeMode } from '@/store/themeStore'
|
||||||
|
|
||||||
|
interface ThemeProviderProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const getThemeClass = (theme: ThemeMode): string => {
|
||||||
|
switch (theme) {
|
||||||
|
case 'dark':
|
||||||
|
return 'dark'
|
||||||
|
case 'deepBlue':
|
||||||
|
return 'deep-blue'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ThemeProvider({ children }: ThemeProviderProps) {
|
||||||
|
const { theme } = useThemeStore()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.documentElement
|
||||||
|
root.classList.remove('dark', 'deep-blue')
|
||||||
|
|
||||||
|
const themeClass = getThemeClass(theme)
|
||||||
|
if (themeClass) {
|
||||||
|
root.classList.add(themeClass)
|
||||||
|
}
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--safe-area-inset-top: env(safe-area-inset-top);
|
||||||
|
--safe-area-inset-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-primary: #ffffff;
|
||||||
|
--bg-secondary: #f5f5f5;
|
||||||
|
--bg-tertiary: #f9fafb;
|
||||||
|
--text-primary: #1f2937;
|
||||||
|
--text-secondary: #6b7280;
|
||||||
|
--text-tertiary: #9ca3af;
|
||||||
|
--border-color: #e5e7eb;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.05);
|
||||||
|
--gradient-start: #f97316;
|
||||||
|
--gradient-end: #ea580c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--bg-primary: #1f2937;
|
||||||
|
--bg-secondary: #111827;
|
||||||
|
--bg-tertiary: #374151;
|
||||||
|
--text-primary: #f9fafb;
|
||||||
|
--text-secondary: #d1d5db;
|
||||||
|
--text-tertiary: #9ca3af;
|
||||||
|
--border-color: #374151;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||||
|
--gradient-start: #f97316;
|
||||||
|
--gradient-end: #ea580c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deep-blue {
|
||||||
|
--bg-primary: #0f172a;
|
||||||
|
--bg-secondary: #020617;
|
||||||
|
--bg-tertiary: #1e293b;
|
||||||
|
--text-primary: #f1f5f9;
|
||||||
|
--text-secondary: #cbd5e1;
|
||||||
|
--text-tertiary: #94a3b8;
|
||||||
|
--border-color: #334155;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.4);
|
||||||
|
--gradient-start: #3b82f6;
|
||||||
|
--gradient-end: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-bottom: calc(60px + env(safe-area-inset-bottom));
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
box-shadow: 0 1px 3px var(--shadow-color);
|
||||||
|
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
@apply bg-primary-500 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200;
|
||||||
|
@apply hover:bg-primary-600 active:scale-95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply bg-gray-100 text-gray-700 px-4 py-2 rounded-lg font-medium transition-all duration-200;
|
||||||
|
@apply hover:bg-gray-200 active:scale-95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
@apply w-full px-4 py-3 border border-gray-200 rounded-lg text-base;
|
||||||
|
@apply focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.text-gradient {
|
||||||
|
@apply bg-gradient-to-r from-primary-500 to-primary-600 bg-clip-text text-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-bottom {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-top {
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
import { Briefcase, Wrench, User } from 'lucide-react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ path: '/admin/workspace', icon: Briefcase, label: '工作台' },
|
||||||
|
{ path: '/admin/toolbox', icon: Wrench, label: '百宝箱' },
|
||||||
|
{ path: '/admin/mine', icon: User, label: '我的' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function AdminLayout() {
|
||||||
|
const location = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const isActive = (path: string) => location.pathname.startsWith(path)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex flex-col">
|
||||||
|
<main className="flex-1 pb-20">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 safe-bottom z-50">
|
||||||
|
<div className="flex justify-around items-center h-14 max-w-lg mx-auto">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const active = isActive(tab.path)
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
key={tab.path}
|
||||||
|
onClick={() => navigate(tab.path)}
|
||||||
|
className="flex flex-col items-center justify-center flex-1 h-full"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<tab.icon
|
||||||
|
size={22}
|
||||||
|
className={active ? 'text-primary-500' : 'text-gray-400'}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`text-xs mt-1 ${
|
||||||
|
active ? 'text-primary-500 font-medium' : 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</span>
|
||||||
|
{active && (
|
||||||
|
<motion.div
|
||||||
|
layoutId="activeAdminTab"
|
||||||
|
className="absolute -top-0.5 w-8 h-0.5 bg-primary-500 rounded-full"
|
||||||
|
initial={false}
|
||||||
|
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
import { Compass, Home, ShoppingBag, Sparkles, User } from 'lucide-react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ path: '/user/home', icon: Home, label: '首页' },
|
||||||
|
{ path: '/user/ai', icon: Sparkles, label: '智能管家' },
|
||||||
|
{ path: '/user/explore', icon: Compass, label: '逛逛' },
|
||||||
|
{ path: '/user/group', icon: ShoppingBag, label: '团购' },
|
||||||
|
{ path: '/user/mine', icon: User, label: '我的' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function UserLayout() {
|
||||||
|
const location = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const isActive = (path: string) => {
|
||||||
|
if (path === '/user/home') {
|
||||||
|
return location.pathname === '/user/home' || location.pathname === '/user/home2'
|
||||||
|
}
|
||||||
|
return location.pathname.startsWith(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||||
|
<main className="flex-1 pb-20">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav className="fixed bottom-0 left-0 right-0 border-t safe-bottom z-50" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-color)' }}>
|
||||||
|
<div className="flex justify-around items-center h-14 max-w-lg mx-auto">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const active = isActive(tab.path)
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
key={tab.path}
|
||||||
|
onClick={() => navigate(tab.path)}
|
||||||
|
className="flex flex-col items-center justify-center flex-1 h-full"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<tab.icon
|
||||||
|
size={22}
|
||||||
|
className={active ? 'text-primary-500' : ''}
|
||||||
|
style={{ color: active ? undefined : 'var(--text-tertiary)' }}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`text-xs mt-1 ${active ? 'text-primary-500 font-medium' : ''}`}
|
||||||
|
style={{ color: active ? undefined : 'var(--text-tertiary)' }}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import App from './App'
|
||||||
|
import ThemeProvider from './components/ThemeProvider'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
|
<ThemeProvider>
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
export const workspaceTasks = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '张先生 - 阳光花园',
|
||||||
|
type: '报价待确认',
|
||||||
|
status: 'urgent',
|
||||||
|
amount: 158000,
|
||||||
|
createTime: '2024-01-28 10:30',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '李女士 - 翠湖花园',
|
||||||
|
type: '二期款待收',
|
||||||
|
status: 'pending',
|
||||||
|
amount: 45000,
|
||||||
|
createTime: '2024-01-27 15:20',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '王先生 - 金色家园',
|
||||||
|
type: '方案待审核',
|
||||||
|
status: 'normal',
|
||||||
|
amount: 0,
|
||||||
|
createTime: '2024-01-26 09:15',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: '赵女士 - 幸福小区',
|
||||||
|
type: '验收待安排',
|
||||||
|
status: 'normal',
|
||||||
|
amount: 0,
|
||||||
|
createTime: '2024-01-25 14:00',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const aiNotifications = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 'payment',
|
||||||
|
title: 'AI催款提醒',
|
||||||
|
content: '李女士的二期款已逾期3天,建议及时跟进',
|
||||||
|
action: '立即催款',
|
||||||
|
amount: 45000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: 'quote',
|
||||||
|
title: 'AI报价推送',
|
||||||
|
content: '张先生的报价已生成,等待客户确认',
|
||||||
|
action: '查看报价',
|
||||||
|
amount: 158000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
type: 'bonus',
|
||||||
|
title: '挽回奖金',
|
||||||
|
content: '成功挽回王先生的订单,奖金已到账',
|
||||||
|
action: '查看详情',
|
||||||
|
amount: 2000,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const statistics = {
|
||||||
|
todayOrders: 5,
|
||||||
|
monthOrders: 28,
|
||||||
|
monthAmount: 458000,
|
||||||
|
completionRate: 92,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toolboxItems = [
|
||||||
|
{ id: 1, title: '报价计算器', icon: 'calculator', desc: '快速生成报价单' },
|
||||||
|
{ id: 2, title: '合同模板', icon: 'file-text', desc: '标准合同下载' },
|
||||||
|
{ id: 3, title: '材料清单', icon: 'list', desc: '材料用量计算' },
|
||||||
|
{ id: 4, title: '进度模板', icon: 'calendar', desc: '施工进度表' },
|
||||||
|
{ id: 5, title: '验收标准', icon: 'check-square', desc: '验收规范查询' },
|
||||||
|
{ id: 6, title: '客户管理', icon: 'users', desc: '客户信息管理' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const adminUserInfo = {
|
||||||
|
id: 1,
|
||||||
|
name: '项目经理张',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200',
|
||||||
|
phone: '139****9999',
|
||||||
|
department: '工程部',
|
||||||
|
position: '项目经理',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
export const budgetCategories = [
|
||||||
|
{ id: 'kitchen', name: '厨房', icon: '🍳', count: 15 },
|
||||||
|
{ id: 'bathroom', name: '卫浴', icon: '🛁', count: 12 },
|
||||||
|
{ id: 'living', name: '厅房', icon: '🛋️', count: 18 },
|
||||||
|
{ id: 'balcony', name: '阳台', icon: '🌿', count: 8 },
|
||||||
|
{ id: 'garden', name: '花园', icon: '🌸', count: 6 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const budgetItems = {
|
||||||
|
kitchen: [
|
||||||
|
{ id: 1, name: '整体橱柜', brand: '欧派', spec: '定制', price: 15000, unit: '套' },
|
||||||
|
{ id: 2, name: '油烟机', brand: '方太', spec: '侧吸式', price: 3500, unit: '台' },
|
||||||
|
{ id: 3, name: '燃气灶', brand: '老板', spec: '嵌入式', price: 2000, unit: '台' },
|
||||||
|
{ id: 4, name: '消毒柜', brand: '美的', spec: '嵌入式', price: 1500, unit: '台' },
|
||||||
|
{ id: 5, name: '水槽龙头', brand: '九牧', spec: '不锈钢', price: 800, unit: '套' },
|
||||||
|
{ id: 6, name: '厨房吊顶', brand: '奥普', spec: '铝扣板', price: 1200, unit: '㎡' },
|
||||||
|
{ id: 7, name: '厨房瓷砖', brand: '东鹏', spec: '300x600', price: 45, unit: '片' },
|
||||||
|
{ id: 8, name: '厨房防水', brand: '东方雨虹', spec: '柔性', price: 65, unit: '㎡' },
|
||||||
|
],
|
||||||
|
bathroom: [
|
||||||
|
{ id: 1, name: '马桶', brand: 'TOTO', spec: '智能款', price: 4500, unit: '个' },
|
||||||
|
{ id: 2, name: '浴室柜', brand: '箭牌', spec: '实木', price: 3500, unit: '套' },
|
||||||
|
{ id: 3, name: '淋浴花洒', brand: '汉斯格雅', spec: '恒温', price: 2800, unit: '套' },
|
||||||
|
{ id: 4, name: '浴缸', brand: '科勒', spec: '亚克力', price: 6000, unit: '个' },
|
||||||
|
{ id: 5, name: '浴室镜', brand: '定制', spec: '带灯', price: 800, unit: '面' },
|
||||||
|
{ id: 6, name: '卫浴五金', brand: '九牧', spec: '套装', price: 1200, unit: '套' },
|
||||||
|
{ id: 7, name: '卫生间瓷砖', brand: '马可波罗', spec: '300x300', price: 35, unit: '片' },
|
||||||
|
{ id: 8, name: '卫生间防水', brand: '东方雨虹', spec: '柔性', price: 75, unit: '㎡' },
|
||||||
|
],
|
||||||
|
living: [
|
||||||
|
{ id: 1, name: '客厅沙发', brand: '顾家', spec: '真皮', price: 12000, unit: '套' },
|
||||||
|
{ id: 2, name: '电视柜', brand: '全友', spec: '实木', price: 3500, unit: '个' },
|
||||||
|
{ id: 3, name: '茶几', brand: '林氏木业', spec: '岩板', price: 1800, unit: '个' },
|
||||||
|
{ id: 4, name: '餐桌餐椅', brand: '曲美', spec: '一桌六椅', price: 4500, unit: '套' },
|
||||||
|
{ id: 5, name: '床', brand: '喜临门', spec: '1.8米', price: 5000, unit: '张' },
|
||||||
|
{ id: 6, name: '床垫', brand: '席梦思', spec: '乳胶', price: 6000, unit: '张' },
|
||||||
|
{ id: 7, name: '衣柜', brand: '索菲亚', spec: '定制', price: 12000, unit: '套' },
|
||||||
|
{ id: 8, name: '书桌', brand: '宜家', spec: '实木', price: 1500, unit: '张' },
|
||||||
|
],
|
||||||
|
balcony: [
|
||||||
|
{ id: 1, name: '阳台推拉门', brand: '凤铝', spec: '断桥铝', price: 800, unit: '㎡' },
|
||||||
|
{ id: 2, name: '阳台护栏', brand: '定制', spec: '不锈钢', price: 300, unit: '米' },
|
||||||
|
{ id: 3, name: '阳台地砖', brand: '东鹏', spec: '防滑', price: 55, unit: '片' },
|
||||||
|
{ id: 4, name: '阳台吊柜', brand: '定制', spec: '防水', price: 800, unit: '米' },
|
||||||
|
{ id: 5, name: '晾衣架', brand: '好太太', spec: '电动', price: 2000, unit: '套' },
|
||||||
|
{ id: 6, name: '阳台防水', brand: '东方雨虹', spec: '柔性', price: 55, unit: '㎡' },
|
||||||
|
],
|
||||||
|
garden: [
|
||||||
|
{ id: 1, name: '花园围栏', brand: '定制', spec: '防腐木', price: 350, unit: '米' },
|
||||||
|
{ id: 2, name: '花园地砖', brand: '户外专用', spec: '透水砖', price: 85, unit: '㎡' },
|
||||||
|
{ id: 3, name: '花架', brand: '定制', spec: '防腐木', price: 3000, unit: '套' },
|
||||||
|
{ id: 4, name: '花园灯具', brand: '欧普', spec: '太阳能', price: 150, unit: '个' },
|
||||||
|
{ id: 5, name: '自动灌溉系统', brand: '雨鸟', spec: '智能', price: 5000, unit: '套' },
|
||||||
|
{ id: 6, name: '花园座椅', brand: '户外专用', spec: '藤编', price: 2500, unit: '套' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectedItems = [
|
||||||
|
{ categoryId: 'kitchen', itemId: 1, quantity: 1 },
|
||||||
|
{ categoryId: 'kitchen', itemId: 2, quantity: 1 },
|
||||||
|
{ categoryId: 'bathroom', itemId: 1, quantity: 2 },
|
||||||
|
{ categoryId: 'living', itemId: 1, quantity: 1 },
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
export const groupBuyPackages = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '极速焕新套餐',
|
||||||
|
subtitle: '7天快速改造',
|
||||||
|
price: 29999,
|
||||||
|
originalPrice: 39999,
|
||||||
|
image: 'https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?w=600',
|
||||||
|
features: ['全屋墙面翻新', '地板更换', '厨卫改造', '软装搭配'],
|
||||||
|
sales: 128,
|
||||||
|
rating: 4.8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '品质整装套餐',
|
||||||
|
subtitle: '一站式服务',
|
||||||
|
price: 89999,
|
||||||
|
originalPrice: 129999,
|
||||||
|
image: 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=600',
|
||||||
|
features: ['全屋设计', '主材包', '施工服务', '家具软装'],
|
||||||
|
sales: 86,
|
||||||
|
rating: 4.9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '智能家装套餐',
|
||||||
|
subtitle: '科技赋能生活',
|
||||||
|
price: 159999,
|
||||||
|
originalPrice: 199999,
|
||||||
|
image: 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=600',
|
||||||
|
features: ['全屋智能', '灯光系统', '安防系统', '环境控制'],
|
||||||
|
sales: 45,
|
||||||
|
rating: 4.7,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const factoryStores = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '佛山陶瓷工厂店',
|
||||||
|
address: '佛山市禅城区南庄镇',
|
||||||
|
distance: '15km',
|
||||||
|
image: 'https://images.unsplash.com/photo-1600566753086-00f18fb6b3ea?w=600',
|
||||||
|
rating: 4.8,
|
||||||
|
products: ['瓷砖', '大理石', '马赛克'],
|
||||||
|
busAvailable: true,
|
||||||
|
busTime: '每周六 9:00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '顺德家具工厂店',
|
||||||
|
address: '佛山市顺德区龙江镇',
|
||||||
|
distance: '25km',
|
||||||
|
image: 'https://images.unsplash.com/photo-1600573472592-401b489a3cdc?w=600',
|
||||||
|
rating: 4.9,
|
||||||
|
products: ['沙发', '床具', '餐桌'],
|
||||||
|
busAvailable: true,
|
||||||
|
busTime: '每周日 9:00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '中山灯饰工厂店',
|
||||||
|
address: '中山市古镇镇',
|
||||||
|
distance: '35km',
|
||||||
|
image: 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=600',
|
||||||
|
rating: 4.7,
|
||||||
|
products: ['吊灯', '台灯', '智能灯'],
|
||||||
|
busAvailable: false,
|
||||||
|
busTime: '',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const busSeats = Array.from({ length: 45 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
row: Math.floor(i / 4) + 1,
|
||||||
|
col: (i % 4) + 1,
|
||||||
|
status: Math.random() > 0.3 ? 'available' : 'occupied',
|
||||||
|
price: 0,
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const renovationSchemes = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '现代简约方案',
|
||||||
|
image: 'https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?w=600',
|
||||||
|
description: '简约不简单,追求功能与美学的完美平衡',
|
||||||
|
price: 15000,
|
||||||
|
duration: '30天',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '北欧风格方案',
|
||||||
|
image: 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=600',
|
||||||
|
description: '自然清新,营造温馨舒适的居家氛围',
|
||||||
|
price: 18000,
|
||||||
|
duration: '35天',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,299 @@
|
||||||
|
export const homeBanners = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '春季家装节',
|
||||||
|
subtitle: '限时优惠',
|
||||||
|
image: 'https://images.unsplash.com/photo-1586023492125-27b2c045efd7?w=800',
|
||||||
|
link: '/user/group',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'AI智能设计',
|
||||||
|
subtitle: '免费体验',
|
||||||
|
image: 'https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?w=800',
|
||||||
|
link: '/user/ai',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '品质团购',
|
||||||
|
subtitle: '工厂直供',
|
||||||
|
image: 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=800',
|
||||||
|
link: '/user/group',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const quickActions = [
|
||||||
|
{ id: 1, icon: 'calculator', title: '装修预算', desc: 'AI智能计算', link: '/user/ai/budget' },
|
||||||
|
{ id: 2, icon: 'palette', title: '设计方案', desc: '专业设计师', link: '/user/mine/designs' },
|
||||||
|
{ id: 3, icon: 'truck', title: '团购优惠', desc: '工厂直供', link: '/user/group' },
|
||||||
|
{ id: 4, icon: 'calendar', title: '预约服务', desc: '上门量房', link: '/user/home' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const designers = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '张设计',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200',
|
||||||
|
title: '首席设计师',
|
||||||
|
experience: '10年',
|
||||||
|
style: '现代简约',
|
||||||
|
rating: 4.9,
|
||||||
|
projects: 128,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '李雅',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200',
|
||||||
|
title: '资深设计师',
|
||||||
|
experience: '8年',
|
||||||
|
style: '北欧风格',
|
||||||
|
rating: 4.8,
|
||||||
|
projects: 96,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '王明',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200',
|
||||||
|
title: '高级设计师',
|
||||||
|
experience: '6年',
|
||||||
|
style: '中式风格',
|
||||||
|
rating: 4.7,
|
||||||
|
projects: 72,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const groupPackages = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '全屋定制套餐',
|
||||||
|
image: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=400',
|
||||||
|
originalPrice: 89999,
|
||||||
|
groupPrice: 59999,
|
||||||
|
sold: 128,
|
||||||
|
tag: '限时秒杀',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '厨房焕新套餐',
|
||||||
|
image: 'https://images.unsplash.com/photo-1556909114-44e3e70034e2?w=400',
|
||||||
|
originalPrice: 39999,
|
||||||
|
groupPrice: 25999,
|
||||||
|
sold: 96,
|
||||||
|
tag: '爆款',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '卫浴升级套餐',
|
||||||
|
image: 'https://images.unsplash.com/photo-1552321554-5fefe8c9ef14?w=400',
|
||||||
|
originalPrice: 29999,
|
||||||
|
groupPrice: 19999,
|
||||||
|
sold: 72,
|
||||||
|
tag: '热销',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const featuredProjects = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '现代简约三居室',
|
||||||
|
area: '120㎡',
|
||||||
|
style: '现代简约',
|
||||||
|
budget: '15-20万',
|
||||||
|
image: 'https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?w=600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '北欧风两居室',
|
||||||
|
area: '85㎡',
|
||||||
|
style: '北欧风格',
|
||||||
|
budget: '10-15万',
|
||||||
|
image: 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '新中式别墅',
|
||||||
|
area: '280㎡',
|
||||||
|
style: '新中式',
|
||||||
|
budget: '50-80万',
|
||||||
|
image: 'https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?w=600',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const balconyDesigns = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '休闲阳台',
|
||||||
|
desc: '打造惬意休闲空间',
|
||||||
|
image: 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=600',
|
||||||
|
features: ['绿植装饰', '休闲座椅', '遮阳设施'],
|
||||||
|
price: '3000-8000',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '洗衣阳台',
|
||||||
|
desc: '实用功能型设计',
|
||||||
|
image: 'https://images.unsplash.com/photo-1600566753086-00f18fb6b3ea?w=600',
|
||||||
|
features: ['洗衣区', '收纳柜', '晾晒区'],
|
||||||
|
price: '2000-5000',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '花园阳台',
|
||||||
|
desc: '都市中的小花园',
|
||||||
|
image: 'https://images.unsplash.com/photo-1600573472592-401b489a3cdc?w=600',
|
||||||
|
features: ['花架', '园艺工具', '自动灌溉'],
|
||||||
|
price: '5000-15000',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const notifications = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 'order',
|
||||||
|
title: '订单发货通知',
|
||||||
|
content: '您的订单 #20240315001 已发货,预计3天内送达',
|
||||||
|
time: '2024-03-15 14:30',
|
||||||
|
isRead: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: 'promotion',
|
||||||
|
title: '限时优惠活动',
|
||||||
|
content: '春季家装节火热进行中,全场套餐低至6折起',
|
||||||
|
time: '2024-03-15 10:00',
|
||||||
|
isRead: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
type: 'system',
|
||||||
|
title: '系统升级通知',
|
||||||
|
content: '系统将于今晚22:00-23:00进行维护升级,届时部分功能暂不可用',
|
||||||
|
time: '2024-03-14 18:00',
|
||||||
|
isRead: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
type: 'design',
|
||||||
|
title: '设计方案完成',
|
||||||
|
content: '您预约的设计方案"现代简约三居室"已完成,点击查看详情',
|
||||||
|
time: '2024-03-14 15:20',
|
||||||
|
isRead: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
type: 'order',
|
||||||
|
title: '订单支付成功',
|
||||||
|
content: '您已成功支付订单 #20240313002,金额¥25999',
|
||||||
|
time: '2024-03-13 09:45',
|
||||||
|
isRead: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
type: 'promotion',
|
||||||
|
title: '优惠券到账',
|
||||||
|
content: '恭喜您获得200元优惠券一张,有效期30天',
|
||||||
|
time: '2024-03-12 16:30',
|
||||||
|
isRead: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const exploreContents = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 'designer',
|
||||||
|
author: {
|
||||||
|
name: '李雅设计',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200',
|
||||||
|
title: '资深设计师',
|
||||||
|
},
|
||||||
|
title: '120㎡现代简约,打造温馨三口之家',
|
||||||
|
content: '这套案例以白色和原木色为主调,搭配简洁的线条设计,营造出明亮通透的居住空间。客厅采用大面积落地窗,引入充足自然光...',
|
||||||
|
images: [
|
||||||
|
'https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?w=600',
|
||||||
|
'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=600',
|
||||||
|
],
|
||||||
|
likes: 328,
|
||||||
|
comments: 56,
|
||||||
|
shares: 23,
|
||||||
|
time: '2小时前',
|
||||||
|
tags: ['现代简约', '三居室', '120㎡'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: 'owner',
|
||||||
|
author: {
|
||||||
|
name: '小王装修记',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200',
|
||||||
|
title: '装修达人',
|
||||||
|
},
|
||||||
|
title: '历时3个月,我的北欧风小窝终于完工啦!',
|
||||||
|
content: '从毛坯到入住,记录每一个装修细节。厨房采用了U型布局,最大化利用空间;卧室选用了浅灰色墙面,搭配原木家具...',
|
||||||
|
images: [
|
||||||
|
'https://images.unsplash.com/photo-1600566753086-00f18fb6b3ea?w=600',
|
||||||
|
'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=600',
|
||||||
|
'https://images.unsplash.com/photo-1600573472592-401b489a3cdc?w=600',
|
||||||
|
],
|
||||||
|
likes: 892,
|
||||||
|
comments: 134,
|
||||||
|
shares: 67,
|
||||||
|
time: '5小时前',
|
||||||
|
tags: ['北欧风', '装修日记', '两居室'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
type: 'designer',
|
||||||
|
author: {
|
||||||
|
name: '张设计工作室',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200',
|
||||||
|
title: '首席设计师',
|
||||||
|
},
|
||||||
|
title: '新中式别墅设计,传承与现代的完美融合',
|
||||||
|
content: '本案将传统中式元素与现代设计手法相结合,通过木质格栅、水墨画背景墙、禅意茶室等设计,打造出既有东方韵味又不失现代感的居住空间...',
|
||||||
|
images: [
|
||||||
|
'https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?w=600',
|
||||||
|
],
|
||||||
|
likes: 567,
|
||||||
|
comments: 89,
|
||||||
|
shares: 45,
|
||||||
|
time: '昨天',
|
||||||
|
tags: ['新中式', '别墅', '280㎡'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
type: 'owner',
|
||||||
|
author: {
|
||||||
|
name: '装修小白成长记',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=200',
|
||||||
|
title: '业主',
|
||||||
|
},
|
||||||
|
title: '小户型逆袭!35㎡变身精致单身公寓',
|
||||||
|
content: '谁说小户型不能有品质?通过巧妙的空间规划和收纳设计,我的35㎡小窝实现了客餐厨卫卧一应俱全,分享我的改造心得...',
|
||||||
|
images: [
|
||||||
|
'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=600',
|
||||||
|
'https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?w=600',
|
||||||
|
],
|
||||||
|
likes: 1256,
|
||||||
|
comments: 234,
|
||||||
|
shares: 189,
|
||||||
|
time: '昨天',
|
||||||
|
tags: ['小户型', '单身公寓', '收纳设计'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
type: 'video',
|
||||||
|
author: {
|
||||||
|
name: '入舍设计官方',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1568602471122-7832951cc4c5?w=200',
|
||||||
|
title: '官方认证',
|
||||||
|
},
|
||||||
|
title: '【视频】全屋定制避坑指南,设计师亲授',
|
||||||
|
content: '全屋定制有哪些坑?如何避免踩雷?本期视频邀请资深设计师为大家详细讲解全屋定制的注意事项...',
|
||||||
|
video: 'https://example.com/video.mp4',
|
||||||
|
videoCover: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=600',
|
||||||
|
likes: 2341,
|
||||||
|
comments: 456,
|
||||||
|
shares: 321,
|
||||||
|
time: '3天前',
|
||||||
|
tags: ['全屋定制', '避坑指南', '设计师'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
export const userInfo = {
|
||||||
|
id: 1,
|
||||||
|
name: '用户小明',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200',
|
||||||
|
phone: '138****8888',
|
||||||
|
level: 'VIP会员',
|
||||||
|
points: 2580,
|
||||||
|
coupons: 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dataAssets = [
|
||||||
|
{ id: 1, title: '设计方案', count: 3, icon: 'palette' },
|
||||||
|
{ id: 2, title: '报价单', count: 2, icon: 'file-text' },
|
||||||
|
{ id: 3, title: '合同文件', count: 1, icon: 'file-signature' },
|
||||||
|
{ id: 4, title: '验收报告', count: 0, icon: 'check-circle' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const designSchemes = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '现代简约三居室',
|
||||||
|
status: '进行中',
|
||||||
|
progress: 65,
|
||||||
|
image: 'https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?w=600',
|
||||||
|
designer: '张设计',
|
||||||
|
updateDate: '2024-01-28',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '北欧风两居室',
|
||||||
|
status: '已完成',
|
||||||
|
progress: 100,
|
||||||
|
image: 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=600',
|
||||||
|
designer: '李雅',
|
||||||
|
updateDate: '2024-01-15',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '新中式方案',
|
||||||
|
status: '待确认',
|
||||||
|
progress: 80,
|
||||||
|
image: 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=600',
|
||||||
|
designer: '王明',
|
||||||
|
updateDate: '2024-01-20',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const partnerInfo = {
|
||||||
|
level: '金牌合伙人',
|
||||||
|
commission: 15800,
|
||||||
|
orders: 28,
|
||||||
|
teamSize: 15,
|
||||||
|
monthlyTarget: 50000,
|
||||||
|
monthlyProgress: 32000,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const partnerProducts = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '极速焕新套餐',
|
||||||
|
commission: 1500,
|
||||||
|
sales: 12,
|
||||||
|
image: 'https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?w=600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '品质整装套餐',
|
||||||
|
commission: 4500,
|
||||||
|
sales: 8,
|
||||||
|
image: 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '智能家装套餐',
|
||||||
|
commission: 8000,
|
||||||
|
sales: 5,
|
||||||
|
image: 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=600',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const menuItems = [
|
||||||
|
{ id: 1, title: '装修进度', icon: 'clock', link: '/user/progress' },
|
||||||
|
{ id: 2, title: '我的订单', icon: 'shopping-bag', link: '/user/mine/orders' },
|
||||||
|
{ id: 3, title: '我的收藏', icon: 'heart', link: '/user/mine/favorites' },
|
||||||
|
{ id: 4, title: '我的地址', icon: 'map-pin', link: '/user/mine/address' },
|
||||||
|
{ id: 5, title: '帮助中心', icon: 'help-circle', link: '/user/mine/help' },
|
||||||
|
{ id: 6, title: '设置', icon: 'settings', link: '/user/mine/settings' },
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
export const progressStages = [
|
||||||
|
{ id: 'prepare', name: '准备阶段', icon: '📋' },
|
||||||
|
{ id: 'design', name: '方案阶段', icon: '✏️' },
|
||||||
|
{ id: 'implement', name: '落地阶段', icon: '🔨' },
|
||||||
|
{ id: 'visit', name: '上门阶段', icon: '🏠' },
|
||||||
|
{ id: 'deliver', name: '交付阶段', icon: '🎉' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const progressDetails = {
|
||||||
|
prepare: {
|
||||||
|
title: '准备阶段',
|
||||||
|
status: 'completed',
|
||||||
|
tasks: [
|
||||||
|
{ id: 1, title: '需求沟通', status: 'completed', date: '2024-01-15' },
|
||||||
|
{ id: 2, title: '上门量房', status: 'completed', date: '2024-01-18' },
|
||||||
|
{ id: 3, title: '方案初稿', status: 'completed', date: '2024-01-22' },
|
||||||
|
],
|
||||||
|
tips: '准备阶段已完成,设计师已获取房屋详细信息。',
|
||||||
|
},
|
||||||
|
design: {
|
||||||
|
title: '方案阶段',
|
||||||
|
status: 'in_progress',
|
||||||
|
tasks: [
|
||||||
|
{ id: 1, title: '平面布局', status: 'completed', date: '2024-01-25' },
|
||||||
|
{ id: 2, title: '效果图设计', status: 'in_progress', date: '2024-01-28' },
|
||||||
|
{ id: 3, title: '施工图绘制', status: 'pending', date: '' },
|
||||||
|
{ id: 4, title: '预算报价', status: 'pending', date: '' },
|
||||||
|
],
|
||||||
|
tips: '效果图设计中,预计3天内完成。',
|
||||||
|
},
|
||||||
|
implement: {
|
||||||
|
title: '落地阶段',
|
||||||
|
status: 'pending',
|
||||||
|
tasks: [
|
||||||
|
{ id: 1, title: '材料采购', status: 'pending', date: '' },
|
||||||
|
{ id: 2, title: '水电改造', status: 'pending', date: '' },
|
||||||
|
{ id: 3, title: '泥瓦工程', status: 'pending', date: '' },
|
||||||
|
{ id: 4, title: '木工工程', status: 'pending', date: '' },
|
||||||
|
{ id: 5, title: '油漆工程', status: 'pending', date: '' },
|
||||||
|
],
|
||||||
|
tips: '等待方案确认后开始施工。',
|
||||||
|
},
|
||||||
|
visit: {
|
||||||
|
title: '上门阶段',
|
||||||
|
status: 'pending',
|
||||||
|
tasks: [
|
||||||
|
{ id: 1, title: '中期验收', status: 'pending', date: '' },
|
||||||
|
{ id: 2, title: '安装调试', status: 'pending', date: '' },
|
||||||
|
{ id: 3, title: '软装进场', status: 'pending', date: '' },
|
||||||
|
],
|
||||||
|
tips: '等待施工完成后进行上门验收。',
|
||||||
|
},
|
||||||
|
deliver: {
|
||||||
|
title: '交付阶段',
|
||||||
|
status: 'pending',
|
||||||
|
tasks: [
|
||||||
|
{ id: 1, title: '最终验收', status: 'pending', date: '' },
|
||||||
|
{ id: 2, title: '交付使用', status: 'pending', date: '' },
|
||||||
|
{ id: 3, title: '售后服务', status: 'pending', date: '' },
|
||||||
|
],
|
||||||
|
tips: '等待所有工程完成后交付。',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const projectInfo = {
|
||||||
|
name: '阳光花园3栋1201室',
|
||||||
|
area: '120㎡',
|
||||||
|
style: '现代简约',
|
||||||
|
budget: '15-20万',
|
||||||
|
startDate: '2024-01-15',
|
||||||
|
expectedEndDate: '2024-04-15',
|
||||||
|
designer: {
|
||||||
|
name: '张设计',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200',
|
||||||
|
phone: '138****8888',
|
||||||
|
},
|
||||||
|
progress: 35,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronRight, Settings, HelpCircle, Shield, LogOut } from 'lucide-react'
|
||||||
|
import { adminUserInfo } from '@/mock/adminData'
|
||||||
|
|
||||||
|
export default function MinePage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ id: 1, title: '账号设置', icon: Settings, link: '/admin/mine/settings' },
|
||||||
|
{ id: 2, title: '帮助中心', icon: HelpCircle, link: '/admin/mine/help' },
|
||||||
|
{ id: 3, title: '隐私政策', icon: Shield, link: '/admin/mine/privacy' },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="bg-gradient-to-br from-primary-500 to-primary-600 text-white px-4 pt-12 pb-8">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<img
|
||||||
|
src={adminUserInfo.avatar}
|
||||||
|
alt={adminUserInfo.name}
|
||||||
|
className="w-16 h-16 rounded-full object-cover border-2 border-white"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="text-xl font-bold">{adminUserInfo.name}</h2>
|
||||||
|
<p className="text-primary-100">{adminUserInfo.phone}</p>
|
||||||
|
<p className="text-sm text-primary-200 mt-1">
|
||||||
|
{adminUserInfo.department} · {adminUserInfo.position}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ChevronRight size={24} className="text-white/60" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 -mt-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||||
|
{menuItems.map((item, index) => (
|
||||||
|
<motion.button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => navigate(item.link)}
|
||||||
|
className={`w-full flex items-center justify-between p-4 ${
|
||||||
|
index !== menuItems.length - 1 ? 'border-b border-gray-50' : ''
|
||||||
|
}`}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-gray-50 flex items-center justify-center text-gray-500">
|
||||||
|
<item.icon size={18} />
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-700">{item.title}</span>
|
||||||
|
</div>
|
||||||
|
<ChevronRight size={20} className="text-gray-400" />
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className="w-full mt-4 flex items-center justify-center gap-2 p-4 bg-white rounded-xl text-red-500"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<LogOut size={18} />
|
||||||
|
退出登录
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Calculator, FileText, List, Calendar, CheckSquare, Users } from 'lucide-react'
|
||||||
|
import { toolboxItems } from '@/mock/adminData'
|
||||||
|
|
||||||
|
export default function ToolboxPage() {
|
||||||
|
const getIcon = (icon: string) => {
|
||||||
|
const icons: Record<string, React.ReactNode> = {
|
||||||
|
'calculator': <Calculator size={24} />,
|
||||||
|
'file-text': <FileText size={24} />,
|
||||||
|
'list': <List size={24} />,
|
||||||
|
'calendar': <Calendar size={24} />,
|
||||||
|
'check-square': <CheckSquare size={24} />,
|
||||||
|
'users': <Users size={24} />,
|
||||||
|
}
|
||||||
|
return icons[icon] || <Calculator size={24} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="bg-gradient-to-br from-primary-500 to-primary-600 text-white px-4 pt-12 pb-6">
|
||||||
|
<h1 className="text-xl font-bold text-center">百宝箱</h1>
|
||||||
|
<p className="text-center text-primary-100 text-sm mt-1">高效工具,助力工作</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 -mt-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-4">
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{toolboxItems.map((item) => (
|
||||||
|
<motion.button
|
||||||
|
key={item.id}
|
||||||
|
className="flex flex-col items-center p-4 rounded-xl bg-gray-50"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-primary-50 text-primary-500 flex items-center justify-center mb-2">
|
||||||
|
{getIcon(item.icon)}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-800">{item.title}</h3>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">{item.desc}</p>
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-primary-50 rounded-xl p-4 mt-4">
|
||||||
|
<h4 className="font-medium text-primary-700 mb-2">常用功能</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center justify-between p-3 bg-white rounded-lg"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary-50 flex items-center justify-center text-primary-500">
|
||||||
|
<Calculator size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="font-medium text-gray-800 text-sm">快速报价</h5>
|
||||||
|
<p className="text-xs text-gray-500">一键生成报价单</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center justify-between p-3 bg-white rounded-lg"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary-50 flex items-center justify-center text-primary-500">
|
||||||
|
<FileText size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="font-medium text-gray-800 text-sm">合同生成</h5>
|
||||||
|
<p className="text-xs text-gray-500">自动生成标准合同</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Bell, ChevronRight, AlertTriangle, DollarSign, FileText, CheckCircle } from 'lucide-react'
|
||||||
|
import { workspaceTasks, aiNotifications, statistics } from '@/mock/adminData'
|
||||||
|
|
||||||
|
export default function WorkspacePage() {
|
||||||
|
const [showNotifications, setShowNotifications] = useState(false)
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'urgent':
|
||||||
|
return 'bg-red-50 text-red-500'
|
||||||
|
case 'pending':
|
||||||
|
return 'bg-yellow-50 text-yellow-500'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-50 text-gray-500'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNotificationIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'payment':
|
||||||
|
return <DollarSign size={20} className="text-red-500" />
|
||||||
|
case 'quote':
|
||||||
|
return <FileText size={20} className="text-blue-500" />
|
||||||
|
case 'bonus':
|
||||||
|
return <CheckCircle size={20} className="text-green-500" />
|
||||||
|
default:
|
||||||
|
return <Bell size={20} className="text-gray-500" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="bg-gradient-to-br from-primary-500 to-primary-600 text-white px-4 pt-12 pb-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h1 className="text-xl font-bold">工作台</h1>
|
||||||
|
<motion.button
|
||||||
|
onClick={() => setShowNotifications(!showNotifications)}
|
||||||
|
className="relative p-2"
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<Bell size={22} />
|
||||||
|
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full" />
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-4 text-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">{statistics.todayOrders}</p>
|
||||||
|
<p className="text-xs text-primary-100">今日订单</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">{statistics.monthOrders}</p>
|
||||||
|
<p className="text-xs text-primary-100">本月订单</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">{(statistics.monthAmount / 10000).toFixed(1)}万</p>
|
||||||
|
<p className="text-xs text-primary-100">本月金额</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">{statistics.completionRate}%</p>
|
||||||
|
<p className="text-xs text-primary-100">完成率</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showNotifications && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="px-4 -mt-2"
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-xl shadow-lg p-4 mb-4">
|
||||||
|
<h3 className="font-bold text-gray-800 mb-3">AI智能提醒</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{aiNotifications.map((notification) => (
|
||||||
|
<div
|
||||||
|
key={notification.id}
|
||||||
|
className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-full bg-white flex items-center justify-center shadow-sm">
|
||||||
|
{getNotificationIcon(notification.type)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium text-gray-800 text-sm">{notification.title}</h4>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">{notification.content}</p>
|
||||||
|
<div className="flex items-center justify-between mt-2">
|
||||||
|
{notification.amount > 0 && (
|
||||||
|
<span className="text-sm font-medium text-primary-500">
|
||||||
|
¥{notification.amount.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<motion.button
|
||||||
|
className="text-xs text-primary-500"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
{notification.action}
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="px-4 mt-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-4 mb-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="font-bold text-gray-800">待办任务</h3>
|
||||||
|
<span className="text-sm text-gray-500">{workspaceTasks.length}项</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{workspaceTasks.map((task) => (
|
||||||
|
<motion.div
|
||||||
|
key={task.id}
|
||||||
|
className="flex items-center gap-3 p-3 bg-gray-50 rounded-xl"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={task.avatar}
|
||||||
|
alt={task.title}
|
||||||
|
className="w-10 h-10 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium text-gray-800 text-sm">{task.title}</h4>
|
||||||
|
<p className="text-xs text-gray-500">{task.createTime}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className={`text-xs px-2 py-1 rounded-full ${getStatusColor(task.status)}`}>
|
||||||
|
{task.type}
|
||||||
|
</span>
|
||||||
|
{task.amount > 0 && (
|
||||||
|
<p className="text-sm font-medium text-primary-500 mt-1">
|
||||||
|
¥{task.amount.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ChevronRight size={20} className="text-gray-400" />
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-primary-50 rounded-xl p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<AlertTriangle size={18} className="text-primary-500" />
|
||||||
|
<h4 className="font-medium text-primary-700">AI工作建议</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-primary-600">
|
||||||
|
今日有2笔款项待跟进,建议优先处理逾期订单,可提高回款率15%。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
import { budgetCategories, budgetItems, selectedItems as initialSelectedItems } from '@/mock/aiData'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronLeft, ShoppingCart, Trash2 } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
export default function AIBudgetPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [selectedItems, setSelectedItems] = useState(initialSelectedItems)
|
||||||
|
|
||||||
|
const getTotalPrice = () => {
|
||||||
|
return selectedItems.reduce((total, item) => {
|
||||||
|
const categoryItems = budgetItems[item.categoryId as keyof typeof budgetItems]
|
||||||
|
const budgetItem = categoryItems?.find((i) => i.id === item.itemId)
|
||||||
|
return total + (budgetItem?.price || 0) * item.quantity
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeItem = (categoryId: string, itemId: number) => {
|
||||||
|
setSelectedItems(
|
||||||
|
selectedItems.filter(
|
||||||
|
(item) => !(item.categoryId === categoryId && item.itemId === itemId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="bg-white px-4 py-3 flex items-center border-b border-gray-100">
|
||||||
|
<button onClick={() => navigate(-1)} className="p-1">
|
||||||
|
<ChevronLeft size={24} className="text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<h1 className="flex-1 text-center font-bold text-gray-800">装修预算</h1>
|
||||||
|
<div className="w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="bg-white rounded-xl p-4 shadow-sm mb-4">
|
||||||
|
<h3 className="font-bold text-gray-800 mb-3">选择空间</h3>
|
||||||
|
<div className="grid grid-cols-5 gap-2">
|
||||||
|
{budgetCategories.map((category) => (
|
||||||
|
<motion.button
|
||||||
|
key={category.id}
|
||||||
|
onClick={() => navigate(`/user/ai/budget/${category.id}`)}
|
||||||
|
className="flex flex-col items-center gap-1 p-2 rounded-xl bg-gray-50"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<span className="text-2xl">{category.icon}</span>
|
||||||
|
<span className="text-xs text-gray-600">{category.name}</span>
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl p-4 shadow-sm mb-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="font-bold text-gray-800">已选清单</h3>
|
||||||
|
<span className="text-sm text-gray-500">{selectedItems.length}项</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedItems.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
<ShoppingCart size={40} className="mx-auto mb-2 opacity-50" />
|
||||||
|
<p>暂无选择项目</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{selectedItems.map((item) => {
|
||||||
|
const categoryItems = budgetItems[item.categoryId as keyof typeof budgetItems]
|
||||||
|
const budgetItem = categoryItems?.find((i) => i.id === item.itemId)
|
||||||
|
const category = budgetCategories.find((c) => c.id === item.categoryId)
|
||||||
|
|
||||||
|
if (!budgetItem) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={`${item.categoryId}-${item.itemId}`}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: 20 }}
|
||||||
|
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg"
|
||||||
|
>
|
||||||
|
<span className="text-xl">{category?.icon}</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium text-gray-800 text-sm">{budgetItem.name}</h4>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
{budgetItem.brand} · {budgetItem.spec}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="font-medium text-primary-500">
|
||||||
|
¥{budgetItem.price.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400">x{item.quantity}{budgetItem.unit}</p>
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
onClick={() => removeItem(item.categoryId, item.itemId)}
|
||||||
|
className="p-1 text-gray-400 hover:text-red-500"
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gradient-to-r from-primary-500 to-primary-600 rounded-xl p-4 text-white mb-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-primary-100 text-sm">预估总价</p>
|
||||||
|
<p className="text-3xl font-bold">¥{getTotalPrice().toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
className="px-6 py-2 bg-white text-primary-500 rounded-lg font-medium"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
生成报价单
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-primary-50 rounded-xl p-4">
|
||||||
|
<h4 className="font-medium text-primary-700 mb-2">AI智能推荐</h4>
|
||||||
|
<p className="text-sm text-primary-600">
|
||||||
|
根据您的选择,建议增加厨房吊顶和卫生间防水项目,确保装修质量。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Calculator, ChevronRight, Mic, Send, Volume2 } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
export default function AIPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [inputText, setInputText] = useState('')
|
||||||
|
const [isListening, setIsListening] = useState(false)
|
||||||
|
const [messages, setMessages] = useState([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 'ai',
|
||||||
|
content: '您好!我是入舍智能管家,我可以帮您:\n\n• 计算装修预算\n• 推荐设计方案\n• 解答装修问题\n• 预约上门服务\n\n请问有什么可以帮您的?',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const quickQuestions = [
|
||||||
|
'帮我计算装修预算',
|
||||||
|
'推荐现代简约风格',
|
||||||
|
'如何选择装修材料',
|
||||||
|
'预约设计师上门',
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleSend = () => {
|
||||||
|
if (!inputText.trim()) return
|
||||||
|
|
||||||
|
setMessages([
|
||||||
|
...messages,
|
||||||
|
{ id: Date.now(), type: 'user', content: inputText },
|
||||||
|
{
|
||||||
|
id: Date.now() + 1,
|
||||||
|
type: 'ai',
|
||||||
|
content: '好的,我来帮您处理这个问题。请问您的房屋面积是多少平方米?',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
setInputText('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMicClick = () => {
|
||||||
|
setIsListening(!isListening)
|
||||||
|
if (!isListening) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsListening(false)
|
||||||
|
setInputText('我想计算一下120平米房子的装修预算')
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||||
|
<div className="bg-gradient-to-br from-primary-500 to-primary-600 text-white px-4 pt-12 pb-6">
|
||||||
|
<h1 className="text-xl font-bold text-center mb-4">智能管家</h1>
|
||||||
|
<div className="flex justify-center gap-4">
|
||||||
|
<motion.button
|
||||||
|
onClick={() => navigate('/user/ai/budget')}
|
||||||
|
className="flex flex-col items-center gap-2"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center">
|
||||||
|
<Calculator size={24} />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm">装修预算</span>
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
className="flex flex-col items-center gap-2"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center">
|
||||||
|
<Volume2 size={24} />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm">语音播报</span>
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
className="flex flex-col items-center gap-2"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center">
|
||||||
|
<Mic size={24} />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm">语音输入</span>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 pb-32 space-y-4">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<motion.div
|
||||||
|
key={message.id}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className={`flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`max-w-[80%] rounded-2xl px-4 py-3 ${message.type === 'user'
|
||||||
|
? 'bg-primary-500 text-white rounded-br-md'
|
||||||
|
: 'rounded-bl-md shadow-sm'
|
||||||
|
}`}
|
||||||
|
style={message.type === 'ai' ? { backgroundColor: 'var(--bg-primary)' } : {}}
|
||||||
|
>
|
||||||
|
<p className="text-sm whitespace-pre-line">{message.content}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="rounded-xl p-4 shadow-sm" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||||||
|
<h3 className="font-medium mb-3" style={{ color: 'var(--text-primary)' }}>快捷问题</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{quickQuestions.map((question, index) => (
|
||||||
|
<motion.button
|
||||||
|
key={index}
|
||||||
|
onClick={() => {
|
||||||
|
if (index === 0) {
|
||||||
|
navigate('/user/ai/budget')
|
||||||
|
} else {
|
||||||
|
setInputText(question)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full flex items-center justify-between p-3 rounded-lg"
|
||||||
|
style={{ backgroundColor: 'var(--bg-tertiary)' }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<span className="text-sm" style={{ color: 'var(--text-secondary)' }}>{question}</span>
|
||||||
|
<ChevronRight size={16} style={{ color: 'var(--text-tertiary)' }} />
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="fixed bottom-16 left-0 right-0 border-t p-4 z-40" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-color)' }}>
|
||||||
|
<div className="flex items-center gap-3 max-w-lg mx-auto">
|
||||||
|
<motion.button
|
||||||
|
onClick={handleMicClick}
|
||||||
|
className={`w-12 h-12 rounded-full flex items-center justify-center ${isListening ? 'bg-red-500 text-white' : ''
|
||||||
|
}`}
|
||||||
|
style={isListening ? {} : { backgroundColor: 'var(--bg-tertiary)', color: 'var(--text-secondary)' }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
animate={isListening ? { scale: [1, 1.1, 1] } : {}}
|
||||||
|
transition={{ repeat: Infinity, duration: 1 }}
|
||||||
|
>
|
||||||
|
<Mic size={20} />
|
||||||
|
</motion.button>
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={inputText}
|
||||||
|
onChange={(e) => setInputText(e.target.value)}
|
||||||
|
placeholder="请输入您的问题..."
|
||||||
|
className="w-full h-12 pl-4 pr-12 rounded-full border focus:outline-none focus:border-primary-500"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--bg-primary)',
|
||||||
|
borderColor: 'var(--border-color)',
|
||||||
|
color: 'var(--text-primary)'
|
||||||
|
}}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
|
||||||
|
/>
|
||||||
|
<motion.button
|
||||||
|
onClick={handleSend}
|
||||||
|
className="absolute right-1 top-1 w-10 h-10 rounded-full bg-primary-500 text-white flex items-center justify-center"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<Send size={18} />
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronLeft, Plus, Minus, Check } from 'lucide-react'
|
||||||
|
import { budgetCategories, budgetItems } from '@/mock/aiData'
|
||||||
|
|
||||||
|
export default function BudgetDetailPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { category } = useParams<{ category: string }>()
|
||||||
|
const [quantities, setQuantities] = useState<Record<number, number>>({})
|
||||||
|
|
||||||
|
const categoryInfo = budgetCategories.find((c) => c.id === category)
|
||||||
|
const items = budgetItems[category as keyof typeof budgetItems] || []
|
||||||
|
|
||||||
|
const updateQuantity = (itemId: number, delta: number) => {
|
||||||
|
setQuantities((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[itemId]: Math.max(0, (prev[itemId] || 0) + delta),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
const selected = Object.entries(quantities)
|
||||||
|
.filter(([_, qty]) => qty > 0)
|
||||||
|
.map(([id, qty]) => ({
|
||||||
|
categoryId: category!,
|
||||||
|
itemId: parseInt(id),
|
||||||
|
quantity: qty,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (selected.length > 0) {
|
||||||
|
alert(`已添加 ${selected.length} 个项目到清单`)
|
||||||
|
}
|
||||||
|
navigate('/user/ai/budget')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTotalPrice = () => {
|
||||||
|
return items.reduce((total, item) => {
|
||||||
|
return total + item.price * (quantities[item.id] || 0)
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="bg-white px-4 py-3 flex items-center border-b border-gray-100">
|
||||||
|
<button onClick={() => navigate(-1)} className="p-1">
|
||||||
|
<ChevronLeft size={24} className="text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<h1 className="flex-1 text-center font-bold text-gray-800">
|
||||||
|
{categoryInfo?.name || '装修预算'}
|
||||||
|
</h1>
|
||||||
|
<div className="w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex gap-2 mb-4 overflow-x-auto pb-2">
|
||||||
|
{budgetCategories.map((cat) => (
|
||||||
|
<motion.button
|
||||||
|
key={cat.id}
|
||||||
|
onClick={() => navigate(`/user/ai/budget/${cat.id}`)}
|
||||||
|
className={`flex-shrink-0 px-4 py-2 rounded-full text-sm ${
|
||||||
|
cat.id === category
|
||||||
|
? 'bg-primary-500 text-white'
|
||||||
|
: 'bg-white text-gray-600 border border-gray-200'
|
||||||
|
}`}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
{cat.icon} {cat.name}
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{items.map((item) => (
|
||||||
|
<motion.div
|
||||||
|
key={item.id}
|
||||||
|
className="bg-white rounded-xl p-4 shadow-sm"
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-gray-800">{item.name}</h3>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{item.brand} · {item.spec}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="font-bold text-primary-500">
|
||||||
|
¥{item.price.toLocaleString()}
|
||||||
|
<span className="text-xs text-gray-400 font-normal">/{item.unit}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<motion.button
|
||||||
|
onClick={() => updateQuantity(item.id, -1)}
|
||||||
|
disabled={!quantities[item.id]}
|
||||||
|
className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||||
|
quantities[item.id]
|
||||||
|
? 'bg-gray-100 text-gray-600'
|
||||||
|
: 'bg-gray-50 text-gray-300'
|
||||||
|
}`}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<Minus size={16} />
|
||||||
|
</motion.button>
|
||||||
|
<span className="w-8 text-center font-medium">
|
||||||
|
{quantities[item.id] || 0}
|
||||||
|
</span>
|
||||||
|
<motion.button
|
||||||
|
onClick={() => updateQuantity(item.id, 1)}
|
||||||
|
className="w-8 h-8 rounded-full bg-primary-500 text-white flex items-center justify-center"
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{quantities[item.id] > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
className="w-6 h-6 rounded-full bg-primary-500 text-white flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Check size={14} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 p-4 safe-bottom">
|
||||||
|
<div className="flex items-center justify-between max-w-lg mx-auto">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">已选 {Object.values(quantities).filter(q => q > 0).length } 项</p>
|
||||||
|
<p className="text-xl font-bold text-primary-500">
|
||||||
|
¥{getTotalPrice().toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
className="px-8 py-3 bg-primary-500 text-white rounded-xl font-medium"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
确认添加
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
import { exploreContents } from '@/mock/homeData'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Heart, MessageCircle, Play, Share2 } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export default function ExplorePage() {
|
||||||
|
const [activeTab, setActiveTab] = useState('all')
|
||||||
|
const [likedPosts, setLikedPosts] = useState<number[]>([])
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ key: 'all', label: '推荐' },
|
||||||
|
{ key: 'designer', label: '设计师' },
|
||||||
|
{ key: 'owner', label: '业主分享' },
|
||||||
|
{ key: 'video', label: '视频' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const filteredContents = activeTab === 'all'
|
||||||
|
? exploreContents
|
||||||
|
: exploreContents.filter((c) => c.type === activeTab)
|
||||||
|
|
||||||
|
const toggleLike = (id: number) => {
|
||||||
|
setLikedPosts((prev) =>
|
||||||
|
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||||
|
<div className="sticky top-0 z-10 px-4 py-3 border-b" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-color)' }}>
|
||||||
|
<h1 className="text-lg font-bold text-center mb-3" style={{ color: 'var(--text-primary)' }}>逛逛</h1>
|
||||||
|
<div className="flex gap-2 overflow-x-auto scrollbar-hide">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.key}
|
||||||
|
onClick={() => setActiveTab(tab.key)}
|
||||||
|
className={`px-4 py-1.5 rounded-full text-sm font-medium whitespace-nowrap transition-all ${
|
||||||
|
activeTab === tab.key
|
||||||
|
? 'bg-primary-500 text-white'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
style={activeTab !== tab.key ? { backgroundColor: 'var(--bg-tertiary)', color: 'var(--text-secondary)' } : {}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 py-4 space-y-4">
|
||||||
|
{filteredContents.map((content, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={content.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 }}
|
||||||
|
className="rounded-xl shadow-sm overflow-hidden"
|
||||||
|
style={{ backgroundColor: 'var(--bg-primary)' }}
|
||||||
|
>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<img
|
||||||
|
src={content.author.avatar}
|
||||||
|
alt={content.author.name}
|
||||||
|
className="w-10 h-10 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-medium text-sm" style={{ color: 'var(--text-primary)' }}>{content.author.name}</h3>
|
||||||
|
{content.type === 'designer' && (
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded-full bg-primary-50 text-primary-500">设计师</span>
|
||||||
|
)}
|
||||||
|
{content.type === 'video' && (
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded-full bg-orange-50 text-orange-500">官方</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs" style={{ color: 'var(--text-tertiary)' }}>{content.author.title}</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs" style={{ color: 'var(--text-tertiary)' }}>{content.time}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 className="font-bold text-sm mb-2" style={{ color: 'var(--text-primary)' }}>{content.title}</h4>
|
||||||
|
<p className="text-sm mb-3 line-clamp-3" style={{ color: 'var(--text-secondary)' }}>{content.content}</p>
|
||||||
|
|
||||||
|
{content.type === 'video' && content.videoCover ? (
|
||||||
|
<div className="relative rounded-lg overflow-hidden mb-3">
|
||||||
|
<img
|
||||||
|
src={content.videoCover}
|
||||||
|
alt={content.title}
|
||||||
|
className="w-full h-48 object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/30 flex items-center justify-center">
|
||||||
|
<div className="w-14 h-14 rounded-full bg-white/90 flex items-center justify-center">
|
||||||
|
<Play size={24} className="text-primary-500 ml-1" fill="currentColor" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : content.images && content.images.length > 0 && (
|
||||||
|
<div className={`grid gap-2 mb-3 ${content.images.length === 1 ? 'grid-cols-1' : content.images.length === 2 ? 'grid-cols-2' : 'grid-cols-3'}`}>
|
||||||
|
{content.images.map((image, imgIndex) => (
|
||||||
|
<img
|
||||||
|
key={imgIndex}
|
||||||
|
src={image}
|
||||||
|
alt=""
|
||||||
|
className={`w-full object-cover rounded-lg ${content.images!.length === 1 ? 'h-48' : content.images!.length === 2 ? 'h-32' : 'h-24'}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
{content.tags.map((tag, tagIndex) => (
|
||||||
|
<span
|
||||||
|
key={tagIndex}
|
||||||
|
className="text-xs px-2 py-1 rounded-full"
|
||||||
|
style={{ backgroundColor: 'var(--bg-tertiary)', color: 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-3 border-t" style={{ borderColor: 'var(--border-color)' }}>
|
||||||
|
<motion.button
|
||||||
|
onClick={() => toggleLike(content.id)}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<Heart
|
||||||
|
size={18}
|
||||||
|
className={likedPosts.includes(content.id) ? 'text-red-500 fill-red-500' : ''}
|
||||||
|
style={likedPosts.includes(content.id) ? {} : { color: 'var(--text-tertiary)' }}
|
||||||
|
/>
|
||||||
|
<span className="text-xs" style={{ color: 'var(--text-tertiary)' }}>
|
||||||
|
{likedPosts.includes(content.id) ? content.likes + 1 : content.likes}
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button className="flex items-center gap-1" whileTap={{ scale: 0.9 }}>
|
||||||
|
<MessageCircle size={18} style={{ color: 'var(--text-tertiary)' }} />
|
||||||
|
<span className="text-xs" style={{ color: 'var(--text-tertiary)' }}>{content.comments}</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button className="flex items-center gap-1" whileTap={{ scale: 0.9 }}>
|
||||||
|
<Share2 size={18} style={{ color: 'var(--text-tertiary)' }} />
|
||||||
|
<span className="text-xs" style={{ color: 'var(--text-tertiary)' }}>{content.shares}</span>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filteredContents.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12" style={{ color: 'var(--text-tertiary)' }}>
|
||||||
|
<p className="text-sm">暂无内容</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronLeft, User, Check } from 'lucide-react'
|
||||||
|
import { busSeats } from '@/mock/groupData'
|
||||||
|
|
||||||
|
export default function BusBookingPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [selectedSeats, setSelectedSeats] = useState<number[]>([])
|
||||||
|
const [passengerName, setPassengerName] = useState('')
|
||||||
|
const [passengerPhone, setPassengerPhone] = useState('')
|
||||||
|
|
||||||
|
const toggleSeat = (seatId: number, status: string) => {
|
||||||
|
if (status === 'occupied') return
|
||||||
|
|
||||||
|
setSelectedSeats((prev) =>
|
||||||
|
prev.includes(seatId)
|
||||||
|
? prev.filter((id) => id !== seatId)
|
||||||
|
: prev.length < 4
|
||||||
|
? [...prev, seatId]
|
||||||
|
: prev
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (selectedSeats.length === 0) {
|
||||||
|
alert('请选择座位')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!passengerName || !passengerPhone) {
|
||||||
|
alert('请填写乘客信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
alert(`预约成功!已为您预留 ${selectedSeats.length} 个座位`)
|
||||||
|
navigate('/user/group')
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderSeat = (seat: typeof busSeats[0]) => {
|
||||||
|
const isSelected = selectedSeats.includes(seat.id)
|
||||||
|
const isOccupied = seat.status === 'occupied'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
key={seat.id}
|
||||||
|
onClick={() => toggleSeat(seat.id, seat.status)}
|
||||||
|
disabled={isOccupied}
|
||||||
|
className={`w-10 h-10 rounded-lg flex items-center justify-center text-xs ${
|
||||||
|
isOccupied
|
||||||
|
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||||
|
: isSelected
|
||||||
|
? 'bg-primary-500 text-white'
|
||||||
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||||
|
}`}
|
||||||
|
whileTap={!isOccupied ? { scale: 0.95 } : {}}
|
||||||
|
>
|
||||||
|
{isSelected ? <Check size={16} /> : seat.id}
|
||||||
|
</motion.button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = []
|
||||||
|
for (let i = 0; i < busSeats.length; i += 4) {
|
||||||
|
const rowSeats = busSeats.slice(i, i + 4)
|
||||||
|
rows.push(rowSeats)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 pb-24">
|
||||||
|
<div className="bg-white px-4 py-3 flex items-center border-b border-gray-100">
|
||||||
|
<button onClick={() => navigate(-1)} className="p-1">
|
||||||
|
<ChevronLeft size={24} className="text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<h1 className="flex-1 text-center font-bold text-gray-800">预约大巴座位</h1>
|
||||||
|
<div className="w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="bg-white rounded-xl p-4 shadow-sm mb-4">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-primary-50 flex items-center justify-center">
|
||||||
|
<User size={24} className="text-primary-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-gray-800">佛山陶瓷工厂店</h3>
|
||||||
|
<p className="text-sm text-gray-500">每周六 9:00 发车</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||||
|
<span>出发:广州市天河区</span>
|
||||||
|
<span>|</span>
|
||||||
|
<span>全程约1小时</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl p-4 shadow-sm mb-4">
|
||||||
|
<h3 className="font-medium text-gray-800 mb-4">选择座位</h3>
|
||||||
|
|
||||||
|
<div className="flex justify-center gap-4 mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-6 h-6 rounded bg-gray-100" />
|
||||||
|
<span className="text-xs text-gray-500">可选</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-6 h-6 rounded bg-primary-500" />
|
||||||
|
<span className="text-xs text-gray-500">已选</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-6 h-6 rounded bg-gray-200" />
|
||||||
|
<span className="text-xs text-gray-500">已占</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
<div className="w-3/4 h-8 bg-gray-200 rounded-t-3xl flex items-center justify-center text-xs text-gray-500">
|
||||||
|
驾驶位
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{rows.map((row, rowIndex) => (
|
||||||
|
<div key={rowIndex} className="flex justify-center gap-2">
|
||||||
|
{renderSeat(row[0])}
|
||||||
|
{renderSeat(row[1])}
|
||||||
|
<div className="w-6" />
|
||||||
|
{renderSeat(row[2])}
|
||||||
|
{renderSeat(row[3])}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-center text-xs text-gray-400 mt-4">
|
||||||
|
已选 {selectedSeats.length} 个座位(最多可选4个)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl p-4 shadow-sm mb-4">
|
||||||
|
<h3 className="font-medium text-gray-800 mb-3">乘客信息</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-gray-500 mb-1 block">姓名</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={passengerName}
|
||||||
|
onChange={(e) => setPassengerName(e.target.value)}
|
||||||
|
placeholder="请输入姓名"
|
||||||
|
className="input-field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-gray-500 mb-1 block">手机号</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
value={passengerPhone}
|
||||||
|
onChange={(e) => setPassengerPhone(e.target.value)}
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
className="input-field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 p-4 safe-bottom">
|
||||||
|
<motion.button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className="w-full h-12 bg-primary-500 text-white rounded-xl font-medium"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
确认预约
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronRight, Star, MapPin, Clock, Users } from 'lucide-react'
|
||||||
|
import { groupBuyPackages, factoryStores } from '@/mock/groupData'
|
||||||
|
|
||||||
|
export default function GroupBuyPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="bg-gradient-to-br from-primary-500 to-primary-600 text-white px-4 pt-12 pb-6">
|
||||||
|
<h1 className="text-xl font-bold text-center mb-4">品质团购</h1>
|
||||||
|
<p className="text-center text-primary-100 text-sm">工厂直供 · 品质保障 · 专属优惠</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 -mt-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-4 mb-4">
|
||||||
|
<h3 className="font-bold text-gray-800 mb-3">热门套餐</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{groupBuyPackages.map((pkg) => (
|
||||||
|
<motion.div
|
||||||
|
key={pkg.id}
|
||||||
|
onClick={() => navigate(`/user/group/package/${pkg.id}`)}
|
||||||
|
className="flex gap-3 p-3 bg-gray-50 rounded-xl"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={pkg.image}
|
||||||
|
alt={pkg.title}
|
||||||
|
className="w-24 h-24 rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium text-gray-800">{pkg.title}</h4>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">{pkg.subtitle}</p>
|
||||||
|
<div className="flex items-center gap-1 mt-1">
|
||||||
|
<Star size={12} className="text-yellow-400 fill-yellow-400" />
|
||||||
|
<span className="text-xs text-gray-600">{pkg.rating}</span>
|
||||||
|
<span className="text-xs text-gray-400">| 已售{pkg.sales}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-baseline gap-2 mt-2">
|
||||||
|
<span className="text-lg font-bold text-primary-500">
|
||||||
|
¥{pkg.price.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-400 line-through">
|
||||||
|
¥{pkg.originalPrice.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight size={20} className="text-gray-400 self-center" />
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-4 mb-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="font-bold text-gray-800">工厂店展厅</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/user/group/store')}
|
||||||
|
className="text-primary-500 text-sm flex items-center"
|
||||||
|
>
|
||||||
|
查看更多 <ChevronRight size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{factoryStores.slice(0, 2).map((store) => (
|
||||||
|
<motion.div
|
||||||
|
key={store.id}
|
||||||
|
onClick={() => navigate('/user/group/store')}
|
||||||
|
className="flex gap-3 p-3 bg-gray-50 rounded-xl"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={store.image}
|
||||||
|
alt={store.name}
|
||||||
|
className="w-20 h-20 rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium text-gray-800">{store.name}</h4>
|
||||||
|
<div className="flex items-center gap-1 mt-1 text-xs text-gray-500">
|
||||||
|
<MapPin size={12} />
|
||||||
|
<span>{store.address}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Star size={12} className="text-yellow-400 fill-yellow-400" />
|
||||||
|
<span className="text-xs text-gray-600">{store.rating}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-400">{store.distance}</span>
|
||||||
|
</div>
|
||||||
|
{store.busAvailable && (
|
||||||
|
<div className="flex items-center gap-1 mt-2 text-xs text-primary-500">
|
||||||
|
<Clock size={12} />
|
||||||
|
<span>免费大巴 · {store.busTime}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
onClick={() => navigate('/user/group/bus-booking')}
|
||||||
|
className="w-full bg-gradient-to-r from-primary-500 to-primary-600 text-white rounded-xl p-4 flex items-center justify-between"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center">
|
||||||
|
<Users size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">免费预约看大巴</h4>
|
||||||
|
<p className="text-xs text-primary-100">工厂店展厅直通车</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight size={20} />
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronLeft, Star, MapPin, Clock, Bus } from 'lucide-react'
|
||||||
|
import { factoryStores } from '@/mock/groupData'
|
||||||
|
|
||||||
|
export default function GroupStorePage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="bg-white px-4 py-3 flex items-center border-b border-gray-100">
|
||||||
|
<button onClick={() => navigate(-1)} className="p-1">
|
||||||
|
<ChevronLeft size={24} className="text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<h1 className="flex-1 text-center font-bold text-gray-800">工厂店展厅</h1>
|
||||||
|
<div className="w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{factoryStores.map((store) => (
|
||||||
|
<motion.div
|
||||||
|
key={store.id}
|
||||||
|
className="bg-white rounded-xl shadow-sm overflow-hidden"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={store.image}
|
||||||
|
alt={store.name}
|
||||||
|
className="w-full h-40 object-cover"
|
||||||
|
/>
|
||||||
|
<div className="p-4">
|
||||||
|
<h3 className="font-bold text-gray-800">{store.name}</h3>
|
||||||
|
<div className="flex items-center gap-1 mt-2 text-sm text-gray-500">
|
||||||
|
<MapPin size={14} />
|
||||||
|
<span>{store.address}</span>
|
||||||
|
<span className="text-gray-300 mx-1">|</span>
|
||||||
|
<span>{store.distance}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Star size={14} className="text-yellow-400 fill-yellow-400" />
|
||||||
|
<span className="text-sm text-gray-600">{store.rating}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2 mt-3">
|
||||||
|
{store.products.map((product, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="px-2 py-1 bg-gray-50 rounded-md text-xs text-gray-600"
|
||||||
|
>
|
||||||
|
{product}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{store.busAvailable && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Bus size={18} className="text-primary-500" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-800">免费大巴</p>
|
||||||
|
<p className="text-xs text-gray-500">{store.busTime}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
onClick={() => navigate('/user/group/bus-booking')}
|
||||||
|
className="px-4 py-2 bg-primary-500 text-white rounded-lg text-sm"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
预约座位
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronLeft, Star, Check, Share2 } from 'lucide-react'
|
||||||
|
import { groupBuyPackages, renovationSchemes } from '@/mock/groupData'
|
||||||
|
|
||||||
|
export default function PackageDetailPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { id } = useParams()
|
||||||
|
|
||||||
|
const pkg = groupBuyPackages.find((p) => p.id === parseInt(id || '1')) || groupBuyPackages[0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 pb-20">
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src={pkg.image}
|
||||||
|
alt={pkg.title}
|
||||||
|
className="w-full h-56 object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute top-0 left-0 right-0 bg-gradient-to-b from-black/50 to-transparent p-4 pt-12">
|
||||||
|
<button onClick={() => navigate(-1)} className="p-1">
|
||||||
|
<ChevronLeft size={24} className="text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 -mt-6 relative">
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-bold text-gray-800">{pkg.title}</h1>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">{pkg.subtitle}</p>
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
className="p-2 rounded-full bg-gray-50"
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<Share2 size={20} className="text-gray-500" />
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mt-3">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Star size={14} className="text-yellow-400 fill-yellow-400" />
|
||||||
|
<span className="text-sm text-gray-600">{pkg.rating}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-gray-400">| 已售{pkg.sales}套</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-baseline gap-2 mt-3">
|
||||||
|
<span className="text-2xl font-bold text-primary-500">
|
||||||
|
¥{pkg.price.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-400 line-through">
|
||||||
|
¥{pkg.originalPrice.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<span className="px-2 py-0.5 bg-red-50 text-red-500 text-xs rounded">
|
||||||
|
省¥{(pkg.originalPrice - pkg.price).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-4 mt-4">
|
||||||
|
<h3 className="font-bold text-gray-800 mb-3">套餐包含</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{pkg.features.map((feature, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-2">
|
||||||
|
<Check size={16} className="text-primary-500" />
|
||||||
|
<span className="text-sm text-gray-600">{feature}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-4 mt-4">
|
||||||
|
<h3 className="font-bold text-gray-800 mb-3">改造方案</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{renovationSchemes.map((scheme) => (
|
||||||
|
<motion.div
|
||||||
|
key={scheme.id}
|
||||||
|
className="flex gap-3 p-3 bg-gray-50 rounded-xl"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={scheme.image}
|
||||||
|
alt={scheme.title}
|
||||||
|
className="w-20 h-20 rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium text-gray-800">{scheme.title}</h4>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">{scheme.description}</p>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<span className="text-primary-500 font-medium">
|
||||||
|
¥{scheme.price.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-400">{scheme.duration}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-primary-50 rounded-xl p-4 mt-4">
|
||||||
|
<h4 className="font-medium text-primary-700 mb-2">服务保障</h4>
|
||||||
|
<ul className="text-sm text-primary-600 space-y-1">
|
||||||
|
<li>· 30天无理由退换</li>
|
||||||
|
<li>· 终身质保服务</li>
|
||||||
|
<li>· 专业施工团队</li>
|
||||||
|
<li>· 一站式售后服务</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 p-4 safe-bottom">
|
||||||
|
<div className="flex gap-3 max-w-lg mx-auto">
|
||||||
|
<motion.button
|
||||||
|
className="flex-1 h-12 rounded-xl border border-primary-500 text-primary-500 font-medium"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
加入购物车
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
className="flex-1 h-12 rounded-xl bg-primary-500 text-white font-medium"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
立即购买
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronLeft, Check } from 'lucide-react'
|
||||||
|
import { balconyDesigns } from '@/mock/homeData'
|
||||||
|
|
||||||
|
export default function BalconyPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="bg-white px-4 py-3 flex items-center border-b border-gray-100">
|
||||||
|
<button onClick={() => navigate(-1)} className="p-1">
|
||||||
|
<ChevronLeft size={24} className="text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<h1 className="flex-1 text-center font-bold text-gray-800">阳台设计</h1>
|
||||||
|
<div className="w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{balconyDesigns.map((design) => (
|
||||||
|
<motion.div
|
||||||
|
key={design.id}
|
||||||
|
className="bg-white rounded-xl shadow-sm overflow-hidden"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={design.image}
|
||||||
|
alt={design.title}
|
||||||
|
className="w-full h-40 object-cover"
|
||||||
|
/>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="font-bold text-gray-800">{design.title}</h3>
|
||||||
|
<span className="text-primary-500 font-medium">¥{design.price}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-500 mb-3">{design.desc}</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{design.features.map((feature, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="px-2 py-1 bg-gray-50 rounded-md text-xs text-gray-600"
|
||||||
|
>
|
||||||
|
{feature}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-4">
|
||||||
|
<h3 className="font-bold text-gray-800 mb-3">阳台设计要点</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{[
|
||||||
|
'合理规划功能区域',
|
||||||
|
'选择防水防晒材料',
|
||||||
|
'注意承重安全',
|
||||||
|
'考虑采光通风',
|
||||||
|
'绿植搭配技巧',
|
||||||
|
].map((item, index) => (
|
||||||
|
<li key={index} className="flex items-center gap-2 text-sm text-gray-600">
|
||||||
|
<Check size={16} className="text-primary-500" />
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronLeft, Star, MessageCircle, Phone, MapPin } from 'lucide-react'
|
||||||
|
import { designers } from '@/mock/homeData'
|
||||||
|
|
||||||
|
export default function DesignerPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const designer = designers[0]
|
||||||
|
|
||||||
|
const works = [
|
||||||
|
{ id: 1, image: 'https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?w=400' },
|
||||||
|
{ id: 2, image: 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=400' },
|
||||||
|
{ id: 3, image: 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400' },
|
||||||
|
{ id: 4, image: 'https://images.unsplash.com/photo-1600566753086-00f18fb6b3ea?w=400' },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="bg-gradient-to-br from-primary-500 to-primary-600 text-white">
|
||||||
|
<div className="px-4 py-3 flex items-center">
|
||||||
|
<button onClick={() => navigate(-1)} className="p-1">
|
||||||
|
<ChevronLeft size={24} />
|
||||||
|
</button>
|
||||||
|
<h1 className="flex-1 text-center font-bold">设计师主页</h1>
|
||||||
|
<div className="w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 pb-6 pt-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<img
|
||||||
|
src={designer.avatar}
|
||||||
|
alt={designer.name}
|
||||||
|
className="w-20 h-20 rounded-full object-cover border-2 border-white"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="text-xl font-bold">{designer.name}</h2>
|
||||||
|
<p className="text-primary-100">{designer.title}</p>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Star size={14} className="fill-yellow-400 text-yellow-400" />
|
||||||
|
<span className="text-sm">{designer.rating}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-primary-200">|</span>
|
||||||
|
<span className="text-sm">{designer.projects}套作品</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 -mt-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-4">
|
||||||
|
<div className="grid grid-cols-3 gap-4 text-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-bold text-gray-800">{designer.experience}</p>
|
||||||
|
<p className="text-xs text-gray-500">从业经验</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-bold text-gray-800">{designer.style}</p>
|
||||||
|
<p className="text-xs text-gray-500">擅长风格</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-bold text-gray-800">98%</p>
|
||||||
|
<p className="text-xs text-gray-500">好评率</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 mt-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-4">
|
||||||
|
<h3 className="font-bold text-gray-800 mb-3">个人简介</h3>
|
||||||
|
<p className="text-sm text-gray-600 leading-relaxed">
|
||||||
|
拥有10年室内设计经验,专注于现代简约、北欧风格设计。曾获多项设计大奖,
|
||||||
|
作品多次刊登于《家居廊》《Elle Decoration》等知名杂志。擅长将空间功能与美学完美结合,
|
||||||
|
为每一位客户打造独一无二的理想之家。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 mt-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-4">
|
||||||
|
<h3 className="font-bold text-gray-800 mb-3">设计作品</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{works.map((work) => (
|
||||||
|
<motion.div
|
||||||
|
key={work.id}
|
||||||
|
className="aspect-square rounded-lg overflow-hidden"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={work.image}
|
||||||
|
alt={`作品${work.id}`}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 mt-4 pb-20">
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-4">
|
||||||
|
<h3 className="font-bold text-gray-800 mb-3">服务范围</h3>
|
||||||
|
<div className="flex items-center gap-2 text-gray-600">
|
||||||
|
<MapPin size={16} className="text-primary-500" />
|
||||||
|
<span className="text-sm">广州市全区及周边城市</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 p-4 safe-bottom">
|
||||||
|
<div className="flex gap-3 max-w-lg mx-auto">
|
||||||
|
<motion.button
|
||||||
|
className="flex-1 flex items-center justify-center gap-2 h-12 rounded-xl border border-gray-200 text-gray-600"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<MessageCircle size={20} />
|
||||||
|
咨询
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
className="flex-1 flex items-center justify-center gap-2 h-12 rounded-xl border border-gray-200 text-gray-600"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Phone size={20} />
|
||||||
|
电话
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
className="flex-1 h-12 rounded-xl bg-primary-500 text-white font-medium"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={() => navigate('/user/home2')}
|
||||||
|
>
|
||||||
|
预约设计
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,313 @@
|
||||||
|
import AIChatModal from '@/components/AIChatModal'
|
||||||
|
import NotificationModal from '@/components/NotificationModal'
|
||||||
|
import { featuredProjects, groupPackages, homeBanners, quickActions } from '@/mock/homeData'
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
Bell,
|
||||||
|
Calculator,
|
||||||
|
Calendar,
|
||||||
|
ChevronRight,
|
||||||
|
Palette,
|
||||||
|
Sparkles,
|
||||||
|
Truck
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
function useHorizontalScroll() {
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = scrollRef.current
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
let isDown = false
|
||||||
|
let startX = 0
|
||||||
|
let scrollLeft = 0
|
||||||
|
|
||||||
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
|
isDown = true
|
||||||
|
el.style.cursor = 'grabbing'
|
||||||
|
startX = e.pageX - el.offsetLeft
|
||||||
|
scrollLeft = el.scrollLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
isDown = false
|
||||||
|
el.style.cursor = 'grab'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
isDown = false
|
||||||
|
el.style.cursor = 'grab'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!isDown) return
|
||||||
|
e.preventDefault()
|
||||||
|
const x = e.pageX - el.offsetLeft
|
||||||
|
const walk = (x - startX) * 1.5
|
||||||
|
el.scrollLeft = scrollLeft - walk
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWheel = (e: WheelEvent) => {
|
||||||
|
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
||||||
|
e.preventDefault()
|
||||||
|
el.scrollLeft += e.deltaY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
el.style.cursor = 'grab'
|
||||||
|
el.addEventListener('mousedown', handleMouseDown)
|
||||||
|
el.addEventListener('mouseleave', handleMouseLeave)
|
||||||
|
el.addEventListener('mouseup', handleMouseUp)
|
||||||
|
el.addEventListener('mousemove', handleMouseMove)
|
||||||
|
el.addEventListener('wheel', handleWheel, { passive: false })
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
el.removeEventListener('mousedown', handleMouseDown)
|
||||||
|
el.removeEventListener('mouseleave', handleMouseLeave)
|
||||||
|
el.removeEventListener('mouseup', handleMouseUp)
|
||||||
|
el.removeEventListener('mousemove', handleMouseMove)
|
||||||
|
el.removeEventListener('wheel', handleWheel)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return scrollRef
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [currentBanner, setCurrentBanner] = useState(0)
|
||||||
|
const [countdown, setCountdown] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 })
|
||||||
|
const [showNotifications, setShowNotifications] = useState(false)
|
||||||
|
const [showAIChat, setShowAIChat] = useState(false)
|
||||||
|
const scrollRef = useHorizontalScroll()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setCurrentBanner((prev) => (prev + 1) % homeBanners.length)
|
||||||
|
}, 4000)
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const endTime = new Date()
|
||||||
|
endTime.setDate(endTime.getDate() + 3)
|
||||||
|
endTime.setHours(23, 59, 59, 999)
|
||||||
|
|
||||||
|
const updateCountdown = () => {
|
||||||
|
const now = new Date()
|
||||||
|
const diff = endTime.getTime() - now.getTime()
|
||||||
|
|
||||||
|
if (diff <= 0) {
|
||||||
|
setCountdown({ days: 0, hours: 0, minutes: 0, seconds: 0 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||||
|
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
||||||
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
||||||
|
const seconds = Math.floor((diff % (1000 * 60)) / 1000)
|
||||||
|
|
||||||
|
setCountdown({ days, hours, minutes, seconds })
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCountdown()
|
||||||
|
const timer = setInterval(updateCountdown, 1000)
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getActionIcon = (icon: string) => {
|
||||||
|
const icons: Record<string, React.ReactNode> = {
|
||||||
|
calculator: <Calculator size={24} />,
|
||||||
|
palette: <Palette size={24} />,
|
||||||
|
truck: <Truck size={24} />,
|
||||||
|
calendar: <Calendar size={24} />,
|
||||||
|
}
|
||||||
|
return icons[icon] || <Calculator size={24} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||||
|
<div className="bg-gradient-to-br from-primary-500 to-primary-600 text-white px-4 pt-12 pb-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-bold">入舍智能家装</h1>
|
||||||
|
<p className="text-primary-100 text-sm mt-1">让家更美好</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNotifications(true)}
|
||||||
|
className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center relative"
|
||||||
|
>
|
||||||
|
<Bell size={20} />
|
||||||
|
<span className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 rounded-full text-xs flex items-center justify-center font-bold">2</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAIChat(true)}
|
||||||
|
className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Sparkles size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative h-40 rounded-xl overflow-hidden">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={currentBanner}
|
||||||
|
initial={{ opacity: 0, x: 100 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -100 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="absolute inset-0"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={homeBanners[currentBanner].image}
|
||||||
|
alt={homeBanners[currentBanner].title}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
||||||
|
<div className="absolute bottom-4 left-4 text-white">
|
||||||
|
<h3 className="font-bold text-lg">{homeBanners[currentBanner].title}</h3>
|
||||||
|
<p className="text-sm opacity-90">{homeBanners[currentBanner].subtitle}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<div className="absolute bottom-2 right-4 flex gap-1">
|
||||||
|
{homeBanners.map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`w-2 h-2 rounded-full transition-all ${index === currentBanner ? 'bg-white w-4' : 'bg-white/50'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 -mt-4">
|
||||||
|
<div className="rounded-xl shadow-sm p-4" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
{quickActions.map((action) => (
|
||||||
|
<motion.button
|
||||||
|
key={action.id}
|
||||||
|
onClick={() => navigate(action.link)}
|
||||||
|
className="flex flex-col items-center"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-primary-50 text-primary-500 flex items-center justify-center mb-2">
|
||||||
|
{getActionIcon(action.icon)}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>{action.title}</span>
|
||||||
|
<span className="text-xs" style={{ color: 'var(--text-tertiary)' }}>{action.desc}</span>
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 mt-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<h2 className="font-bold text-base" style={{ color: 'var(--text-primary)' }}>团购秒杀</h2>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-xs font-medium" style={{ color: 'var(--text-secondary)' }}>距结束</span>
|
||||||
|
<span className="bg-primary-500 text-white text-xs font-bold px-1.5 py-0.5 rounded min-w-[22px] text-center leading-tight">
|
||||||
|
{String(countdown.days).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
<span className="text-primary-500 font-bold text-xs">天</span>
|
||||||
|
<span className="bg-primary-500 text-white text-xs font-bold px-1.5 py-0.5 rounded min-w-[22px] text-center leading-tight">
|
||||||
|
{String(countdown.hours).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
<span className="text-primary-500 font-bold text-xs">时</span>
|
||||||
|
<span className="bg-primary-500 text-white text-xs font-bold px-1.5 py-0.5 rounded min-w-[22px] text-center leading-tight">
|
||||||
|
{String(countdown.minutes).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
<span className="text-primary-500 font-bold text-xs">分</span>
|
||||||
|
<span className="bg-primary-500 text-white text-xs font-bold px-1.5 py-0.5 rounded min-w-[22px] text-center leading-tight">
|
||||||
|
{String(countdown.seconds).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
<span className="text-primary-500 font-bold text-xs">秒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/user/group')}
|
||||||
|
className="text-primary-500 text-sm flex items-center font-medium"
|
||||||
|
>
|
||||||
|
查看更多 <ChevronRight size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={scrollRef} className="flex gap-3 overflow-x-auto pb-2 -mx-4 px-4 scrollbar-hide select-none">
|
||||||
|
{groupPackages.map((pkg) => (
|
||||||
|
<motion.div
|
||||||
|
key={pkg.id}
|
||||||
|
onClick={() => navigate('/user/group')}
|
||||||
|
className="flex-shrink-0 w-36 rounded-xl overflow-hidden shadow-sm"
|
||||||
|
style={{ backgroundColor: 'var(--bg-primary)' }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={pkg.image}
|
||||||
|
alt={pkg.name}
|
||||||
|
className="w-full h-24 object-cover"
|
||||||
|
/>
|
||||||
|
<div className="p-3">
|
||||||
|
<h3 className="font-medium text-sm truncate" style={{ color: 'var(--text-primary)' }}>{pkg.name}</h3>
|
||||||
|
<div className="flex items-center gap-1 mt-1">
|
||||||
|
<span className="text-primary-500 font-bold text-sm">¥{pkg.groupPrice}</span>
|
||||||
|
<span className="text-xs line-through" style={{ color: 'var(--text-tertiary)' }}>¥{pkg.originalPrice}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between mt-2">
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded-full bg-primary-50 text-primary-500">{pkg.tag}</span>
|
||||||
|
<span className="text-xs" style={{ color: 'var(--text-tertiary)' }}>已售{pkg.sold}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 mt-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h2 className="font-bold" style={{ color: 'var(--text-primary)' }}>精选案例</h2>
|
||||||
|
<button className="text-primary-500 text-sm flex items-center">
|
||||||
|
查看更多 <ChevronRight size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{featuredProjects.map((project) => (
|
||||||
|
<motion.div
|
||||||
|
key={project.id}
|
||||||
|
className="rounded-xl overflow-hidden shadow-sm"
|
||||||
|
style={{ backgroundColor: 'var(--bg-primary)' }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={project.image}
|
||||||
|
alt={project.title}
|
||||||
|
className="w-full h-28 object-cover"
|
||||||
|
/>
|
||||||
|
<div className="p-3">
|
||||||
|
<h3 className="font-medium text-sm" style={{ color: 'var(--text-primary)' }}>{project.title}</h3>
|
||||||
|
<div className="flex items-center gap-2 mt-1 text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
<span>{project.area}</span>
|
||||||
|
<span>|</span>
|
||||||
|
<span>{project.style}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-primary-500 text-xs mt-1">{project.budget}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NotificationModal isOpen={showNotifications} onClose={() => setShowNotifications(false)} />
|
||||||
|
<AIChatModal isOpen={showAIChat} onClose={() => setShowAIChat(false)} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronLeft, ChevronRight, Check } from 'lucide-react'
|
||||||
|
|
||||||
|
const timeSlots = [
|
||||||
|
{ id: 1, time: '09:00-11:00', available: true },
|
||||||
|
{ id: 2, time: '11:00-13:00', available: true },
|
||||||
|
{ id: 3, time: '14:00-16:00', available: false },
|
||||||
|
{ id: 4, time: '16:00-18:00', available: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
const dates = Array.from({ length: 7 }, (_, i) => {
|
||||||
|
const date = new Date()
|
||||||
|
date.setDate(date.getDate() + i + 1)
|
||||||
|
return {
|
||||||
|
id: i + 1,
|
||||||
|
day: date.getDate(),
|
||||||
|
week: ['日', '一', '二', '三', '四', '五', '六'][date.getDay()],
|
||||||
|
fullDate: date.toLocaleDateString('zh-CN'),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function HomePage2() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [selectedDate, setSelectedDate] = useState(1)
|
||||||
|
const [selectedTime, setSelectedTime] = useState<number | null>(null)
|
||||||
|
const [address, setAddress] = useState('')
|
||||||
|
const [phone, setPhone] = useState('')
|
||||||
|
const [remark, setRemark] = useState('')
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!selectedTime || !address || !phone) {
|
||||||
|
alert('请填写完整信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
alert('预约成功!我们将尽快与您联系确认。')
|
||||||
|
navigate('/user/home')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="bg-white px-4 py-3 flex items-center border-b border-gray-100">
|
||||||
|
<button onClick={() => navigate(-1)} className="p-1">
|
||||||
|
<ChevronLeft size={24} className="text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<h1 className="flex-1 text-center font-bold text-gray-800">预约管家上门</h1>
|
||||||
|
<div className="w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="bg-white rounded-xl p-4 shadow-sm mb-4">
|
||||||
|
<h3 className="font-medium text-gray-800 mb-3">选择日期</h3>
|
||||||
|
<div className="flex gap-2 overflow-x-auto pb-2">
|
||||||
|
{dates.map((date) => (
|
||||||
|
<motion.button
|
||||||
|
key={date.id}
|
||||||
|
onClick={() => setSelectedDate(date.id)}
|
||||||
|
className={`flex-shrink-0 w-14 h-16 rounded-xl flex flex-col items-center justify-center ${
|
||||||
|
selectedDate === date.id
|
||||||
|
? 'bg-primary-500 text-white'
|
||||||
|
: 'bg-gray-50 text-gray-600'
|
||||||
|
}`}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<span className="text-lg font-bold">{date.day}</span>
|
||||||
|
<span className="text-xs">周{date.week}</span>
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl p-4 shadow-sm mb-4">
|
||||||
|
<h3 className="font-medium text-gray-800 mb-3">选择时间</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{timeSlots.map((slot) => (
|
||||||
|
<motion.button
|
||||||
|
key={slot.id}
|
||||||
|
onClick={() => slot.available && setSelectedTime(slot.id)}
|
||||||
|
disabled={!slot.available}
|
||||||
|
className={`h-12 rounded-xl flex items-center justify-center gap-2 ${
|
||||||
|
!slot.available
|
||||||
|
? 'bg-gray-100 text-gray-400'
|
||||||
|
: selectedTime === slot.id
|
||||||
|
? 'bg-primary-500 text-white'
|
||||||
|
: 'bg-gray-50 text-gray-600 border border-gray-200'
|
||||||
|
}`}
|
||||||
|
whileTap={slot.available ? { scale: 0.95 } : {}}
|
||||||
|
>
|
||||||
|
{slot.time}
|
||||||
|
{selectedTime === slot.id && <Check size={16} />}
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl p-4 shadow-sm mb-4">
|
||||||
|
<h3 className="font-medium text-gray-800 mb-3">联系信息</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-gray-500 mb-1 block">服务地址</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={address}
|
||||||
|
onChange={(e) => setAddress(e.target.value)}
|
||||||
|
placeholder="请输入详细地址"
|
||||||
|
className="input-field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-gray-500 mb-1 block">联系电话</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
value={phone}
|
||||||
|
onChange={(e) => setPhone(e.target.value)}
|
||||||
|
placeholder="请输入联系电话"
|
||||||
|
className="input-field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-gray-500 mb-1 block">备注信息</label>
|
||||||
|
<textarea
|
||||||
|
value={remark}
|
||||||
|
onChange={(e) => setRemark(e.target.value)}
|
||||||
|
placeholder="请输入备注(选填)"
|
||||||
|
rows={3}
|
||||||
|
className="input-field resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-primary-50 rounded-xl p-4 mb-4">
|
||||||
|
<h4 className="font-medium text-primary-700 mb-2">服务说明</h4>
|
||||||
|
<ul className="text-sm text-primary-600 space-y-1">
|
||||||
|
<li>· 免费上门量房服务</li>
|
||||||
|
<li>· 专业设计师一对一沟通</li>
|
||||||
|
<li>· 提供3套设计方案参考</li>
|
||||||
|
<li>· 服务时长约1-2小时</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className="w-full btn-primary h-12 text-lg"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
确认预约
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronLeft, FileText, FileSignature, CheckCircle } from 'lucide-react'
|
||||||
|
import { dataAssets } from '@/mock/mineData'
|
||||||
|
|
||||||
|
export default function DataAssetsPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const getIcon = (icon: string) => {
|
||||||
|
const icons: Record<string, React.ReactNode> = {
|
||||||
|
'palette': <FileText size={24} />,
|
||||||
|
'file-text': <FileText size={24} />,
|
||||||
|
'file-signature': <FileSignature size={24} />,
|
||||||
|
'check-circle': <CheckCircle size={24} />,
|
||||||
|
}
|
||||||
|
return icons[icon] || <FileText size={24} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="bg-white px-4 py-3 flex items-center border-b border-gray-100">
|
||||||
|
<button onClick={() => navigate(-1)} className="p-1">
|
||||||
|
<ChevronLeft size={24} className="text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<h1 className="flex-1 text-center font-bold text-gray-800">数据资产</h1>
|
||||||
|
<div className="w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{dataAssets.map((asset) => (
|
||||||
|
<motion.div
|
||||||
|
key={asset.id}
|
||||||
|
className="bg-white rounded-xl shadow-sm p-4"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-14 h-14 rounded-xl bg-primary-50 flex items-center justify-center text-primary-500">
|
||||||
|
{getIcon(asset.icon)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-medium text-gray-800">{asset.title}</h3>
|
||||||
|
<p className="text-sm text-gray-500">{asset.count} 个文件</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-2xl font-bold text-primary-500">{asset.count}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="bg-primary-50 rounded-xl p-4">
|
||||||
|
<h4 className="font-medium text-primary-700 mb-2">数据安全说明</h4>
|
||||||
|
<ul className="text-sm text-primary-600 space-y-1">
|
||||||
|
<li>· 所有数据均加密存储</li>
|
||||||
|
<li>· 支持随时下载备份</li>
|
||||||
|
<li>· 数据永久保存</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronLeft, Eye, Edit, Share2 } from 'lucide-react'
|
||||||
|
import { designSchemes } from '@/mock/mineData'
|
||||||
|
|
||||||
|
export default function DesignSchemesPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case '已完成':
|
||||||
|
return 'bg-green-50 text-green-500'
|
||||||
|
case '进行中':
|
||||||
|
return 'bg-primary-50 text-primary-500'
|
||||||
|
case '待确认':
|
||||||
|
return 'bg-yellow-50 text-yellow-500'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-50 text-gray-500'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="bg-white px-4 py-3 flex items-center border-b border-gray-100">
|
||||||
|
<button onClick={() => navigate(-1)} className="p-1">
|
||||||
|
<ChevronLeft size={24} className="text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<h1 className="flex-1 text-center font-bold text-gray-800">设计方案</h1>
|
||||||
|
<div className="w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{designSchemes.map((scheme) => (
|
||||||
|
<motion.div
|
||||||
|
key={scheme.id}
|
||||||
|
className="bg-white rounded-xl shadow-sm overflow-hidden"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={scheme.image}
|
||||||
|
alt={scheme.title}
|
||||||
|
className="w-full h-40 object-cover"
|
||||||
|
/>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="font-bold text-gray-800">{scheme.title}</h3>
|
||||||
|
<span className={`text-xs px-2 py-1 rounded-full ${getStatusColor(scheme.status)}`}>
|
||||||
|
{scheme.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-500 mb-3">
|
||||||
|
<span>设计师:{scheme.designer}</span>
|
||||||
|
<span>|</span>
|
||||||
|
<span>{scheme.updateDate}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative pt-1 mb-3">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-xs text-gray-500">完成进度</span>
|
||||||
|
<span className="text-xs font-medium text-primary-500">{scheme.progress}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
className="h-full bg-primary-500 rounded-full"
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${scheme.progress}%` }}
|
||||||
|
transition={{ duration: 0.8, ease: 'easeOut' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<motion.button
|
||||||
|
className="flex-1 flex items-center justify-center gap-1 py-2 rounded-lg border border-gray-200 text-gray-600 text-sm"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<Eye size={16} />
|
||||||
|
预览
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
className="flex-1 flex items-center justify-center gap-1 py-2 rounded-lg border border-gray-200 text-gray-600 text-sm"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<Edit size={16} />
|
||||||
|
编辑
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
className="flex-1 flex items-center justify-center gap-1 py-2 rounded-lg bg-primary-500 text-white text-sm"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<Share2 size={16} />
|
||||||
|
分享
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { dataAssets, menuItems, userInfo } from '@/mock/mineData'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronRight, Clock, Heart, HelpCircle, MapPin, Settings, ShoppingBag } from 'lucide-react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
export default function MinePage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const getIcon = (icon: string) => {
|
||||||
|
const icons: Record<string, React.ReactNode> = {
|
||||||
|
'clock': <Clock size={20} />,
|
||||||
|
'shopping-bag': <ShoppingBag size={20} />,
|
||||||
|
'heart': <Heart size={20} />,
|
||||||
|
'map-pin': <MapPin size={20} />,
|
||||||
|
'help-circle': <HelpCircle size={20} />,
|
||||||
|
'settings': <Settings size={20} />,
|
||||||
|
}
|
||||||
|
return icons[icon] || <ShoppingBag size={20} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||||
|
<div className="bg-gradient-to-br from-primary-500 to-primary-600 text-white px-4 pt-12 pb-8">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<img
|
||||||
|
src={userInfo.avatar}
|
||||||
|
alt={userInfo.name}
|
||||||
|
className="w-16 h-16 rounded-full object-cover border-2 border-white"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="text-xl font-bold">{userInfo.name}</h2>
|
||||||
|
<p className="text-primary-100">{userInfo.phone}</p>
|
||||||
|
<span className="inline-block mt-1 px-2 py-0.5 bg-white/20 rounded text-xs">
|
||||||
|
{userInfo.level}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronRight size={24} className="text-white/60" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 -mt-4">
|
||||||
|
<div className="rounded-xl shadow-sm p-4 mb-4" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||||||
|
<div className="grid grid-cols-3 gap-4 text-center">
|
||||||
|
<motion.div
|
||||||
|
onClick={() => navigate('/user/mine/data')}
|
||||||
|
className="cursor-pointer"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<p className="text-2xl font-bold text-primary-500">{userInfo.points}</p>
|
||||||
|
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>积分</p>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
onClick={() => navigate('/user/mine/designs')}
|
||||||
|
className="cursor-pointer"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<p className="text-2xl font-bold text-primary-500">{userInfo.coupons}</p>
|
||||||
|
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>优惠券</p>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
onClick={() => navigate('/user/mine/partner')}
|
||||||
|
className="cursor-pointer"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<p className="text-2xl font-bold text-primary-500">合伙人</p>
|
||||||
|
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>中心</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl shadow-sm p-4 mb-4" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="font-bold" style={{ color: 'var(--text-primary)' }}>数据资产</h3>
|
||||||
|
<ChevronRight size={20} style={{ color: 'var(--text-tertiary)' }} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
{dataAssets.map((asset) => (
|
||||||
|
<motion.div
|
||||||
|
key={asset.id}
|
||||||
|
onClick={() => navigate('/user/mine/data')}
|
||||||
|
className="text-center cursor-pointer"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 mx-auto rounded-full bg-primary-50 flex items-center justify-center mb-1">
|
||||||
|
<span className="text-lg">{asset.count}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>{asset.title}</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl shadow-sm overflow-hidden" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||||||
|
{menuItems.map((item, index) => (
|
||||||
|
<motion.button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => navigate(item.link)}
|
||||||
|
className={`w-full flex items-center justify-between p-4 ${index !== menuItems.length - 1 ? 'border-b' : ''}`}
|
||||||
|
style={{ borderColor: 'var(--border-color)' }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg flex items-center justify-center" style={{ backgroundColor: 'var(--bg-tertiary)', color: 'var(--text-secondary)' }}>
|
||||||
|
{getIcon(item.icon)}
|
||||||
|
</div>
|
||||||
|
<span style={{ color: 'var(--text-primary)' }}>{item.title}</span>
|
||||||
|
</div>
|
||||||
|
<ChevronRight size={20} style={{ color: 'var(--text-tertiary)' }} />
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronLeft, Users, TrendingUp, Gift, ChevronRight, Share2 } from 'lucide-react'
|
||||||
|
import { partnerInfo, partnerProducts } from '@/mock/mineData'
|
||||||
|
|
||||||
|
export default function PartnerCenterPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="bg-white px-4 py-3 flex items-center border-b border-gray-100">
|
||||||
|
<button onClick={() => navigate(-1)} className="p-1">
|
||||||
|
<ChevronLeft size={24} className="text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<h1 className="flex-1 text-center font-bold text-gray-800">合伙人中心</h1>
|
||||||
|
<div className="w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="bg-gradient-to-br from-primary-500 to-primary-600 rounded-xl p-4 text-white mb-4">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Users size={20} />
|
||||||
|
<span className="font-medium">{partnerInfo.level}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4 text-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">¥{partnerInfo.commission.toLocaleString()}</p>
|
||||||
|
<p className="text-xs text-primary-100">累计佣金</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">{partnerInfo.orders}</p>
|
||||||
|
<p className="text-xs text-primary-100">推广订单</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">{partnerInfo.teamSize}</p>
|
||||||
|
<p className="text-xs text-primary-100">团队人数</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-4 mb-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="font-bold text-gray-800">本月目标</h3>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
¥{partnerInfo.monthlyProgress.toLocaleString()} / ¥{partnerInfo.monthlyTarget.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-3 bg-gray-100 rounded-full overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
className="h-full bg-gradient-to-r from-primary-400 to-primary-500 rounded-full"
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${(partnerInfo.monthlyProgress / partnerInfo.monthlyTarget) * 100}%` }}
|
||||||
|
transition={{ duration: 1, ease: 'easeOut' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-400 mt-2">
|
||||||
|
还差 ¥{(partnerInfo.monthlyTarget - partnerInfo.monthlyProgress).toLocaleString()} 达成本月目标
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-4 mb-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="font-bold text-gray-800">选品推广</h3>
|
||||||
|
<ChevronRight size={20} className="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{partnerProducts.map((product) => (
|
||||||
|
<motion.div
|
||||||
|
key={product.id}
|
||||||
|
className="flex gap-3 p-3 bg-gray-50 rounded-xl"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.title}
|
||||||
|
className="w-16 h-16 rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium text-gray-800 text-sm">{product.title}</h4>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">已售 {product.sales} 件</p>
|
||||||
|
<p className="text-sm text-primary-500 font-medium mt-1">
|
||||||
|
佣金 ¥{product.commission.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
className="self-center p-2 rounded-full bg-primary-50 text-primary-500"
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<Share2 size={18} />
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<motion.div
|
||||||
|
className="bg-white rounded-xl shadow-sm p-4 text-center"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<TrendingUp size={24} className="mx-auto text-primary-500 mb-2" />
|
||||||
|
<p className="text-sm text-gray-600">推广数据</p>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="bg-white rounded-xl shadow-sm p-4 text-center"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Gift size={24} className="mx-auto text-primary-500 mb-2" />
|
||||||
|
<p className="text-sm text-gray-600">奖励中心</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
import { useThemeStore, ThemeMode } from '@/store/themeStore'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Check, Moon, Sun, Waves } from 'lucide-react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
interface ThemeOption {
|
||||||
|
mode: ThemeMode
|
||||||
|
label: string
|
||||||
|
description: string
|
||||||
|
icon: React.ReactNode
|
||||||
|
previewBg: string
|
||||||
|
previewGradient: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeOptions: ThemeOption[] = [
|
||||||
|
{
|
||||||
|
mode: 'light',
|
||||||
|
label: '亮色模式',
|
||||||
|
description: '明亮清新的界面风格',
|
||||||
|
icon: <Sun size={24} />,
|
||||||
|
previewBg: 'bg-white',
|
||||||
|
previewGradient: 'from-orange-400 to-orange-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'dark',
|
||||||
|
label: '暗色模式',
|
||||||
|
description: '护眼舒适的深色界面',
|
||||||
|
icon: <Moon size={24} />,
|
||||||
|
previewBg: 'bg-gray-800',
|
||||||
|
previewGradient: 'from-orange-400 to-orange-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'deepBlue',
|
||||||
|
label: '深蓝模式',
|
||||||
|
description: '沉稳专业的蓝色主题',
|
||||||
|
icon: <Waves size={24} />,
|
||||||
|
previewBg: 'bg-slate-900',
|
||||||
|
previewGradient: 'from-blue-500 to-blue-600',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { theme, setTheme } = useThemeStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||||
|
<div className="sticky top-0 z-10 backdrop-blur-md" style={{ backgroundColor: 'var(--bg-primary)', borderBottom: '1px solid var(--border-color)' }}>
|
||||||
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
className="text-base font-medium"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
返回
|
||||||
|
</button>
|
||||||
|
<h1 className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
设置
|
||||||
|
</h1>
|
||||||
|
<div className="w-12" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-base font-semibold mb-1" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
主题设置
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
选择您喜欢的界面主题风格
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{themeOptions.map((option) => {
|
||||||
|
const isActive = theme === option.mode
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
key={option.mode}
|
||||||
|
onClick={() => setTheme(option.mode)}
|
||||||
|
className="w-full rounded-xl overflow-hidden transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--bg-primary)',
|
||||||
|
boxShadow: isActive ? `0 4px 12px var(--shadow-color)` : '0 1px 3px var(--shadow-color)',
|
||||||
|
border: isActive ? '2px solid var(--gradient-start)' : '2px solid transparent',
|
||||||
|
}}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
className={`w-16 h-20 rounded-lg overflow-hidden flex-shrink-0 ${option.previewBg}`}
|
||||||
|
>
|
||||||
|
<div className={`h-8 bg-gradient-to-r ${option.previewGradient}`} />
|
||||||
|
<div className="p-2 space-y-1">
|
||||||
|
<div className="h-2 rounded opacity-30" style={{ backgroundColor: 'var(--text-primary)' }} />
|
||||||
|
<div className="h-2 w-3/4 rounded opacity-20" style={{ backgroundColor: 'var(--text-primary)' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 text-left">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span
|
||||||
|
className="p-1.5 rounded-lg"
|
||||||
|
style={{ backgroundColor: 'var(--bg-tertiary)', color: isActive ? 'var(--gradient-start)' : 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
|
{option.icon}
|
||||||
|
</span>
|
||||||
|
<h3 className="font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
{option.label}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
{option.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0 mt-1">
|
||||||
|
{isActive && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
className="w-6 h-6 rounded-full flex items-center justify-center"
|
||||||
|
style={{ backgroundColor: 'var(--gradient-start)' }}
|
||||||
|
>
|
||||||
|
<Check size={14} className="text-white" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 p-4 rounded-xl" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||||||
|
<h3 className="font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
当前主题
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
{themeOptions.find(t => t.mode === theme)?.label} - {themeOptions.find(t => t.mode === theme)?.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
import { progressDetails, progressStages, projectInfo } from '@/mock/progressData'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Check, ChevronRight, Clock, MessageCircle, Phone } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export default function ProgressPage() {
|
||||||
|
const [activeStage, setActiveStage] = useState('design')
|
||||||
|
|
||||||
|
const currentStageIndex = progressStages.findIndex((s) => s.id === activeStage)
|
||||||
|
const currentDetail = progressDetails[activeStage as keyof typeof progressDetails]
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed':
|
||||||
|
return 'text-green-500'
|
||||||
|
case 'in_progress':
|
||||||
|
return 'text-primary-500'
|
||||||
|
default:
|
||||||
|
return 'text-gray-400'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusBg = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed':
|
||||||
|
return 'bg-green-500'
|
||||||
|
case 'in_progress':
|
||||||
|
return 'bg-primary-500'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-200'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||||
|
<div className="bg-gradient-to-br from-primary-500 to-primary-600 text-white px-4 pt-12 pb-6">
|
||||||
|
<h1 className="text-xl font-bold text-center mb-4">装修进度</h1>
|
||||||
|
|
||||||
|
<div className="bg-white/10 rounded-xl p-4">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-white/20 flex items-center justify-center text-2xl">
|
||||||
|
🏠
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium">{projectInfo.name}</h3>
|
||||||
|
<p className="text-sm text-primary-100">{projectInfo.area} · {projectInfo.style}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative pt-1">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm">整体进度</span>
|
||||||
|
<span className="text-sm font-medium">{projectInfo.progress}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 bg-white/20 rounded-full overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
className="h-full bg-white rounded-full"
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${projectInfo.progress}%` }}
|
||||||
|
transition={{ duration: 1, ease: 'easeOut' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 -mt-4">
|
||||||
|
<div className="rounded-xl shadow-sm p-4 mb-4" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<img
|
||||||
|
src={projectInfo.designer.avatar}
|
||||||
|
alt={projectInfo.designer.name}
|
||||||
|
className="w-12 h-12 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium" style={{ color: 'var(--text-primary)' }}>{projectInfo.designer.name}</h4>
|
||||||
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>设计师</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<motion.button
|
||||||
|
className="w-10 h-10 rounded-full flex items-center justify-center"
|
||||||
|
style={{ backgroundColor: 'var(--bg-tertiary)' }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<MessageCircle size={18} style={{ color: 'var(--text-secondary)' }} />
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
className="w-10 h-10 rounded-full bg-primary-50 flex items-center justify-center"
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<Phone size={18} className="text-primary-500" />
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl shadow-sm p-4 mb-4" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||||||
|
<h3 className="font-bold mb-4" style={{ color: 'var(--text-primary)' }}>施工阶段</h3>
|
||||||
|
|
||||||
|
<div className="flex justify-between mb-6">
|
||||||
|
{progressStages.map((stage, index) => {
|
||||||
|
const isActive = stage.id === activeStage
|
||||||
|
const isPast = index < currentStageIndex
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
key={stage.id}
|
||||||
|
onClick={() => setActiveStage(stage.id)}
|
||||||
|
className="flex flex-col items-center"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-10 h-10 rounded-full flex items-center justify-center text-lg mb-1 ${isPast
|
||||||
|
? 'bg-green-500 text-white'
|
||||||
|
: isActive
|
||||||
|
? 'bg-primary-500 text-white'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
style={(!isPast && !isActive) ? { backgroundColor: 'var(--bg-tertiary)', color: 'var(--text-tertiary)' } : {}}
|
||||||
|
>
|
||||||
|
{isPast ? <Check size={18} /> : stage.icon}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`text-xs ${isActive ? 'text-primary-500 font-medium' : ''}`}
|
||||||
|
style={!isActive ? { color: 'var(--text-secondary)' } : {}}
|
||||||
|
>
|
||||||
|
{stage.name}
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t pt-4" style={{ borderColor: 'var(--border-color)' }}>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h4 className="font-medium" style={{ color: 'var(--text-primary)' }}>{currentDetail.title}</h4>
|
||||||
|
<span
|
||||||
|
className={`text-xs px-2 py-1 rounded-full ${currentDetail.status === 'completed'
|
||||||
|
? 'bg-green-50 text-green-500'
|
||||||
|
: currentDetail.status === 'in_progress'
|
||||||
|
? 'bg-primary-50 text-primary-500'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
style={currentDetail.status === 'pending' ? { backgroundColor: 'var(--bg-tertiary)', color: 'var(--text-tertiary)' } : {}}
|
||||||
|
>
|
||||||
|
{currentDetail.status === 'completed'
|
||||||
|
? '已完成'
|
||||||
|
: currentDetail.status === 'in_progress'
|
||||||
|
? '进行中'
|
||||||
|
: '待开始'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{currentDetail.tasks.map((task) => (
|
||||||
|
<div
|
||||||
|
key={task.id}
|
||||||
|
className="flex items-center justify-between p-3 rounded-lg"
|
||||||
|
style={{ backgroundColor: 'var(--bg-tertiary)' }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className={`w-6 h-6 rounded-full flex items-center justify-center ${getStatusBg(
|
||||||
|
task.status
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{task.status === 'completed' ? (
|
||||||
|
<Check size={14} className="text-white" />
|
||||||
|
) : task.status === 'in_progress' ? (
|
||||||
|
<Clock size={14} className="text-white" />
|
||||||
|
) : (
|
||||||
|
<div className="w-2 h-2 rounded-full bg-white" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`text-sm ${getStatusColor(task.status)}`}
|
||||||
|
>
|
||||||
|
{task.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{task.date && (
|
||||||
|
<span className="text-xs" style={{ color: 'var(--text-tertiary)' }}>{task.date}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 p-3 bg-primary-50 rounded-lg">
|
||||||
|
<p className="text-sm text-primary-600">{currentDetail.tips}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl shadow-sm p-4" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="font-bold" style={{ color: 'var(--text-primary)' }}>项目信息</h3>
|
||||||
|
<ChevronRight size={20} style={{ color: 'var(--text-tertiary)' }} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>预算范围</p>
|
||||||
|
<p className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>{projectInfo.budget}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>开工日期</p>
|
||||||
|
<p className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>{projectInfo.startDate}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>预计完工</p>
|
||||||
|
<p className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>{projectInfo.expectedEndDate}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>装修风格</p>
|
||||||
|
<p className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>{projectInfo.style}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { persist } from 'zustand/middleware'
|
||||||
|
|
||||||
|
export type ThemeMode = 'light' | 'dark' | 'deepBlue'
|
||||||
|
|
||||||
|
interface ThemeState {
|
||||||
|
theme: ThemeMode
|
||||||
|
setTheme: (theme: ThemeMode) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useThemeStore = create<ThemeState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
theme: 'light',
|
||||||
|
setTheme: (theme) => set({ theme }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'theme-storage',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#fff7ed',
|
||||||
|
100: '#ffedd5',
|
||||||
|
200: '#fed7aa',
|
||||||
|
300: '#fdba74',
|
||||||
|
400: '#fb923c',
|
||||||
|
500: '#f97316',
|
||||||
|
600: '#ea580c',
|
||||||
|
700: '#c2410c',
|
||||||
|
800: '#9a3412',
|
||||||
|
900: '#7c2d12',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
50: '#f0fdf4',
|
||||||
|
100: '#dcfce7',
|
||||||
|
200: '#bbf7d0',
|
||||||
|
300: '#86efac',
|
||||||
|
400: '#4ade80',
|
||||||
|
500: '#22c55e',
|
||||||
|
600: '#16a34a',
|
||||||
|
700: '#15803d',
|
||||||
|
800: '#166534',
|
||||||
|
900: '#14532d',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: [
|
||||||
|
'-apple-system',
|
||||||
|
'BlinkMacSystemFont',
|
||||||
|
'Segoe UI',
|
||||||
|
'Roboto',
|
||||||
|
'PingFang SC',
|
||||||
|
'Microsoft YaHei',
|
||||||
|
'Helvetica Neue',
|
||||||
|
'Arial',
|
||||||
|
'sans-serif',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue