Initial commit

This commit is contained in:
2026-04-13 20:28:09 +08:00
commit e2a2eb4666
62 changed files with 17589 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
# GEMINI_API_KEY: Required for Gemini AI API calls.
# AI Studio automatically injects this at runtime from user secrets.
# Users configure this via the Secrets panel in the AI Studio UI.
GEMINI_API_KEY="MY_GEMINI_API_KEY"
# APP_URL: The URL where this applet is hosted.
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
# Used for self-referential links, OAuth callbacks, and API endpoints.
APP_URL="MY_APP_URL"

8
audi-red-note-mini-app/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules/
build/
dist/
coverage/
.DS_Store
*.log
.env*
!.env.example

View File

@@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/546f4d9a-c8ab-40ae-a5f0-bdd95abfca93
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Google AI Studio App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
{
"name": "Audi Red Note Mini App",
"description": "A high-fidelity Audi brand experience for car exploration and lead generation.",
"requestFramePermissions": []
}

3992
audi-red-note-mini-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
{
"name": "react-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port=3000 --host=0.0.0.0",
"build": "vite build",
"preview": "vite preview",
"clean": "rm -rf dist",
"lint": "tsc --noEmit"
},
"dependencies": {
"@google/genai": "^1.29.0",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
"lucide-react": "^0.546.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"vite": "^6.2.0",
"express": "^4.21.2",
"dotenv": "^17.2.3",
"motion": "^12.23.24"
},
"devDependencies": {
"@types/node": "^22.14.0",
"autoprefixer": "^10.4.21",
"tailwindcss": "^4.1.14",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.0",
"@types/express": "^4.17.21"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@@ -0,0 +1,463 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useEffect, FormEvent } from "react";
import { motion, AnimatePresence } from "motion/react";
import {
ChevronRight,
X,
Check,
Phone,
User,
MapPin,
ShieldCheck,
ArrowLeft,
Loader2
} from "lucide-react";
// --- Types ---
interface CarModel {
id: string;
name: string;
price: string;
image: string;
tagline: string;
}
const CAR_MODELS: CarModel[] = [
{
id: "a3",
name: "Audi A3 Sportback",
price: "203,100",
image: "https://images.unsplash.com/photo-1606152421802-db97b9c7a11b?auto=format&fit=crop&q=80&w=800",
tagline: "进取,不负期待"
},
{
id: "a4l",
name: "Audi A4L",
price: "321,800",
image: "https://images.unsplash.com/photo-1614162692292-7ac56d7f7f1e?auto=format&fit=crop&q=80&w=800",
tagline: "做更强大的自己"
},
{
id: "a6l",
name: "Audi A6L",
price: "427,900",
image: "https://images.unsplash.com/photo-1541348263662-e0c86433610a?auto=format&fit=crop&q=80&w=800",
tagline: "懂你,更懂未来"
},
{
id: "q3",
name: "Audi Q3",
price: "279,800",
image: "https://images.unsplash.com/photo-1533473359331-0135ef1b58bf?auto=format&fit=crop&q=80&w=800",
tagline: "活出生命的辽阔"
},
{
id: "q5l",
name: "Audi Q5L",
price: "396,800",
image: "https://images.unsplash.com/photo-1566473065146-d215f068671b?auto=format&fit=crop&q=80&w=800",
tagline: "自由,由我定义"
},
{
id: "etron-gt",
name: "Audi e-tron GT",
price: "999,800",
image: "https://images.unsplash.com/photo-1617469767053-d3b523a0b982?auto=format&fit=crop&q=80&w=800",
tagline: "静谧,亦能澎湃"
}
];
const CITIES = ["北京", "上海", "广州", "深圳", "杭州", "成都", "南京", "武汉"];
// --- Components ---
const Header = ({ onBack, showBack }: { onBack?: () => void; showBack?: boolean }) => (
<header className="sticky top-0 z-40 bg-white/80 backdrop-blur-md border-b border-gray-100 px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-4">
{showBack && (
<button onClick={onBack} className="p-1 -ml-2 hover:bg-gray-100 rounded-full transition-colors">
<ArrowLeft size={24} />
</button>
)}
<div className="flex flex-col">
<span className="text-xl font-bold tracking-[0.2em] uppercase">Audi</span>
<span className="text-[10px] text-gray-400 tracking-widest uppercase -mt-1">Vorsprung durch Technik</span>
</div>
</div>
<div className="flex gap-4">
<div className="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center">
<User size={16} className="text-gray-600" />
</div>
</div>
</header>
);
const PIPLPopup = ({ onAccept }: { onAccept: () => void }) => (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center p-6 bg-black/60 backdrop-blur-sm"
>
<motion.div
initial={{ scale: 0.9, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
className="bg-white w-full max-w-md p-8 shadow-2xl"
>
<div className="flex items-center gap-3 mb-6">
<ShieldCheck className="text-audi-red" size={32} />
<h2 className="text-2xl font-bold tracking-tight"></h2>
</div>
<div className="space-y-4 text-gray-600 text-sm leading-relaxed mb-8">
<p>
访PIPL
</p>
<p>
<b></b>
</p>
<p>
</p>
</div>
<button
onClick={onAccept}
className="audi-button w-full flex items-center justify-center gap-2"
>
<ChevronRight size={18} />
</button>
<p className="text-center text-[10px] text-gray-400 mt-4 uppercase tracking-widest">
Audi Privacy Compliance
</p>
</motion.div>
</motion.div>
);
export default function App() {
const [currentPage, setCurrentPage] = useState<"gallery" | "form">("gallery");
const [selectedCar, setSelectedCar] = useState<CarModel | null>(null);
const [showPIPL, setShowPIPL] = useState(true);
const [piplAccepted, setPiplAccepted] = useState(false);
// Form State
const [formData, setFormData] = useState({
name: "",
phone: "",
city: "",
agreed: false
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const handleLearnMore = (car: CarModel) => {
setSelectedCar(car);
setCurrentPage("form");
window.scrollTo({ top: 0, behavior: "smooth" });
};
const handleBack = () => {
setCurrentPage("gallery");
setIsSuccess(false);
};
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) newErrors.name = "请输入姓名";
if (!/^1[3-9]\d{9}$/.test(formData.phone)) newErrors.phone = "请输入有效的手机号";
if (!formData.city) newErrors.city = "请选择城市";
if (!formData.agreed) newErrors.agreed = "请阅读并同意隐私政策";
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!validateForm()) return;
setIsSubmitting(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
setIsSubmitting(false);
setIsSuccess(true);
};
return (
<div className="min-h-screen bg-white selection:bg-audi-red selection:text-white">
<AnimatePresence>
{showPIPL && <PIPLPopup onAccept={() => { setShowPIPL(false); setPiplAccepted(true); }} />}
</AnimatePresence>
<Header
showBack={currentPage === "form"}
onBack={handleBack}
/>
<main className="max-w-screen-xl mx-auto">
<AnimatePresence mode="wait">
{currentPage === "gallery" ? (
<motion.section
key="gallery"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="px-6 py-12"
>
<div className="mb-12">
<h1 className="text-4xl md:text-6xl font-black tracking-tighter uppercase mb-4">
</h1>
<p className="text-gray-500 max-w-2xl text-lg">
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{CAR_MODELS.map((car) => (
<motion.div
key={car.id}
layoutId={car.id}
className="audi-card group"
>
<div className="relative aspect-[16/10] overflow-hidden">
<img
src={car.image}
alt={car.name}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
referrerPolicy="no-referrer"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
</div>
<div className="p-6">
<div className="flex justify-between items-start mb-2">
<h3 className="text-xl font-bold tracking-tight">{car.name}</h3>
<span className="text-audi-red font-mono text-sm">CNY {car.price} </span>
</div>
<p className="text-gray-400 text-sm mb-6">{car.tagline}</p>
<button
onClick={() => handleLearnMore(car)}
className="audi-button-outline w-full flex items-center justify-center gap-2 group/btn"
>
<ChevronRight size={18} className="transition-transform group-hover/btn:translate-x-1" />
</button>
</div>
</motion.div>
))}
</div>
</motion.section>
) : (
<motion.section
key="form"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="px-6 py-12 lg:flex lg:gap-16 lg:items-start"
>
{/* Hero Image Section */}
<div className="lg:w-1/2 mb-12 lg:mb-0 lg:sticky lg:top-32">
<div className="relative rounded-lg overflow-hidden shadow-2xl">
<img
src={selectedCar?.image}
alt={selectedCar?.name}
className="w-full aspect-video object-cover"
referrerPolicy="no-referrer"
/>
<div className="absolute bottom-0 left-0 right-0 p-8 bg-gradient-to-t from-black/80 to-transparent text-white">
<h2 className="text-3xl font-bold mb-2">{selectedCar?.name}</h2>
<p className="text-gray-300 opacity-80">{selectedCar?.tagline}</p>
</div>
</div>
<div className="mt-8 grid grid-cols-3 gap-4">
{[1, 2, 3].map(i => (
<div key={i} className="aspect-square bg-gray-100 rounded overflow-hidden">
<img
src={`https://picsum.photos/seed/audi-${selectedCar?.id}-${i}/400/400`}
alt="Detail"
className="w-full h-full object-cover opacity-60 hover:opacity-100 transition-opacity"
referrerPolicy="no-referrer"
/>
</div>
))}
</div>
</div>
{/* Form Section */}
<div className="lg:w-1/2 max-w-md">
{isSuccess ? (
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="text-center py-12"
>
<div className="w-20 h-20 bg-green-50 text-green-500 rounded-full flex items-center justify-center mx-auto mb-6">
<Check size={40} />
</div>
<h2 className="text-3xl font-bold mb-4"></h2>
<p className="text-gray-500 mb-8">
24
</p>
<button onClick={handleBack} className="audi-button w-full">
</button>
</motion.div>
) : (
<>
<div className="mb-10">
<h2 className="text-3xl font-bold tracking-tight mb-2"></h2>
<p className="text-gray-500"></p>
</div>
<form onSubmit={handleSubmit} className="space-y-8">
<div className="space-y-1">
<label className="text-[10px] uppercase tracking-widest text-gray-400 font-bold"></label>
<div className="relative">
<User className="absolute left-0 top-1/2 -translate-y-1/2 text-gray-300" size={18} />
<input
type="text"
placeholder="请输入您的真实姓名"
className={`audi-input pl-8 ${errors.name ? 'border-audi-red' : ''}`}
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
/>
</div>
{errors.name && <p className="text-audi-red text-xs mt-1">{errors.name}</p>}
</div>
<div className="space-y-1">
<label className="text-[10px] uppercase tracking-widest text-gray-400 font-bold"></label>
<div className="relative">
<Phone className="absolute left-0 top-1/2 -translate-y-1/2 text-gray-300" size={18} />
<input
type="tel"
placeholder="1xx xxxx xxxx"
className={`audi-input pl-8 ${errors.phone ? 'border-audi-red' : ''}`}
value={formData.phone}
onChange={(e) => setFormData({...formData, phone: e.target.value})}
/>
</div>
{errors.phone && <p className="text-audi-red text-xs mt-1">{errors.phone}</p>}
</div>
<div className="space-y-1">
<label className="text-[10px] uppercase tracking-widest text-gray-400 font-bold"></label>
<div className="relative">
<MapPin className="absolute left-0 top-1/2 -translate-y-1/2 text-gray-300" size={18} />
<select
className={`audi-input pl-8 appearance-none ${errors.city ? 'border-audi-red' : ''}`}
value={formData.city}
onChange={(e) => setFormData({...formData, city: e.target.value})}
>
<option value=""></option>
{CITIES.map(city => <option key={city} value={city}>{city}</option>)}
</select>
</div>
{errors.city && <p className="text-audi-red text-xs mt-1">{errors.city}</p>}
</div>
<div className="flex items-start gap-3">
<div className="relative flex items-center h-5">
<input
id="pipl"
type="checkbox"
className="w-4 h-4 text-audi-black border-gray-300 rounded focus:ring-audi-black"
checked={formData.agreed}
onChange={(e) => setFormData({...formData, agreed: e.target.checked})}
/>
</div>
<div className="text-xs text-gray-500 leading-relaxed">
<label htmlFor="pipl">
<span className="text-audi-black underline cursor-pointer"></span> <span className="text-audi-black underline cursor-pointer"></span>
</label>
{errors.agreed && <p className="text-audi-red text-xs mt-1">{errors.agreed}</p>}
</div>
</div>
<button
type="submit"
disabled={isSubmitting}
className="audi-button w-full flex items-center justify-center gap-3 h-14"
>
{isSubmitting ? (
<>
<Loader2 className="animate-spin" size={20} />
...
</>
) : (
<>
<ChevronRight size={20} />
</>
)}
</button>
</form>
</>
)}
</div>
</motion.section>
)}
</AnimatePresence>
</main>
<footer className="bg-audi-black text-white py-16 px-6 mt-20">
<div className="max-w-screen-xl mx-auto flex flex-col md:flex-row justify-between items-start gap-12">
<div className="space-y-6">
<div className="flex flex-col">
<span className="text-3xl font-bold tracking-[0.3em] uppercase">Audi</span>
<span className="text-xs text-gray-500 tracking-[0.5em] uppercase">Vorsprung durch Technik</span>
</div>
<p className="text-gray-500 text-sm max-w-xs">
</p>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-12">
<div className="space-y-4">
<h4 className="text-xs font-bold uppercase tracking-widest"></h4>
<ul className="text-gray-500 text-sm space-y-2">
<li>轿</li>
<li>SUV</li>
<li>e-tron </li>
<li>Audi Sport</li>
</ul>
</div>
<div className="space-y-4">
<h4 className="text-xs font-bold uppercase tracking-widest"></h4>
<ul className="text-gray-500 text-sm space-y-2">
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<div className="space-y-4">
<h4 className="text-xs font-bold uppercase tracking-widest"></h4>
<ul className="text-gray-500 text-sm space-y-2">
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
</div>
<div className="max-w-screen-xl mx-auto mt-20 pt-8 border-t border-white/10 flex flex-col md:flex-row justify-between gap-6 text-[10px] text-gray-600 uppercase tracking-widest">
<p>© 2026 </p>
<div className="flex gap-6">
<span className="hover:text-white cursor-pointer transition-colors"></span>
<span className="hover:text-white cursor-pointer transition-colors"></span>
<span className="hover:text-white cursor-pointer transition-colors">ICP备00000000号</span>
</div>
</div>
</footer>
</div>
);
}

View File

@@ -0,0 +1,31 @@
@import "tailwindcss";
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--color-audi-red: #BB0A30;
--color-audi-black: #000000;
--color-audi-silver: #F0F0F0;
--color-audi-dark-gray: #333333;
}
@layer base {
body {
@apply bg-white text-audi-black font-sans antialiased;
}
}
.audi-button {
@apply px-6 py-3 bg-audi-black text-white font-medium uppercase tracking-wider transition-all duration-300 hover:bg-audi-dark-gray active:scale-95 disabled:opacity-50 disabled:pointer-events-none;
}
.audi-button-outline {
@apply px-6 py-3 border border-audi-black text-audi-black font-medium uppercase tracking-wider transition-all duration-300 hover:bg-audi-black hover:text-white active:scale-95;
}
.audi-input {
@apply w-full px-4 py-3 border-b border-gray-300 focus:border-audi-black outline-none transition-colors duration-300 bg-transparent;
}
.audi-card {
@apply bg-white border border-gray-100 overflow-hidden transition-all duration-500 hover:shadow-2xl hover:-translate-y-1;
}

View File

@@ -0,0 +1,10 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

View File

@@ -0,0 +1,24 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import {defineConfig, loadEnv} from 'vite';
export default defineConfig(({mode}) => {
const env = loadEnv(mode, '.', '');
return {
plugins: [react(), tailwindcss()],
define: {
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
server: {
// HMR is disabled in AI Studio via DISABLE_HMR env var.
// Do not modify—file watching is disabled to prevent flickering during agent edits.
hmr: process.env.DISABLE_HMR !== 'true',
},
};
});