feat: add PWA — mobile multi-model chat app (Vite + React)
- Vite + React + TypeScript + Tailwind v4 - vite-plugin-pwa for service worker and manifest - Dark mode, borderless design with glow effects - Reuses Elec components, adapted for mobile viewport - Standalone-ready: can install to phone home screen Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
3
PWA/.gitignore
vendored
Normal file
3
PWA/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
15
PWA/index.html
Normal file
15
PWA/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Elec</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
6907
PWA/package-lock.json
generated
Normal file
6907
PWA/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
PWA/package.json
Normal file
30
PWA/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "elec-pwa",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.3.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.11.0",
|
||||
"postcss": "^8.5.12",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-pwa": "^0.21.0"
|
||||
}
|
||||
}
|
||||
5
PWA/postcss.config.js
Normal file
5
PWA/postcss.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
}
|
||||
}
|
||||
4
PWA/public/favicon.svg
Normal file
4
PWA/public/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" fill="#000" rx="6"/>
|
||||
<text x="16" y="22" text-anchor="middle" fill="#fff" font-size="18" font-family="system-ui">E</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 223 B |
BIN
PWA/public/icon-192.png
Normal file
BIN
PWA/public/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 187 B |
BIN
PWA/public/icon-512.png
Normal file
BIN
PWA/public/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 842 B |
18
PWA/src/App.tsx
Normal file
18
PWA/src/App.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useState } from 'react'
|
||||
import ModelSelector from './components/ModelSelector'
|
||||
import ChatArea from './components/ChatArea'
|
||||
import InputArea from './components/InputArea'
|
||||
|
||||
export type Model = 'Omni' | 'Qwen3.5' | 'Deepseek'
|
||||
|
||||
export default function App() {
|
||||
const [activeModel, setActiveModel] = useState<Model>('Deepseek')
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-black">
|
||||
<ModelSelector active={activeModel} onSelect={setActiveModel} />
|
||||
<ChatArea />
|
||||
<InputArea />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
16
PWA/src/components/ChatArea.tsx
Normal file
16
PWA/src/components/ChatArea.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
export default function ChatArea() {
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto no-scrollbar relative z-10">
|
||||
<div className="glow-area px-6 py-8 min-h-full flex items-center justify-center">
|
||||
<div className="w-full max-w-sm space-y-4">
|
||||
<p className="text-sm leading-relaxed" style={{ color: 'rgba(255,255,255,0.55)' }}>
|
||||
1. 示例文字:
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed" style={{ color: 'rgba(255,255,255,0.35)' }}>
|
||||
2.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
14
PWA/src/components/InputArea.tsx
Normal file
14
PWA/src/components/InputArea.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function InputArea() {
|
||||
return (
|
||||
<div className="px-6 pb-8 pt-4 relative z-10">
|
||||
<div className="glow-area">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="输入"
|
||||
className="w-full bg-transparent text-sm outline-none placeholder:text-[rgba(255,255,255,0.25)]"
|
||||
style={{ color: 'rgba(255,255,255,0.5)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
31
PWA/src/components/ModelSelector.tsx
Normal file
31
PWA/src/components/ModelSelector.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { Model } from '../App'
|
||||
|
||||
const models: Model[] = ['Omni', 'Qwen3.5', 'Deepseek']
|
||||
|
||||
export default function ModelSelector({
|
||||
active,
|
||||
onSelect
|
||||
}: {
|
||||
active: Model
|
||||
onSelect: (m: Model) => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex justify-center gap-8 pt-6 pb-4 z-10 relative"
|
||||
>
|
||||
{models.map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => onSelect(m)}
|
||||
className="text-sm tracking-wide transition-colors duration-300"
|
||||
style={{
|
||||
color: m === active ? '#ffffff' : 'rgba(255,255,255,0.35)',
|
||||
fontWeight: m === active ? 500 : 400,
|
||||
}}
|
||||
>
|
||||
{m}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
36
PWA/src/index.css
Normal file
36
PWA/src/index.css
Normal file
@@ -0,0 +1,36 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
background: #000;
|
||||
color: #e5e5e5;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.glow-area {
|
||||
position: relative;
|
||||
}
|
||||
.glow-area::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 65% 80% at 50% 50%, rgba(255,255,255,0.035) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
10
PWA/src/main.tsx
Normal file
10
PWA/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
11
PWA/tailwind.config.js
Normal file
11
PWA/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
17
PWA/tsconfig.json
Normal file
17
PWA/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
33
PWA/vite.config.ts
Normal file
33
PWA/vite.config.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
manifest: {
|
||||
name: 'Elec',
|
||||
short_name: 'Elec',
|
||||
description: '多模型AI对话',
|
||||
theme_color: '#000000',
|
||||
background_color: '#000000',
|
||||
display: 'standalone',
|
||||
orientation: 'portrait',
|
||||
icons: [
|
||||
{
|
||||
src: '/icon-192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: '/icon-512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
Reference in New Issue
Block a user