diff --git a/.gitignore b/.gitignore index f5a3f3d..930f656 100644 --- a/.gitignore +++ b/.gitignore @@ -168,10 +168,4 @@ $RECYCLE.BIN/ # Temporary files *.tmp -*.temp - -# Client directory (separate git repository) -client/ - -# Server directory if it exists -server/ \ No newline at end of file +*.temp \ No newline at end of file diff --git a/client/.editorconfig b/client/.editorconfig new file mode 100644 index 0000000..5760be5 --- /dev/null +++ b/client/.editorconfig @@ -0,0 +1,12 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/client/.env.development b/client/.env.development new file mode 100644 index 0000000..a3f1b48 --- /dev/null +++ b/client/.env.development @@ -0,0 +1,2 @@ +# 配置文档参考 https://taro-docs.jd.com/docs/next/env-mode-config +# TARO_APP_ID="开发环境下的小程序appid" \ No newline at end of file diff --git a/client/.env.production b/client/.env.production new file mode 100644 index 0000000..be6f45e --- /dev/null +++ b/client/.env.production @@ -0,0 +1 @@ +# TARO_APP_ID="生产环境下的小程序appid" \ No newline at end of file diff --git a/client/.env.test b/client/.env.test new file mode 100644 index 0000000..0215b61 --- /dev/null +++ b/client/.env.test @@ -0,0 +1 @@ +# TARO_APP_ID="测试环境下的小程序appid" \ No newline at end of file diff --git a/client/.eslintrc b/client/.eslintrc new file mode 100644 index 0000000..13d38a6 --- /dev/null +++ b/client/.eslintrc @@ -0,0 +1,5 @@ +// ESLint 检查 .vue 文件需要单独配置编辑器: +// https://eslint.vuejs.org/user-guide/#editor-integrations +{ + "extends": ["taro/vue3"] +} diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..331e8f6 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,7 @@ +dist/ +deploy_versions/ +.temp/ +.rn_temp/ +node_modules/ +.DS_Store +.swc \ No newline at end of file diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..101b830 --- /dev/null +++ b/client/README.md @@ -0,0 +1,263 @@ +# LIMO来刻 - 基于Taro+Vue3的小程序开发框架 + +## 项目简介 + +LIMO来刻是一个基于 Taro 3.6 + Vue3 + TypeScript 的小程序开发框架,提供了完整的开发工具链和组件库,帮助开发者快速构建高质量的小程序应用。 + +## 技术栈 + +- **框架**: Taro 3.6.34 +- **前端**: Vue3 + TypeScript +- **样式**: Sass +- **构建工具**: Webpack5 +- **包管理**: npm + +## 目录结构 + +``` +client/ +├── src/ +│ ├── components/ # 通用组件 +│ │ ├── CommonButton/ # 通用按钮组件 +│ │ └── index.ts # 组件导出 +│ ├── pages/ # 页面 +│ │ └── index/ # 首页 +│ ├── services/ # 服务层 +│ │ └── request.ts # 网络请求服务 +│ ├── utils/ # 工具函数 +│ │ └── index.ts # 工具函数集合 +│ ├── constants/ # 常量 +│ │ └── index.ts # 应用常量 +│ ├── stores/ # 状态管理 +│ ├── hooks/ # 自定义hooks +│ ├── assets/ # 静态资源 +│ │ ├── images/ # 图片资源 +│ │ └── icons/ # 图标资源 +│ ├── app.ts # 应用入口 +│ ├── app.scss # 全局样式 +│ └── app.config.ts # 应用配置 +├── config/ # 项目配置 +├── types/ # 类型定义 +├── package.json +├── tsconfig.json +└── README.md +``` + +## 开发指南 + +### 安装依赖 + +```bash +npm install +``` + +### 开发命令 + +```bash +# 微信小程序开发 +npm run dev:weapp + +# 支付宝小程序开发 +npm run dev:alipay + +# 抖音小程序开发 +npm run dev:tt + +# H5开发 +npm run dev:h5 + +# 百度小程序开发 +npm run dev:swan + +# QQ小程序开发 +npm run dev:qq + +# 京东小程序开发 +npm run dev:jd +``` + +### 构建命令 + +```bash +# 构建微信小程序 +npm run build:weapp + +# 构建支付宝小程序 +npm run build:alipay + +# 构建抖音小程序 +npm run build:tt + +# 构建H5 +npm run build:h5 +``` + +### 测试命令 + +```bash +# 运行测试 +npm test +``` + +## 主要特性 + +### 🚀 快速开发 +- 基于 Taro 框架,一次编写,多端运行 +- 提供完整的项目脚手架和开发工具链 +- 支持热重载和快速编译 + +### 💎 现代化技术栈 +- Vue3 Composition API +- TypeScript 类型支持 +- Sass 样式预处理器 +- ESLint 代码规范 + +### 🛠️ 工具完善 +- 内置常用工具函数 +- 封装网络请求服务 +- 提供通用组件库 +- 完整的类型定义 + +### 📱 多端兼容 +- 微信小程序 +- 支付宝小程序 +- 抖音小程序 +- H5 +- 百度小程序 +- QQ小程序 +- 京东小程序 + +## 核心功能 + +### 网络请求服务 + +```typescript +import request from '@/services/request' + +// GET请求 +const data = await request.get('/api/user/info') + +// POST请求 +const result = await request.post('/api/user/login', { + username: 'admin', + password: '123456' +}) +``` + +### 工具函数 + +```typescript +import { formatDate, debounce, throttle } from '@/utils' + +// 格式化日期 +const formattedDate = formatDate(new Date(), 'YYYY-MM-DD') + +// 防抖函数 +const debouncedFn = debounce(handleSearch, 300) + +// 节流函数 +const throttledFn = throttle(handleScroll, 100) +``` + +### 通用组件 + +```vue + + + +``` + +## 开发规范 + +### 文件命名 +- 组件文件使用 PascalCase: `CommonButton.vue` +- 页面文件使用 kebab-case: `user-profile.vue` +- 工具函数文件使用 kebab-case: `format-utils.ts` + +### 代码规范 +- 使用 ESLint 进行代码检查 +- 使用 TypeScript 进行类型检查 +- 使用 Prettier 进行代码格式化 + +### 提交规范 +- feat: 新功能 +- fix: 修复bug +- docs: 文档更新 +- style: 代码格式调整 +- refactor: 重构代码 +- test: 测试相关 + +## 环境配置 + +### 开发环境 +- Node.js >= 18.0.0 +- npm >= 8.0.0 + +### 微信小程序开发 +1. 下载并安装微信开发者工具 +2. 导入项目的 `dist` 目录 +3. 配置小程序 AppID + +### 支付宝小程序开发 +1. 下载并安装支付宝小程序开发者工具 +2. 导入项目的 `dist` 目录 +3. 配置小程序 AppID + +## 部署说明 + +### 小程序发布 +1. 使用对应的开发者工具打开构建后的 `dist` 目录 +2. 点击"上传"按钮上传代码 +3. 在小程序管理后台提交审核 + +### H5部署 +1. 构建H5版本: `npm run build:h5` +2. 将 `dist` 目录部署到服务器 +3. 配置nginx或apache服务器 + +## 常见问题 + +### Q: 如何添加新页面? +A: 在 `src/pages` 目录下创建新页面文件夹,并在 `app.config.ts` 中添加页面路径。 + +### Q: 如何使用自定义组件? +A: 在 `src/components` 目录下创建组件,并在 `components/index.ts` 中导出。 + +### Q: 如何配置网络请求? +A: 在 `src/constants/index.ts` 中修改 `API_CONFIG` 配置。 + +### Q: 如何添加全局样式? +A: 在 `src/app.scss` 中添加全局样式。 + +## 贡献指南 + +1. Fork 本项目 +2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 创建 Pull Request + +## 许可证 + +本项目使用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。 + +## 联系我们 + +- 项目地址: [GitHub仓库链接] +- 问题反馈: [GitHub Issues] +- 邮箱: [联系邮箱] + +--- + +感谢使用 LIMO来刻!如果这个项目对您有帮助,请给我们一个 ⭐️ \ No newline at end of file diff --git a/client/__tests__/index.test.js b/client/__tests__/index.test.js new file mode 100644 index 0000000..c3904d1 --- /dev/null +++ b/client/__tests__/index.test.js @@ -0,0 +1,12 @@ +import TestUtils from '@tarojs/test-utils-vue3' + +describe('Testing', () => { + + test('Test', async () => { + const testUtils = new TestUtils() + await testUtils.createApp() + await testUtils.PageLifecycle.onShow('pages/index/index') + expect(testUtils.html()).toMatchSnapshot() + }) + +}) diff --git a/client/babel.config.js b/client/babel.config.js new file mode 100644 index 0000000..dc20faf --- /dev/null +++ b/client/babel.config.js @@ -0,0 +1,10 @@ +// babel-preset-taro 更多选项和默认值: +// https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md +module.exports = { + presets: [ + ['taro', { + framework: 'vue3', + ts: true + }] + ] +} diff --git a/client/config/dev.ts b/client/config/dev.ts new file mode 100644 index 0000000..c37eb43 --- /dev/null +++ b/client/config/dev.ts @@ -0,0 +1,9 @@ +import type { UserConfigExport } from "@tarojs/cli"; +export default { + logger: { + quiet: false, + stats: true + }, + mini: {}, + h5: {} +} satisfies UserConfigExport diff --git a/client/config/index.ts b/client/config/index.ts new file mode 100644 index 0000000..6e6af08 --- /dev/null +++ b/client/config/index.ts @@ -0,0 +1,110 @@ +import { defineConfig, type UserConfigExport } from '@tarojs/cli' +import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin' +import devConfig from './dev' +import prodConfig from './prod' +import path from 'path' + +// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数 +export default defineConfig(async (merge, { command, mode }) => { + const baseConfig: UserConfigExport = { + projectName: 'mini-app', + date: '2025-7-11', + designWidth: 750, + deviceRatio: { + 640: 2.34 / 2, + 750: 1, + 375: 2, + 828: 1.81 / 2 + }, + sourceRoot: 'src', + outputRoot: 'dist', + plugins: [], + defineConstants: { + }, + copy: { + patterns: [ + { from: 'static/', to: 'dist/static/' } + ], + options: { + } + }, + framework: 'vue3', + compiler: 'webpack5', + cache: { + enable: false // Webpack 持久化缓存配置,建议开启。默认配置请参考:https://docs.taro.zone/docs/config-detail#cache + }, + mini: { + postcss: { + pxtransform: { + enable: true, + config: { + + } + }, + url: { + enable: true, + config: { + limit: 1024 // 设定转换尺寸上限 + } + }, + cssModules: { + enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true + config: { + namingPattern: 'module', // 转换模式,取值为 global/module + generateScopedName: '[name]__[local]___[hash:base64:5]' + } + } + }, + webpackChain(chain) { + chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin) + chain.resolve.alias + .set('@', path.resolve(__dirname, '..', 'src')) + } + }, + h5: { + publicPath: '/', + staticDirectory: 'static', + output: { + filename: 'js/[name].[hash:8].js', + chunkFilename: 'js/[name].[chunkhash:8].js' + }, + miniCssExtractPluginOption: { + ignoreOrder: true, + filename: 'css/[name].[hash].css', + chunkFilename: 'css/[name].[chunkhash].css' + }, + postcss: { + autoprefixer: { + enable: true, + config: {} + }, + cssModules: { + enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true + config: { + namingPattern: 'module', // 转换模式,取值为 global/module + generateScopedName: '[name]__[local]___[hash:base64:5]' + } + } + }, + webpackChain(chain) { + chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin) + chain.resolve.alias + .set('@', path.resolve(__dirname, '..', 'src')) + } + }, + rn: { + appName: 'taroDemo', + postcss: { + cssModules: { + enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true + } + } + } + } + if (process.env.NODE_ENV === 'development') { + // 本地开发构建配置(不混淆压缩) + return merge({}, baseConfig, devConfig) + } + // 生产构建配置(默认开启压缩混淆等) + return merge({}, baseConfig, prodConfig) +}) diff --git a/client/config/prod.ts b/client/config/prod.ts new file mode 100644 index 0000000..c9ba67f --- /dev/null +++ b/client/config/prod.ts @@ -0,0 +1,32 @@ +import type { UserConfigExport } from "@tarojs/cli"; +export default { + mini: {}, + h5: { + /** + * WebpackChain 插件配置 + * @docs https://github.com/neutrinojs/webpack-chain + */ + // webpackChain (chain) { + // /** + // * 如果 h5 端编译后体积过大,可以使用 webpack-bundle-analyzer 插件对打包体积进行分析。 + // * @docs https://github.com/webpack-contrib/webpack-bundle-analyzer + // */ + // chain.plugin('analyzer') + // .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, []) + // /** + // * 如果 h5 端首屏加载时间过长,可以使用 prerender-spa-plugin 插件预加载首页。 + // * @docs https://github.com/chrisvfritz/prerender-spa-plugin + // */ + // const path = require('path') + // const Prerender = require('prerender-spa-plugin') + // const staticDir = path.join(__dirname, '..', 'dist') + // chain + // .plugin('prerender') + // .use(new Prerender({ + // staticDir, + // routes: [ '/pages/index/index' ], + // postProcess: (context) => ({ ...context, outputPath: path.join(staticDir, 'index.html') }) + // })) + // } + } +} satisfies UserConfigExport diff --git a/client/jest.config.ts b/client/jest.config.ts new file mode 100644 index 0000000..e720a3c --- /dev/null +++ b/client/jest.config.ts @@ -0,0 +1,6 @@ +const defineJestConfig = require('@tarojs/test-utils-vue3/dist/jest.js').default + +module.exports = defineJestConfig({ + testEnvironment: 'jsdom', + testMatch: ['/__tests__/**/*.(spec|test).[jt]s?(x)'] +}) diff --git a/client/limo-mobile-h5/.gitignore b/client/limo-mobile-h5/.gitignore new file mode 100644 index 0000000..f650315 --- /dev/null +++ b/client/limo-mobile-h5/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/client/limo-mobile-h5/app/globals.css b/client/limo-mobile-h5/app/globals.css new file mode 100644 index 0000000..8dec748 --- /dev/null +++ b/client/limo-mobile-h5/app/globals.css @@ -0,0 +1,79 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96%; + --secondary-foreground: 222.2 84% 4.9%; + --muted: 210 40% 96%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96%; + --accent-foreground: 222.2 84% 4.9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer utilities { + .scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; + } + .scrollbar-hide::-webkit-scrollbar { + display: none; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/client/limo-mobile-h5/app/immersive/page.tsx b/client/limo-mobile-h5/app/immersive/page.tsx new file mode 100644 index 0000000..e31582e --- /dev/null +++ b/client/limo-mobile-h5/app/immersive/page.tsx @@ -0,0 +1,257 @@ +"use client" + +import type React from "react" + +import { useState, useRef } from "react" +import Image from "next/image" +import { ArrowLeft, Heart, MessageCircle, Share, Play, Volume2, VolumeX } from "lucide-react" +import Link from "next/link" + +interface VideoData { + id: number + venue: string + title: string + description: string + likes: number + comments: number + shares: number + videoUrl: string + thumbnail: string + isLiked: boolean +} + +const mockVideos: VideoData[] = [ + { + id: 1, + venue: "RONIN黄金篮球馆", + title: "精彩三分球集锦", + description: "今日比赛精彩瞬间,连续三分命中!#篮球 #精彩瞬间", + likes: 1234, + comments: 89, + shares: 45, + videoUrl: "/placeholder.svg?height=800&width=400&text=Basketball+Highlights", + thumbnail: "/placeholder.svg?height=800&width=400&text=Basketball+Game+1", + isLiked: false, + }, + { + id: 2, + venue: "Panda惊怒熊猫运动俱乐部", + title: "激烈对抗瞬间", + description: "双方激烈对抗,精彩防守反击!#运动 #篮球对抗", + likes: 2156, + comments: 156, + shares: 78, + videoUrl: "/placeholder.svg?height=800&width=400&text=Basketball+Defense", + thumbnail: "/placeholder.svg?height=800&width=400&text=Basketball+Game+2", + isLiked: true, + }, + { + id: 3, + venue: "星光篮球场", + title: "完美扣篮时刻", + description: "力量与技巧的完美结合,震撼扣篮!#扣篮 #力量", + likes: 3421, + comments: 234, + shares: 123, + videoUrl: "/placeholder.svg?height=800&width=400&text=Basketball+Dunk", + thumbnail: "/placeholder.svg?height=800&width=400&text=Basketball+Game+3", + isLiked: false, + }, + { + id: 4, + venue: "蓝天体育馆", + title: "团队配合精彩", + description: "完美的团队配合,流畅的传球配合!#团队 #配合", + likes: 987, + comments: 67, + shares: 34, + videoUrl: "/placeholder.svg?height=800&width=400&text=Team+Play", + thumbnail: "/placeholder.svg?height=800&width=400&text=Basketball+Game+4", + isLiked: false, + }, +] + +export default function ImmersivePage() { + const [currentIndex, setCurrentIndex] = useState(0) + const [videos, setVideos] = useState(mockVideos) + const [isPlaying, setIsPlaying] = useState(true) + const [isMuted, setIsMuted] = useState(false) + const containerRef = useRef(null) + const startY = useRef(0) + const currentY = useRef(0) + + const handleTouchStart = (e: React.TouchEvent) => { + startY.current = e.touches[0].clientY + } + + const handleTouchMove = (e: React.TouchEvent) => { + currentY.current = e.touches[0].clientY + } + + const handleTouchEnd = () => { + const deltaY = startY.current - currentY.current + const threshold = 50 + + if (Math.abs(deltaY) > threshold) { + if (deltaY > 0 && currentIndex < videos.length - 1) { + // 向上滑动,下一个视频 + setCurrentIndex(currentIndex + 1) + } else if (deltaY < 0 && currentIndex > 0) { + // 向下滑动,上一个视频 + setCurrentIndex(currentIndex - 1) + } + } + } + + const handleLike = (videoId: number) => { + setVideos( + videos.map((video) => { + if (video.id === videoId) { + return { + ...video, + isLiked: !video.isLiked, + likes: video.isLiked ? video.likes - 1 : video.likes + 1, + } + } + return video + }), + ) + } + + const formatNumber = (num: number) => { + if (num >= 1000) { + return (num / 1000).toFixed(1) + "k" + } + return num.toString() + } + + const currentVideo = videos[currentIndex] + + return ( +
+ {/* Header - Minimized */} +
+
+ +
+ +
+ +
沉浸模式
+
+
+
+ + {/* Video Container */} +
+ {videos.map((video, index) => ( +
+ {/* Video Background */} +
+ {video.title} + + {/* Video Overlay */} +
+ + {/* Play/Pause Button - Minimized */} +
setIsPlaying(!isPlaying)} + > + {!isPlaying && ( +
+ +
+ )} +
+
+ + {/* Right Side Actions - Minimized */} +
+ {/* Like Button */} +
+ + {formatNumber(video.likes)} +
+ + {/* Comment Button */} +
+ + {formatNumber(video.comments)} +
+ + {/* Share Button */} +
+ + {formatNumber(video.shares)} +
+ + {/* Volume Button */} + +
+ + {/* Bottom Info - Minimized */} +
+
+

{video.venue}

+

{video.description}

+
+
+ + {/* Progress Indicator - Minimized */} +
+ {videos.map((_, idx) => ( +
+ ))} +
+
+ ))} +
+ + {/* Swipe Hint - Minimized */} + {currentIndex === 0 && ( +
+ 上滑查看更多 +
+ )} +
+ ) +} diff --git a/client/limo-mobile-h5/app/layout.tsx b/client/limo-mobile-h5/app/layout.tsx new file mode 100644 index 0000000..17b2ce8 --- /dev/null +++ b/client/limo-mobile-h5/app/layout.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from 'next' +import './globals.css' + +export const metadata: Metadata = { + title: 'v0 App', + description: 'Created with v0', + generator: 'v0.dev', +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + {children} + + ) +} diff --git a/client/limo-mobile-h5/app/m-space/page.tsx b/client/limo-mobile-h5/app/m-space/page.tsx new file mode 100644 index 0000000..e60375f --- /dev/null +++ b/client/limo-mobile-h5/app/m-space/page.tsx @@ -0,0 +1,257 @@ +"use client" + +import type React from "react" + +import { useState, useRef } from "react" +import Image from "next/image" +import { X, Heart, MessageCircle, Share, Play, Volume2, VolumeX } from "lucide-react" +import Link from "next/link" + +interface VideoData { + id: number + venue: string + title: string + description: string + likes: number + comments: number + shares: number + videoUrl: string + thumbnail: string + isLiked: boolean +} + +const mockVideos: VideoData[] = [ + { + id: 1, + venue: "RONIN黄金篮球馆", + title: "精彩三分球集锦", + description: "今日比赛精彩瞬间,连续三分命中!#篮球 #精彩瞬间", + likes: 1234, + comments: 89, + shares: 45, + videoUrl: "/placeholder.svg?height=800&width=400&text=Basketball+Highlights", + thumbnail: "/placeholder.svg?height=800&width=400&text=Basketball+Game+1", + isLiked: false, + }, + { + id: 2, + venue: "Panda惊怒熊猫运动俱乐部", + title: "激烈对抗瞬间", + description: "双方激烈对抗,精彩防守反击!#运动 #篮球对抗", + likes: 2156, + comments: 156, + shares: 78, + videoUrl: "/placeholder.svg?height=800&width=400&text=Basketball+Defense", + thumbnail: "/placeholder.svg?height=800&width=400&text=Basketball+Game+2", + isLiked: true, + }, + { + id: 3, + venue: "星光篮球场", + title: "完美扣篮时刻", + description: "力量与技巧的完美结合,震撼扣篮!#扣篮 #力量", + likes: 3421, + comments: 234, + shares: 123, + videoUrl: "/placeholder.svg?height=800&width=400&text=Basketball+Dunk", + thumbnail: "/placeholder.svg?height=800&width=400&text=Basketball+Game+3", + isLiked: false, + }, + { + id: 4, + venue: "蓝天体育馆", + title: "团队配合精彩", + description: "完美的团队配合,流畅的传球配合!#团队 #配合", + likes: 987, + comments: 67, + shares: 34, + videoUrl: "/placeholder.svg?height=800&width=400&text=Team+Play", + thumbnail: "/placeholder.svg?height=800&width=400&text=Basketball+Game+4", + isLiked: false, + }, +] + +export default function MSpacePage() { + const [currentIndex, setCurrentIndex] = useState(0) + const [videos, setVideos] = useState(mockVideos) + const [isPlaying, setIsPlaying] = useState(true) + const [isMuted, setIsMuted] = useState(false) + const containerRef = useRef(null) + const startY = useRef(0) + const currentY = useRef(0) + + const handleTouchStart = (e: React.TouchEvent) => { + startY.current = e.touches[0].clientY + } + + const handleTouchMove = (e: React.TouchEvent) => { + currentY.current = e.touches[0].clientY + } + + const handleTouchEnd = () => { + const deltaY = startY.current - currentY.current + const threshold = 50 + + if (Math.abs(deltaY) > threshold) { + if (deltaY > 0 && currentIndex < videos.length - 1) { + // 向上滑动,下一个视频 + setCurrentIndex(currentIndex + 1) + } else if (deltaY < 0 && currentIndex > 0) { + // 向下滑动,上一个视频 + setCurrentIndex(currentIndex - 1) + } + } + } + + const handleLike = (videoId: number) => { + setVideos( + videos.map((video) => { + if (video.id === videoId) { + return { + ...video, + isLiked: !video.isLiked, + likes: video.isLiked ? video.likes - 1 : video.likes + 1, + } + } + return video + }), + ) + } + + const formatNumber = (num: number) => { + if (num >= 1000) { + return (num / 1000).toFixed(1) + "k" + } + return num.toString() + } + + const currentVideo = videos[currentIndex] + + return ( +
+ {/* Header - Minimized */} +
+
+ +
+ +
+ +
M空间
+
+
+
+ + {/* Video Container */} +
+ {videos.map((video, index) => ( +
+ {/* Video Background */} +
+ {video.title} + + {/* Video Overlay */} +
+ + {/* Play/Pause Button - Minimized */} +
setIsPlaying(!isPlaying)} + > + {!isPlaying && ( +
+ +
+ )} +
+
+ + {/* Right Side Actions - Minimized */} +
+ {/* Like Button */} +
+ + {formatNumber(video.likes)} +
+ + {/* Comment Button */} +
+ + {formatNumber(video.comments)} +
+ + {/* Share Button */} +
+ + {formatNumber(video.shares)} +
+ + {/* Volume Button */} + +
+ + {/* Bottom Info - Minimized */} +
+
+

{video.venue}

+

{video.description}

+
+
+ + {/* Progress Indicator - Minimized */} +
+ {videos.map((_, idx) => ( +
+ ))} +
+
+ ))} +
+ + {/* Swipe Hint - Minimized */} + {currentIndex === 0 && ( +
+ 上滑查看更多 +
+ )} +
+ ) +} diff --git a/client/limo-mobile-h5/app/page.tsx b/client/limo-mobile-h5/app/page.tsx new file mode 100644 index 0000000..eb9b00a --- /dev/null +++ b/client/limo-mobile-h5/app/page.tsx @@ -0,0 +1,211 @@ +import Image from "next/image" +import { MapPin, Play, Home, User, Clock, Zap } from "lucide-react" +import { Button } from "@/components/ui/button" +import Link from "next/link" + +export default function LimoMobile() { + return ( +
+ {/* Header with subtle gradient */} +
+
+ LIMO来刻 + + + +
+ + {/* Navigation Tabs with enhanced styling */} +
+
+ 发现 +
+
+ 关注 +
+
+ + {/* Sports Categories with glassmorphism effect */} +
+
+
+
+ 全部 + 项目 +
+
+
+
+ 🏀 +
+ 篮球 +
+
+
+ +
+ 足球 +
+
+
+ 🎾 +
+ 网球 +
+
+
+ + {/* Location with enhanced styling */} +
+
+ + 杭州市文一西路未来科技城万利大厦0701 + 切换 +
+
+ + {/* Venue Cards with premium styling */} +
+ {/* Distance Timeline */} +
+
+ + {/* First Venue */} +
+
+
+
+
120km
+
+
+ + 2分钟前更新 +
+
+
+
+ Basketball court +
+
+ 🔥 热门 +
+
+
+
+
+
+
+
+
+
+ R +
+
+

RONIN黄金篮球馆

+

杭州拱墅区黑马路124号(肯德基对面)

+
+
+ + + +
+
+
+
+
+ + {/* Second Venue */} +
+
+
+
+
140km
+
+
+ + 5分钟前更新 +
+
+
+
+ Basketball court with crowd +
+
+ +
+
+
+
+
+ 直播中 +
+
+
+
+
+
+
+ P +
+
+

Panda惊怒熊猫运动俱乐部

+

杭州拱墅区黑马路124号(肯德基对面)

+
+
+ + + +
+
+
+
+
+
+
+ + {/* Bottom Spacing for Fixed Navigation */} +
+ + {/* Bottom Navigation with glassmorphism */} +
+
+ +
+ +
+ 首页 + + +
+ +
+ 我的 + +
+
+ + {/* Bottom Watermark with fade effect */} +
+
You Only Live Once.
+
+
+ ) +} diff --git a/client/limo-mobile-h5/app/profile/page.tsx b/client/limo-mobile-h5/app/profile/page.tsx new file mode 100644 index 0000000..388a015 --- /dev/null +++ b/client/limo-mobile-h5/app/profile/page.tsx @@ -0,0 +1,215 @@ +import { + ArrowLeft, + Settings, + Heart, + Clock, + MapPin, + Home, + User, + Crown, + Star, + ChevronRight, + Bell, + Shield, +} from "lucide-react" +import { Button } from "@/components/ui/button" +import Link from "next/link" + +export default function ProfilePage() { + return ( +
+ {/* Header with glassmorphism */} +
+
+ +
+ +
+ +

我的

+
+ +
+
+
+ + {/* User Profile Section with premium styling */} +
+
+
+
+ +
+
+
+
+
+
+
+

用户昵称

+
+ +
+
+

ID: 123456789

+
+
+ + 积分: 2,580 +
+
+ 等级: LV.8 +
+
+
+ +
+ + {/* Stats Cards */} +
+
+
12
+
观看场次
+
+
+
5
+
收藏场馆
+
+
+
28
+
观看时长(h)
+
+
+ + {/* Menu Items with premium styling */} +
+ +
+
+ +
+
+ VIP会员 +
+ 开通享特权 +
+
+
+ + + +
+
+
+ +
+ 我的收藏 +
+
+
5
+ +
+
+ +
+
+
+ +
+ 观看历史 +
+ +
+ +
+
+
+ +
+ 常用地址 +
+ +
+ +
+
+
+ +
+ 消息通知 +
+
+
+ +
+
+ +
+
+
+ +
+ 隐私设置 +
+ +
+
+ + {/* Recent Activity with enhanced styling */} +
+

+
+ 最近活动 +

+
+
+
+
+
+
+

观看了 RONIN黄金篮球馆

+

2小时前 · 观看时长 45分钟

+
+
+10积分
+
+
+
+
+
+
+

收藏了 Panda惊怒熊猫运动俱乐部

+

1天前

+
+
+5积分
+
+
+
+ + {/* Bottom Spacing for Fixed Navigation */} +
+ + {/* Bottom Navigation with glassmorphism */} +
+
+ +
+ +
+ 首页 + + +
+ +
+ 我的 + +
+
+
+ ) +} diff --git a/client/limo-mobile-h5/app/vip/page.tsx b/client/limo-mobile-h5/app/vip/page.tsx new file mode 100644 index 0000000..6014b67 --- /dev/null +++ b/client/limo-mobile-h5/app/vip/page.tsx @@ -0,0 +1,317 @@ +"use client" + +import { ArrowLeft, Crown, Check, Star, Zap, Shield, Gift, Sparkles, Trophy, Users } from "lucide-react" +import { Button } from "@/components/ui/button" +import Link from "next/link" +import { useState } from "react" + +export default function VipPage() { + const [selectedPlan, setSelectedPlan] = useState("monthly") + + const plans = [ + { + id: "daily", + name: "体验版", + duration: "1天", + price: 9.9, + originalPrice: 19.9, + discount: "限时5折", + popular: false, + badge: "试用", + color: "from-blue-400 to-blue-500", + }, + { + id: "monthly", + name: "月度会员", + duration: "1个月", + price: 68, + originalPrice: 98, + discount: "7折优惠", + popular: true, + badge: "推荐", + color: "from-purple-400 to-purple-500", + }, + { + id: "quarterly", + name: "季度会员", + duration: "3个月", + price: 168, + originalPrice: 294, + discount: "超值4.3折", + popular: false, + badge: "超值", + color: "from-pink-400 to-pink-500", + }, + { + id: "yearly", + name: "年度会员", + duration: "1年", + price: 588, + originalPrice: 1176, + discount: "超值5折", + popular: false, + badge: "最划算", + color: "from-orange-400 to-orange-500", + }, + ] + + const privileges = [ + { + icon: , + title: "专属身份标识", + desc: "VIP专属标识,彰显尊贵身份", + color: "from-yellow-400 to-orange-500", + }, + { + icon: , + title: "优先预约权", + desc: "热门场馆优先预约,不再排队等待", + color: "from-blue-400 to-blue-500", + }, + { + icon: , + title: "专属折扣", + desc: "场馆预订享受VIP专属折扣优惠", + color: "from-green-400 to-green-500", + }, + { + icon: , + title: "免费取消", + desc: "预约后可免费取消,无违约金", + color: "from-purple-400 to-purple-500", + }, + { + icon: , + title: "专属客服", + desc: "7x24小时VIP专属客服服务", + color: "from-pink-400 to-pink-500", + }, + { + icon: , + title: "高清回放", + desc: "无限制观看高清比赛回放视频", + color: "from-indigo-400 to-indigo-500", + }, + ] + + const stats = [ + { label: "活跃用户", value: "50万+", icon: }, + { label: "满意度", value: "98%", icon: }, + { label: "场馆覆盖", value: "1000+", icon: }, + ] + + return ( +
+ {/* Header with premium gradient */} +
+
+ +
+ +
+ +

VIP会员

+
+
+
+ + {/* VIP Banner with enhanced styling */} +
+ {/* Background decorations */} +
+
+
+ +
+
+ +
+

LIMO VIP会员

+

开启专属运动体验

+ + {/* Stats */} +
+ {stats.map((stat, index) => ( +
+
+ {stat.icon} +
+
{stat.value}
+
{stat.label}
+
+ ))} +
+
+
+ + {/* Pricing Plans with premium styling */} +
+
+
+

选择会员套餐

+

解锁更多专属特权

+
+ +
+ {plans.map((plan) => ( +
setSelectedPlan(plan.id)} + className={`relative border-2 rounded-3xl p-5 cursor-pointer transition-all duration-300 ${ + selectedPlan === plan.id + ? "border-transparent bg-gradient-to-br from-[#05C7C7]/10 to-[#04B5B5]/10 shadow-xl scale-[1.02]" + : "border-gray-200/50 bg-white/50 hover:border-gray-300/50 hover:shadow-lg" + } ${plan.popular ? "ring-2 ring-yellow-400/50" : ""}`} + > + {plan.popular && ( +
+
+ + 最受欢迎 +
+
+ )} + +
+
+ +
+ +

{plan.name}

+

{plan.duration}

+ +
+ ¥{plan.price} + ¥{plan.originalPrice} +
+ +
+ {plan.discount} +
+
+ + {selectedPlan === plan.id && ( +
+
+ +
+
+ )} +
+ ))} +
+
+
+ + {/* VIP Privileges with enhanced styling */} +
+
+

+
+ +
+ VIP专属特权 +

+ +
+ {privileges.map((privilege, index) => ( +
+
+ {privilege.icon} +
+
+

{privilege.title}

+

{privilege.desc}

+
+
+ ))} +
+
+
+ + {/* Testimonials */} +
+
+

用户好评

+
+
+
+
+ +
+ 张先生 +
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+
+

"VIP服务真的很棒,预约场馆再也不用排队了!"

+
+
+
+
+ +
+ 李女士 +
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+
+

"专属客服响应很快,解决问题很及时,值得推荐!"

+
+
+
+
+ + {/* Purchase Section with enhanced styling */} +
+
+
+ 选择套餐: +
+
{plans.find((p) => p.id === selectedPlan)?.name}
+
+ ¥{plans.find((p) => p.id === selectedPlan)?.price} +
+
+
+ + + +
+
+ + 安全支付 +
+
+
+ + 随时取消 +
+
+ 7天无理由退款 +
+ +

开通即表示同意《VIP会员服务协议》

+
+
+ + {/* Bottom Spacing */} +
+
+ ) +} diff --git a/client/limo-mobile-h5/app/zone/page.tsx b/client/limo-mobile-h5/app/zone/page.tsx new file mode 100644 index 0000000..f58a24c --- /dev/null +++ b/client/limo-mobile-h5/app/zone/page.tsx @@ -0,0 +1,250 @@ +"use client" + +import Image from "next/image" +import { + ArrowLeft, + Play, + ChevronDown, + Clock, + MapPin, + Maximize2, + Users, + Eye, + Share2, + Heart, + MoreHorizontal, +} from "lucide-react" +import { Button } from "@/components/ui/button" +import { useState } from "react" + +export default function ZoneVenuePage() { + const [activeTab, setActiveTab] = useState("highlights") + + return ( +
+ {/* Header with premium background */} +
+
+
+ + {/* Header Controls */} +
+
+ +
+
+
+ +
+
+ +
+
+
+ + {/* Venue Info Card with glassmorphism */} +
+
+
+
+ R +
+
+
+
+
+
+

RONIN黄金篮球馆

+

+ + 杭州拱墅区黑马路124号(肯德基对面) +

+
+
+ + 1,234 关注 +
+
+
+ + 今日 856 观看 +
+
+
+
+
+
+ + {/* Main Video Player with enhanced styling */} +
+
+
+ Basketball game +
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ 直播中 +
+
+
+
+
+
+
+ + 2.1k +
+
+ + 856 +
+
+
2分钟前更新
+
+
+
+
+ + {/* Navigation Tabs with enhanced styling */} +
+
+
+ + +
+
+
+ + {/* Court and Time Selection with premium styling */} +
+
+
+ 1号篮球场 + +
+
+ + 今日 08:20 ~ 22:00 + +
+
+ + {/* Video Grid with enhanced styling */} +
+ {/* Live Video */} +
+
+
+ Live basketball game +
+
+
+ 正在播放 +
+
+
+
+
+
+
+
+ + {/* Other Videos */} + {[1, 2, 3, 4, 5].map((index) => ( +
+
+
+ {`Basketball +
+
+ +
+
+
+
+
+
+
+
+ ))} +
+ + {/* Bottom Notice with enhanced styling */} +
+
+
+ ! +
+ 视频云端保存仅7天,请及时下载 +
+
+ + {/* Bottom Actions - Fixed with glassmorphism */} +
+ + +
+ + {/* Bottom Spacing for Fixed Actions */} +
+
+ ) +} diff --git a/client/limo-mobile-h5/components.json b/client/limo-mobile-h5/components.json new file mode 100644 index 0000000..d9ef0ae --- /dev/null +++ b/client/limo-mobile-h5/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/client/limo-mobile-h5/components/theme-provider.tsx b/client/limo-mobile-h5/components/theme-provider.tsx new file mode 100644 index 0000000..55c2f6e --- /dev/null +++ b/client/limo-mobile-h5/components/theme-provider.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as React from 'react' +import { + ThemeProvider as NextThemesProvider, + type ThemeProviderProps, +} from 'next-themes' + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/client/limo-mobile-h5/components/ui/accordion.tsx b/client/limo-mobile-h5/components/ui/accordion.tsx new file mode 100644 index 0000000..24c788c --- /dev/null +++ b/client/limo-mobile-h5/components/ui/accordion.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/client/limo-mobile-h5/components/ui/alert-dialog.tsx b/client/limo-mobile-h5/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..25e7b47 --- /dev/null +++ b/client/limo-mobile-h5/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/client/limo-mobile-h5/components/ui/alert.tsx b/client/limo-mobile-h5/components/ui/alert.tsx new file mode 100644 index 0000000..41fa7e0 --- /dev/null +++ b/client/limo-mobile-h5/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/client/limo-mobile-h5/components/ui/aspect-ratio.tsx b/client/limo-mobile-h5/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..d6a5226 --- /dev/null +++ b/client/limo-mobile-h5/components/ui/aspect-ratio.tsx @@ -0,0 +1,7 @@ +"use client" + +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/client/limo-mobile-h5/components/ui/avatar.tsx b/client/limo-mobile-h5/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/client/limo-mobile-h5/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/client/limo-mobile-h5/components/ui/badge.tsx b/client/limo-mobile-h5/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/client/limo-mobile-h5/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/client/limo-mobile-h5/components/ui/breadcrumb.tsx b/client/limo-mobile-h5/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..60e6c96 --- /dev/null +++ b/client/limo-mobile-h5/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>