feat: 组件与页面

This commit is contained in:
Tony 2024-03-19 14:25:34 +08:00
parent f472ee455d
commit 3b8bb32574
31 changed files with 420 additions and 8 deletions

View File

@ -53,7 +53,9 @@
"@tarojs/runtime": "3.6.23",
"@tarojs/shared": "3.6.23",
"@tarojs/taro": "3.6.23",
"axios": "^1.6.8",
"clsx": "^2.1.0",
"qs": "^6.12.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},

View File

@ -14,7 +14,8 @@
"ignore": [],
"disablePlugins": [],
"outputPath": ""
}
},
"packNpmRelationList": []
},
"compileType": "miniprogram",
"libVersion": "2.30.3",

View File

@ -2,6 +2,7 @@ export default defineAppConfig({
pages: [
// 首页y以及首页tab页内容
'pages/index/index', // 首页
'pages/login/index', // 登录页
// 被其他页面引用的组件不要写在这里
// 'pages/iot/index', // 物联网
@ -9,12 +10,14 @@ export default defineAppConfig({
// 'pages/equipment/index', // 巡检设备
// 跳转页面
'pages/weatherStation/index', // 气象站
'pages/msgDetail/index', // 消息详情
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#0fc87c',
navigationBarTitleText: 'WeChat',
navigationBarTextStyle: 'white'
navigationBarTextStyle: 'white',
navigationStyle: 'custom'
}
})

BIN
src/assets/images/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,6 @@
export {default as icon1} from './icon1.png';
export {default as icon2} from './icon2.png';
export {default as icon3} from './icon3.png';
export {default as icon4} from './icon4.png';
export {default as icon5} from './icon5.png';
export {default as icon6} from './icon6.png';

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 B

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 580 B

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 B

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 B

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 B

After

Width:  |  Height:  |  Size: 537 B

View File

@ -0,0 +1,20 @@
import { Image, View } from "@tarojs/components"
import Taro from "@tarojs/taro";
import arrow from '../../../assets/images/icons/back.png'
const HeaderNation = ({ title = '' }) => {
const showBack = Taro.getCurrentPages().length > 1
return (
<View className="w-full h-[180rpx] flex items-center justify-between pt-[90rpx] px-3">
<View className="w-8 flex justify-center" onClick={() => {
Taro.navigateBack()
}}>
{ showBack ? <Image src={arrow} className="w-6 h-6"></Image> : null }
</View>
<View className=" from-neutral-100 text-base text-center" style={{ color: 'white' }}>{ title }</View>
<View className="w-8"></View>
</View>
)
}
export default HeaderNation;

View File

@ -0,0 +1,6 @@
export default {
'401': '认证失败,无法访问系统资源',
'403': '当前操作没有权限',
'404': '访问资源不存在',
default: '系统未知错误,请反馈给管理员'
}

View File

109
src/config/axios/service.ts Normal file
View File

@ -0,0 +1,109 @@
import axios, { AxiosError, AxiosInstance, AxiosRequestHeaders, AxiosResponse, InternalAxiosRequestConfig } from "axios";
import { axiosConfig } from "..";
import Taro from "@tarojs/taro";
import qs from 'qs'
import errorCode from './errorCode'
// 请求白名单无须token的接口
const whiteList: string[] = ['/login', '/refresh-token']
// 是否正在刷新中
let isRefreshToken = false
// 请求队列
let requestList: any[] = []
// 创建实例
const service: AxiosInstance = axios.create({
baseURL: axiosConfig.baseUrl,
timeout: axiosConfig.timeout,
withCredentials: axiosConfig.withCredentials
});
const getAccessToken = () => {
return Taro.getStorageSync('token')
}
// request拦截器
service.interceptors.request.use((config: InternalAxiosRequestConfig) => {
// 是否需要设置 token
let isToken = (config!.headers || {}).isToken === false
whiteList.some((v) => {
if (config.url) {
config.url.indexOf(v) > -1
return (isToken = false)
}
})
if (getAccessToken() && !isToken) {
; (config as any).headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token
}
const params = config.params || {}
const data = config.data || false
if (
config.method?.toUpperCase() === 'POST' &&
(config.headers as AxiosRequestHeaders)['Content-Type'] ===
'application/x-www-form-urlencoded'
) { config.data = qs.stringify(data) }
// get参数编码
if (config.method?.toUpperCase() === 'GET' && params) {
config.params = {}
const paramsStr = qs.stringify(params, { allowDots: true })
if (paramsStr) {
config.url = config.url + '?' + paramsStr
}
}
return config
}, (error: AxiosError) => {
Promise.reject(error)
})
// response 拦截器
service.interceptors.response.use(
async (response: AxiosResponse<any>) => {
let { data } = response
const config = response.config
if (!data) { throw new Error() }
const code = data.code || 200 // 默认成功
// 获取错误信息
const msg = data.msg || errorCode[code] || errorCode['default']
if (code === 401) {
// 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
// isRefreshToken false 未开始刷新token 进入刷新token程序
if (!isRefreshToken) {
isRefreshToken = true
try {
// 使用默认的用户名密码进行登录操作
const token = 'request login'
Taro.setStorageSync('token', token)
config.headers!.Authorization = 'Bearer ' + getAccessToken()
requestList.forEach((cb: any) => { cb() })
requestList = []
return service(config)
} catch (e) {
// 刷新失败时 处理错误
requestList = [] // 清空队列
return Promise.reject()
} finally {
requestList = []
isRefreshToken = false
}
} else {
// 正在尝试刷新token放入等待队列
return new Promise((resolve) => {
requestList.push(() => {
config.headers!.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改
resolve(service(config))
})
})
}
} else if (code === 500) {
return Promise.reject(new Error(msg))
} else {
return data
}
}
)

5
src/config/index.ts Normal file
View File

@ -0,0 +1,5 @@
export const axiosConfig = {
baseUrl: '',
timeout: 30000,
withCredentials: false // 禁用 Cookie 等信息
}

View File

@ -2,6 +2,7 @@ import { Image, View } from "@tarojs/components";
import "./index.scss";
import { useState } from "react";
import Iot from "../iot";
import basicBg from '../../assets/images/bg.png'
import MsgCenter from "../msgCenter";
import EquipMent from "../equipment";
import tab1Icon from '../../assets/images/tab/tab1.png'
@ -12,10 +13,23 @@ import tab3Icon from '../../assets/images/tab/tab3.png'
import tab3Icon_selected from '../../assets/images/tab/tab3_selected.png'
import Taro from "@tarojs/taro";
import HeaderNation from "../../components/customized/navigation";
const Index = () => {
const [curSelectedItem, setCurSelectedItem] = useState('iot')
Taro.setNavigationBarTitle({ title: '物联网' })
// 判断权限,没有权限进入登录页面
const handleAuth = () => {
const token = Taro.getStorageSync('token')
if (token) {
// Taro.redirectTo({ url: '/pages/login/index' })
} else {
Taro.redirectTo({ url: '/pages/login/index' })
}
}
handleAuth()
const bottomMenuList = [
{
id: 'iot',
@ -41,7 +55,10 @@ const Index = () => {
]
return (
<View className="h-[100vh] flex flex-col-reverse">
<View
className="h-[100vh] flex flex-col-reverse"
style={{ 'backgroundImage': `url(${basicBg})`, 'backgroundSize': '100% auto', 'backgroundRepeat': 'no-repeat' }}
>
<View className="flex justify-evenly h-[6rem] align-top">
{
bottomMenuList.map(item => {
@ -79,6 +96,15 @@ const Index = () => {
: null
}
</View>
<HeaderNation title={
curSelectedItem === 'iot'
? '物联网'
: curSelectedItem === 'msg'
? '消息中心'
: curSelectedItem === 'equip'
? '巡检设备'
: ''
} />
</View>
);
};

View File

@ -4,6 +4,7 @@ import onlineIcon from '../../assets/images/icons/online.png'
import offlineIcon from '../../assets/images/icons/offline.png'
import errorIcon from '../../assets/images/icons/error.png'
import img from '../../assets/images/img.png'
import Taro from "@tarojs/taro"
const Iot = () => {
const [selectedMenuId, setSelectedMenuId] = useState('all')
@ -40,24 +41,30 @@ const Iot = () => {
return (
<View className="p-1 h-full">
<View className="h-full flex flex-col">
<View className="flex px-2 py-1 border-b-2 border-slate-100">
<View className="flex px-2 py-1">
{
topMenus.map(item => {
return (
<View
className={`p-1 px-3 ${selectedMenuId === item.id ? 'text-lime-600 border-b-4 border-lime-800' : ''}`}
className={`p-1 px-3 ${selectedMenuId === item.id ? 'border-b-4 border-lime-800' : ''}`}
key={item.id}
style={{ color: 'white' }}
onClick={() => setSelectedMenuId(item.id)}
>{ item.title }</View>
)
})
}
</View>
<View className="h-full border flex flex-col space-y-2 p-2 rounded-sm border-slate-50">
<View className="h-full flex flex-col space-y-3 p-2 rounded-sm">
{
dataList.map(item => {
return (
<View className="border border-slate-300 px-4 py-2 rounded-md shadow-lg flex space-x-4">
<View
className="px-4 py-2 rounded-md shadow-xl flex space-x-4 bg-white border border-slate-200"
onClick={() => {
Taro.navigateTo({ url: '/pages/weatherStation/index?id=' + item.id })
}}
>
<View className="w-12 h-12 relative top-1">
<Image src={img} className="w-12 h-12"></Image>
</View>

View File

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '登录'
})

76
src/pages/login/index.tsx Normal file
View File

@ -0,0 +1,76 @@
import { Button, Image, Input, Text, View } from "@tarojs/components"
import loginBg from '../../assets/images/loginBg.png'
import questionIcon from '../../assets/images/icons/question-line.png'
import Taro from "@tarojs/taro"
import { useState } from "react"
const Login = () => {
const [username, setUserName] = useState('')
const [password, setPassWord] = useState('')
const handleLogin = () => {
if (!username || !password) return
Taro.setStorageSync('username', username)
Taro.setStorageSync('password', password)
// 请求完成后跳转
Taro.showLoading({ title: '加载中...'})
setTimeout(() => {
Taro.setStorageSync('token', 'testToken')
Taro.hideLoading()
Taro.redirectTo({ url: '/pages/index/index' })
}, 2000)
}
return (
<View className="w-full h-[100vh] relative">
<Image src={loginBg} className="w-full h-full" />
<View className="absolute left-0 top-20 pt-3">
<View className="text-slate-800 font-bold text-2xl px-6 pt-4">
</View>
<View className="text-slate-800 font-bold text-2xl px-6 pt-3">
</View>
<View className="w-[100vw] mt-7 p-3 bg-white rounded-md">
<View className="p-2">
<View></View>
<View className=" border-b-2 border-slate-400 py-2">
<Input
type="text"
placeholder="请输入用户名"
onInput={(_username) => setUserName((_username.target as any).value)}
></Input>
</View>
</View>
<View className="p-2">
<View></View>
<View className=" border-b-2 border-slate-400 py-2">
<Input
type="text"
placeholder="请输入密码"
onInput={(_password) => setPassWord((_password.target as any).value)}
></Input>
</View>
</View>
<View className="flex flex-row-reverse p-1">
<View className="flex items-center">
<Image src={questionIcon} className="w-[1.1rem] h-[1.1rem]"></Image>
<Text className="text-[1rem] p-1" style={{ color: '#0fc87c' }}>?</Text>
</View>
</View>
<View className="p-2">
<Button
className="rounded-full"
style={{
background: 'linear-gradient(to right, #19b3c2, #0fc87e)',
color: 'white'
}}
onClick={() => handleLogin()}
></Button>
</View>
</View>
</View>
</View>
)
}
export default Login;

View File

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '气象站'
})

View File

@ -0,0 +1,142 @@
import { Image, View } from "@tarojs/components"
import Taro from "@tarojs/taro";
import { useState } from "react";
import img from '../../assets/images/img.png'
import basicBg from '../../assets/images/bg.png'
import HeaderNation from "../../components/customized/navigation";
import { icon1, icon2, icon3, icon4 } from "../../assets/images/icons/data";
const SubTitle = ({title = '', updateTime = ''}) => {
return (
<View className="w-full flex justify-between text-[.8rem] p-2 pb-1">
<View className=" font-bold">{ title }</View>
<View style={{ color: '#999999' }}>{ updateTime !== '' ? '数据更新于' + updateTime : '' }</View>
</View>
)
}
const IconLabelValue = ({ value, unit, label, img }) => {
return (
<View className="flex items-center justify-center">
<View className="flex flex-col items-start px-3 w-[calc(90% - 2.5rem)]">
<View className="flex items-end pb-2">
<View className="text-[1rem] font-bold text-primary">{ value }</View>
<View
className="text-slate-600 text-[.7rem] px-2"
style={{ 'color': '#252525A5' }}
>{ unit }</View>
</View>
<View className="text-[.7rem]">{ label }</View>
</View>
<View className="w-10 h-10 rounded-sm shadow-sm">
<Image src={img} className="w-full h-full"></Image>
</View>
</View>
)
}
const GridContainer = ({ children }) => {
return (
<View className="border border-slate-200 px-4 py-3 rounded-md shadow-xl grid grid-cols-2">
{ children }
</View>
)
}
const BasicStatus = (status = true) => {
return (
<View
style={{ backgroundColor: status ? '#0fc87c' : '#ff0000', 'color': 'white' }}
className={ 'text-[.7rem] px-[.4rem] py-[.1rem] ' + (status ? 'rounded-sm text-white' : '') }
>{ status ? '在线' : '离线' }</View>
)
}
const BasicInfoCard = ({ title, info, status, imgSrc }) => {
return (
<View
className="bg-white px-4 py-3 rounded-md shadow-xl flex space-x-4"
>
<View className="w-16 h-14">
<Image src={imgSrc} className="w-16 h-14"></Image>
</View>
<View className="w-full">
<View className="font-bold text-[.9rem]">{ title }</View>
<View className="text-[.8rem] text-slate-600 pb-2">{ info }</View>
<View className="flex">{ status ? BasicStatus() : '' }</View>
</View>
</View>
)
}
const WeatherStation = () => {
const baseId = Taro.getCurrentInstance().router?.params.id || 0; // 基地id
const [curDeviceId, setCurDeviceId] = useState('1')
const deviceList = [
{
id: '1',
title: '设备1'
},
{
id: '2',
title: '设备2'
},
{
id: '3',
title: '设备3'
},
]
const [data, setData] = useState([
{ genre: 'Sports', sold: 275 },
{ genre: 'Strategy', sold: 115 },
{ genre: 'Action', sold: 120 },
{ genre: 'Shooter', sold: 350 },
{ genre: 'Other', sold: 150 },
])
return (
<View className="h-[100vh] flex flex-col" style={{ 'backgroundImage': `url(${basicBg})`, 'backgroundSize': '100% auto', 'backgroundRepeat': 'no-repeat' }}>
<HeaderNation title="气象站" />
<View className="h-full flex flex-col px-1">
<View className="flex px-2 py-1">
{
deviceList.map(item => {
return (
<View
className={`p-1 px-3 ${curDeviceId === item.id ? 'text-lime-600 border-b-4 border-lime-800' : ''}`}
key={item.id}
style={{ color: 'white' }}
onClick={() => setCurDeviceId(item.id)}
>{ item.title }</View>
)
})
}
</View>
<View className="h-full flex flex-col space-y-2 p-2 rounded-sm">
<BasicInfoCard imgSrc={img} title={'大棚环境监测一体机'} info={'BNYKLH09LK001'} status={true} />
<SubTitle title="实时数据" updateTime="2023-04-04" />
<GridContainer>
<View className="p-2 py-3">
<IconLabelValue value={27.34} label={'大气温度'} unit={'℃'} img={icon1} />
</View>
<View className="p-2 py-3">
<IconLabelValue value={27.34} label={'大气温度'} unit={'℃'} img={icon2} />
</View>
<View className="p-2 py-3">
<IconLabelValue value={27.34} label={'大气温度'} unit={'℃'} img={icon3} />
</View>
<View className="p-2 py-3">
<IconLabelValue value={27.34} label={'大气温度'} unit={'℃'} img={icon4} />
</View>
</GridContainer>
<SubTitle title="数据监测" />
<View className="border border-slate-200 px-4 py-3 rounded-md shadow-xl">
</View>
</View>
</View>
</View>
)
}
export default WeatherStation;

View File

@ -2,7 +2,10 @@
module.exports = {
content: ["./src/**/*.{html,js,ts,jsx,tsx}"],
theme: {
extend: {}
extend: {},
textColor: {
'primary': '#0fc87c'
}
},
plugins: [],
// v3 版本的 tailwindcss 有些不同