init: 初始化 H5 和 大屏

This commit is contained in:
Zzc 2025-10-15 14:07:39 +08:00
commit ffaaa56af7
31 changed files with 2873 additions and 0 deletions

4
.env.development Normal file
View File

@ -0,0 +1,4 @@
# VITE环境变量
# 开发环境
VITE_API_BASE_URL=http://localhost:3000/api

4
.env.production Normal file
View File

@ -0,0 +1,4 @@
# VITE环境变量
# 生产环境
VITE_API_BASE_URL=https://api.example.com

22
.eslintrc.cjs Normal file
View File

@ -0,0 +1,22 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended'
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: ['vue'],
rules: {
'vue/multi-word-component-names': 'off',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/no-v-html': 'off'
}
}

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules
dist
.DS_Store
*.log
.vscode
.idea
*.local

276
README.md Normal file
View File

@ -0,0 +1,276 @@
# H5 Monorepo 工程
基于 pnpm workspace 的 Monorepo 项目,包含 Vue3 大屏和 Vue3 移动端两个子项目。
## 项目结构
```
H5/
├── packages/
│ ├── screen/ # Vue3 大屏项目
│ │ ├── src/
│ │ │ ├── views/
│ │ │ ├── components/
│ │ │ ├── router/
│ │ │ ├── store/
│ │ │ └── styles/
│ │ ├── vite.config.js
│ │ └── package.json
│ ├── mobile/ # Vue3 移动端项目
│ │ ├── src/
│ │ │ ├── views/
│ │ │ ├── components/
│ │ │ ├── router/
│ │ │ └── styles/
│ │ ├── vite.config.js
│ │ └── package.json
│ └── shared/ # 共享代码库
│ ├── api/ # 接口封装
│ ├── utils/ # 工具函数
│ ├── config/ # 配置文件
│ └── package.json
├── pnpm-workspace.yaml
├── package.json
└── README.md
```
## 技术栈
### 大屏项目 (screen)
- Vue 3
- Vite 5
- Vue Router 4
- Pinia
- ECharts 5
- Vue-ECharts
- Sass
### 移动端项目 (mobile)
- Vue 3
- Vite 5
- Vue Router 4
- Pinia
- Vant 4移动端 UI 组件库)
- 自动按需引入组件
- Sass
### 共享库 (shared)
- Axios请求封装
- 工具函数(防抖/节流/日期格式化/深拷贝等)
- 统一配置管理
## 快速开始
### 安装依赖
```bash
# 安装 pnpm (如果还没安装)
npm install -g pnpm
# 安装所有依赖
pnpm install
```
### 开发命令
```bash
# 启动大屏项目端口3000
pnpm dev:screen
# 启动 H5 移动端端口8080
pnpm dev:mobile
# 构建大屏项目
pnpm build:screen
# 构建 H5 移动端
pnpm build:mobile
# 构建所有项目
pnpm build:all
# 代码检查
pnpm lint
```
## 项目说明
### 1. 大屏项目 (screen)
**访问地址:** `http://localhost:3000`
**特点:**
- 适配大屏分辨率 (1920x1080)
- 数据可视化ECharts
- 暗色主题设计
- 响应式布局
**目录结构:**
```
packages/screen/src/
├── views/ # 页面组件
├── components/ # 通用组件
├── router/ # 路由配置
├── store/ # Pinia 状态管理
├── assets/ # 静态资源
└── styles/ # 全局样式
```
### 2. 移动端项目 (mobile)
**访问地址:** `http://localhost:8080`
**特点:**
- 响应式设计,适配移动端
- Vant UI 组件库
- 自动按需引入,减小包体积
- 支持 PWA
- 适合嵌入第三方 APP
**可用组件:**
- NavBar导航栏
- Tabbar底部导航
- Cell/CellGroup单元格列表
- Grid宫格
- Button按钮
- Toast轻提示
- Dialog弹窗
- Image图片
- 等 60+ 组件
**目录结构:**
```
packages/mobile/src/
├── views/ # 页面组件
├── components/ # 通用组件
├── router/ # 路由配置
├── store/ # Pinia 状态管理
├── assets/ # 静态资源
└── styles/ # 全局样式
```
### 3. 共享库 (shared)
共享代码可在两个项目中复用:
```javascript
// 在项目中引入 API
import { getUserInfo, getDataList } from '@shared/api'
// 引入工具函数
import { formatDate, debounce, throttle } from '@shared/utils'
// 引入配置
import { API_CONFIG, APP_CONFIG } from '@shared/config'
```
**可用工具函数:**
- `formatDate()` - 日期格式化
- `debounce()` - 防抖
- `throttle()` - 节流
- `deepClone()` - 深拷贝
## 环境变量
项目使用 `.env` 文件管理环境变量:
- `.env.development` - 开发环境
- `.env.production` - 生产环境
**示例:**
```bash
# .env.development
VITE_API_BASE_URL=http://localhost:3000/api
```
## 开发建议
### 1. 共享代码复用
将通用的业务逻辑、API 接口、工具函数放在 `packages/shared` 中,避免重复代码。
### 2. 组件开发
- **大屏项目**:使用 ECharts 开发数据可视化组件
- **移动端项目**:优先使用 Vant 组件,减少自定义开发
### 3. 样式规范
- 使用 Sass 预处理器
- 遵循 BEM 命名规范
- 组件样式使用 `scoped`
### 4. API 请求
统一使用 `@shared/api` 中封装的请求方法,自动处理 token、错误等。
## 注意事项
1. 本项目使用 **JavaScript**,未使用 TypeScript
2. 使用 **pnpm** 作为包管理工具,不要使用 npm 或 yarn
3. 大屏和移动端项目独立运行,通过 shared 包共享代码
4. 移动端项目基于 Vue3 + Vant**不是 uni-app**,仅支持 H5
5. 如需扩展到小程序,建议使用 uni-app 或 Taro 重新搭建
## 部署说明
### 大屏项目部署
```bash
# 构建
pnpm build:screen
# 构建产物在 packages/screen/dist
# 部署到 Nginx 或其他静态服务器
```
### 移动端部署
```bash
# 构建
pnpm build:mobile
# 构建产物在 packages/mobile/dist
# 可以嵌入到第三方 APP 的 WebView 中
```
## 浏览器支持
### 大屏项目
- Chrome >= 87
- Firefox >= 78
- Safari >= 14
- Edge >= 88
### 移动端项目
- iOS Safari >= 10
- Android Chrome >= 5.0
- 微信浏览器
## 常见问题
### 1. 端口被占用
修改对应项目的 `vite.config.js` 中的 `server.port` 配置。
### 2. 组件按需引入不生效
移动端项目使用 `unplugin-vue-components` 自动按需引入 Vant 组件,无需手动引入。
### 3. 如何添加新页面?
```bash
# 1. 在 src/views 中创建页面组件
# 2. 在 src/router/index.js 中添加路由配置
```
## 技术支持
- Vue 3 文档https://cn.vuejs.org/
- Vant 文档https://vant-ui.github.io/vant/
- ECharts 文档https://echarts.apache.org/zh/index.html
- Pinia 文档https://pinia.vuejs.org/zh/
## 许可证
MIT

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "h5-workspace",
"version": "1.0.0",
"private": true,
"description": "大屏 + H5 Monorepo 工程",
"scripts": {
"dev:screen": "pnpm --filter @h5/screen dev",
"dev:mobile": "pnpm --filter @h5/mobile dev",
"build:screen": "pnpm --filter @h5/screen build",
"build:mobile": "pnpm --filter @h5/mobile build",
"build:all": "pnpm -r build",
"lint": "pnpm -r lint"
},
"keywords": ["monorepo", "vue3", "vant", "大屏", "h5"],
"author": "",
"license": "MIT",
"engines": {
"node": ">=16.0.0",
"pnpm": ">=8.0.0"
}
}

View File

@ -0,0 +1,14 @@
<!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=0, viewport-fit=cover" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<title>H5移动端</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,25 @@
{
"name": "@h5/mobile",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"vant": "^4.8.0",
"axios": "^1.6.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0",
"sass": "^1.70.0",
"unplugin-vue-components": "^0.26.0",
"unplugin-auto-import": "^0.17.0"
}
}

View File

@ -0,0 +1,17 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script setup>
// App
</script>
<style>
#app {
width: 100%;
min-height: 100vh;
background: #f7f8fa;
}
</style>

View File

@ -0,0 +1,13 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'
import 'vant/lib/index.css'
import './styles/index.css'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@ -0,0 +1,21 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: '/user',
name: 'User',
component: () => import('../views/User.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router

View File

@ -0,0 +1,16 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
width: 100%;
min-height: 100vh;
}

View File

@ -0,0 +1,58 @@
<template>
<div class="home">
<van-nav-bar title="首页" fixed placeholder />
<div class="content">
<van-cell-group inset>
<van-cell title="欢迎使用" value="H5移动端" />
</van-cell-group>
<van-grid :column-num="2" class="grid">
<van-grid-item icon="photo-o" text="功能1" @click="showToast('功能1')" />
<van-grid-item icon="chat-o" text="功能2" @click="showToast('功能2')" />
<van-grid-item icon="setting-o" text="功能3" @click="showToast('功能3')" />
<van-grid-item icon="star-o" text="功能4" @click="showToast('功能4')" />
</van-grid>
<van-button type="primary" block class="btn" @click="goToUser">
进入个人中心
</van-button>
</div>
<van-tabbar v-model="active" route>
<van-tabbar-item icon="home-o" to="/">首页</van-tabbar-item>
<van-tabbar-item icon="user-o" to="/user">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
const router = useRouter()
const active = ref(0)
const goToUser = () => {
router.push('/user')
}
</script>
<style scoped>
.home {
padding-bottom: 50px;
}
.content {
padding: 16px;
}
.grid {
margin-top: 16px;
}
.btn {
margin-top: 24px;
}
</style>

View File

@ -0,0 +1,78 @@
<template>
<div class="user">
<van-nav-bar title="我的" fixed placeholder />
<div class="user-header">
<van-image
round
width="80"
height="80"
src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
/>
<div class="user-name">用户名</div>
</div>
<van-cell-group inset class="menu-group">
<van-cell title="个人信息" is-link @click="showToast('个人信息')" />
<van-cell title="我的订单" is-link @click="showToast('我的订单')" />
<van-cell title="收货地址" is-link @click="showToast('收货地址')" />
<van-cell title="设置" is-link @click="showToast('设置')" />
</van-cell-group>
<van-button type="danger" block class="logout-btn" @click="handleLogout">
退出登录
</van-button>
<van-tabbar v-model="active" route>
<van-tabbar-item icon="home-o" to="/">首页</van-tabbar-item>
<van-tabbar-item icon="user-o" to="/user">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { showToast, showDialog } from 'vant'
const router = useRouter()
const active = ref(1)
const handleLogout = () => {
showDialog({
title: '提示',
message: '确定要退出登录吗?'
}).then(() => {
showToast('已退出')
})
}
</script>
<style scoped>
.user {
padding-bottom: 50px;
}
.user-header {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 0;
background: #fff;
}
.user-name {
margin-top: 16px;
font-size: 18px;
font-weight: bold;
color: #323233;
}
.menu-group {
margin-top: 16px;
}
.logout-btn {
margin: 24px 16px;
}
</style>

View File

@ -0,0 +1,30 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [VantResolver()]
})
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@shared': resolve(__dirname, '../shared')
}
},
server: {
port: 8080,
host: '0.0.0.0',
open: true
},
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false
}
})

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据大屏</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,24 @@
{
"name": "@h5/screen",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"axios": "^1.6.0",
"echarts": "^5.5.0",
"vue-echarts": "^6.6.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0",
"sass": "^1.70.0"
}
}

View File

@ -0,0 +1,19 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style lang="scss">
#app {
width: 100%;
height: 100vh;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,12 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'
import './styles/index.scss'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@ -0,0 +1,16 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router

View File

@ -0,0 +1,16 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
width: 100%;
height: 100vh;
}

View File

@ -0,0 +1,102 @@
<template>
<div class="screen-container">
<header class="screen-header">
<h1>数据大屏</h1>
</header>
<div class="screen-content">
<div class="screen-left">
<div class="chart-box">
<h3>左侧图表1</h3>
</div>
<div class="chart-box">
<h3>左侧图表2</h3>
</div>
</div>
<div class="screen-center">
<div class="chart-box center-main">
<h3>中间主图表</h3>
</div>
</div>
<div class="screen-right">
<div class="chart-box">
<h3>右侧图表1</h3>
</div>
<div class="chart-box">
<h3>右侧图表2</h3>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Home',
setup() {
return {}
}
}
</script>
<style lang="scss" scoped>
.screen-container {
width: 100%;
height: 100vh;
background: #0a1e3e;
color: #fff;
padding: 20px;
box-sizing: border-box;
}
.screen-header {
text-align: center;
margin-bottom: 20px;
h1 {
font-size: 36px;
font-weight: bold;
background: linear-gradient(to right, #4facfe, #00f2fe);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
.screen-content {
display: flex;
gap: 20px;
height: calc(100% - 80px);
}
.screen-left,
.screen-right {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.screen-center {
flex: 2;
}
.chart-box {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 20px;
flex: 1;
h3 {
margin: 0 0 15px 0;
font-size: 18px;
color: #4facfe;
}
&.center-main {
height: 100%;
}
}
</style>

View File

@ -0,0 +1,31 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@shared': resolve(__dirname, '../shared')
}
},
server: {
port: 3000,
open: true,
cors: true
},
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false,
minify: 'terser',
rollupOptions: {
output: {
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]'
}
}
}
})

View File

@ -0,0 +1,17 @@
import request from './request'
// 示例API接口
export const getUserInfo = () => {
return request({
url: '/user/info',
method: 'get'
})
}
export const getDataList = (params) => {
return request({
url: '/data/list',
method: 'get',
params
})
}

View File

@ -0,0 +1,42 @@
import axios from 'axios'
// 创建axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000
})
// 请求拦截器
request.interceptors.request.use(
config => {
// 可以在这里添加token等
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => {
console.error('请求错误:', error)
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
response => {
const res = response.data
// 根据实际情况处理响应
if (res.code !== 200) {
console.error('接口错误:', res.message)
return Promise.reject(new Error(res.message || 'Error'))
}
return res
},
error => {
console.error('响应错误:', error)
return Promise.reject(error)
}
)
export default request

View File

@ -0,0 +1,18 @@
// 环境配置
export const ENV = import.meta.env.MODE || 'development'
// API配置
export const API_CONFIG = {
development: {
baseURL: 'http://localhost:3000/api'
},
production: {
baseURL: 'https://api.example.com'
}
}
// 应用配置
export const APP_CONFIG = {
title: '数据大屏',
version: '1.0.0'
}

3
packages/shared/index.js Normal file
View File

@ -0,0 +1,3 @@
export * from './api'
export * from './utils'
export * from './config'

View File

@ -0,0 +1,11 @@
{
"name": "@h5/shared",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "index.js",
"scripts": {},
"dependencies": {
"axios": "^1.6.0"
}
}

View File

@ -0,0 +1,75 @@
/**
* 格式化日期
* @param {Date|string|number} date
* @param {string} format
* @returns {string}
*/
export function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds)
}
/**
* 防抖函数
* @param {Function} fn
* @param {number} delay
* @returns {Function}
*/
export function debounce(fn, delay = 300) {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
/**
* 节流函数
* @param {Function} fn
* @param {number} delay
* @returns {Function}
*/
export function throttle(fn, delay = 300) {
let last = 0
return function(...args) {
const now = Date.now()
if (now - last >= delay) {
last = now
fn.apply(this, args)
}
}
}
/**
* 深拷贝
* @param {any} obj
* @returns {any}
*/
export function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof Array) return obj.map(item => deepClone(item))
const clonedObj = {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key])
}
}
return clonedObj
}

1867
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,2 @@
packages:
- 'packages/*'