feat: 登录流程完成接口联调

main
donghao 2 months ago
parent 8018774a29
commit bb0b06533a

@ -1,4 +1,12 @@
###
# @Author: donghao donghao@supervision.ltd
# @Date: 2025-03-12 15:26:57
# @LastEditors: donghao donghao@supervision.ltd
# @LastEditTime: 2025-03-13 09:17:19
# @FilePath: \5G-Loading-Bay-Web\.env.development
# @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
###
# .env.development
NODE_ENV = development
VITE_APP_ENV = development
VITE_APP_BASE_API = http://192.168.10.14:8888
VITE_APP_BASE_API = http://192.168.10.14:8000

@ -1 +1,20 @@
// 接口层
/*
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-03-07 15:09:18
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-03-13 10:13:49
* @FilePath: \5G-Loading-Bay-Web\src\api\dashboard.ts
* @Description: ,`customMade`, koroFileHeader : https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
// 接口层
import request from "@/utils/request/instance";
import { config } from "@/config";
//TODO 定义响应类型
export const getDeviceStatusApi = (params: any) => {
return request.get(`/api/v1/device/device/`, params);
};

@ -2,7 +2,7 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-03-12 15:13:38
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-03-12 19:20:43
* @LastEditTime: 2025-03-13 09:29:20
* @FilePath: \5G-Loading-Bay-Web\src\api\user.ts
* @Description: ,`customMade`, koroFileHeader : https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
@ -19,11 +19,16 @@ interface LoginRes {
}
export const loginApi = (data: { username: string; password: string }) => {
return request.post<LoginRes>(`${config.baseURL}/api/v1/user/login`, data, {
return request.post<LoginRes>(`/api/v1/user/login/`, data, {
showLoading: false // 单独关闭loading
})
}
export const loginOutApi = () => {
return request.post(`/api/v1/user/logout/`, {}, {
showLoading: false // 单独关闭loading
})
}
// export const getUserInfo = (userId: number) => {
// return request.get(`/user/info/${userId}`)
// }

@ -2,12 +2,13 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-03-06 15:42:11
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-03-11 15:42:37
* @LastEditTime: 2025-03-13 10:49:12
* @FilePath: \vite-ai\data-dashboard\src\components\Navbar.vue
* @Description: 标题栏
-->
<script setup lang="ts">
// import { useNav } from "@/layout/hooks/useNav";
// import router, { resetRouter } from "@/router";
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
@ -46,9 +47,9 @@ function getTime() {
_formatNum(nowDate.getHours()) +
":" +
_formatNum(nowDate.getMinutes())
// +
// ":" +
// _formatNum(nowDate.getSeconds());
// +
// ":" +
// _formatNum(nowDate.getSeconds());
let week = "";
switch (nowDate.getDay()) {
case 0:
@ -122,6 +123,7 @@ onMounted(() => {
.left {
width: 30vw;
}
.center_title {
// margin-top: 19px;
background: url("@/assets/common/nav_title.png") no-repeat;
@ -131,13 +133,16 @@ onMounted(() => {
text-align: center;
font-size: 32px;
}
.right {
width: 30vw;
.date_box {
color: #009dff;
font-size: 16px;
margin-right: 24px;
font-family: DingTalk JinBuTi;
&>span {
padding-left: 5px;
}

@ -2,13 +2,17 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-03-06 17:57:05
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-03-12 15:20:34
* @LastEditTime: 2025-03-13 10:49:15
* @FilePath: \5G-Loading-Bay-Web\src\stores\user.ts
* @Description: ,`customMade`, koroFileHeader : https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import { defineStore } from "pinia";
import router, { resetRouter } from "@/router";
import { getLocal, setLocal, removeLocal} from "@/utils/local";
import { getLocal, setLocal, removeLocal } from "@/utils/local";
import { loginOutApi } from "@/api/user";
import { isSuccessApi } from "@/utils/forApi";
import { ElMessage } from 'element-plus'
interface UserState {
token: string | null;
}
@ -23,16 +27,20 @@ export const useUserStore = defineStore("user", {
form: { remember: boolean; username: string; password: string }
) {
this.token = token;
setLocal("token", token)
setLocal("token", token);
form.remember
? setLocal("userLoginInfo", form)
: removeLocal("userLoginInfo");
},
logout() {
async logout() {
const res = await loginOutApi();
if(isSuccessApi(res)){
ElMessage.success(`退出登录`)
}
this.token = null;
removeLocal("token");
// resetRouter();
// router.replace("/login");
resetRouter();
router.replace("/login");
},
},
});

@ -0,0 +1,19 @@
/*
* @Author: donghao donghao@supervision.ltd
* @Date: 2024-08-14 14:42:09
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-03-13 10:20:54
* @FilePath: \General-AI-Platform-Web-Client\src\utils\forApi.ts
* @Description:
*/
/**
* @
* @param result
* @returns boolean
*/
export function isSuccessApi(result): boolean {
if ([200].includes(result.code)) {
return true;
}
return false;
}

@ -2,106 +2,113 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-03-12 15:11:56
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-03-12 15:12:06
* @LastEditTime: 2025-03-13 10:48:58
* @FilePath: \5G-Loading-Bay-Web\src\utils\request\index.ts
* @Description: ,`customMade`, koroFileHeader : https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { RequestConfig, RequestInterceptors } from './type'
import { useUserStore } from '@/stores/user'
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import { ElMessage, ElMessageBox } from "element-plus";
import type { RequestConfig, RequestInterceptors } from "./type";
import { useUserStore } from "@/stores/user";
class Request {
instance: AxiosInstance
interceptors?: RequestInterceptors
instance: AxiosInstance;
interceptors?: RequestInterceptors;
constructor(config: RequestConfig) {
this.instance = axios.create(config)
this.interceptors = config.interceptors
this.instance = axios.create(config);
this.interceptors = config.interceptors;
// 全局请求拦截器
this.instance.interceptors.request.use(
(config: RequestConfig) => {
// 处理 token
const userStore = useUserStore()
const userStore = useUserStore();
if (userStore.token) {
config.headers!.Authorization = `Bearer ${userStore.token}`
config.headers!.Access = `Bearer ${userStore.token}`;
}
return config
return config;
},
(error: any) => Promise.reject(error)
)
);
// 实例拦截器
this.instance.interceptors.request.use(
this.interceptors?.requestInterceptor,
this.interceptors?.requestInterceptorCatch
)
);
this.instance.interceptors.response.use(
this.interceptors?.responseInterceptor,
this.interceptors?.responseInterceptorCatch
)
);
// 全局响应拦截器
this.instance.interceptors.response.use(
(res: AxiosResponse) => {
const { code, message } = res.data
const { code, message } = res.data;
if (code !== 200) {
ElMessage.error(message || '请求失败')
return Promise.reject(message)
ElMessage.error(message || "请求失败");
return Promise.reject(message);
}
return res.data
return res.data;
},
(error: any) => {
// 处理 HTTP 状态码
if (error.response?.status === 401) {
ElMessageBox.confirm('登录已过期,请重新登录', '提示', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
ElMessageBox.confirm("登录已过期,请重新登录", "提示", {
confirmButtonText: "重新登录",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
const userStore = useUserStore()
userStore.logout()
location.reload()
})
const userStore = useUserStore();
userStore.logout();
});
}
ElMessage.error(error.message || '请求错误')
return Promise.reject(error)
ElMessage.error(error.message || "请求错误");
return Promise.reject(error);
}
)
);
}
request<T = any>(config: RequestConfig<T>): Promise<T> {
return new Promise((resolve, reject) => {
// 单个请求的拦截器
if (config.interceptors?.requestInterceptor) {
config = config.interceptors.requestInterceptor(config)
config = config.interceptors.requestInterceptor(config);
}
this.instance
.request<any, T>(config)
.then(res => {
.then((res) => {
if (config.interceptors?.responseInterceptor) {
res = config.interceptors.responseInterceptor(res)
res = config.interceptors.responseInterceptor(res);
}
resolve(res)
})
.catch(err => {
reject(err)
resolve(res);
})
})
.catch((err) => {
reject(err);
});
});
}
get<T = any>(url: string, config?: RequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: 'GET', url })
get<T = any>(
url: string,
params?: any,
config?: RequestConfig<T>
): Promise<T> {
return this.request<T>({ ...config, method: "GET", url, params });
}
post<T = any>(url: string, data?: any, config?: RequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: 'POST', url, data })
post<T = any>(
url: string,
data?: any,
config?: RequestConfig<T>
): Promise<T> {
return this.request<T>({ ...config, method: "POST", url, data });
}
// 其他方法类似...
}
export default Request
export default Request;

@ -10,8 +10,7 @@
</div>
<div class="px-[16px] device-status-content-box">
<div class="mt-[16px] bg-transparent baseTable_wrap full_table" v-loading="dataLoading"
:element-loading-svg="svg" element-loading-svg-view-box="-10, -10, 50, 50">
<div class="mt-[16px] bg-transparent baseTable_wrap full_table">
<template v-if="pagination.total > 0">
<BaseTable class="bg-transparent baseTable_box" :total="pagination.total"
:pageSize="pagination.pageSize" :dataSource="listData" :isFixedPagination="true"
@ -58,12 +57,13 @@ import RealVideoModal from './components/RealVideoModal.vue';
import onLineIcon from '@/assets/common/online_icon.png';
import outLineIcon from '@/assets/common/outline_icon.png';
import errorIcon from '@/assets/common/error_icon.png';
import Demo from '../../components/videoPlayer/Demo.vue';
import { getDeviceStatusApi } from '@/api/dashboard';
import { isSuccessApi } from "@/utils/forApi";
defineOptions({
name: "DeviceStatusIndex"
});
const alarmLevelStatusEnum: Record<string, any>[] = [
{
const alarmLevelStatusEnum = {
'online': {
color: "#52C41A",
value: "1",
label: "在线",
@ -71,56 +71,56 @@ const alarmLevelStatusEnum: Record<string, any>[] = [
icon: onLineIcon,
id: "1"
},
{
'offline': {
color: "#999999",
value: "2",
label: "离线",
isDelete: false,
icon: outLineIcon,
id: "2"
},
{
'error': {
color: "#E80D0D",
value: "3",
label: "故障",
isDelete: false,
icon: errorIcon,
id: "3"
},
'notFound': { //
color: "#E80D0D",
value: "4444",
label: "未知",
isDelete: false,
icon: errorIcon,
id: "4444"
}
];
const deleteModel = reactive<{
isShowDelete: boolean;
}>({
isShowDelete: false
});
}
const currentRow = ref<Record<string, any>>({});
const isRealOpen = ref<Boolean>(false);
const isRealOpen = ref<Boolean>(false); //
const isHistoryOpen = ref<Boolean>(false); //
const columns = [
{
label: "设备名称",
property: "name"
property: "device_name"
},
{
label: "设备ID",
property: "code"
property: "device_number"
},
{
label: "设备位置",
property: "deviceGroup"
property: "device_position"
},
{
label: "设备状态",
property: "status",
property: "device_status",
formatter: val => {
console.log(val);
const currentLevelObj =
alarmLevelStatusEnum[Number(val?.status) - 1];
alarmLevelStatusEnum[val?.device_status] || alarmLevelStatusEnum.notFound;
return h(
"div",
@ -130,13 +130,12 @@ const columns = [
display: "flex",
alignItems: "center",
lineHeight: "20px",
color: currentLevelObj.color
color: currentLevelObj?.color
}
},
[
h('img', {
// 使 @ assets/common
src: currentLevelObj.icon,
src: currentLevelObj?.icon,
style: {
width: '20px',
height: '20px',
@ -148,7 +147,7 @@ const columns = [
{
fontSize: "14px",
},
currentLevelObj.label
currentLevelObj?.label
)
]
);
@ -160,40 +159,27 @@ const columns = [
label: "操作"
}
];
const svg = `
<path class="path" d="
M 30 15
L 28 17
M 25.61 25.61
A 15 15, 0, 0, 1, 15 30
A 15 15, 0, 1, 1, 27.99 7.5
L 15 15
" style="stroke-width: 4px; fill: rgba(0, 0, 0, 0)"/>
`;
const pagination = ref({ currentPage: 1, pageSize: 10, total: 0 });
const listData = ref([]);
const dataLoading = ref(true);
const getList = async () => {
const { currentPage, pageSize } = pagination.value;
const res = await fetch('/api/getDeviceStatusList', {
method: 'POST',
body: JSON.stringify({ page: currentPage, pageSize })
})
const { data } = await res.json()
console.log(data, 'getList_data')
listData.value = data.list;
console.log(data.list);
pagination.value = {
...pagination.value,
total: data.total
};
dataLoading.value = false;
};
try {
const { currentPage, pageSize } = pagination.value;
const res = await getDeviceStatusApi({ current: currentPage, pageSize })
console.log(res.data, 'getList_data')
if (isSuccessApi(res)) {
listData.value = res.data.data;
pagination.value = {
...pagination.value,
total: res.data.total
};
}
} catch (error) {
console.error('获取数据失败:', error)
}
}
function handleTableChange(record) {
console.log("handleTableChange_record", record);
pagination.value = {
@ -203,20 +189,20 @@ function handleTableChange(record) {
};
getList();
}
/**打开视频 */
/**打开实时视频 */
function openCurrent(row) {
console.log(row, "openCurrent");
currentRow.value = row;
isRealOpen.value = true;
}
/**打开历史视频 */
function openHistory(row) {
console.log(row, "openHistory");
currentRow.value = row;
// TODO
isHistoryOpen.value = true;
}
onMounted(() => {
getList();
});

@ -1,93 +0,0 @@
.pole-monitor-wrap {
background-image: url("@/assets/common/bg_banner_1.png");
background-size: cover;
background-position: bottom;
background-repeat: no-repeat;
height: 823px;
.search-section {
padding: 16px 0;
}
.pole-main-content {
width: 100%;
}
.pole-monitor-search-box {
display: flex;
align-items: center;
gap: 12px;
margin: 16px 0;
}
.right-panel {
.el-scrollbar__view {
background: transparent !important;
height: 600px;
}
}
.pole-monitor-main {
.left-panel {
.main-image {
box-sizing: border-box;
min-height: 511px;
position: relative;
background-color: #090F48;
border-radius: 4px;
img {
width: 100%;
max-height: 460px;
}
.image-info{
position: absolute;
height: 52px;
line-height: 52px;
bottom: 0;
font-size: 14px;
padding: 0 16px;
&> span {
margin-right: 10px;
}
}
}
.thumbnail-container {
width: 100%;
overflow: visible;
.swiper {
width: 100%;
height: 100%;
.swiper-slide {
width: 20%;
border-radius: 4px;
img {
width: 100%;
height: 144px;
border-radius: 4px;
}
}
.active-slide img {
border-radius: 4px;
border: 2px solid #2ecce0;
}
.swiper-button-prev,
.swiper-button-next {
background-color: rgba(0, 0, 0, 0.5);
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
}
.swiper-button-prev::after,
.swiper-button-next::after {
font-size: 12px;
color: #fff;
}
/* 修改按钮悬停样式 */
.swiper-button-prev:hover,
.swiper-button-next:hover {
background-color: rgba(0, 0, 0, 0.8);
}
}
}
}
}
}

@ -2,7 +2,7 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-03-06 17:57:05
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-03-12 19:39:47
* @LastEditTime: 2025-03-13 10:50:03
* @FilePath: \5G-Loading-Bay-Web\src\views\login\Login.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE?
-->
@ -49,54 +49,61 @@ import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import accountIconUrl from '@/assets/login/account_icon.png';
import passwordIconUrl from '@/assets/login/password_icon.png';
import { getLocal} from "@/utils/local";
import { getLocal } from "@/utils/local";
import { loginApi } from '@/api/user'
import { isSuccessApi } from "@/utils/forApi";
const router = useRouter()
const userStore = useUserStore()
const form = reactive({
username: '',
password: '',
username: 'admin',
password: 'admin',
remember: false
})
// TODO
// const handleLogin = async () => {
// try {
// const res = await loginApi({
// username: 'admin',
// password: 'admin123'
// })
// ElMessage.success(`${res.userInfo.username}`)
// } catch (error) {
// console.error(':', error)
// }
// }
const handleLogin = async () => {
if (form.username !== 'admin' || form.password !== 'admin123') {
ElMessage.error('用户名或密码错误')
return
}
try {
//
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(form)
const res = await loginApi({
username: form.username,
password: form.password
})
const data = await res.json()
if (data.code === 200) {
userStore.login(data.data.token, form)
console.log(res, 'handleLogin')
if (isSuccessApi(res)) {
ElMessage.success(`欢迎回来,${form.username}`)
userStore.login(res.data.access, form)
router.push('/dashboard')
}
} catch (err) {
} catch (error) {
ElMessage.error('登录失败')
console.error('登录失败:', error)
}
}
// mock
// const handleLogin = async () => {
// if (form.username !== 'admin' || form.password !== 'admin123') {
// ElMessage.error('')
// return
// }
// try {
// //
// const res = await fetch('/api/login', {
// method: 'POST',
// body: JSON.stringify(form)
// })
// const data = await res.json()
// if (data.code === 200) {
// userStore.login(data.data.token, form)
// router.push('/dashboard')
// }
// } catch (err) {
// ElMessage.error('')
// }
// }
onMounted(() => {
const loginInfoCache = getLocal('userLoginInfo')
if(loginInfoCache) {
if (loginInfoCache) {
form.username = loginInfoCache?.username
form.remember = loginInfoCache?.remember
form.password = loginInfoCache?.password

@ -1,196 +0,0 @@
<template>
<div class="login-container">
<div class="login-box">
<el-form
:model="form"
:rules="rules"
ref="formRef"
class="login-form"
>
<el-form-item prop="username">
<div class="input-group">
<img :src="accountIcon" alt="账号图标" class="input-icon">
<el-input
v-model="form.username"
placeholder="请输入您的账号"
class="custom-input"
/>
</div>
</el-form-item>
<el-form-item prop="password">
<div class="input-group">
<img :src="passwordIcon" alt="密码图标" class="input-icon">
<el-input
v-model="form.password"
placeholder="请输入登录密码"
type="password"
class="custom-input"
/>
</div>
</el-form-item>
<el-form-item>
<el-checkbox v-model="form.remember"></el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin" class="login-btn">
登录
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import accountIconUrl from '@/assets/icons/account-icon.png';
import passwordIconUrl from '@/assets/icons/password-icon.png';
//
const form = ref({
username: '',
password: '',
remember: false
});
//
const rules = ref({
username: [
{ required: true, message: '请输入账号', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 30, message: '长度在 6 到 30 个字符', trigger: 'blur' }
]
});
//
const accountIcon = ref(accountIconUrl);
const passwordIcon = ref(passwordIconUrl);
// mock
const mockLogin = (params: { username: string; password: string }) => {
return new Promise<{ success: boolean; message?: string }>((resolve) => {
// admin
if (params.username === 'admin' && params.password === 'admin') {
resolve({ success: true });
} else {
resolve({
success: false,
message: '账号或密码错误'
});
}
});
};
const handleLogin = async () => {
const formRef = getCurrentInstance()?.$refs.formRef as any;
await formRef.validate(async (valid) => {
if (valid) {
try {
const response = await mockLogin(form.value);
if (response.success) {
console.log('登录成功');
//
} else {
ElMessage.error(response.message || '登录失败');
}
} catch (error) {
ElMessage.error('登录请求异常');
}
}
});
};
</script>
<style scoped lang="scss">
/* 保留之前的样式代码,此处省略重复样式 */
.login-container {
height: 100vh;
background: #002a5c;
display: flex;
justify-content: center;
align-items: center;
background-image: url('https://via.placeholder.com/1920x1080?text=铁路货车背景图');
background-size: cover;
background-position: center;
}
.login-box {
background: rgba(10, 34, 74, 0.9);
padding: 40px 60px;
border-radius: 20px;
box-shadow: 0 0 30px rgba(58, 145, 255, 0.6);
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 2px solid rgba(58, 145, 255, 0.3);
border-radius: 20px;
}
}
.login-form {
.input-group {
display: flex;
align-items: center;
margin-bottom: 20px;
border: 1px solid rgba(58, 145, 255, 0.3);
border-radius: 10px;
overflow: hidden;
.input-icon {
width: 30px;
height: 30px;
background: rgba(58, 145, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.custom-input {
.el-input__inner {
background: transparent;
border: none;
color: white;
height: 50px;
&::placeholder {
color: rgba(255, 255, 255, 0.6);
}
}
}
}
.login-btn {
width: 100%;
background: linear-gradient(135deg, #42a5f5, #1976d2);
border: none;
box-shadow: 0 4px 12px rgba(58, 145, 255, 0.4);
color: white;
height: 50px;
border-radius: 10px;
font-size: 18px;
&:hover {
background: linear-gradient(135deg, #1976d2, #42a5f5);
}
}
.el-checkbox {
margin: 15px 0;
.el-checkbox__input.is-checked .el-checkbox__inner {
background-color: #42a5f5;
border-color: #42a5f5;
}
.el-checkbox__label {
color: white;
font-size: 16px;
}
}
}
</style>

@ -2,7 +2,7 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-03-06 11:27:03
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-03-12 19:38:36
* @LastEditTime: 2025-03-13 09:35:33
* @FilePath: \vite-ai\data-dashboard\vite.config.ts
* @Description: ,`customMade`, koroFileHeader : https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/

Loading…
Cancel
Save