Compare commits
No commits in common. "fb5ad57dc158c085df59656aa661185c18b375d6" and "12f41faa403a501ae40149698a8996aa3c6bbe6b" have entirely different histories.
fb5ad57dc1
...
12f41faa40
25
.env
25
.env
@ -1,25 +0,0 @@
|
||||
# 所有环境都会加载的配置
|
||||
|
||||
# 项目名称
|
||||
VITE_APP_TITLE=智慧湿地管理平台
|
||||
|
||||
# 项目描述
|
||||
VITE_APP_DESC=科技赋能生态保护 智慧守护绿色家园
|
||||
|
||||
# 版本号
|
||||
VITE_APP_VERSION=1.0.0
|
||||
|
||||
# 版权信息
|
||||
VITE_APP_COPYRIGHT=Copyright © 2025 智慧湿地管理平台 All Rights Reserved.
|
||||
|
||||
# 是否启用 gzip 压缩
|
||||
VITE_BUILD_GZIP=false
|
||||
|
||||
# 是否启用 brotli 压缩
|
||||
VITE_BUILD_BROTLI=false
|
||||
|
||||
# 是否删除 console
|
||||
VITE_DROP_CONSOLE=true
|
||||
|
||||
# 后端 API 的基础 URL
|
||||
VITE_API_BASE_URL=http://localhost:3000
|
||||
@ -1,14 +0,0 @@
|
||||
# 开发环境
|
||||
VITE_NODE_ENV=development
|
||||
|
||||
# API 基础路径
|
||||
VITE_API_BASE_URL=http://localhost:3000
|
||||
|
||||
# 项目基础路径
|
||||
VITE_BASE_URL=/
|
||||
|
||||
# Mock API 路径
|
||||
VITE_MOCK_API=true
|
||||
|
||||
# 是否开启调试工具
|
||||
VITE_DEV_TOOLS=true
|
||||
@ -1,14 +0,0 @@
|
||||
# 生产环境
|
||||
VITE_NODE_ENV=production
|
||||
|
||||
# API 基础路径 - 实际项目中替换为真实的后端接口地址
|
||||
VITE_API_BASE_URL=https://api.your-domain.com
|
||||
|
||||
# 项目基础路径
|
||||
VITE_BASE_URL=/
|
||||
|
||||
# Mock API 路径
|
||||
VITE_MOCK_API=false
|
||||
|
||||
# 是否开启调试工具
|
||||
VITE_DEV_TOOLS=false
|
||||
14
.env.test
14
.env.test
@ -1,14 +0,0 @@
|
||||
# 测试环境
|
||||
VITE_NODE_ENV=test
|
||||
|
||||
# API 基础路径
|
||||
VITE_API_BASE_URL=https://test-api.your-domain.com
|
||||
|
||||
# 项目基础路径
|
||||
VITE_BASE_URL=/
|
||||
|
||||
# Mock API 路径
|
||||
VITE_MOCK_API=false
|
||||
|
||||
# 是否开启调试工具
|
||||
VITE_DEV_TOOLS=true
|
||||
@ -8,6 +8,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@ -15,8 +15,7 @@
|
||||
"json-server": "^1.0.0-beta.3",
|
||||
"pinia": "^2.3.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"wetlandguard-admin": "file:"
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.0",
|
||||
@ -2751,10 +2750,6 @@
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wetlandguard-admin": {
|
||||
"resolved": "",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz",
|
||||
|
||||
@ -16,8 +16,7 @@
|
||||
"json-server": "^1.0.0-beta.3",
|
||||
"pinia": "^2.3.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"wetlandguard-admin": "file:"
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.0",
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* @param {Object} data - 登录参数
|
||||
* @param {string} data.username - 用户名
|
||||
* @param {string} data.password - 密码
|
||||
* @returns {Promise} 返回包含token和用户信息的Promise
|
||||
*/
|
||||
export function login(data) {
|
||||
return request.post('/api/users/login', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function logout() {
|
||||
return request.post('/api/users/logout')
|
||||
}
|
||||
49
src/api/report.ts
Normal file
49
src/api/report.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import request from './request';
|
||||
|
||||
// 日常报告接口
|
||||
export const dailyReportApi = {
|
||||
// 获取日常报告列表
|
||||
getList: (params: any) => request.get('/report/daily/list', { params }),
|
||||
|
||||
// 获取日常报告详情
|
||||
getDetail: (id: number) => request.get(`/report/daily/${id}`),
|
||||
|
||||
// 创建日常报告
|
||||
create: (data: any) => request.post('/report/daily', data),
|
||||
|
||||
// 更新日常报告
|
||||
update: (id: number, data: any) => request.put(`/report/daily/${id}`, data),
|
||||
|
||||
// 删除日常报告
|
||||
delete: (id: number) => request.delete(`/report/daily/${id}`),
|
||||
|
||||
// 导出日常报告
|
||||
export: (id: number) => request.get(`/report/daily/export/${id}`, { responseType: 'blob' })
|
||||
};
|
||||
|
||||
// 分析报告接口
|
||||
export const analysisReportApi = {
|
||||
// 获取分析报告列表
|
||||
getList: (params: any) => request.get('/report/analysis/list', { params }),
|
||||
|
||||
// 获取分析报告详情
|
||||
getDetail: (id: number) => request.get(`/report/analysis/${id}`),
|
||||
|
||||
// 创建分析报告
|
||||
create: (data: any) => request.post('/report/analysis', data),
|
||||
|
||||
// 更新分析报告
|
||||
update: (id: number, data: any) => request.put(`/report/analysis/${id}`, data),
|
||||
|
||||
// 删除分析报告
|
||||
delete: (id: number) => request.delete(`/report/analysis/${id}`),
|
||||
|
||||
// 导出分析报告
|
||||
export: (id: number) => request.get(`/report/analysis/export/${id}`, { responseType: 'blob' }),
|
||||
|
||||
// 获取监测数据统计
|
||||
getMonitorStats: (params: any) => request.get('/report/analysis/monitor-stats', { params }),
|
||||
|
||||
// 获取物种数据统计
|
||||
getSpeciesStats: (params: any) => request.get('/report/analysis/species-stats', { params })
|
||||
};
|
||||
35
src/api/request.ts
Normal file
35
src/api/request.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
const { data } = response
|
||||
return data
|
||||
},
|
||||
(error) => {
|
||||
ElMessage.error(error.message || '请求失败')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
@ -1,52 +0,0 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
* @returns {Promise} 返回用户列表数据
|
||||
*/
|
||||
export function getUserList() {
|
||||
return request.get('/api/users')
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增用户
|
||||
* @param {Object} data - 用户数据
|
||||
* @param {string} data.username - 用户名(必选)
|
||||
* @param {string} data.password - 密码(必选)
|
||||
* @param {string} data.role_id - 角色ID(必选)
|
||||
* @param {string} [data.real_name] - 真实姓名(可选)
|
||||
* @param {string} [data.email] - 邮箱(可选)
|
||||
* @param {string} [data.phone] - 手机号(可选)
|
||||
* @param {string} [data.status] - 状态(可选)
|
||||
* @param {string} [data.expire_time] - 过期时间(可选)
|
||||
* @returns {Promise} 返回创建结果
|
||||
*/
|
||||
export function createUser(data) {
|
||||
return request.post('/api/users', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑用户
|
||||
* @param {string|number} id - 用户ID
|
||||
* @param {Object} data - 用户数据
|
||||
* @param {string} [data.real_name] - 真实姓名(可选)
|
||||
* @param {string} [data.email] - 邮箱(可选)
|
||||
* @param {string} [data.phone] - 手机号(可选)
|
||||
* @param {string} [data.role_id] - 角色ID(可选)
|
||||
* @param {string} [data.status] - 状态(可选)
|
||||
* @param {string} [data.expire_time] - 过期时间(可选)
|
||||
* @param {string} [data.password] - 新密码(可选)
|
||||
* @returns {Promise} 返回更新结果
|
||||
*/
|
||||
export function updateUser(id, data) {
|
||||
return request.put(`/api/users/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
* @param {string|number} id - 用户ID
|
||||
* @returns {Promise} 返回删除结果
|
||||
*/
|
||||
export function deleteUser(id) {
|
||||
return request.delete(`/api/users/${id}`)
|
||||
}
|
||||
41
src/components/HelloWorld.vue
Normal file
41
src/components/HelloWorld.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
@ -1,23 +1,15 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (value) => {
|
||||
return (
|
||||
value.qualityTrend &&
|
||||
Array.isArray(value.indicators) &&
|
||||
value.indicators.every(
|
||||
(indicator) =>
|
||||
indicator.name &&
|
||||
typeof indicator.value === 'number' &&
|
||||
typeof indicator.threshold === 'number' &&
|
||||
indicator.status
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
qualityTrend: string;
|
||||
indicators: {
|
||||
name: string;
|
||||
value: number;
|
||||
threshold: number;
|
||||
status: string;
|
||||
}[];
|
||||
};
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -27,7 +19,7 @@ const props = defineProps({
|
||||
:type="data.qualityTrend === '改善' ? 'success' : data.qualityTrend === '恶化' ? 'warning' : 'info'"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
|
||||
<div class="indicators-table">
|
||||
<el-table :data="data.indicators" border style="width: 100%">
|
||||
<el-table-column prop="name" label="监测指标" />
|
||||
@ -51,4 +43,4 @@ const props = defineProps({
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@ -1,29 +1,19 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: [Object, Array],
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
validator: (value) => ['line', 'bar'].includes(value),
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
});
|
||||
const props = defineProps<{
|
||||
data: any;
|
||||
type: 'line' | 'bar';
|
||||
title?: string;
|
||||
}>();
|
||||
|
||||
let chart = null;
|
||||
let chart: echarts.ECharts | null = null;
|
||||
|
||||
const initChart = () => {
|
||||
const chartDom = document.getElementById('monitorChart');
|
||||
if (!chartDom) return;
|
||||
|
||||
|
||||
chart = echarts.init(chartDom);
|
||||
const option = {
|
||||
title: {
|
||||
@ -34,7 +24,7 @@ const initChart = () => {
|
||||
},
|
||||
// ... 其他配置
|
||||
};
|
||||
|
||||
|
||||
chart.setOption(option);
|
||||
};
|
||||
|
||||
@ -55,4 +45,4 @@ const handleResize = () => {
|
||||
|
||||
<template>
|
||||
<div id="monitorChart" style="width: 100%; height: 400px"></div>
|
||||
</template>
|
||||
</template>
|
||||
@ -1,19 +1,13 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
const props = defineProps<{
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (value) => {
|
||||
return (
|
||||
typeof value.diversity === 'number' &&
|
||||
typeof value.richness === 'number' &&
|
||||
Array.isArray(value.distribution)
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
diversity: number;
|
||||
richness: number;
|
||||
distribution: any[];
|
||||
};
|
||||
}>();
|
||||
|
||||
const diversityLevel = computed(() => {
|
||||
const value = props.data.diversity;
|
||||
@ -36,7 +30,7 @@ const diversityLevel = computed(() => {
|
||||
{{ data.richness }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
|
||||
<!-- 分布情况图表 -->
|
||||
<div class="distribution-chart">
|
||||
<!-- 这里可以添加物种分布的图表展示 -->
|
||||
@ -50,4 +44,4 @@ const diversityLevel = computed(() => {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
@ -45,7 +45,7 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const handleSelect = (key) => {
|
||||
const handleSelect = (key: string) => {
|
||||
router.push(key);
|
||||
};
|
||||
|
||||
|
||||
@ -14,4 +14,4 @@ export const menuData = [
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
];
|
||||
@ -17,4 +17,4 @@ app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
app.mount('#app')
|
||||
app.mount('#app')
|
||||
@ -112,13 +112,13 @@ const router = createRouter({
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
|
||||
// 如果访问登录页且已登录,重定向到首页
|
||||
if (to.path === '/login' && userStore.isLoggedIn) {
|
||||
next('/')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 如果需要认证但未登录,重定向到登录页
|
||||
if (to.matched.some(record => record.meta.requiresAuth) && !userStore.isLoggedIn) {
|
||||
next({
|
||||
@ -130,4 +130,4 @@ router.beforeEach((to, from, next) => {
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
export default router
|
||||
@ -1,14 +1,25 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface LogEntry {
|
||||
id: number;
|
||||
type: string;
|
||||
user: string;
|
||||
action: string;
|
||||
ip: string;
|
||||
status: string;
|
||||
detail: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
export const useSystemLogStore = defineStore('systemLog', {
|
||||
state: () => ({
|
||||
logs: []
|
||||
logs: [] as LogEntry[]
|
||||
}),
|
||||
|
||||
actions: {
|
||||
// 添加日志
|
||||
addLog(log) {
|
||||
const newLog = {
|
||||
addLog(log: Omit<LogEntry, 'id' | 'createTime'>) {
|
||||
const newLog: LogEntry = {
|
||||
...log,
|
||||
id: Date.now(),
|
||||
createTime: new Date().toLocaleString('zh-CN', {
|
||||
@ -21,7 +32,7 @@ export const useSystemLogStore = defineStore('systemLog', {
|
||||
hour12: false
|
||||
}).replace(/\//g, '-')
|
||||
};
|
||||
|
||||
|
||||
this.logs.unshift(newLog);
|
||||
this.saveLogs();
|
||||
},
|
||||
@ -49,4 +60,4 @@ export const useSystemLogStore = defineStore('systemLog', {
|
||||
getters: {
|
||||
getLogs: (state) => state.logs
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -1,72 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { login as loginApi, logout as logoutApi } from '@/api/login'
|
||||
|
||||
// 安全的 JSON 解析函数
|
||||
const safeJSONParse = (str, defaultValue = null) => {
|
||||
if (!str || str === 'undefined' || str === 'null') {
|
||||
return defaultValue
|
||||
}
|
||||
try {
|
||||
return JSON.parse(str)
|
||||
} catch (error) {
|
||||
console.warn('JSON解析错误,使用默认值:', error)
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => {
|
||||
// 从 localStorage 获取数据
|
||||
const token = localStorage.getItem('token')
|
||||
const userInfoStr = localStorage.getItem('userInfo')
|
||||
|
||||
return {
|
||||
token: token || null,
|
||||
userInfo: safeJSONParse(userInfoStr)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 登录
|
||||
async login(username, password) {
|
||||
try {
|
||||
// 调用登录接口
|
||||
const { token, userInfo } = await loginApi({ username, password })
|
||||
|
||||
// 保存token和用户信息
|
||||
this.token = token
|
||||
this.userInfo = userInfo
|
||||
|
||||
// 持久化存储
|
||||
localStorage.setItem('token', token)
|
||||
localStorage.setItem('userInfo', JSON.stringify(userInfo))
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
async logout() {
|
||||
try {
|
||||
await logoutApi()
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error)
|
||||
} finally {
|
||||
// 无论是否成功调用退出接口,都清除本地存储
|
||||
this.token = null
|
||||
this.userInfo = null
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
isLoggedIn: (state) => !!state.token,
|
||||
username: (state) => state.userInfo?.username,
|
||||
userRole: (state) => state.userInfo?.role
|
||||
}
|
||||
})
|
||||
44
src/stores/user.ts
Normal file
44
src/stores/user.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface UserState {
|
||||
token: string | null;
|
||||
userInfo: {
|
||||
username: string;
|
||||
role: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: (): UserState => ({
|
||||
token: localStorage.getItem('token'),
|
||||
userInfo: null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
// 登录
|
||||
async login(username: string, password: string) {
|
||||
if (username === 'admin' && password === '123456') {
|
||||
this.token = 'demo-token';
|
||||
this.userInfo = {
|
||||
username: 'admin',
|
||||
role: '管理员'
|
||||
};
|
||||
localStorage.setItem('token', this.token);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
logout() {
|
||||
this.token = null;
|
||||
this.userInfo = null;
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
isLoggedIn: (state) => !!state.token,
|
||||
username: (state) => state.userInfo?.username
|
||||
}
|
||||
})
|
||||
@ -1,68 +0,0 @@
|
||||
/**
|
||||
* 格式化日期时间
|
||||
* @param {string|number|Date} time 需要格式化的时间
|
||||
* @param {string} [format='YYYY-MM-DD HH:mm:ss'] 格式化的格式
|
||||
* @returns {string} 格式化后的时间字符串
|
||||
*/
|
||||
export function formatDateTime(time, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
if (!time) return '';
|
||||
|
||||
const date = new Date(time);
|
||||
|
||||
if (isNaN(date.getTime())) return '';
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return format
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param {string|number|Date} time 需要格式化的时间
|
||||
* @returns {string} 格式化后的日期字符串 YYYY-MM-DD
|
||||
*/
|
||||
export function formatDate(time) {
|
||||
return formatDateTime(time, 'YYYY-MM-DD');
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间为相对时间
|
||||
* @param {string|number|Date} time 需要格式化的时间
|
||||
* @returns {string} 相对时间描述
|
||||
*/
|
||||
export function formatRelativeTime(time) {
|
||||
if (!time) return '';
|
||||
|
||||
const date = new Date(time);
|
||||
if (isNaN(date.getTime())) return '';
|
||||
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 30) {
|
||||
return formatDateTime(time);
|
||||
} else if (days > 0) {
|
||||
return `${days}天前`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}小时前`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分钟前`;
|
||||
} else {
|
||||
return '刚刚';
|
||||
}
|
||||
}
|
||||
@ -1,156 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import router from '../router'
|
||||
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
// 从环境变量获取基础URL,如果没有则使用默认值
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||
timeout: 15000, // 请求超时时间
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
const userStore = useUserStore()
|
||||
const token = userStore.token
|
||||
|
||||
// 如果有token,添加到请求头
|
||||
if (token) {
|
||||
config.headers = config.headers || {}
|
||||
config.headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
console.error('请求错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
const res = response.data
|
||||
|
||||
// 如果响应成功
|
||||
if (res.success) {
|
||||
return res
|
||||
}
|
||||
|
||||
// 处理特定错误码
|
||||
switch (res.code) {
|
||||
case 401:
|
||||
handleUnauthorized()
|
||||
break
|
||||
case 403:
|
||||
ElMessage.error('没有权限访问该资源')
|
||||
break
|
||||
case 500:
|
||||
console.error('服务器错误详情:', res)
|
||||
ElMessage.error(res.message || '服务器错误,请稍后重试')
|
||||
break
|
||||
default:
|
||||
ElMessage.error(res.message || '请求失败')
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(res.message || '请求失败'))
|
||||
},
|
||||
(error) => {
|
||||
console.error('响应错误详情:', {
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
data: error.response?.data,
|
||||
headers: error.response?.headers,
|
||||
config: {
|
||||
url: error.config?.url,
|
||||
method: error.config?.method,
|
||||
headers: error.config?.headers,
|
||||
params: error.config?.params
|
||||
}
|
||||
})
|
||||
|
||||
// 处理网络错误
|
||||
if (!error.response) {
|
||||
ElMessage.error('网络错误,请检查您的网络连接')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 处理HTTP状态码错误
|
||||
const status = error.response.status
|
||||
switch (status) {
|
||||
case 401:
|
||||
handleUnauthorized()
|
||||
break
|
||||
case 403:
|
||||
ElMessage.error('没有权限访问该资源')
|
||||
break
|
||||
case 404:
|
||||
ElMessage.error('请求的资源不存在')
|
||||
break
|
||||
case 500:
|
||||
ElMessage.error('服务器错误,请稍后重试')
|
||||
break
|
||||
default:
|
||||
ElMessage.error(`请求失败:${error.message}`)
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 处理未授权情况
|
||||
const handleUnauthorized = () => {
|
||||
const userStore = useUserStore()
|
||||
userStore.logout() // 清除用户信息
|
||||
|
||||
// 跳转到登录页,并携带当前页面路径
|
||||
const currentPath = router.currentRoute.value.fullPath
|
||||
router.push({
|
||||
path: '/login',
|
||||
query: {
|
||||
redirect: currentPath
|
||||
}
|
||||
})
|
||||
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
}
|
||||
|
||||
// 封装请求方法
|
||||
export const request = {
|
||||
get(url, config) {
|
||||
return service.get(url, config)
|
||||
},
|
||||
|
||||
post(url, data, config) {
|
||||
return service.post(url, data, config)
|
||||
},
|
||||
|
||||
put(url, data, config) {
|
||||
return service.put(url, data, config)
|
||||
},
|
||||
|
||||
delete(url, config) {
|
||||
return service.delete(url, config)
|
||||
},
|
||||
|
||||
// 上传文件的专用方法
|
||||
upload(url, file, config) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
return service.post(url, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
...config
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default request
|
||||
@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import { markRaw } from 'vue'
|
||||
@ -377,8 +377,8 @@ onMounted(() => {
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<div class="stats-grid">
|
||||
<div v-for="card in statsCards"
|
||||
:key="card.title"
|
||||
<div v-for="card in statsCards"
|
||||
:key="card.title"
|
||||
class="stats-card"
|
||||
:style="{ backgroundColor: card.bgColor }">
|
||||
<div class="card-header">
|
||||
@ -394,16 +394,16 @@ onMounted(() => {
|
||||
{{ card.value }}
|
||||
<span class="unit">{{ card.unit }}</span>
|
||||
</div>
|
||||
<div class="change-value"
|
||||
:style="{ color: card.change.value.includes('+') ? '#67C23A' :
|
||||
<div class="change-value"
|
||||
:style="{ color: card.change.value.includes('+') ? '#67C23A' :
|
||||
card.change.value.includes('%') ? card.color : '#F56C6C' }">
|
||||
{{ card.change.value }}
|
||||
<span class="label">{{ card.change.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div v-for="feature in card.features"
|
||||
:key="feature"
|
||||
<div v-for="feature in card.features"
|
||||
:key="feature"
|
||||
class="feature-item">
|
||||
{{ feature }}
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
@ -42,7 +42,7 @@ const getCurrentTime = () => {
|
||||
.replace(/\//g, "-");
|
||||
};
|
||||
|
||||
const handleLogin = async (formEl) => {
|
||||
const handleLogin = async (formEl: any) => {
|
||||
if (!formEl) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
@ -67,7 +67,7 @@ const handleLogin = async (formEl) => {
|
||||
});
|
||||
|
||||
// 获取重定向地址
|
||||
const redirect = route.query.redirect;
|
||||
const redirect = route.query.redirect as string;
|
||||
router.push(redirect || "/dashboard");
|
||||
} else {
|
||||
// 记录失败日志
|
||||
@ -92,7 +92,7 @@ const handleLogin = async (formEl) => {
|
||||
};
|
||||
|
||||
// 添加输入框获得焦点时的处理函数
|
||||
const handleFocus = (prop) => {
|
||||
const handleFocus = (prop: "username" | "password") => {
|
||||
if (formRef.value) {
|
||||
// 清除对应字段的验证错误
|
||||
formRef.value.clearValidate(prop);
|
||||
@ -161,7 +161,7 @@ const handleFocus = (prop) => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "../../styles/variables.scss" as *;
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.login-container {
|
||||
height: 100vh;
|
||||
|
||||
@ -1,10 +1,19 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Monitor, Warning, TrendCharts } from "@element-plus/icons-vue";
|
||||
|
||||
const tableData = ref([
|
||||
interface EnvData {
|
||||
id: number;
|
||||
location: string;
|
||||
temperature: number;
|
||||
humidity: number;
|
||||
waterQuality: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
const tableData = ref<EnvData[]>([
|
||||
{
|
||||
id: 1,
|
||||
location: "A区监测点",
|
||||
@ -81,9 +90,9 @@ const initChart = () => {
|
||||
textStyle: {
|
||||
color: "#666",
|
||||
},
|
||||
formatter: function (params) {
|
||||
formatter: function (params: any) {
|
||||
let result = `${params[0].axisValue}<br/>`;
|
||||
params.forEach((item) => {
|
||||
params.forEach((item: any) => {
|
||||
result += `${item.marker} ${item.seriesName}: ${item.value}${
|
||||
item.seriesName.includes("温度")
|
||||
? "°C"
|
||||
@ -238,30 +247,33 @@ const initChart = () => {
|
||||
|
||||
myChart.setOption(option);
|
||||
|
||||
// 处理窗口大小变化
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener("resize", () => {
|
||||
myChart.resize();
|
||||
});
|
||||
};
|
||||
|
||||
// 导出相关数据
|
||||
// 添加导出相关的数据和方法
|
||||
const exportDialogVisible = ref(false);
|
||||
const exportForm = reactive({
|
||||
timeRange: [],
|
||||
dataType: ["temperature", "humidity", "waterQuality"],
|
||||
timeRange: [] as string[],
|
||||
format: "excel",
|
||||
dataType: ["temperature", "humidity", "waterQuality"],
|
||||
});
|
||||
|
||||
// 处理导出确认
|
||||
const handleExport = () => {
|
||||
exportDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleExportConfirm = () => {
|
||||
ElMessage.success("数据导出成功");
|
||||
console.log("导出数据:", exportForm);
|
||||
exportDialogVisible.value = false;
|
||||
};
|
||||
|
||||
// 初始化迷你图表
|
||||
const initMiniChart = (el, data) => {
|
||||
const initMiniChart = (el: HTMLElement, data: number[]) => {
|
||||
const chart = echarts.init(el);
|
||||
const option = {
|
||||
chart.setOption({
|
||||
grid: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
@ -345,8 +357,7 @@ const initMiniChart = (el, data) => {
|
||||
},
|
||||
padding: [4, 8],
|
||||
},
|
||||
};
|
||||
chart.setOption(option);
|
||||
});
|
||||
|
||||
// 监听卡片hover事件
|
||||
const card = el.closest(".env-card");
|
||||
@ -367,12 +378,15 @@ const initMiniChart = (el, data) => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initChart();
|
||||
// 初始化所有迷你图表
|
||||
const chartEls = document.querySelectorAll(".env-chart");
|
||||
chartEls.forEach((el, index) => {
|
||||
initMiniChart(el, envStats.value[index].trend);
|
||||
envStats.value.forEach((stat, index) => {
|
||||
const el = document.getElementById(`miniChart${index}`);
|
||||
if (el) {
|
||||
initMiniChart(el, stat.trend);
|
||||
}
|
||||
});
|
||||
// 原有的图表初始化
|
||||
initChart();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -468,7 +482,7 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.env-container {
|
||||
.env-card {
|
||||
|
||||
@ -1,9 +1,18 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
const tableData = ref([
|
||||
interface SpeciesData {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
count: number;
|
||||
location: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
const tableData = ref<SpeciesData[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: "东方白鹳",
|
||||
@ -139,7 +148,7 @@ const initChart = () => {
|
||||
label: {
|
||||
show: true,
|
||||
position: 'outside',
|
||||
formatter: function(params) {
|
||||
formatter: function(params: any) {
|
||||
return `${params.name}\n${params.percent.toFixed(1)}%`;
|
||||
},
|
||||
color: '#666',
|
||||
@ -163,10 +172,10 @@ const initChart = () => {
|
||||
animationDurationUpdate: 500,
|
||||
animationType: 'expansion',
|
||||
animationEasing: 'cubicInOut',
|
||||
animationDelay: function(idx) {
|
||||
animationDelay: function(idx: number) {
|
||||
return idx * 100;
|
||||
},
|
||||
animationDelayUpdate: function(idx) {
|
||||
animationDelayUpdate: function(idx: number) {
|
||||
return idx * 100;
|
||||
},
|
||||
emphasis: {
|
||||
@ -192,7 +201,7 @@ const initChart = () => {
|
||||
};
|
||||
|
||||
myChart.setOption(option);
|
||||
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
myChart.resize();
|
||||
});
|
||||
@ -205,7 +214,7 @@ onMounted(() => {
|
||||
// 添加导出相关的数据和方法
|
||||
const exportDialogVisible = ref(false);
|
||||
const exportForm = reactive({
|
||||
timeRange: [],
|
||||
timeRange: [] as string[],
|
||||
format: "excel",
|
||||
});
|
||||
|
||||
@ -297,7 +306,7 @@ const handleExportConfirm = () => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.species-container {
|
||||
padding: 20px;
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
|
||||
const tableData = ref([
|
||||
interface PointData {
|
||||
id: number;
|
||||
name: string;
|
||||
location: string;
|
||||
type: string;
|
||||
description: string;
|
||||
status: string;
|
||||
lastPatrolTime: string;
|
||||
}
|
||||
|
||||
const tableData = ref<PointData[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: "A区-1号点位",
|
||||
@ -46,25 +56,25 @@ const handleAdd = () => {
|
||||
|
||||
// 添加搜索相关
|
||||
const searchText = ref("");
|
||||
const typeFilter = ref([]);
|
||||
const statusFilter = ref([]);
|
||||
const typeFilter = ref<string[]>([]);
|
||||
const statusFilter = ref<string[]>([]);
|
||||
|
||||
// 过滤表格数据
|
||||
const filteredTableData = computed(() => {
|
||||
return tableData.value.filter(point => {
|
||||
// 文本搜索
|
||||
const searchLower = searchText.value.toLowerCase();
|
||||
const textMatch = !searchText.value ||
|
||||
const textMatch = !searchText.value ||
|
||||
point.name.toLowerCase().includes(searchLower) ||
|
||||
point.location.toLowerCase().includes(searchLower) ||
|
||||
point.description.toLowerCase().includes(searchLower);
|
||||
|
||||
// 类型筛选
|
||||
const typeMatch = typeFilter.value.length === 0 ||
|
||||
const typeMatch = typeFilter.value.length === 0 ||
|
||||
typeFilter.value.includes(point.type);
|
||||
|
||||
// 状态筛选
|
||||
const statusMatch = statusFilter.value.length === 0 ||
|
||||
const statusMatch = statusFilter.value.length === 0 ||
|
||||
statusFilter.value.includes(point.status);
|
||||
|
||||
return textMatch && typeMatch && statusMatch;
|
||||
@ -80,9 +90,9 @@ const resetFilters = () => {
|
||||
|
||||
// 添加编辑相关
|
||||
const isEdit = ref(false);
|
||||
const currentPoint = ref(null);
|
||||
const currentPoint = ref<PointData | null>(null);
|
||||
|
||||
const handleEdit = (row) => {
|
||||
const handleEdit = (row: PointData) => {
|
||||
isEdit.value = true;
|
||||
currentPoint.value = row;
|
||||
formData.name = row.name;
|
||||
@ -95,7 +105,7 @@ const handleEdit = (row) => {
|
||||
// 更新提交处理
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
if (isEdit.value) {
|
||||
@ -135,16 +145,16 @@ const paginatedData = computed(() => {
|
||||
return filteredTableData.value.slice(start, end);
|
||||
});
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
const handleSizeChange = (val: number) => {
|
||||
pageSize.value = val;
|
||||
currentPage.value = 1;
|
||||
};
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
const handleCurrentChange = (val: number) => {
|
||||
currentPage.value = val;
|
||||
};
|
||||
|
||||
const handleDelete = (row) => {
|
||||
const handleDelete = (row: PointData) => {
|
||||
ElMessageBox.confirm("确认删除该巡护点位?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
@ -243,9 +253,9 @@ const pointTypes = [
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
@ -270,8 +280,8 @@ const pointTypes = [
|
||||
</el-card>
|
||||
|
||||
<!-- 新增点位弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑巡护点位' : '新增巡护点位'"
|
||||
width="500px"
|
||||
@close="handleDialogClose"
|
||||
@ -318,7 +328,7 @@ const pointTypes = [
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.point-container {
|
||||
.el-card {
|
||||
@ -408,4 +418,4 @@ const pointTypes = [
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@ -1,9 +1,20 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, reactive } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
|
||||
const tableData = ref([
|
||||
interface RecordData {
|
||||
id: number;
|
||||
taskTitle: string;
|
||||
patroller: string;
|
||||
route: string;
|
||||
findings: string;
|
||||
images: string[];
|
||||
status: string;
|
||||
completedTime: string;
|
||||
}
|
||||
|
||||
const tableData = ref<RecordData[]>([
|
||||
{
|
||||
id: 1,
|
||||
taskTitle: "A区日常巡查",
|
||||
@ -27,9 +38,9 @@ const tableData = ref([
|
||||
]);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const currentRecord = ref(null);
|
||||
const currentRecord = ref<RecordData | null>(null);
|
||||
|
||||
const handleView = (row) => {
|
||||
const handleView = (row: RecordData) => {
|
||||
currentRecord.value = row;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
@ -50,7 +61,7 @@ const filteredTableData = computed(() => {
|
||||
|
||||
const exportDialogVisible = ref(false);
|
||||
const exportForm = reactive({
|
||||
timeRange: [],
|
||||
timeRange: [] as string[],
|
||||
format: "excel",
|
||||
types: ["basic", "findings", "images"],
|
||||
});
|
||||
@ -74,12 +85,12 @@ const paginatedData = computed(() => {
|
||||
return filteredTableData.value.slice(start, end);
|
||||
});
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
const handleSizeChange = (val: number) => {
|
||||
pageSize.value = val;
|
||||
currentPage.value = 1;
|
||||
};
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
const handleCurrentChange = (val: number) => {
|
||||
currentPage.value = val;
|
||||
};
|
||||
</script>
|
||||
@ -231,7 +242,7 @@ const handleCurrentChange = (val) => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.record-container {
|
||||
.el-card {
|
||||
|
||||
@ -1,8 +1,19 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
|
||||
const tableData = ref([
|
||||
interface TaskData {
|
||||
id: number;
|
||||
title: string;
|
||||
area: string;
|
||||
assignee: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}
|
||||
|
||||
const tableData = ref<TaskData[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: "A区日常巡查",
|
||||
@ -25,8 +36,8 @@ const tableData = ref([
|
||||
},
|
||||
]);
|
||||
|
||||
const getStatusType = (status) => {
|
||||
const map = {
|
||||
const getStatusType = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
进行中: "success",
|
||||
待开始: "info",
|
||||
已完成: "primary",
|
||||
@ -35,8 +46,8 @@ const getStatusType = (status) => {
|
||||
return map[status] || "info";
|
||||
};
|
||||
|
||||
const getPriorityType = (priority) => {
|
||||
const map = {
|
||||
const getPriorityType = (priority: string) => {
|
||||
const map: Record<string, string> = {
|
||||
高: "danger",
|
||||
中: "warning",
|
||||
低: "info",
|
||||
@ -44,7 +55,7 @@ const getPriorityType = (priority) => {
|
||||
return map[priority] || "info";
|
||||
};
|
||||
|
||||
// 新建任务相关
|
||||
// 添加新建任务相关
|
||||
const newTaskDialogVisible = ref(false);
|
||||
const newTaskForm = reactive({
|
||||
title: "",
|
||||
@ -56,23 +67,24 @@ const newTaskForm = reactive({
|
||||
description: "",
|
||||
});
|
||||
|
||||
// 任务详情相关
|
||||
// 添加任务详情相关
|
||||
const detailDialogVisible = ref(false);
|
||||
const currentTask = ref(null);
|
||||
const currentTask = ref<TaskData | null>(null);
|
||||
|
||||
// 任务状态更新相关
|
||||
const handleView = (row) => {
|
||||
// 添加任务状态更新相关
|
||||
const handleView = (row: TaskData) => {
|
||||
currentTask.value = row;
|
||||
detailDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleComplete = (row) => {
|
||||
const handleComplete = (row: TaskData) => {
|
||||
ElMessageBox.confirm("确认完成该巡护任务?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
// 这里添加完成任务的逻辑
|
||||
ElMessage.success("任务已完成");
|
||||
})
|
||||
.catch(() => {
|
||||
@ -80,13 +92,14 @@ const handleComplete = (row) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = (row) => {
|
||||
const handleCancel = (row: TaskData) => {
|
||||
ElMessageBox.confirm("确认取消该巡护任务?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
// 这里添加取消任务的逻辑
|
||||
ElMessage.success("任务已取消");
|
||||
})
|
||||
.catch(() => {
|
||||
@ -94,8 +107,14 @@ const handleCancel = (row) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 任务时间线数据
|
||||
const taskTimeline = ref([
|
||||
// 添加任务时间线数据
|
||||
interface TimelineItem {
|
||||
type: 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||
timestamp: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const taskTimeline = ref<TimelineItem[]>([
|
||||
{
|
||||
type: 'primary',
|
||||
timestamp: '2024-03-20 09:00',
|
||||
@ -118,8 +137,8 @@ const taskTimeline = ref([
|
||||
},
|
||||
]);
|
||||
|
||||
// 表单验证规则
|
||||
const validateTime = (rule, value, callback) => {
|
||||
// 添加表单验证规则
|
||||
const validateTime = (rule: any, value: string, callback: Function) => {
|
||||
if (!value) {
|
||||
callback(new Error('请选择时间'));
|
||||
return;
|
||||
@ -155,7 +174,7 @@ const handleNewTask = () => {
|
||||
|
||||
const handleNewTaskSubmit = async () => {
|
||||
if (!newTaskFormRef.value) return;
|
||||
|
||||
|
||||
try {
|
||||
await newTaskFormRef.value.validate();
|
||||
console.log("提交新任务:", newTaskForm);
|
||||
@ -180,18 +199,18 @@ const resetForm = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭弹窗时重置表单
|
||||
// 添加关闭弹窗时重置表单
|
||||
const handleDialogClose = () => {
|
||||
resetForm();
|
||||
};
|
||||
|
||||
// 搜索和筛选相关
|
||||
// 添加搜索和筛选相关
|
||||
const searchText = ref("");
|
||||
const statusFilter = ref([]);
|
||||
const priorityFilter = ref([]);
|
||||
const dateRange = ref(['', '']);
|
||||
const statusFilter = ref<string[]>([]);
|
||||
const priorityFilter = ref<string[]>([]);
|
||||
const dateRange = ref<[string, string]>(['', '']);
|
||||
|
||||
// 统计数据
|
||||
// 添加统计数据
|
||||
const statistics = computed(() => {
|
||||
const total = tableData.value.length;
|
||||
const statusCount = {
|
||||
@ -207,8 +226,8 @@ const statistics = computed(() => {
|
||||
};
|
||||
|
||||
tableData.value.forEach(task => {
|
||||
statusCount[task.status]++;
|
||||
priorityCount[task.priority]++;
|
||||
statusCount[task.status as keyof typeof statusCount]++;
|
||||
priorityCount[task.priority as keyof typeof priorityCount]++;
|
||||
});
|
||||
|
||||
return {
|
||||
@ -223,17 +242,17 @@ const filteredTableData = computed(() => {
|
||||
return tableData.value.filter(task => {
|
||||
// 文本搜索
|
||||
const searchLower = searchText.value.toLowerCase();
|
||||
const textMatch = !searchText.value ||
|
||||
const textMatch = !searchText.value ||
|
||||
task.title.toLowerCase().includes(searchLower) ||
|
||||
task.area.toLowerCase().includes(searchLower) ||
|
||||
task.assignee.toLowerCase().includes(searchLower);
|
||||
|
||||
// 状态筛选
|
||||
const statusMatch = statusFilter.value.length === 0 ||
|
||||
const statusMatch = statusFilter.value.length === 0 ||
|
||||
statusFilter.value.includes(task.status);
|
||||
|
||||
// 优先级筛选
|
||||
const priorityMatch = priorityFilter.value.length === 0 ||
|
||||
const priorityMatch = priorityFilter.value.length === 0 ||
|
||||
priorityFilter.value.includes(task.priority);
|
||||
|
||||
// 日期范围筛选
|
||||
@ -470,9 +489,9 @@ const resetFilters = () => {
|
||||
</el-dialog>
|
||||
|
||||
<!-- 修改新建任务弹窗,添加表单验证 -->
|
||||
<el-dialog
|
||||
v-model="newTaskDialogVisible"
|
||||
title="新建巡护任务"
|
||||
<el-dialog
|
||||
v-model="newTaskDialogVisible"
|
||||
title="新建巡护任务"
|
||||
width="600px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
@ -527,7 +546,7 @@ const resetFilters = () => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.task-container {
|
||||
.el-card {
|
||||
@ -650,7 +669,7 @@ const resetFilters = () => {
|
||||
.el-radio-group {
|
||||
.el-radio {
|
||||
margin-right: 20px;
|
||||
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
@ -673,7 +692,7 @@ const resetFilters = () => {
|
||||
.statistics-content {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
|
||||
.number {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { markRaw } from "vue";
|
||||
import {
|
||||
|
||||
@ -1,11 +1,43 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import { ElMessageBox, ElMessage } from "element-plus";
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
|
||||
interface AnalysisReport {
|
||||
id: number;
|
||||
title: string;
|
||||
type: "species" | "environment"; // 分析类型:物种/环境
|
||||
timeRange: {
|
||||
// 分析时间范围
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
dataSource: {
|
||||
// 数据来源
|
||||
type: string;
|
||||
points: string[]; // 监测点位
|
||||
}[];
|
||||
analysis: {
|
||||
summary: string; // 分析总结
|
||||
trends: {
|
||||
// 趋势分析
|
||||
indicator: string; // 指标
|
||||
trend: string; // 变化趋势
|
||||
data: any[]; // 数据
|
||||
}[];
|
||||
abnormal: {
|
||||
// 异常分析
|
||||
type: string;
|
||||
description: string;
|
||||
level: string;
|
||||
}[];
|
||||
};
|
||||
recommendations: string[]; // 建议措施
|
||||
}
|
||||
|
||||
// 示例数据
|
||||
const tableData = ref([
|
||||
const tableData = ref<AnalysisReport[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: "2024年第一季度水质监测分析报告",
|
||||
@ -55,15 +87,15 @@ const filterForm = ref({
|
||||
|
||||
// 添加详情查看功能
|
||||
const detailVisible = ref(false);
|
||||
const currentReport = ref(null);
|
||||
const currentReport = ref<AnalysisReport | null>(null);
|
||||
|
||||
const handleView = (row) => {
|
||||
const handleView = (row: AnalysisReport) => {
|
||||
currentReport.value = row;
|
||||
detailVisible.value = true;
|
||||
};
|
||||
|
||||
// 添加导出功能
|
||||
const handleExport = (row) => {
|
||||
const handleExport = (row: AnalysisReport) => {
|
||||
ElMessageBox.confirm("确认导出该分析报告?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
@ -78,7 +110,7 @@ const handleExport = (row) => {
|
||||
};
|
||||
|
||||
// 添加图表resize监听
|
||||
let myChart = null;
|
||||
let myChart: echarts.ECharts | null = null;
|
||||
|
||||
const initChart = () => {
|
||||
const chartDom = document.getElementById("trendChart");
|
||||
@ -234,12 +266,12 @@ const paginatedData = computed(() => {
|
||||
return tableData.value.slice(start, end);
|
||||
});
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
const handleSizeChange = (val: number) => {
|
||||
pageSize.value = val;
|
||||
currentPage.value = 1;
|
||||
};
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
const handleCurrentChange = (val: number) => {
|
||||
currentPage.value = val;
|
||||
};
|
||||
|
||||
@ -448,7 +480,7 @@ const handleRefresh = () => {
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
@click="handleExport(currentReport)"
|
||||
@click="handleExport(currentReport!)"
|
||||
:disabled="!currentReport"
|
||||
>
|
||||
导出
|
||||
@ -460,7 +492,7 @@ const handleRefresh = () => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.analysis-report {
|
||||
.card-header {
|
||||
|
||||
@ -1,11 +1,29 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
|
||||
import { Plus, Document, Download, TrendCharts, Connection } from '@element-plus/icons-vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
interface DailyReport {
|
||||
id: number;
|
||||
title: string;
|
||||
reporter: string;
|
||||
department: string;
|
||||
type: string;
|
||||
content: string;
|
||||
summary: string;
|
||||
data: {
|
||||
type: string;
|
||||
value: string | number;
|
||||
trend?: string;
|
||||
}[];
|
||||
attachments: string[];
|
||||
status: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
// 示例数据
|
||||
const tableData = ref([
|
||||
const tableData = ref<DailyReport[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: "A区巡护日报",
|
||||
@ -42,9 +60,9 @@ const formData = ref({
|
||||
|
||||
// 查看详情
|
||||
const detailVisible = ref(false);
|
||||
const currentReport = ref(null);
|
||||
const currentReport = ref<DailyReport | null>(null);
|
||||
|
||||
const handleView = (row) => {
|
||||
const handleView = (row: DailyReport) => {
|
||||
currentReport.value = row;
|
||||
detailVisible.value = true;
|
||||
};
|
||||
@ -60,12 +78,13 @@ const handleSubmit = () => {
|
||||
};
|
||||
|
||||
// 趋势图表
|
||||
let trendChart = null;
|
||||
let trendChart: echarts.ECharts | null = null;
|
||||
|
||||
// 初始化趋势图表
|
||||
const initTrendChart = () => {
|
||||
const chartDom = document.getElementById('trendChart');
|
||||
if (!chartDom) return;
|
||||
|
||||
const myChart = echarts.init(chartDom);
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
@ -216,10 +235,11 @@ const handleGenerate = () => {
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
}).then(() => {
|
||||
// 模拟自动汇总数据过程
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: '正在汇总数据,请稍候...'
|
||||
});
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
loadingInstance.close();
|
||||
ElMessage.success('报告生成成功,已自动整合今日巡护记录和监测数据');
|
||||
@ -395,8 +415,8 @@ const showTrendAnalysis = ref(false);
|
||||
{{ currentReport.content }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="附件" :span="2">
|
||||
<el-link
|
||||
v-for="file in currentReport.attachments"
|
||||
<el-link
|
||||
v-for="file in currentReport.attachments"
|
||||
:key="file"
|
||||
type="primary"
|
||||
style="margin-right: 16px"
|
||||
@ -423,7 +443,7 @@ const showTrendAnalysis = ref(false);
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
<el-button type="warning" @click="handleExport(currentReport)">导出</el-button>
|
||||
<el-button type="warning" @click="handleExport(currentReport!)">导出</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -461,7 +481,7 @@ const showTrendAnalysis = ref(false);
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.daily-report {
|
||||
.card-header {
|
||||
@ -478,18 +498,18 @@ const showTrendAnalysis = ref(false);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: $primary-color;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: $text-secondary;
|
||||
@ -509,19 +529,19 @@ const showTrendAnalysis = ref(false);
|
||||
|
||||
.stat-grid {
|
||||
padding: 12px;
|
||||
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
|
||||
|
||||
.stat-item-label {
|
||||
font-size: 13px;
|
||||
color: $text-secondary;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
|
||||
.stat-item-value {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
@ -530,11 +550,11 @@ const showTrendAnalysis = ref(false);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
|
||||
|
||||
.up {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
|
||||
.down {
|
||||
color: $danger-color;
|
||||
}
|
||||
@ -546,7 +566,7 @@ const showTrendAnalysis = ref(false);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
|
||||
.el-icon {
|
||||
color: $text-secondary;
|
||||
cursor: help;
|
||||
@ -558,15 +578,15 @@ const showTrendAnalysis = ref(false);
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
|
||||
|
||||
p {
|
||||
margin: 8px 0;
|
||||
color: $text-regular;
|
||||
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@ -587,4 +607,4 @@ const showTrendAnalysis = ref(false);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@ -1,10 +1,21 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, reactive } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import * as echarts from "echarts";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
const tableData = ref([
|
||||
interface AnalysisReport {
|
||||
id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
period: string;
|
||||
author: string;
|
||||
summary: string;
|
||||
charts: string[];
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
const tableData = ref<AnalysisReport[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: "2024年第一季度水质分析报告",
|
||||
@ -31,7 +42,7 @@ const tableData = ref([
|
||||
const initChart = () => {
|
||||
const chartDom = document.getElementById("analysisChart");
|
||||
if (!chartDom) return;
|
||||
|
||||
|
||||
const myChart = echarts.init(chartDom);
|
||||
const option = {
|
||||
title: {
|
||||
@ -96,7 +107,7 @@ const handleSubmit = () => {
|
||||
};
|
||||
|
||||
// 添加导出处理函数
|
||||
const handleExport = (row) => {
|
||||
const handleExport = (row: AnalysisReport) => {
|
||||
ElMessageBox.confirm(
|
||||
'确认导出该分析报告?',
|
||||
'提示',
|
||||
@ -119,9 +130,9 @@ const handleExport = (row) => {
|
||||
|
||||
// 添加查看报告功能
|
||||
const detailVisible = ref(false);
|
||||
const currentReport = ref(null);
|
||||
const currentReport = ref<AnalysisReport | null>(null);
|
||||
|
||||
const handleView = (row) => {
|
||||
const handleView = (row: AnalysisReport) => {
|
||||
currentReport.value = row;
|
||||
detailVisible.value = true;
|
||||
};
|
||||
@ -157,8 +168,8 @@ const handleView = (row) => {
|
||||
<el-button type="primary" size="small" @click="handleView(row)">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
<el-button
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="handleExport(row)"
|
||||
>
|
||||
@ -260,9 +271,9 @@ const handleView = (row) => {
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
<el-button
|
||||
<el-button
|
||||
type="warning"
|
||||
@click="handleExport(currentReport)"
|
||||
@click="handleExport(currentReport!)"
|
||||
:disabled="!currentReport"
|
||||
>
|
||||
导出
|
||||
@ -274,7 +285,7 @@ const handleView = (row) => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.analysis-report-container {
|
||||
.el-card {
|
||||
@ -331,4 +342,4 @@ const handleView = (row) => {
|
||||
color: $text-secondary;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@ -1,9 +1,21 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, reactive } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
|
||||
const tableData = ref([
|
||||
interface DailyReport {
|
||||
id: number;
|
||||
title: string;
|
||||
reporter: string;
|
||||
department: string;
|
||||
type: string;
|
||||
content: string;
|
||||
attachments: string[];
|
||||
status: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
const tableData = ref<DailyReport[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: "A区巡护日报",
|
||||
@ -30,22 +42,22 @@ const tableData = ref([
|
||||
|
||||
// 搜索和筛选
|
||||
const searchText = ref("");
|
||||
const typeFilter = ref([]);
|
||||
const statusFilter = ref([]);
|
||||
const dateRange = ref(["", ""]);
|
||||
const typeFilter = ref<string[]>([]);
|
||||
const statusFilter = ref<string[]>([]);
|
||||
const dateRange = ref<[string, string]>(["", ""]);
|
||||
|
||||
// 过滤数据
|
||||
const filteredData = computed(() => {
|
||||
return tableData.value.filter(report => {
|
||||
const searchLower = searchText.value.toLowerCase();
|
||||
const textMatch = !searchText.value ||
|
||||
const textMatch = !searchText.value ||
|
||||
report.title.toLowerCase().includes(searchLower) ||
|
||||
report.reporter.toLowerCase().includes(searchLower);
|
||||
|
||||
const typeMatch = typeFilter.value.length === 0 ||
|
||||
const typeMatch = typeFilter.value.length === 0 ||
|
||||
typeFilter.value.includes(report.type);
|
||||
|
||||
const statusMatch = statusFilter.value.length === 0 ||
|
||||
const statusMatch = statusFilter.value.length === 0 ||
|
||||
statusFilter.value.includes(report.status);
|
||||
|
||||
return textMatch && typeMatch && statusMatch;
|
||||
@ -58,7 +70,7 @@ const formData = reactive({
|
||||
title: "",
|
||||
type: "",
|
||||
content: "",
|
||||
attachments: [],
|
||||
attachments: [] as string[],
|
||||
});
|
||||
|
||||
const handleCreate = () => {
|
||||
@ -73,15 +85,15 @@ const handleSubmit = () => {
|
||||
|
||||
// 查看报告
|
||||
const detailVisible = ref(false);
|
||||
const currentReport = ref(null);
|
||||
const currentReport = ref<DailyReport | null>(null);
|
||||
|
||||
const handleView = (row) => {
|
||||
const handleView = (row: DailyReport) => {
|
||||
currentReport.value = row;
|
||||
detailVisible.value = true;
|
||||
};
|
||||
|
||||
// 添加导出处理函数
|
||||
const handleExport = (row) => {
|
||||
const handleExport = (row: DailyReport) => {
|
||||
ElMessageBox.confirm(
|
||||
'确认导出该报告?',
|
||||
'提示',
|
||||
@ -169,15 +181,15 @@ const handleExport = (row) => {
|
||||
<el-button type="primary" size="small" @click="handleView(row)">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
<el-button
|
||||
type="success"
|
||||
size="small"
|
||||
:disabled="row.status === '已审核'"
|
||||
>
|
||||
审核
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
<el-button
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="handleExport(row)"
|
||||
>
|
||||
@ -252,8 +264,8 @@ const handleExport = (row) => {
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="附件" :span="2">
|
||||
<div class="attachment-list">
|
||||
<el-link
|
||||
v-for="file in currentReport.attachments"
|
||||
<el-link
|
||||
v-for="file in currentReport.attachments"
|
||||
:key="file"
|
||||
type="primary"
|
||||
style="margin-right: 16px"
|
||||
@ -267,9 +279,9 @@ const handleExport = (row) => {
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
<el-button
|
||||
<el-button
|
||||
type="warning"
|
||||
@click="handleExport(currentReport)"
|
||||
@click="handleExport(currentReport!)"
|
||||
:disabled="!currentReport"
|
||||
>
|
||||
导出
|
||||
@ -281,7 +293,7 @@ const handleExport = (row) => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.daily-report-container {
|
||||
.el-card {
|
||||
@ -329,4 +341,4 @@ const handleExport = (row) => {
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@ -1,11 +1,22 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import type { UploadProps } from "element-plus";
|
||||
import { markRaw } from 'vue';
|
||||
import { Upload, Download, Refresh } from "@element-plus/icons-vue";
|
||||
|
||||
// 备份记录
|
||||
interface BackupRecord {
|
||||
id: number;
|
||||
name: string;
|
||||
size: string;
|
||||
type: string;
|
||||
status: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
// 模拟备份记录数据
|
||||
const backupRecords = ref([
|
||||
const backupRecords = ref<BackupRecord[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: "系统完整备份_20240320",
|
||||
@ -25,7 +36,7 @@ const backupRecords = ref([
|
||||
]);
|
||||
|
||||
// 数据导入上传配置
|
||||
const uploadConfig = {
|
||||
const uploadConfig: UploadProps = {
|
||||
action: "/api/upload",
|
||||
multiple: true,
|
||||
accept: ".xlsx,.csv",
|
||||
@ -64,7 +75,7 @@ const handleBackup = () => {
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
ElMessage.success("备份任务已启动");
|
||||
const newBackup = {
|
||||
const newBackup: BackupRecord = {
|
||||
id: Date.now(),
|
||||
name: `系统备份_${new Date().toISOString().split("T")[0].replace(/-/g, "")}`,
|
||||
size: "处理中",
|
||||
@ -86,7 +97,7 @@ const handleBackup = () => {
|
||||
};
|
||||
|
||||
// 恢复备份
|
||||
const handleRestore = (record) => {
|
||||
const handleRestore = (record: BackupRecord) => {
|
||||
if (record.status !== "成功") {
|
||||
ElMessage.warning("只能恢复成功的备份");
|
||||
return;
|
||||
@ -102,7 +113,7 @@ const handleRestore = (record) => {
|
||||
};
|
||||
|
||||
// 删除备份
|
||||
const handleDelete = (record) => {
|
||||
const handleDelete = (record: BackupRecord) => {
|
||||
ElMessageBox.confirm("确认删除该备份?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
@ -237,7 +248,7 @@ const icons = {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.data-container {
|
||||
.mb-20 {
|
||||
|
||||
@ -1,11 +1,22 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, onUnmounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import * as echarts from "echarts";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
|
||||
interface Device {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
location: string;
|
||||
status: string;
|
||||
ip: string;
|
||||
lastHeartbeat: string;
|
||||
lastMaintenance: string;
|
||||
}
|
||||
|
||||
// 模拟设备数据
|
||||
const tableData = ref([
|
||||
const tableData = ref<Device[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: "A区-水质监测器-01",
|
||||
@ -39,7 +50,7 @@ const deviceTypes = [
|
||||
// 新增/编辑设备
|
||||
const dialogVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const currentDevice = ref(null);
|
||||
const currentDevice = ref<Device | null>(null);
|
||||
|
||||
const formData = ref({
|
||||
name: "",
|
||||
@ -67,7 +78,7 @@ const handleAdd = () => {
|
||||
};
|
||||
|
||||
// 打开编辑弹窗
|
||||
const handleEdit = (row) => {
|
||||
const handleEdit = (row: Device) => {
|
||||
isEdit.value = true;
|
||||
currentDevice.value = row;
|
||||
formData.value = {
|
||||
@ -88,7 +99,7 @@ const handleSubmit = async () => {
|
||||
if (isEdit.value && currentDevice.value) {
|
||||
// 编辑模式
|
||||
const index = tableData.value.findIndex(
|
||||
(item) => item.id === currentDevice.value.id
|
||||
(item) => item.id === currentDevice.value!.id
|
||||
);
|
||||
if (index !== -1) {
|
||||
tableData.value[index] = {
|
||||
@ -100,7 +111,7 @@ const handleSubmit = async () => {
|
||||
}
|
||||
} else {
|
||||
// 新增模式
|
||||
const newDevice = {
|
||||
const newDevice: Device = {
|
||||
id: tableData.value.length + 1,
|
||||
...formData.value,
|
||||
status: "在线",
|
||||
@ -130,7 +141,7 @@ const resetForm = () => {
|
||||
};
|
||||
|
||||
// 删除设备
|
||||
const handleDelete = (row) => {
|
||||
const handleDelete = (row: Device) => {
|
||||
ElMessageBox.confirm("确认删除该设备?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
@ -147,12 +158,12 @@ const handleDelete = (row) => {
|
||||
};
|
||||
|
||||
// 远程配置
|
||||
const handleConfig = (row) => {
|
||||
const handleConfig = (row: Device) => {
|
||||
ElMessage.success("正在连接设备...");
|
||||
};
|
||||
|
||||
// 设备维护
|
||||
const handleMaintenance = (row) => {
|
||||
const handleMaintenance = (row: Device) => {
|
||||
ElMessage.success("已记录维护时间");
|
||||
const index = tableData.value.findIndex((item) => item.id === row.id);
|
||||
if (index !== -1) {
|
||||
@ -195,8 +206,8 @@ const defaultProps = {
|
||||
|
||||
// 监控相关
|
||||
const monitorVisible = ref(false);
|
||||
const currentMonitorDevice = ref(null);
|
||||
const monitorChart = ref(null);
|
||||
const currentMonitorDevice = ref<Device | null>(null);
|
||||
const monitorChart = ref<echarts.ECharts | null>(null);
|
||||
|
||||
// 模拟实时数据
|
||||
const realTimeData = ref({
|
||||
@ -246,13 +257,13 @@ const handleAddGroup = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleNodeClick = (data) => {
|
||||
const handleNodeClick = (data: any) => {
|
||||
console.log("选中分组:", data);
|
||||
// 这里可以根据分组筛选设备列表
|
||||
};
|
||||
|
||||
// 监控处理
|
||||
const handleMonitor = (device) => {
|
||||
const handleMonitor = (device: Device) => {
|
||||
currentMonitorDevice.value = device;
|
||||
monitorVisible.value = true;
|
||||
initMonitorChart();
|
||||
@ -319,7 +330,7 @@ const initMonitorChart = () => {
|
||||
};
|
||||
|
||||
// 模拟实时数据更新
|
||||
let dataTimer;
|
||||
let dataTimer: number;
|
||||
const startRealTimeData = () => {
|
||||
clearInterval(dataTimer);
|
||||
dataTimer = setInterval(() => {
|
||||
@ -365,7 +376,7 @@ const startRealTimeData = () => {
|
||||
};
|
||||
|
||||
// 检查告警
|
||||
const checkAlarms = (data) => {
|
||||
const checkAlarms = (data: any) => {
|
||||
const { thresholds } = alarmConfig.value;
|
||||
|
||||
if (
|
||||
@ -599,7 +610,7 @@ onUnmounted(() => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.device-container {
|
||||
.group-card {
|
||||
|
||||
@ -1,10 +1,21 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useSystemLogStore } from '../../../stores/systemLog';
|
||||
import { markRaw } from 'vue';
|
||||
import { Download, Delete } from "@element-plus/icons-vue";
|
||||
|
||||
interface LogEntry {
|
||||
id: number;
|
||||
type: string;
|
||||
user: string;
|
||||
action: string;
|
||||
ip: string;
|
||||
status: string;
|
||||
detail: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
const systemLogStore = useSystemLogStore();
|
||||
|
||||
// 日志类型选项
|
||||
@ -23,7 +34,7 @@ const statusOptions = [
|
||||
];
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status) => {
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case "成功":
|
||||
return "success";
|
||||
@ -173,7 +184,7 @@ const icons = {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.logs-container {
|
||||
.card-header {
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
|
||||
const tableData = ref([
|
||||
interface Permission {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
description: string;
|
||||
type: string;
|
||||
status: boolean;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
const tableData = ref<Permission[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: "用户管理",
|
||||
@ -35,7 +45,7 @@ const tableData = ref([
|
||||
// 新增/编辑权限
|
||||
const dialogVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const currentPermission = ref(null);
|
||||
const currentPermission = ref<Permission | null>(null);
|
||||
|
||||
const formData = ref({
|
||||
name: "",
|
||||
@ -63,7 +73,7 @@ const handleAdd = () => {
|
||||
};
|
||||
|
||||
// 打开编辑弹窗
|
||||
const handleEdit = (row) => {
|
||||
const handleEdit = (row: Permission) => {
|
||||
isEdit.value = true;
|
||||
currentPermission.value = row;
|
||||
formData.value = {
|
||||
@ -85,7 +95,7 @@ const handleSubmit = async () => {
|
||||
if (isEdit.value && currentPermission.value) {
|
||||
// 编辑模式:更新表格中的数据
|
||||
const index = tableData.value.findIndex(
|
||||
(item) => item.id === currentPermission.value.id
|
||||
(item) => item.id === currentPermission.value!.id
|
||||
);
|
||||
if (index !== -1) {
|
||||
tableData.value[index] = {
|
||||
@ -96,7 +106,7 @@ const handleSubmit = async () => {
|
||||
}
|
||||
} else {
|
||||
// 新增模式:添加到表格
|
||||
const newPermission = {
|
||||
const newPermission: Permission = {
|
||||
id: tableData.value.length + 1,
|
||||
...formData.value,
|
||||
createTime: new Date().toLocaleString(),
|
||||
@ -132,7 +142,7 @@ const handleDialogClose = () => {
|
||||
};
|
||||
|
||||
// 删除权限
|
||||
const handleDelete = (row) => {
|
||||
const handleDelete = (row: Permission) => {
|
||||
ElMessageBox.confirm("确认删除该权限?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
@ -236,7 +246,7 @@ const handleDelete = (row) => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.permission-container {
|
||||
.el-card {
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const tableData = ref([
|
||||
interface RoleData {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
permissions: string[];
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
const tableData = ref<RoleData[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: "超级管理员",
|
||||
@ -18,11 +26,11 @@ const tableData = ref([
|
||||
},
|
||||
]);
|
||||
|
||||
const handleEdit = (row) => {
|
||||
const handleEdit = (row: RoleData) => {
|
||||
console.log("编辑角色:", row);
|
||||
};
|
||||
|
||||
const handleDelete = (row) => {
|
||||
const handleDelete = (row: RoleData) => {
|
||||
console.log("删除角色:", row);
|
||||
};
|
||||
</script>
|
||||
@ -65,7 +73,7 @@ const handleDelete = (row) => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "../../../styles/variables.scss" as *;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.role-container {
|
||||
.el-card {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
// 系统参数配置
|
||||
@ -31,7 +31,7 @@ const alertConfig = ref({
|
||||
});
|
||||
|
||||
// 保存配置
|
||||
const handleSave = (type) => {
|
||||
const handleSave = (type: string) => {
|
||||
// 这里模拟保存配置
|
||||
console.log(
|
||||
`保存${type}配置:`,
|
||||
@ -174,7 +174,7 @@ const handleSave = (type) => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "../../../styles/variables.scss" as *;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.settings-container {
|
||||
.mb-20 {
|
||||
|
||||
@ -1,260 +1,41 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { getUserList, createUser, updateUser, deleteUser } from "@/api/user";
|
||||
import { formatDateTime } from "@/utils/format";
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const tableData = ref([]);
|
||||
const allData = ref([]); // 保存所有数据
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
interface UserData {
|
||||
id: number;
|
||||
username: string;
|
||||
role: string;
|
||||
email: string;
|
||||
status: boolean;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
// 查询参数
|
||||
const queryParams = ref({
|
||||
username: '',
|
||||
role: '',
|
||||
status: ''
|
||||
});
|
||||
const tableData = ref<UserData[]>([
|
||||
{
|
||||
id: 1,
|
||||
username: "admin",
|
||||
role: "超级管理员",
|
||||
email: "admin@example.com",
|
||||
status: true,
|
||||
createTime: "2024-03-20",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: "manager",
|
||||
role: "管理人员",
|
||||
email: "manager@example.com",
|
||||
status: true,
|
||||
createTime: "2024-03-20",
|
||||
},
|
||||
]);
|
||||
|
||||
// 过滤后的数据
|
||||
const filteredData = computed(() => {
|
||||
return allData.value.filter(item => {
|
||||
// 用户名搜索
|
||||
if (queryParams.value.username &&
|
||||
!item.real_name.toLowerCase().includes(queryParams.value.username.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 角色搜索
|
||||
if (queryParams.value.role &&
|
||||
item.role.name !== queryParams.value.role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 状态搜索
|
||||
if (queryParams.value.status !== '' &&
|
||||
item.status !== queryParams.value.status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).sort((a, b) => a.id - b.id); // 按ID升序排序
|
||||
});
|
||||
|
||||
// 获取用户列表
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getUserList();
|
||||
|
||||
if (res.success) {
|
||||
allData.value = res.data.list; // 保存所有数据
|
||||
tableData.value = filteredData.value; // 显示过滤后的数据
|
||||
total.value = filteredData.value.length;
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取用户列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
ElMessage.error(error.response.data.message || '获取用户列表失败');
|
||||
} else if (error.request) {
|
||||
ElMessage.error('网络错误,请检查网络连接');
|
||||
} else {
|
||||
ElMessage.error(error.message || '获取用户列表失败');
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
const handleEdit = (row: UserData) => {
|
||||
console.log("编辑用户:", row);
|
||||
};
|
||||
|
||||
// 处理查询
|
||||
const handleQuery = () => {
|
||||
tableData.value = filteredData.value;
|
||||
total.value = filteredData.value.length;
|
||||
const handleDelete = (row: UserData) => {
|
||||
console.log("删除用户:", row);
|
||||
};
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryParams.value = {
|
||||
username: '',
|
||||
role: '',
|
||||
status: ''
|
||||
};
|
||||
tableData.value = allData.value;
|
||||
total.value = allData.value.length;
|
||||
};
|
||||
|
||||
// 新增/编辑对话框
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增用户');
|
||||
const formData = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
role_id: undefined,
|
||||
real_name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
status: 1,
|
||||
expire_time: undefined
|
||||
});
|
||||
|
||||
// 表单规则
|
||||
const rules = {
|
||||
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' },
|
||||
{
|
||||
pattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6,30}$/,
|
||||
message: '密码必须包含字母和数字',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
role_id: [
|
||||
{ required: true, message: '请选择角色', trigger: 'change' }
|
||||
],
|
||||
email: [
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/,
|
||||
message: '请输入正确的邮箱格式',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
phone: [
|
||||
{
|
||||
pattern: /^1[3-9]\d{9}$/,
|
||||
message: '请输入正确的手机号格式',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const formRef = ref(null);
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
|
||||
// 打开新增对话框
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新增用户';
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
formData.value = {
|
||||
username: '',
|
||||
password: '',
|
||||
role_id: undefined,
|
||||
real_name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
status: 1,
|
||||
expire_time: undefined
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 打开编辑对话框
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑用户';
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
formData.value = {
|
||||
real_name: row.real_name || '',
|
||||
email: row.email || '',
|
||||
phone: row.phone || '',
|
||||
role_id: row.role.id.toString(),
|
||||
status: row.status.toString(),
|
||||
expire_time: row.expire_time || '',
|
||||
password: '' // 编辑时密码为可选
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
|
||||
const submitData = { ...formData.value };
|
||||
|
||||
// 处理数据格式
|
||||
if (submitData.status) {
|
||||
submitData.status = parseInt(submitData.status); // 转换为数字
|
||||
}
|
||||
if (submitData.role_id) {
|
||||
submitData.role_id = parseInt(submitData.role_id); // 转换为数字
|
||||
}
|
||||
// 如果过期时间为空,则不提交该字段
|
||||
if (!submitData.expire_time) {
|
||||
delete submitData.expire_time;
|
||||
}
|
||||
|
||||
console.log('提交的数据:', submitData); // 打印提交的数据
|
||||
|
||||
if (isEdit.value) {
|
||||
// 编辑时,如果密码为空,则不提交密码字段
|
||||
if (!submitData.password) {
|
||||
delete submitData.password;
|
||||
}
|
||||
await updateUser(editId.value, submitData);
|
||||
ElMessage.success('编辑成功');
|
||||
} else {
|
||||
await createUser(submitData);
|
||||
ElMessage.success('添加成功');
|
||||
}
|
||||
|
||||
dialogVisible.value = false;
|
||||
getList(); // 刷新列表
|
||||
} catch (error) {
|
||||
console.error('表单提交错误:', error);
|
||||
if (error.response) {
|
||||
console.error('错误响应数据:', error.response.data);
|
||||
ElMessage.error(error.response.data.message || '提交失败,请检查表单数据');
|
||||
} else if (error.request) {
|
||||
ElMessage.error('网络错误,请检查网络连接');
|
||||
} else {
|
||||
ElMessage.error(error.message || '操作失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
'确认删除该用户吗?',
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
await deleteUser(row.id);
|
||||
ElMessage.success('删除成功');
|
||||
getList(); // 刷新列表
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '删除失败');
|
||||
}
|
||||
}).catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
};
|
||||
|
||||
// 角色选项
|
||||
const roleOptions = [
|
||||
{ label: '超级管理员', value: '1' },
|
||||
{ label: '管理人员', value: '2' },
|
||||
{ label: '普通用户', value: '3' }
|
||||
];
|
||||
|
||||
// 页面加载时获取列表
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -263,50 +44,14 @@ onMounted(() => {
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户管理</span>
|
||||
<el-button type="primary" @click="handleAdd">新增用户</el-button>
|
||||
<el-button type="primary">新增用户</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :model="queryParams" ref="queryForm" :inline="true" class="search-form">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input
|
||||
v-model="queryParams.username"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" prop="role">
|
||||
<el-select v-model="queryParams.role" placeholder="请选择角色" clearable>
|
||||
<el-option
|
||||
v-for="item in roleOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
|
||||
<el-option :value="1" label="启用" />
|
||||
<el-option :value="0" label="禁用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">查询</el-button>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table :data="tableData" style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="real_name" label="用户名" width="120" />
|
||||
<el-table-column prop="role.name" label="角色" width="120" />
|
||||
<el-table-column prop="username" label="用户名" width="120" />
|
||||
<el-table-column prop="role" label="角色" width="120" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
@ -315,121 +60,24 @@ onMounted(() => {
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">
|
||||
<el-button type="danger" size="small" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
:size="'small'"
|
||||
:total="total"
|
||||
:default-current-page="1"
|
||||
:default-page-size="10"
|
||||
layout="total, prev, pager, next"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="500px"
|
||||
@close="dialogVisible = false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="isEdit ? {
|
||||
...rules,
|
||||
password: [] // 编辑模式下移除密码验证规则
|
||||
} : rules"
|
||||
label-width="100px"
|
||||
class="user-form"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username" v-if="!isEdit">
|
||||
<el-input v-model="formData.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="isEdit ? '新密码' : '密码'"
|
||||
prop="password"
|
||||
>
|
||||
<el-input
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
show-password
|
||||
:placeholder="isEdit ? '不修改请留空' : '请输入密码'"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" prop="role_id">
|
||||
<el-select v-model="formData.role_id" placeholder="请选择角色" class="w-full">
|
||||
<el-option
|
||||
v-for="item in roleOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="真实姓名" prop="real_name">
|
||||
<el-input
|
||||
v-model="formData.real_name"
|
||||
placeholder="请输入真实姓名"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="formData.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="formData.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="'1'">启用</el-radio>
|
||||
<el-radio :value="'0'">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="过期时间" prop="expire_time">
|
||||
<el-date-picker
|
||||
v-model="formData.expire_time"
|
||||
type="datetime"
|
||||
placeholder="请选择过期时间"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "../../../styles/variables.scss" as *;
|
||||
@import "../../../styles/variables.scss";
|
||||
|
||||
.user-container {
|
||||
.el-card {
|
||||
@ -457,25 +105,6 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 16px;
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-select) {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
th.el-table__cell {
|
||||
background-color: #fafafa;
|
||||
@ -491,23 +120,4 @@ onMounted(() => {
|
||||
.el-tag {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.user-form {
|
||||
.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 20px 40px;
|
||||
}
|
||||
</style>
|
||||
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
14
tsconfig.app.json
Normal file
14
tsconfig.app.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
24
tsconfig.node.json
Normal file
24
tsconfig.node.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@ -9,12 +9,5 @@ export default defineConfig({
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: path.resolve(__dirname, 'index.html')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user