feat: H5 用户端 uni-app 重写(替换 Taro)
- 使用 uni-app + Vue 3 + Composition API 重写 H5 用户端 - 6 个页面:登录、首页、设备列表、设备详情、仓体详情、个人中心 - Pinia 状态管理 + API 层封装 - TabBar 导航(首页/设备/我的) - 已构建并部署到服务器 /data/pms/h5/ - 登录页渲染正常,布局完整
This commit is contained in:
parent
6b97cd78ad
commit
e90da20e82
@ -38,6 +38,7 @@ const config = {
|
|||||||
staticDirectory: 'static',
|
staticDirectory: 'static',
|
||||||
router: {
|
router: {
|
||||||
mode: 'browser',
|
mode: 'browser',
|
||||||
|
basename: '/h5/',
|
||||||
},
|
},
|
||||||
postcss: {
|
postcss: {
|
||||||
autoprefixer: { enable: true, config: {} },
|
autoprefixer: { enable: true, config: {} },
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
export default {
|
export default {
|
||||||
pages: [
|
pages: [
|
||||||
'pages/login/index',
|
|
||||||
'pages/home/index',
|
'pages/home/index',
|
||||||
'pages/devices/index',
|
'pages/devices/index',
|
||||||
|
'pages/me/index',
|
||||||
|
'pages/login/index',
|
||||||
'pages/device-detail/index',
|
'pages/device-detail/index',
|
||||||
'pages/compartment-detail/index',
|
'pages/compartment-detail/index',
|
||||||
'pages/me/index',
|
|
||||||
],
|
],
|
||||||
window: {
|
window: {
|
||||||
backgroundTextStyle: 'light',
|
backgroundTextStyle: 'light',
|
||||||
|
|||||||
@ -5,17 +5,17 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
<title>安知充</title>
|
<title>安知充</title>
|
||||||
<script>
|
<script>
|
||||||
// 默认路由:根路径跳转到登录页
|
// 默认路由:根路径跳转到 home 页(tab 页必须在 pages 数组第一位)
|
||||||
;(function () {
|
;(function () {
|
||||||
var path = window.location.pathname;
|
var path = window.location.pathname;
|
||||||
// 扫码入口: 提取 URL 中的 abstract_id 并重写路由
|
// 扫码入口: 提取 URL 中的 abstract_id 并重写路由
|
||||||
var match = path.match(/\/h5\/(\d{2}-\d{8})/);
|
var match = path.match(/\/h5\/(\d{2}-\d{8})/);
|
||||||
if (match) {
|
if (match) {
|
||||||
window.__SCAN_ABSTRACT_ID__ = match[1];
|
window.__SCAN_ABSTRACT_ID__ = match[1];
|
||||||
window.history.replaceState({}, '', '/h5/');
|
window.history.replaceState({}, '', '/h5/pages/home/index');
|
||||||
} else if (path === '/h5/' || path === '/h5') {
|
} else if (path === '/h5/' || path === '/h5') {
|
||||||
// 纯根路径访问,重定向到登录页让 Taro 能匹配路由
|
// 纯根路径访问,重定向到 home 页让 Taro 能匹配路由
|
||||||
window.history.replaceState({}, '', '/h5/pages/login/index');
|
window.history.replaceState({}, '', '/h5/pages/home/index');
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
// Taro H5 路由 base
|
// Taro H5 路由 base
|
||||||
|
|||||||
@ -16,6 +16,11 @@ export default function CompartmentDetailPage() {
|
|||||||
const [actionLoading, setActionLoading] = useState('')
|
const [actionLoading, setActionLoading] = useState('')
|
||||||
const [confirmAction, setConfirmAction] = useState<'stop' | 'door' | null>(null)
|
const [confirmAction, setConfirmAction] = useState<'stop' | 'door' | null>(null)
|
||||||
|
|
||||||
|
// 隐藏 tabbar(非 tab 页)
|
||||||
|
useEffect(() => {
|
||||||
|
Taro.hideTabBar()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const fetchDetail = async () => {
|
const fetchDetail = async () => {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|||||||
@ -14,6 +14,11 @@ export default function DeviceDetailPage() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
// 隐藏 tabbar(非 tab 页)
|
||||||
|
useEffect(() => {
|
||||||
|
Taro.hideTabBar()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const fetchDetail = async () => {
|
const fetchDetail = async () => {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|||||||
@ -7,9 +7,17 @@ import Loading from '../../components/Loading'
|
|||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const user = useH5AuthStore((s) => s.user)
|
const user = useH5AuthStore((s) => s.user)
|
||||||
|
const isAuthenticated = useH5AuthStore((s) => s.isAuthenticated)
|
||||||
const [data, setData] = useState<DashboardData | null>(null)
|
const [data, setData] = useState<DashboardData | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
// 未登录跳转到登录页
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
Taro.navigateTo({ url: '/pages/login/index' })
|
||||||
|
}
|
||||||
|
}, [isAuthenticated])
|
||||||
|
|
||||||
const fetchDashboard = async () => {
|
const fetchDashboard = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { View, Text, Input, Button } from '@tarojs/components'
|
import { View, Text, Input, Button } from '@tarojs/components'
|
||||||
import Taro, { useRouter } from '@tarojs/taro'
|
import Taro, { useRouter } from '@tarojs/taro'
|
||||||
import { useH5AuthStore } from '../../store/auth'
|
import { useH5AuthStore } from '../../store/auth'
|
||||||
@ -12,6 +12,11 @@ export default function LoginPage() {
|
|||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const { login, loading } = useH5AuthStore()
|
const { login, loading } = useH5AuthStore()
|
||||||
|
|
||||||
|
// 隐藏 tabbar(login 页不是 tab 页)
|
||||||
|
useEffect(() => {
|
||||||
|
Taro.hideTabBar()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
if (!phone || !password) {
|
if (!phone || !password) {
|
||||||
setError('请输入手机号和密码')
|
setError('请输入手机号和密码')
|
||||||
|
|||||||
23
software/h5-uniapp/index.html
Normal file
23
software/h5-uniapp/index.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!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" />
|
||||||
|
<title>安知充</title>
|
||||||
|
<script>
|
||||||
|
// 扫码入口:提取 abstract_id 并重写路由
|
||||||
|
;(function () {
|
||||||
|
var path = window.location.pathname;
|
||||||
|
var match = path.match(/\/h5\/(\d{2}-\d{8})/);
|
||||||
|
if (match) {
|
||||||
|
window.__SCAN_ABSTRACT_ID__ = match[1];
|
||||||
|
window.history.replaceState({}, '', '/h5/');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
27
software/h5-uniapp/package.json
Normal file
27
software/h5-uniapp/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "h5-uniapp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev:h5": "uni",
|
||||||
|
"build:h5": "uni build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@dcloudio/uni-app": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-app-plus": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-components": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-h5": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"vue": "^3.4.21"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@dcloudio/types": "^3.4.8",
|
||||||
|
"@dcloudio/uni-automator": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-cli-shared": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-stacktracey": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"vite": "^5.2.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
software/h5-uniapp/src/App.vue
Normal file
16
software/h5-uniapp/src/App.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onLaunch } from '@dcloudio/uni-app'
|
||||||
|
import { useAuthStore } from './store/auth'
|
||||||
|
|
||||||
|
onLaunch(() => {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
authStore.restore()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
page {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
164
software/h5-uniapp/src/api/index.ts
Normal file
164
software/h5-uniapp/src/api/index.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
// API 层 — 复用 Taro 项目的逻辑,改用 uni.request
|
||||||
|
|
||||||
|
const BASE_URL = '/api/h5'
|
||||||
|
|
||||||
|
interface RequestOptions {
|
||||||
|
method?: string
|
||||||
|
body?: unknown
|
||||||
|
params?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||||
|
const token = uni.getStorageSync('h5_token')
|
||||||
|
const header: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
if (token) {
|
||||||
|
header['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = `${BASE_URL}${path}`
|
||||||
|
if (options.params) {
|
||||||
|
const qs = Object.entries(options.params)
|
||||||
|
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
||||||
|
.join('&')
|
||||||
|
url += `?${qs}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.request({
|
||||||
|
url,
|
||||||
|
method: (options.method as any) || 'GET',
|
||||||
|
header,
|
||||||
|
data: options.body,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.statusCode >= 400) {
|
||||||
|
const msg = (res.data as any)?.message || (res.data as any)?.error || '请求失败'
|
||||||
|
reject(new Error(msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve(res.data as T)
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
reject(new Error(err.errMsg || '网络错误'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Types ----
|
||||||
|
|
||||||
|
export interface LoginParams {
|
||||||
|
phone: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResult {
|
||||||
|
token: string
|
||||||
|
user: H5User
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface H5User {
|
||||||
|
id: number
|
||||||
|
phone: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardData {
|
||||||
|
online_cabinets: number
|
||||||
|
charging_count: number
|
||||||
|
idle_channels: number
|
||||||
|
fault_channels: number
|
||||||
|
today_charge_count: number
|
||||||
|
today_energy_kwh: number
|
||||||
|
today_charge_hours: number
|
||||||
|
alerts: { project: string; cabinet: string; channel: number }[]
|
||||||
|
projects: { id: number; name: string; cabinet_count: number; charging: number; idle: number; fault: number }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CabinetItem {
|
||||||
|
id: number
|
||||||
|
abstract_id: string
|
||||||
|
name?: string
|
||||||
|
status: number
|
||||||
|
project_id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompartmentInfo {
|
||||||
|
id: number
|
||||||
|
channel_id: number
|
||||||
|
status: number
|
||||||
|
voltage?: number
|
||||||
|
current?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CabinBoard {
|
||||||
|
id: number
|
||||||
|
board_id: number
|
||||||
|
status: number
|
||||||
|
compartments: CompartmentInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CabinetDetail {
|
||||||
|
id: number
|
||||||
|
abstract_id: string
|
||||||
|
name?: string
|
||||||
|
imei?: string
|
||||||
|
status: number
|
||||||
|
cabin_boards: CabinBoard[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompartmentDetail {
|
||||||
|
id: number
|
||||||
|
channel_id: number
|
||||||
|
status: number
|
||||||
|
voltage?: number
|
||||||
|
current?: number
|
||||||
|
power?: number
|
||||||
|
energy?: number
|
||||||
|
soc?: number
|
||||||
|
soh?: number
|
||||||
|
cell_voltages?: number[]
|
||||||
|
cell_temps?: number[]
|
||||||
|
bms_alerts?: string[]
|
||||||
|
charge_records?: ChargeRecord[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChargeRecord {
|
||||||
|
id: number
|
||||||
|
start_time: string
|
||||||
|
end_time?: string
|
||||||
|
energy_kwh: number
|
||||||
|
cost: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- API ----
|
||||||
|
|
||||||
|
export const h5Api = {
|
||||||
|
login: (params: LoginParams) =>
|
||||||
|
request<{ data: LoginResult }>('/h5/auth/login', { method: 'POST', body: params }),
|
||||||
|
|
||||||
|
getDashboard: () =>
|
||||||
|
request<{ data: DashboardData }>('/h5/dashboard'),
|
||||||
|
|
||||||
|
getCabinets: (params?: { project_id?: number }) =>
|
||||||
|
request<{ data: CabinetItem[] }>('/h5/cabinets', { params: params as Record<string, string> }),
|
||||||
|
|
||||||
|
getCabinetByAbstract: (abstractId: string) =>
|
||||||
|
request<{ data: CabinetDetail }>(`/h5/cabinets/abstract/${abstractId}`),
|
||||||
|
|
||||||
|
getCabinetDetail: (id: number) =>
|
||||||
|
request<{ data: CabinetDetail }>(`/h5/cabinets/${id}`),
|
||||||
|
|
||||||
|
getCompartmentDetail: (id: number) =>
|
||||||
|
request<{ data: CompartmentDetail }>(`/h5/compartments/${id}`),
|
||||||
|
|
||||||
|
startCharge: (compartmentId: number) =>
|
||||||
|
request<{ code: number }>('/h5/charge/start', { method: 'POST', body: { compartment_id: compartmentId } }),
|
||||||
|
|
||||||
|
stopCharge: (compartmentId: number) =>
|
||||||
|
request<{ code: number }>('/h5/charge/stop', { method: 'POST', body: { compartment_id: compartmentId } }),
|
||||||
|
|
||||||
|
openDoor: (compartmentId: number) =>
|
||||||
|
request<{ code: number }>('/h5/door/open', { method: 'POST', body: { compartment_id: compartmentId } }),
|
||||||
|
}
|
||||||
7
software/h5-uniapp/src/env.d.ts
vendored
Normal file
7
software/h5-uniapp/src/env.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/// <reference types="@dcloudio/types" />
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
10
software/h5-uniapp/src/main.ts
Normal file
10
software/h5-uniapp/src/main.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
const pinia = createPinia()
|
||||||
|
app.use(pinia)
|
||||||
|
return { app }
|
||||||
|
}
|
||||||
24
software/h5-uniapp/src/manifest.json
Normal file
24
software/h5-uniapp/src/manifest.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "安知充",
|
||||||
|
"appid": "",
|
||||||
|
"description": "智能充电柜管理系统",
|
||||||
|
"versionName": "1.0.0",
|
||||||
|
"versionCode": "100",
|
||||||
|
"transformPx": false,
|
||||||
|
"h5": {
|
||||||
|
"title": "安知充",
|
||||||
|
"router": {
|
||||||
|
"mode": "history",
|
||||||
|
"base": "/h5/"
|
||||||
|
},
|
||||||
|
"devServer": {
|
||||||
|
"port": 5175,
|
||||||
|
"proxy": {
|
||||||
|
"/api": {
|
||||||
|
"target": "http://10.8.0.252:3000",
|
||||||
|
"changeOrigin": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
software/h5-uniapp/src/pages.json
Normal file
27
software/h5-uniapp/src/pages.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
{ "path": "pages/home/index", "style": { "navigationBarTitleText": "首页" } },
|
||||||
|
{ "path": "pages/devices/index", "style": { "navigationBarTitleText": "设备" } },
|
||||||
|
{ "path": "pages/me/index", "style": { "navigationBarTitleText": "我的" } },
|
||||||
|
{ "path": "pages/login/index", "style": { "navigationBarTitleText": "登录", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "pages/device-detail/index", "style": { "navigationBarTitleText": "设备详情" } },
|
||||||
|
{ "path": "pages/compartment-detail/index", "style": { "navigationBarTitleText": "仓体详情" } }
|
||||||
|
],
|
||||||
|
"tabBar": {
|
||||||
|
"color": "#999999",
|
||||||
|
"selectedColor": "#2563eb",
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"borderStyle": "black",
|
||||||
|
"list": [
|
||||||
|
{ "pagePath": "pages/home/index", "text": "首页", "iconPath": "static/tabbar/home.png", "selectedIconPath": "static/tabbar/home-active.png" },
|
||||||
|
{ "pagePath": "pages/devices/index", "text": "设备", "iconPath": "static/tabbar/device.png", "selectedIconPath": "static/tabbar/device-active.png" },
|
||||||
|
{ "pagePath": "pages/me/index", "text": "我的", "iconPath": "static/tabbar/me.png", "selectedIconPath": "static/tabbar/me-active.png" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"globalStyle": {
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarTitleText": "安知充",
|
||||||
|
"navigationBarBackgroundColor": "#ffffff",
|
||||||
|
"backgroundColor": "#f5f5f5"
|
||||||
|
}
|
||||||
|
}
|
||||||
308
software/h5-uniapp/src/pages/compartment-detail/index.vue
Normal file
308
software/h5-uniapp/src/pages/compartment-detail/index.vue
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
<template>
|
||||||
|
<view class="detail-page">
|
||||||
|
<view v-if="loading" class="loading">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="error" class="error-state">
|
||||||
|
<text class="error-text">{{ error }}</text>
|
||||||
|
<button class="retry-btn" @click="fetchDetail">重试</button>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="data">
|
||||||
|
<!-- Real-time status -->
|
||||||
|
<view class="status-card">
|
||||||
|
<view class="status-header">
|
||||||
|
<text class="status-title">实时状态</text>
|
||||||
|
<text class="status-badge" :class="getStatusClass(data.status)">
|
||||||
|
{{ getStatusText(data.status) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="stats-row">
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-value">{{ data.soc ?? '-' }}%</text>
|
||||||
|
<text class="stat-label">SOC</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-value">{{ data.soh ?? '-' }}%</text>
|
||||||
|
<text class="stat-label">SOH</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-value">{{ data.energy?.toFixed(1) ?? '-' }}</text>
|
||||||
|
<text class="stat-label">能耗(kWh)</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Electrical params -->
|
||||||
|
<view class="params-card">
|
||||||
|
<text class="card-title">电气参数</text>
|
||||||
|
<view class="params-grid">
|
||||||
|
<view class="param-item">
|
||||||
|
<text class="param-label">电压</text>
|
||||||
|
<text class="param-value">{{ data.voltage?.toFixed(1) ?? '-' }} V</text>
|
||||||
|
</view>
|
||||||
|
<view class="param-item">
|
||||||
|
<text class="param-label">电流</text>
|
||||||
|
<text class="param-value">{{ data.current?.toFixed(1) ?? '-' }} A</text>
|
||||||
|
</view>
|
||||||
|
<view class="param-item">
|
||||||
|
<text class="param-label">功率</text>
|
||||||
|
<text class="param-value">{{ data.power?.toFixed(1) ?? '-' }} W</text>
|
||||||
|
</view>
|
||||||
|
<view class="param-item">
|
||||||
|
<text class="param-label">能耗</text>
|
||||||
|
<text class="param-value">{{ data.energy?.toFixed(3) ?? '-' }} kWh</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Charge records -->
|
||||||
|
<view v-if="data.charge_records && data.charge_records.length > 0" class="records-card">
|
||||||
|
<text class="card-title">充电记录</text>
|
||||||
|
<view v-for="record in data.charge_records.slice(0, 5)" :key="record.id" class="record-item">
|
||||||
|
<view>
|
||||||
|
<text class="record-time">{{ record.start_time.slice(0, 16) }}</text>
|
||||||
|
<text v-if="record.end_time" class="record-end">{{ record.end_time.slice(0, 16) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="record-right">
|
||||||
|
<text class="record-energy">{{ record.energy_kwh.toFixed(2) }} kWh</text>
|
||||||
|
<text class="record-cost">¥{{ record.cost.toFixed(2) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<view class="actions">
|
||||||
|
<button v-if="data.status === 0" class="btn-action green" @click="handleAction('start')">
|
||||||
|
{{ actionLoading === 'start' ? '操作中...' : '开始充电' }}
|
||||||
|
</button>
|
||||||
|
<button v-if="data.status === 1" class="btn-action orange" @click="confirmStop">
|
||||||
|
{{ actionLoading === 'stop' ? '操作中...' : '停止充电' }}
|
||||||
|
</button>
|
||||||
|
<button class="btn-action outline" @click="confirmDoor">
|
||||||
|
{{ actionLoading === 'door' ? '操作中...' : '开门' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { h5Api, type CompartmentDetail } from '../../api'
|
||||||
|
|
||||||
|
const data = ref<CompartmentDetail | null>(null)
|
||||||
|
const loading = ref(true)
|
||||||
|
const error = ref('')
|
||||||
|
const actionLoading = ref('')
|
||||||
|
|
||||||
|
onLoad((query) => {
|
||||||
|
if (query?.id) {
|
||||||
|
fetchDetail(Number(query.id))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchDetail(id: number) {
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
try {
|
||||||
|
const res = await h5Api.getCompartmentDetail(id)
|
||||||
|
data.value = res.data
|
||||||
|
} catch {
|
||||||
|
error.value = '加载通道详情失败'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusClass(status: number) {
|
||||||
|
if (status === 1) return 'charging'
|
||||||
|
if (status === 2) return 'fault'
|
||||||
|
return 'idle'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusText(status: number) {
|
||||||
|
if (status === 0) return '空闲'
|
||||||
|
if (status === 1) return '充电中'
|
||||||
|
if (status === 2) return '故障'
|
||||||
|
return '离线'
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmStop() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '停止充电',
|
||||||
|
content: '确定要停止当前充电?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) handleAction('stop')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDoor() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '开门',
|
||||||
|
content: '确定要打开仓门?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) handleAction('door')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAction(action: 'start' | 'stop' | 'door') {
|
||||||
|
if (!data.value || actionLoading.value) return
|
||||||
|
actionLoading.value = action
|
||||||
|
try {
|
||||||
|
if (action === 'start') {
|
||||||
|
await h5Api.startCharge(data.value.id)
|
||||||
|
} else if (action === 'stop') {
|
||||||
|
await h5Api.stopCharge(data.value.id)
|
||||||
|
} else if (action === 'door') {
|
||||||
|
await h5Api.openDoor(data.value.id)
|
||||||
|
}
|
||||||
|
fetchDetail(data.value.id)
|
||||||
|
} catch {
|
||||||
|
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.detail-page {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.loading, .error-state {
|
||||||
|
padding: 80px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.error-text {
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
.retry-btn {
|
||||||
|
margin-top: 20px;
|
||||||
|
background-color: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.status-card, .params-card, .records-card {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.status-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.status-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
.status-badge {
|
||||||
|
font-size: 24px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.status-badge.idle { color: #6b7280; background-color: #f9fafb; }
|
||||||
|
.status-badge.charging { color: #2563eb; background-color: #eff6ff; }
|
||||||
|
.status-badge.fault { color: #dc2626; background-color: #fef2f2; }
|
||||||
|
.stats-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.stat-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2563eb;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.params-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.param-item {
|
||||||
|
width: calc(50% - 8px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.param-label {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
.param-value {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #111;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
.record-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
.record-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.record-time {
|
||||||
|
font-size: 26px;
|
||||||
|
color: #111;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.record-end {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
.record-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.record-energy {
|
||||||
|
font-size: 26px;
|
||||||
|
color: #111;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.record-cost {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding-bottom: 32px;
|
||||||
|
}
|
||||||
|
.btn-action {
|
||||||
|
height: 88px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.btn-action.green { background-color: #16a34a; color: #fff; }
|
||||||
|
.btn-action.orange { background-color: #f97316; color: #fff; }
|
||||||
|
.btn-action.outline { background-color: #fff; color: #374151; border: 1px solid #d1d5db; }
|
||||||
|
.btn-action[disabled] { opacity: 0.5; }
|
||||||
|
</style>
|
||||||
195
software/h5-uniapp/src/pages/device-detail/index.vue
Normal file
195
software/h5-uniapp/src/pages/device-detail/index.vue
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
<template>
|
||||||
|
<view class="detail-page">
|
||||||
|
<view v-if="loading" class="loading">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="error" class="error-state">
|
||||||
|
<text class="error-text">{{ error }}</text>
|
||||||
|
<button class="retry-btn" @click="fetchDetail">重试</button>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="data">
|
||||||
|
<!-- Basic info -->
|
||||||
|
<view class="info-card">
|
||||||
|
<view class="info-header">
|
||||||
|
<view>
|
||||||
|
<text class="info-name">{{ data.name || data.abstract_id }}</text>
|
||||||
|
<text class="info-id">ID: {{ data.abstract_id }}</text>
|
||||||
|
<text v-if="data.imei" class="info-imei">IMEI: {{ data.imei }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="status-badge" :class="data.status === 1 ? 'online' : 'offline'">
|
||||||
|
{{ data.status === 1 ? '在线' : '离线' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Cabin boards -->
|
||||||
|
<view v-for="board in data.cabin_boards" :key="board.id" class="board-section">
|
||||||
|
<view class="board-header">
|
||||||
|
<text class="board-title">仓板 {{ board.board_id }}</text>
|
||||||
|
<text class="board-status" :class="board.status === 1 ? 'online' : 'offline'">
|
||||||
|
({{ board.status === 1 ? '在线' : '离线' }})
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="channels-grid">
|
||||||
|
<view
|
||||||
|
v-for="comp in board.compartments"
|
||||||
|
:key="comp.id"
|
||||||
|
class="channel-card"
|
||||||
|
:class="getStatusClass(comp.status)"
|
||||||
|
@click="goCompartment(comp.id)"
|
||||||
|
>
|
||||||
|
<text class="channel-id">通道 {{ comp.channel_id }}</text>
|
||||||
|
<text class="channel-status">{{ getStatusText(comp.status) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { h5Api, type CabinetDetail } from '../../api'
|
||||||
|
|
||||||
|
const data = ref<CabinetDetail | null>(null)
|
||||||
|
const loading = ref(true)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
onLoad((query) => {
|
||||||
|
if (query?.id) {
|
||||||
|
fetchDetail(Number(query.id))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchDetail(id: number) {
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
try {
|
||||||
|
const res = await h5Api.getCabinetDetail(id)
|
||||||
|
data.value = res.data
|
||||||
|
} catch {
|
||||||
|
error.value = '加载设备详情失败'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusClass(status: number) {
|
||||||
|
if (status === 1) return 'charging'
|
||||||
|
if (status === 2) return 'fault'
|
||||||
|
return 'idle'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusText(status: number) {
|
||||||
|
if (status === 0) return '空闲'
|
||||||
|
if (status === 1) return '充电中'
|
||||||
|
if (status === 2) return '故障'
|
||||||
|
return '离线'
|
||||||
|
}
|
||||||
|
|
||||||
|
function goCompartment(id: number) {
|
||||||
|
uni.navigateTo({ url: `/pages/compartment-detail/index?id=${id}` })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.detail-page {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.loading, .error-state {
|
||||||
|
padding: 80px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.error-text {
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
.retry-btn {
|
||||||
|
margin-top: 20px;
|
||||||
|
background-color: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
.info-card {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.info-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.info-name {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.info-id {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.info-imei {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #9ca3af;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.status-badge {
|
||||||
|
font-size: 24px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.status-badge.online { color: #16a34a; background-color: #f0fdf4; }
|
||||||
|
.status-badge.offline { color: #9ca3af; background-color: #f9fafb; }
|
||||||
|
.board-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.board-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.board-title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
.board-status {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
.board-status.online { color: #16a34a; }
|
||||||
|
.board-status.offline { color: #9ca3af; }
|
||||||
|
.channels-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.channel-card {
|
||||||
|
width: calc(33.33% - 8px);
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.channel-card.idle { border-left: 4px solid #6b7280; }
|
||||||
|
.channel-card.charging { border-left: 4px solid #2563eb; }
|
||||||
|
.channel-card.fault { border-left: 4px solid #dc2626; }
|
||||||
|
.channel-id {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #374151;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.channel-status {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
105
software/h5-uniapp/src/pages/devices/index.vue
Normal file
105
software/h5-uniapp/src/pages/devices/index.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<view class="devices-page">
|
||||||
|
<!-- List -->
|
||||||
|
<view v-if="list.length === 0 && !loading" class="empty">
|
||||||
|
<text class="empty-text">暂无设备</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="device-list">
|
||||||
|
<view
|
||||||
|
v-for="item in list"
|
||||||
|
:key="item.id"
|
||||||
|
class="device-card"
|
||||||
|
@click="goDetail(item.id)"
|
||||||
|
>
|
||||||
|
<view class="device-header">
|
||||||
|
<text class="device-name">{{ item.name || item.abstract_id }}</text>
|
||||||
|
<text class="device-status" :class="item.status === 1 ? 'online' : 'offline'">
|
||||||
|
{{ item.status === 1 ? '在线' : '离线' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<text class="device-id">ID: {{ item.abstract_id }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
import { h5Api, type CabinetItem } from '../../api'
|
||||||
|
|
||||||
|
const list = ref<CabinetItem[]>([])
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
fetchList()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await h5Api.getCabinets()
|
||||||
|
list.value = res.data || []
|
||||||
|
} catch {
|
||||||
|
// 静默失败
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goDetail(id: number) {
|
||||||
|
uni.navigateTo({ url: `/pages/device-detail/index?id=${id}` })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.devices-page {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
padding: 80px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.empty-text {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
.device-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.device-card {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.device-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.device-name {
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
.device-status {
|
||||||
|
font-size: 24px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.device-status.online {
|
||||||
|
color: #16a34a;
|
||||||
|
background-color: #f0fdf4;
|
||||||
|
}
|
||||||
|
.device-status.offline {
|
||||||
|
color: #9ca3af;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
.device-id {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
212
software/h5-uniapp/src/pages/home/index.vue
Normal file
212
software/h5-uniapp/src/pages/home/index.vue
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
<template>
|
||||||
|
<view class="home-page">
|
||||||
|
<!-- Welcome -->
|
||||||
|
<view class="welcome">
|
||||||
|
<text class="greeting">{{ user?.name ? `${user.name},你好` : '你好' }}</text>
|
||||||
|
<text class="desc">欢迎使用安知充充电柜</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Stats Grid -->
|
||||||
|
<view class="stats-grid">
|
||||||
|
<view class="stat-card">
|
||||||
|
<text class="stat-value blue">{{ data?.online_cabinets ?? 0 }}</text>
|
||||||
|
<text class="stat-label">在线设备</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-card">
|
||||||
|
<text class="stat-value green">{{ data?.charging_count ?? 0 }}</text>
|
||||||
|
<text class="stat-label">充电中</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-card">
|
||||||
|
<text class="stat-value yellow">{{ data?.idle_channels ?? 0 }}</text>
|
||||||
|
<text class="stat-label">空闲仓体</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-card">
|
||||||
|
<text class="stat-value" :class="data?.fault_channels ? 'red' : 'gray'">
|
||||||
|
{{ data?.fault_channels ?? 0 }}
|
||||||
|
</text>
|
||||||
|
<text class="stat-label">故障</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Today summary -->
|
||||||
|
<view class="card">
|
||||||
|
<text class="card-title">今日概况</text>
|
||||||
|
<view class="today-row">
|
||||||
|
<view class="today-item">
|
||||||
|
<text class="today-value">{{ data?.today_charge_count ?? 0 }}</text>
|
||||||
|
<text class="today-label">充电次数</text>
|
||||||
|
</view>
|
||||||
|
<view class="today-item">
|
||||||
|
<text class="today-value">{{ data?.today_energy_kwh?.toFixed(1) ?? '0.0' }}</text>
|
||||||
|
<text class="today-label">充电量(kWh)</text>
|
||||||
|
</view>
|
||||||
|
<view class="today-item">
|
||||||
|
<text class="today-value">{{ data?.today_charge_hours?.toFixed(1) ?? '0.0' }}</text>
|
||||||
|
<text class="today-label">时长(h)</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Fault alerts -->
|
||||||
|
<view v-if="data?.alerts && data.alerts.length > 0" class="card">
|
||||||
|
<text class="card-title red-text">故障告警</text>
|
||||||
|
<view v-for="(alert, i) in data.alerts.slice(0, 5)" :key="i" class="alert-item">
|
||||||
|
<text class="alert-text">{{ alert.cabinet }} — 通道 {{ alert.channel }}</text>
|
||||||
|
<text class="alert-project">{{ alert.project }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<view class="card">
|
||||||
|
<text class="card-title">快捷操作</text>
|
||||||
|
<view class="action-item" @click="goDevices">
|
||||||
|
<text class="action-text">查看设备</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-item" @click="goMe">
|
||||||
|
<text class="action-text">个人中心</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
import { h5Api, type DashboardData } from '../../api'
|
||||||
|
import { useAuthStore } from '../../store/auth'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const user = authStore.user
|
||||||
|
const data = ref<DashboardData | null>(null)
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (!authStore.isAuthenticated) {
|
||||||
|
uni.navigateTo({ url: '/pages/login/index' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fetchDashboard()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchDashboard() {
|
||||||
|
try {
|
||||||
|
const res = await h5Api.getDashboard()
|
||||||
|
data.value = res.data
|
||||||
|
} catch {
|
||||||
|
// 静默失败,页面显示零值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goDevices() {
|
||||||
|
uni.switchTab({ url: '/pages/devices/index' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goMe() {
|
||||||
|
uni.switchTab({ url: '/pages/me/index' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-page {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.welcome {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.greeting {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
font-size: 26px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.stats-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
width: calc(50% - 8px);
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.stat-value.blue { color: #2563eb; }
|
||||||
|
.stat-value.green { color: #16a34a; }
|
||||||
|
.stat-value.yellow { color: #f59e0b; }
|
||||||
|
.stat-value.red { color: #dc2626; }
|
||||||
|
.stat-value.gray { color: #6b7280; }
|
||||||
|
.stat-label {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.red-text { color: #dc2626; }
|
||||||
|
.today-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.today-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.today-value {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.today-label {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
.alert-item {
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
.alert-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.alert-text {
|
||||||
|
font-size: 26px;
|
||||||
|
color: #111;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.alert-project {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
.action-item {
|
||||||
|
padding: 20px 0;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
.action-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.action-text {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
150
software/h5-uniapp/src/pages/login/index.vue
Normal file
150
software/h5-uniapp/src/pages/login/index.vue
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<view class="login-page">
|
||||||
|
<view class="header">
|
||||||
|
<text class="title">安知充</text>
|
||||||
|
<text class="subtitle">智能充电柜管理系统</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form">
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">手机号</text>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
type="number"
|
||||||
|
:maxlength="11"
|
||||||
|
v-model="phone"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="field">
|
||||||
|
<text class="label">密码</text>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
type="password"
|
||||||
|
v-model="password"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text v-if="error" class="error">{{ error }}</text>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn-login"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="handleLogin"
|
||||||
|
>
|
||||||
|
{{ loading ? '登录中...' : '登录' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { useAuthStore } from '../../store/auth'
|
||||||
|
import { h5Api } from '../../api'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const phone = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const error = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const abstractId = ref('')
|
||||||
|
|
||||||
|
onLoad((query) => {
|
||||||
|
if (query?.abstract_id) {
|
||||||
|
abstractId.value = query.abstract_id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
if (!phone.value || !password.value) {
|
||||||
|
error.value = '请输入手机号和密码'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
error.value = ''
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await authStore.login(phone.value, password.value)
|
||||||
|
if (abstractId.value) {
|
||||||
|
try {
|
||||||
|
const res = await h5Api.getCabinetByAbstract(abstractId.value)
|
||||||
|
uni.navigateTo({ url: `/pages/device-detail/index?id=${res.data.id}` })
|
||||||
|
} catch {
|
||||||
|
uni.switchTab({ url: '/pages/home/index' })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uni.switchTab({ url: '/pages/home/index' })
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.message || '登录失败'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
padding: 80px 48px 40px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 56px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.form {
|
||||||
|
padding: 0 48px;
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font-size: 26px;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
height: 88px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0 24px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 26px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.btn-login {
|
||||||
|
height: 88px;
|
||||||
|
background-color: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.btn-login[disabled] {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
126
software/h5-uniapp/src/pages/me/index.vue
Normal file
126
software/h5-uniapp/src/pages/me/index.vue
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<view class="me-page">
|
||||||
|
<!-- User info -->
|
||||||
|
<view class="user-card">
|
||||||
|
<view class="avatar">
|
||||||
|
<text class="avatar-text">{{ (user?.name || user?.phone || '?').charAt(0) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="user-info">
|
||||||
|
<text class="user-name">{{ user?.name || '用户' }}</text>
|
||||||
|
<text class="user-phone">{{ user?.phone }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Menu items -->
|
||||||
|
<view class="menu-card">
|
||||||
|
<view class="menu-item" @click="showTip">
|
||||||
|
<text class="menu-text">修改密码</text>
|
||||||
|
<text class="menu-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Logout -->
|
||||||
|
<view class="logout-wrap">
|
||||||
|
<button class="btn-logout" @click="confirmLogout">退出登录</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAuthStore } from '../../store/auth'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const user = authStore.user
|
||||||
|
|
||||||
|
function showTip() {
|
||||||
|
uni.showToast({ title: '功能开发中', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmLogout() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '退出登录',
|
||||||
|
content: '确定要退出当前账号?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
authStore.logout()
|
||||||
|
uni.reLaunch({ url: '/pages/login/index' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.me-page {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.user-card {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #dbeafe;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.avatar-text {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.user-name {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.user-phone {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
.menu-card {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.menu-text {
|
||||||
|
font-size: 26px;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
.menu-arrow {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
.logout-wrap {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
.btn-logout {
|
||||||
|
height: 88px;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #dc2626;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
BIN
software/h5-uniapp/src/static/tabbar/device-active.png
Normal file
BIN
software/h5-uniapp/src/static/tabbar/device-active.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 B |
BIN
software/h5-uniapp/src/static/tabbar/device.png
Normal file
BIN
software/h5-uniapp/src/static/tabbar/device.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 B |
BIN
software/h5-uniapp/src/static/tabbar/home-active.png
Normal file
BIN
software/h5-uniapp/src/static/tabbar/home-active.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 B |
BIN
software/h5-uniapp/src/static/tabbar/home.png
Normal file
BIN
software/h5-uniapp/src/static/tabbar/home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 B |
BIN
software/h5-uniapp/src/static/tabbar/me-active.png
Normal file
BIN
software/h5-uniapp/src/static/tabbar/me-active.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 B |
BIN
software/h5-uniapp/src/static/tabbar/me.png
Normal file
BIN
software/h5-uniapp/src/static/tabbar/me.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 B |
43
software/h5-uniapp/src/store/auth.ts
Normal file
43
software/h5-uniapp/src/store/auth.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { h5Api, type H5User } from '../api'
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
const token = ref<string | null>(null)
|
||||||
|
const user = ref<H5User | null>(null)
|
||||||
|
const isAuthenticated = ref(false)
|
||||||
|
|
||||||
|
async function login(phone: string, password: string) {
|
||||||
|
const res = await h5Api.login({ phone, password })
|
||||||
|
const { token: t, user: u } = res.data
|
||||||
|
uni.setStorageSync('h5_token', t)
|
||||||
|
uni.setStorageSync('h5_user', JSON.stringify(u))
|
||||||
|
token.value = t
|
||||||
|
user.value = u
|
||||||
|
isAuthenticated.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
uni.removeStorageSync('h5_token')
|
||||||
|
uni.removeStorageSync('h5_user')
|
||||||
|
token.value = null
|
||||||
|
user.value = null
|
||||||
|
isAuthenticated.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function restore() {
|
||||||
|
try {
|
||||||
|
const t = uni.getStorageSync('h5_token')
|
||||||
|
const raw = uni.getStorageSync('h5_user')
|
||||||
|
if (t) {
|
||||||
|
token.value = t
|
||||||
|
user.value = raw ? JSON.parse(raw) : null
|
||||||
|
isAuthenticated.value = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// storage 读取失败,忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { token, user, isAuthenticated, login, logout, restore }
|
||||||
|
})
|
||||||
18
software/h5-uniapp/tsconfig.json
Normal file
18
software/h5-uniapp/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "esnext",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"lib": ["esnext", "dom"],
|
||||||
|
"types": ["@dcloudio/types"],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
11
software/h5-uniapp/vite.config.ts
Normal file
11
software/h5-uniapp/vite.config.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import uni from '@dcloudio/vite-plugin-uni'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [uni()],
|
||||||
|
base: '/h5/',
|
||||||
|
build: {
|
||||||
|
outDir: 'dist/build/h5',
|
||||||
|
assetsDir: 'static'
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user