更新项目文件结构,统一文档风格
This commit is contained in:
4047
admin-system/dashboard/dist/assets/index-da04cff0.js
vendored
Normal file
4047
admin-system/dashboard/dist/assets/index-da04cff0.js
vendored
Normal file
File diff suppressed because one or more lines are too long
138
admin-system/dashboard/dist/assets/index-e21ede74.css
vendored
Normal file
138
admin-system/dashboard/dist/assets/index-e21ede74.css
vendored
Normal file
File diff suppressed because one or more lines are too long
15
admin-system/dashboard/dist/index.html
vendored
Normal file
15
admin-system/dashboard/dist/index.html
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>养殖管理系统 - 锡林郭勒盟智慧养殖数字化管理平台</title>
|
||||
<script type="module" crossorigin src="/assets/index-da04cff0.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-e21ede74.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +1,62 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<nav class="main-nav">
|
||||
<router-link to="/" class="nav-item">首页</router-link>
|
||||
<router-link to="/monitor" class="nav-item">监控中心</router-link>
|
||||
<router-link to="/government" class="nav-item">政府平台</router-link>
|
||||
<router-link to="/finance" class="nav-item">金融服务</router-link>
|
||||
<router-link to="/transport" class="nav-item">运输跟踪</router-link>
|
||||
<router-link to="/risk" class="nav-item">风险预警</router-link>
|
||||
<router-link to="/eco" class="nav-item">生态指标</router-link>
|
||||
<router-link to="/gov" class="nav-item">政府监管</router-link>
|
||||
<router-link to="/trade" class="nav-item">交易统计</router-link>
|
||||
<!-- 只在登录后显示导航栏 -->
|
||||
<nav v-if="authStore.isAuthenticated && $route.path !== '/login'" class="main-nav">
|
||||
<div class="nav-left">
|
||||
<router-link to="/" class="nav-item">首页</router-link>
|
||||
<router-link to="/monitor" class="nav-item">监控中心</router-link>
|
||||
<router-link to="/government" class="nav-item">政府平台</router-link>
|
||||
<router-link to="/finance" class="nav-item">金融服务</router-link>
|
||||
<router-link to="/transport" class="nav-item">运输跟踪</router-link>
|
||||
<router-link to="/risk" class="nav-item">风险预警</router-link>
|
||||
<router-link to="/eco" class="nav-item">生态指标</router-link>
|
||||
<router-link to="/gov" class="nav-item">政府监管</router-link>
|
||||
<router-link to="/trade" class="nav-item">交易统计</router-link>
|
||||
<router-link to="/users" class="nav-item">用户管理</router-link>
|
||||
</div>
|
||||
|
||||
<div class="nav-right">
|
||||
<span class="user-info">
|
||||
欢迎,{{ authStore.realName || authStore.username }}
|
||||
</span>
|
||||
<a-button type="text" @click="handleLogout" class="logout-btn">
|
||||
退出登录
|
||||
</a-button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAuthStore } from './store/auth.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
export default {
|
||||
name: 'App'
|
||||
name: 'App',
|
||||
setup() {
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
// 处理登出
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await authStore.logout()
|
||||
message.success('退出登录成功')
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
console.error('登出失败:', error)
|
||||
message.error('登出失败')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
authStore,
|
||||
handleLogout
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -33,12 +72,41 @@ export default {
|
||||
padding: 15px 20px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.nav-left {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
color: #fff !important;
|
||||
border-color: #ff4d4f !important;
|
||||
background: rgba(255, 77, 79, 0.2) !important;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
|
||||
116
admin-system/dashboard/src/components/ApiTest.vue
Normal file
116
admin-system/dashboard/src/components/ApiTest.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="api-test-container">
|
||||
<a-card title="API连接测试" style="margin-bottom: 20px;">
|
||||
<div class="test-buttons">
|
||||
<a-button @click="testHealth" :loading="healthLoading" type="primary">
|
||||
测试服务器健康状态
|
||||
</a-button>
|
||||
<a-button @click="testMapData" :loading="mapLoading">
|
||||
测试地图数据
|
||||
</a-button>
|
||||
<a-button @click="testLogin" :loading="loginLoading">
|
||||
测试登录功能
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="test-results">
|
||||
<h4>测试结果:</h4>
|
||||
<pre class="result-output">{{ testResults }}</pre>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { systemAPI, dashboardAPI, authAPI } from '../services/api.js';
|
||||
|
||||
const healthLoading = ref(false);
|
||||
const mapLoading = ref(false);
|
||||
const loginLoading = ref(false);
|
||||
const testResults = ref('等待测试...');
|
||||
|
||||
// 测试服务器健康状态
|
||||
const testHealth = async () => {
|
||||
healthLoading.value = true;
|
||||
try {
|
||||
const response = await systemAPI.getHealth();
|
||||
testResults.value = JSON.stringify(response, null, 2);
|
||||
message.success('健康检查成功');
|
||||
} catch (error) {
|
||||
testResults.value = `健康检查失败: ${error.message}`;
|
||||
message.error('健康检查失败');
|
||||
} finally {
|
||||
healthLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 测试地图数据
|
||||
const testMapData = async () => {
|
||||
mapLoading.value = true;
|
||||
try {
|
||||
const response = await dashboardAPI.getMapRegions();
|
||||
testResults.value = JSON.stringify(response, null, 2);
|
||||
message.success('地图数据获取成功');
|
||||
} catch (error) {
|
||||
testResults.value = `地图数据获取失败: ${error.message}`;
|
||||
message.error('地图数据获取失败');
|
||||
} finally {
|
||||
mapLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 测试登录功能
|
||||
const testLogin = async () => {
|
||||
loginLoading.value = true;
|
||||
try {
|
||||
const response = await authAPI.login({
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
});
|
||||
testResults.value = JSON.stringify(response, null, 2);
|
||||
message.success('登录测试成功');
|
||||
} catch (error) {
|
||||
testResults.value = `登录测试失败: ${error.message}`;
|
||||
message.error('登录测试失败');
|
||||
} finally {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.api-test-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.test-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.test-results {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.test-results h4 {
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.result-output {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
134
admin-system/dashboard/src/components/StatsCard.vue
Normal file
134
admin-system/dashboard/src/components/StatsCard.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="stats-card">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6" v-for="(stat, index) in stats" :key="index">
|
||||
<a-card :bordered="false" class="stat-item">
|
||||
<a-statistic
|
||||
:title="stat.title"
|
||||
:value="stat.value"
|
||||
:prefix="stat.prefix"
|
||||
:suffix="stat.suffix"
|
||||
:value-style="{ color: stat.color }"
|
||||
/>
|
||||
<div class="stat-extra">
|
||||
<span :class="['trend', stat.trend]">
|
||||
{{ stat.trend === 'up' ? '↗' : '↘' }} {{ stat.change }}%
|
||||
</span>
|
||||
<span class="compare">较昨日</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { cattleAPI, financeAPI, tradingAPI, mallAPI } from '../services/api.js';
|
||||
|
||||
const stats = ref([
|
||||
{ title: '总牛只数量', value: 0, suffix: '头', color: '#3f8600', trend: 'up', change: 0 },
|
||||
{ title: '总产值', value: 0, prefix: '¥', suffix: '万', color: '#cf1322', trend: 'up', change: 0 },
|
||||
{ title: '活跃交易', value: 0, suffix: '笔', color: '#1890ff', trend: 'up', change: 0 },
|
||||
{ title: '在线用户', value: 0, suffix: '人', color: '#722ed1', trend: 'up', change: 0 },
|
||||
]);
|
||||
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
// 并发请求各模块数据
|
||||
const [cattleData, financeData, tradingData, mallData] = await Promise.allSettled([
|
||||
cattleAPI.getStatistics(),
|
||||
financeAPI.getStatistics(),
|
||||
tradingAPI.getStatistics(),
|
||||
mallAPI.getStatistics(),
|
||||
]);
|
||||
|
||||
// 更新统计数据
|
||||
if (cattleData.status === 'fulfilled' && cattleData.value.success) {
|
||||
const data = cattleData.value.data;
|
||||
stats.value[0].value = data.total_cattle || 0;
|
||||
stats.value[0].change = Math.random() * 10; // 模拟变化率
|
||||
}
|
||||
|
||||
if (financeData.status === 'fulfilled' && financeData.value.success) {
|
||||
const data = financeData.value.data;
|
||||
stats.value[1].value = Math.round((data.total_loan_amount || 0) / 10000);
|
||||
stats.value[1].change = Math.random() * 8;
|
||||
}
|
||||
|
||||
if (tradingData.status === 'fulfilled' && tradingData.value.success) {
|
||||
const data = tradingData.value.data;
|
||||
stats.value[2].value = data.total_transactions || 0;
|
||||
stats.value[2].change = Math.random() * 12;
|
||||
}
|
||||
|
||||
if (mallData.status === 'fulfilled' && mallData.value.success) {
|
||||
const data = mallData.value.data;
|
||||
stats.value[3].value = data.active_users || 0;
|
||||
stats.value[3].change = Math.random() * 5;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadStats();
|
||||
// 每30秒更新一次数据
|
||||
setInterval(loadStats, 30000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stats-card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-item :deep(.ant-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stat-item :deep(.ant-statistic-title) {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-item :deep(.ant-statistic-content) {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stat-extra {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.trend {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.trend.up {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.trend.down {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.compare {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
</style>
|
||||
@@ -3,15 +3,20 @@ import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { useAuthStore } from './store/auth.js'
|
||||
import 'ant-design-vue/dist/antd.css'
|
||||
import './styles/global.css'
|
||||
|
||||
// DataV组件按需引入,避免Vue 3兼容性问题
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(Antd)
|
||||
|
||||
// 初始化认证状态
|
||||
const authStore = useAuthStore()
|
||||
authStore.initAuth()
|
||||
|
||||
app.mount('#app')
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useAuthStore } from '../store/auth.js'
|
||||
import Dashboard from '@/views/Dashboard.vue'
|
||||
import Monitor from '@/views/Monitor.vue'
|
||||
import Government from '@/views/Government.vue'
|
||||
@@ -8,52 +9,89 @@ import Risk from '@/views/Risk.vue'
|
||||
import Eco from '@/views/Eco.vue'
|
||||
import Gov from '@/views/Gov.vue'
|
||||
import Trade from '@/views/Trade.vue'
|
||||
import Login from '@/views/Login.vue'
|
||||
import UserManagement from '@/views/UserManagement.vue'
|
||||
import CattleManagement from '@/views/CattleManagement.vue'
|
||||
import MallManagement from '@/views/MallManagement.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login,
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'Dashboard',
|
||||
component: Dashboard
|
||||
component: Dashboard,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/monitor',
|
||||
name: 'Monitor',
|
||||
component: Monitor
|
||||
component: Monitor,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/government',
|
||||
name: 'Government',
|
||||
component: Government
|
||||
component: Government,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/finance',
|
||||
name: 'Finance',
|
||||
component: Finance
|
||||
component: Finance,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/transport',
|
||||
name: 'Transport',
|
||||
component: Transport
|
||||
component: Transport,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/risk',
|
||||
name: 'Risk',
|
||||
component: Risk
|
||||
component: Risk,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/eco',
|
||||
name: 'Eco',
|
||||
component: Eco
|
||||
component: Eco,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/gov',
|
||||
name: 'Gov',
|
||||
component: Gov
|
||||
component: Gov,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/trade',
|
||||
name: 'Trade',
|
||||
component: Trade
|
||||
component: Trade,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
name: 'UserManagement',
|
||||
component: UserManagement,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/cattle',
|
||||
name: 'CattleManagement',
|
||||
component: CattleManagement,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/mall',
|
||||
name: 'MallManagement',
|
||||
component: MallManagement,
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -62,4 +100,26 @@ const router = createRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 初始化认证状态
|
||||
if (!authStore.isAuthenticated) {
|
||||
authStore.initAuth()
|
||||
}
|
||||
|
||||
// 检查是否需要认证
|
||||
if (to.meta.requiresAuth !== false && !authStore.isAuthenticated) {
|
||||
// 需要认证但未登录,跳转到登录页
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && authStore.isAuthenticated) {
|
||||
// 已登录用户访问登录页,跳转到首页
|
||||
next('/')
|
||||
} else {
|
||||
// 正常访问
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
268
admin-system/dashboard/src/services/api.js
Normal file
268
admin-system/dashboard/src/services/api.js
Normal file
@@ -0,0 +1,268 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// API配置
|
||||
const API_BASE_URL = 'http://localhost:8889';
|
||||
const API_VERSION = '/api/v1';
|
||||
|
||||
// 创建axios实例
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_BASE_URL + API_VERSION,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// 请求拦截器 - 添加认证token
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器 - 处理错误
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => {
|
||||
return response.data;
|
||||
},
|
||||
(error) => {
|
||||
console.error('API请求错误:', error);
|
||||
|
||||
// 处理认证错误
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user_info');
|
||||
// 可以在这里跳转到登录页
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// ======================================
|
||||
// 认证相关API
|
||||
// ======================================
|
||||
export const authAPI = {
|
||||
// 用户登录
|
||||
login: (credentials) => apiClient.post('/auth/login', credentials),
|
||||
|
||||
// 获取用户信息
|
||||
getProfile: () => apiClient.get('/auth/profile'),
|
||||
|
||||
// 获取用户权限
|
||||
getPermissions: () => apiClient.get('/auth/permissions'),
|
||||
|
||||
// 用户注册
|
||||
register: (userData) => apiClient.post('/auth/register', userData),
|
||||
|
||||
// 刷新token
|
||||
refreshToken: () => apiClient.post('/auth/refresh'),
|
||||
|
||||
// 用户登出
|
||||
logout: () => apiClient.post('/auth/logout'),
|
||||
};
|
||||
|
||||
// ======================================
|
||||
// 用户管理API
|
||||
// ======================================
|
||||
export const userAPI = {
|
||||
// 获取用户列表
|
||||
getUsers: (params) => apiClient.get('/users', { params }),
|
||||
|
||||
// 创建用户
|
||||
createUser: (userData) => apiClient.post('/users', userData),
|
||||
|
||||
// 更新用户
|
||||
updateUser: (id, userData) => apiClient.put(`/users/${id}`, userData),
|
||||
|
||||
// 删除用户
|
||||
deleteUser: (id) => apiClient.delete(`/users/${id}`),
|
||||
|
||||
// 获取角色列表
|
||||
getRoles: () => apiClient.get('/users/roles'),
|
||||
|
||||
// 获取权限列表
|
||||
getPermissions: () => apiClient.get('/users/permissions'),
|
||||
};
|
||||
|
||||
// ======================================
|
||||
// 牛只档案API
|
||||
// ======================================
|
||||
export const cattleAPI = {
|
||||
// 获取牛只列表
|
||||
getCattle: (params) => apiClient.get('/cattle', { params }),
|
||||
|
||||
// 获取牛只详情
|
||||
getCattleDetail: (id) => apiClient.get(`/cattle/${id}`),
|
||||
|
||||
// 创建牛只档案
|
||||
createCattle: (cattleData) => apiClient.post('/cattle', cattleData),
|
||||
|
||||
// 更新牛只信息
|
||||
updateCattle: (id, cattleData) => apiClient.put(`/cattle/${id}`, cattleData),
|
||||
|
||||
// 删除牛只档案
|
||||
deleteCattle: (id) => apiClient.delete(`/cattle/${id}`),
|
||||
|
||||
// 获取饲养记录
|
||||
getFeedingRecords: (cattleId, params) => apiClient.get(`/cattle/${cattleId}/feeding`, { params }),
|
||||
|
||||
// 添加饲养记录
|
||||
addFeedingRecord: (cattleId, recordData) => apiClient.post(`/cattle/${cattleId}/feeding`, recordData),
|
||||
|
||||
// 获取统计数据
|
||||
getStatistics: () => apiClient.get('/cattle/statistics'),
|
||||
};
|
||||
|
||||
// ======================================
|
||||
// 金融服务API
|
||||
// ======================================
|
||||
export const financeAPI = {
|
||||
// 贷款管理
|
||||
getLoans: (params) => apiClient.get('/finance/loans', { params }),
|
||||
getLoanDetail: (id) => apiClient.get(`/finance/loans/${id}`),
|
||||
createLoan: (loanData) => apiClient.post('/finance/loans', loanData),
|
||||
updateLoanStatus: (id, statusData) => apiClient.put(`/finance/loans/${id}/status`, statusData),
|
||||
|
||||
// 保险管理
|
||||
getInsurance: (params) => apiClient.get('/finance/insurance', { params }),
|
||||
getInsuranceDetail: (id) => apiClient.get(`/finance/insurance/${id}`),
|
||||
createInsurance: (insuranceData) => apiClient.post('/finance/insurance', insuranceData),
|
||||
|
||||
// 理赔管理
|
||||
getClaims: (params) => apiClient.get('/finance/claims', { params }),
|
||||
createClaim: (claimData) => apiClient.post('/finance/claims', claimData),
|
||||
|
||||
// 统计数据
|
||||
getStatistics: () => apiClient.get('/finance/statistics'),
|
||||
};
|
||||
|
||||
// ======================================
|
||||
// 交易管理API
|
||||
// ======================================
|
||||
export const tradingAPI = {
|
||||
// 交易记录
|
||||
getTransactions: (params) => apiClient.get('/trading/transactions', { params }),
|
||||
getTransactionDetail: (id) => apiClient.get(`/trading/transactions/${id}`),
|
||||
createTransaction: (transactionData) => apiClient.post('/trading/transactions', transactionData),
|
||||
updateTransactionStatus: (id, statusData) => apiClient.put(`/trading/transactions/${id}/status`, statusData),
|
||||
|
||||
// 合同管理
|
||||
getContracts: (params) => apiClient.get('/trading/contracts', { params }),
|
||||
getContractDetail: (id) => apiClient.get(`/trading/contracts/${id}`),
|
||||
createContract: (contractData) => apiClient.post('/trading/contracts', contractData),
|
||||
|
||||
// 统计数据
|
||||
getStatistics: () => apiClient.get('/trading/statistics'),
|
||||
};
|
||||
|
||||
// ======================================
|
||||
// 政府监管API
|
||||
// ======================================
|
||||
export const governmentAPI = {
|
||||
// 牧场监管
|
||||
getFarmSupervision: (params) => apiClient.get('/government/farms/supervision', { params }),
|
||||
|
||||
// 检查记录
|
||||
getInspections: (params) => apiClient.get('/government/inspections', { params }),
|
||||
createInspection: (inspectionData) => apiClient.post('/government/inspections', inspectionData),
|
||||
|
||||
// 质量追溯
|
||||
getTraceability: (productId) => apiClient.get(`/government/traceability/${productId}`),
|
||||
|
||||
// 政策法规
|
||||
getPolicies: (params) => apiClient.get('/government/policies', { params }),
|
||||
|
||||
// 统计数据
|
||||
getStatistics: () => apiClient.get('/government/statistics'),
|
||||
|
||||
// 生成报告
|
||||
generateReport: (reportData) => apiClient.post('/government/reports', reportData),
|
||||
};
|
||||
|
||||
// ======================================
|
||||
// 商城管理API
|
||||
// ======================================
|
||||
export const mallAPI = {
|
||||
// 商品管理
|
||||
getProducts: (params) => apiClient.get('/mall/products', { params }),
|
||||
getProductDetail: (id) => apiClient.get(`/mall/products/${id}`),
|
||||
createProduct: (productData) => apiClient.post('/mall/products', productData),
|
||||
updateProduct: (id, productData) => apiClient.put(`/mall/products/${id}`, productData),
|
||||
deleteProduct: (id) => apiClient.delete(`/mall/products/${id}`),
|
||||
|
||||
// 订单管理
|
||||
getOrders: (params) => apiClient.get('/mall/orders', { params }),
|
||||
getOrderDetail: (id) => apiClient.get(`/mall/orders/${id}`),
|
||||
createOrder: (orderData) => apiClient.post('/mall/orders', orderData),
|
||||
updateOrderStatus: (id, statusData) => apiClient.put(`/mall/orders/${id}/status`, statusData),
|
||||
|
||||
// 商品评价
|
||||
getProductReviews: (productId, params) => apiClient.get(`/mall/products/${productId}/reviews`, { params }),
|
||||
|
||||
// 统计数据
|
||||
getStatistics: () => apiClient.get('/mall/statistics'),
|
||||
};
|
||||
|
||||
// ======================================
|
||||
// 大屏数据API
|
||||
// ======================================
|
||||
export const dashboardAPI = {
|
||||
// 概览数据
|
||||
getOverview: () => apiClient.get('/dashboard/overview'),
|
||||
|
||||
// 实时数据
|
||||
getRealtime: () => apiClient.get('/dashboard/realtime'),
|
||||
|
||||
// 地图数据
|
||||
getMapRegions: () => apiClient.get('/dashboard/map/regions'),
|
||||
getRegionDetail: (regionId) => apiClient.get(`/dashboard/map/region/${regionId}`),
|
||||
|
||||
// 各模块数据
|
||||
getFarmData: () => cattleAPI.getStatistics(),
|
||||
getFinanceData: () => financeAPI.getStatistics(),
|
||||
getTradingData: () => tradingAPI.getStatistics(),
|
||||
getGovernmentData: () => governmentAPI.getStatistics(),
|
||||
getMallData: () => mallAPI.getStatistics(),
|
||||
};
|
||||
|
||||
// ======================================
|
||||
// 系统管理API
|
||||
// ======================================
|
||||
export const systemAPI = {
|
||||
// 健康检查
|
||||
getHealth: () => axios.get(`${API_BASE_URL}/health`),
|
||||
|
||||
// 数据库状态
|
||||
getDatabaseStatus: () => apiClient.get('/database/status'),
|
||||
|
||||
// 数据库表信息
|
||||
getDatabaseTables: () => apiClient.get('/database/tables'),
|
||||
|
||||
// 操作日志
|
||||
getOperationLogs: (params) => apiClient.get('/logs/operations', { params }),
|
||||
};
|
||||
|
||||
// 导出所有API
|
||||
export default {
|
||||
auth: authAPI,
|
||||
user: userAPI,
|
||||
cattle: cattleAPI,
|
||||
finance: financeAPI,
|
||||
trading: tradingAPI,
|
||||
government: governmentAPI,
|
||||
mall: mallAPI,
|
||||
dashboard: dashboardAPI,
|
||||
system: systemAPI,
|
||||
};
|
||||
|
||||
// 导出axios实例供其他地方使用
|
||||
export { apiClient };
|
||||
@@ -1,11 +1,10 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_BASE_URL = 'http://localhost:8000/api/v1/dashboard';
|
||||
import { dashboardAPI } from './api.js';
|
||||
|
||||
// 使用新的API服务
|
||||
export const fetchOverviewData = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/overview`);
|
||||
return response.data;
|
||||
const response = await dashboardAPI.getOverview();
|
||||
return response.data || {};
|
||||
} catch (error) {
|
||||
console.error('Error fetching overview data:', error);
|
||||
return {};
|
||||
@@ -14,8 +13,8 @@ export const fetchOverviewData = async () => {
|
||||
|
||||
export const fetchRealtimeData = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/realtime`);
|
||||
return response.data;
|
||||
const response = await dashboardAPI.getRealtime();
|
||||
return response.data || {};
|
||||
} catch (error) {
|
||||
console.error('Error fetching realtime data:', error);
|
||||
return {};
|
||||
@@ -24,8 +23,8 @@ export const fetchRealtimeData = async () => {
|
||||
|
||||
export const fetchFarmData = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/farm`);
|
||||
return response.data;
|
||||
const response = await dashboardAPI.getFarmData();
|
||||
return response.data || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching farm data:', error);
|
||||
return [];
|
||||
@@ -34,8 +33,8 @@ export const fetchFarmData = async () => {
|
||||
|
||||
export const fetchGovernmentData = async (type) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/government/${type}`);
|
||||
return response.data;
|
||||
const response = await dashboardAPI.getGovernmentData();
|
||||
return response.data || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching government data:', error);
|
||||
return [];
|
||||
@@ -44,8 +43,8 @@ export const fetchGovernmentData = async (type) => {
|
||||
|
||||
export const fetchFinanceData = async (type) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/finance/${type}`);
|
||||
return response.data;
|
||||
const response = await dashboardAPI.getFinanceData();
|
||||
return response.data || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching finance data:', error);
|
||||
return [];
|
||||
@@ -54,8 +53,8 @@ export const fetchFinanceData = async (type) => {
|
||||
|
||||
export const fetchMapData = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/map/regions`);
|
||||
return response.data;
|
||||
const response = await dashboardAPI.getMapRegions();
|
||||
return response.regions || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching map data:', error);
|
||||
return [];
|
||||
@@ -64,8 +63,8 @@ export const fetchMapData = async () => {
|
||||
|
||||
export const fetchRegionDetail = async (regionId) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/map/region/${regionId}`);
|
||||
return response.data;
|
||||
const response = await dashboardAPI.getRegionDetail(regionId);
|
||||
return response || {};
|
||||
} catch (error) {
|
||||
console.error('Error fetching region detail:', error);
|
||||
return {};
|
||||
|
||||
154
admin-system/dashboard/src/store/auth.js
Normal file
154
admin-system/dashboard/src/store/auth.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { authAPI } from '../services/api.js';
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
user: null,
|
||||
token: localStorage.getItem('auth_token'),
|
||||
permissions: [],
|
||||
isAuthenticated: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 检查用户是否有特定权限
|
||||
hasPermission: (state) => (permission) => {
|
||||
return state.permissions.includes(permission);
|
||||
},
|
||||
|
||||
// 检查用户是否有任一权限
|
||||
hasAnyPermission: (state) => (permissions) => {
|
||||
return permissions.some(permission => state.permissions.includes(permission));
|
||||
},
|
||||
|
||||
// 检查用户是否有所有权限
|
||||
hasAllPermissions: (state) => (permissions) => {
|
||||
return permissions.every(permission => state.permissions.includes(permission));
|
||||
},
|
||||
|
||||
// 获取用户类型
|
||||
userType: (state) => state.user?.user_type,
|
||||
|
||||
// 获取用户名
|
||||
username: (state) => state.user?.username,
|
||||
|
||||
// 获取真实姓名
|
||||
realName: (state) => state.user?.real_name,
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 用户登录
|
||||
async login(credentials) {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await authAPI.login(credentials);
|
||||
|
||||
if (response.success) {
|
||||
const { token, user } = response.data;
|
||||
|
||||
// 保存token和用户信息
|
||||
this.token = token;
|
||||
this.user = user;
|
||||
this.isAuthenticated = true;
|
||||
|
||||
// 存储到localStorage
|
||||
localStorage.setItem('auth_token', token);
|
||||
localStorage.setItem('user_info', JSON.stringify(user));
|
||||
|
||||
// 获取用户权限
|
||||
await this.loadPermissions();
|
||||
|
||||
return { success: true };
|
||||
} else {
|
||||
this.error = response.message || '登录失败';
|
||||
return { success: false, message: this.error };
|
||||
}
|
||||
} catch (error) {
|
||||
this.error = error.response?.data?.message || '登录失败,请检查网络连接';
|
||||
return { success: false, message: this.error };
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取用户权限
|
||||
async loadPermissions() {
|
||||
try {
|
||||
const response = await authAPI.getPermissions();
|
||||
if (response.success) {
|
||||
this.permissions = response.data.permissions || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取权限失败:', error);
|
||||
this.permissions = [];
|
||||
}
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
async loadProfile() {
|
||||
try {
|
||||
const response = await authAPI.getProfile();
|
||||
if (response.success) {
|
||||
this.user = response.data;
|
||||
localStorage.setItem('user_info', JSON.stringify(this.user));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 用户登出
|
||||
async logout() {
|
||||
try {
|
||||
await authAPI.logout();
|
||||
} catch (error) {
|
||||
console.error('登出失败:', error);
|
||||
} finally {
|
||||
// 清除本地数据
|
||||
this.user = null;
|
||||
this.token = null;
|
||||
this.permissions = [];
|
||||
this.isAuthenticated = false;
|
||||
this.error = null;
|
||||
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user_info');
|
||||
}
|
||||
},
|
||||
|
||||
// 初始化认证状态
|
||||
initAuth() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const userInfo = localStorage.getItem('user_info');
|
||||
|
||||
if (token && userInfo) {
|
||||
try {
|
||||
this.token = token;
|
||||
this.user = JSON.parse(userInfo);
|
||||
this.isAuthenticated = true;
|
||||
|
||||
// 重新获取权限
|
||||
this.loadPermissions();
|
||||
} catch (error) {
|
||||
console.error('初始化认证状态失败:', error);
|
||||
this.clearAuth();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 清除认证状态
|
||||
clearAuth() {
|
||||
this.user = null;
|
||||
this.token = null;
|
||||
this.permissions = [];
|
||||
this.isAuthenticated = false;
|
||||
this.error = null;
|
||||
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user_info');
|
||||
},
|
||||
},
|
||||
});
|
||||
223
admin-system/dashboard/src/store/dashboard.js
Normal file
223
admin-system/dashboard/src/store/dashboard.js
Normal file
@@ -0,0 +1,223 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { dashboardAPI } from '../services/api.js';
|
||||
|
||||
export const useDashboardStore = defineStore('dashboard', {
|
||||
state: () => ({
|
||||
// 概览数据
|
||||
overview: {
|
||||
totalCattle: 0,
|
||||
totalFarms: 0,
|
||||
totalValue: 0,
|
||||
monthlyGrowth: 0,
|
||||
loading: false,
|
||||
},
|
||||
|
||||
// 实时数据
|
||||
realtime: {
|
||||
activeTransactions: 0,
|
||||
onlineUsers: 0,
|
||||
systemStatus: 'normal',
|
||||
lastUpdate: null,
|
||||
loading: false,
|
||||
},
|
||||
|
||||
// 地图数据
|
||||
mapData: {
|
||||
regions: [],
|
||||
selectedRegion: null,
|
||||
loading: false,
|
||||
},
|
||||
|
||||
// 各模块统计数据
|
||||
statistics: {
|
||||
cattle: null,
|
||||
finance: null,
|
||||
trading: null,
|
||||
government: null,
|
||||
mall: null,
|
||||
loading: false,
|
||||
},
|
||||
|
||||
// 错误状态
|
||||
error: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 获取总览卡片数据
|
||||
overviewCards: (state) => [
|
||||
{
|
||||
title: '总牛只数量',
|
||||
value: state.overview.totalCattle,
|
||||
unit: '头',
|
||||
icon: 'cattle',
|
||||
trend: 'up',
|
||||
change: '+12%',
|
||||
},
|
||||
{
|
||||
title: '注册牧场',
|
||||
value: state.overview.totalFarms,
|
||||
unit: '个',
|
||||
icon: 'farm',
|
||||
trend: 'up',
|
||||
change: '+8%',
|
||||
},
|
||||
{
|
||||
title: '总产值',
|
||||
value: state.overview.totalValue,
|
||||
unit: '万元',
|
||||
icon: 'money',
|
||||
trend: 'up',
|
||||
change: '+15%',
|
||||
},
|
||||
{
|
||||
title: '月增长率',
|
||||
value: state.overview.monthlyGrowth,
|
||||
unit: '%',
|
||||
icon: 'growth',
|
||||
trend: 'up',
|
||||
change: '+2.3%',
|
||||
},
|
||||
],
|
||||
|
||||
// 地图区域数据
|
||||
mapRegions: (state) => state.mapData.regions,
|
||||
|
||||
// 选中的区域详情
|
||||
selectedRegionDetail: (state) => state.mapData.selectedRegion,
|
||||
|
||||
// 系统状态指示器
|
||||
systemStatus: (state) => ({
|
||||
status: state.realtime.systemStatus,
|
||||
color: state.realtime.systemStatus === 'normal' ? 'green' :
|
||||
state.realtime.systemStatus === 'warning' ? 'orange' : 'red',
|
||||
text: state.realtime.systemStatus === 'normal' ? '正常' :
|
||||
state.realtime.systemStatus === 'warning' ? '警告' : '异常',
|
||||
}),
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 加载概览数据
|
||||
async loadOverview() {
|
||||
this.overview.loading = true;
|
||||
try {
|
||||
const response = await dashboardAPI.getOverview();
|
||||
if (response.success) {
|
||||
this.overview = {
|
||||
...this.overview,
|
||||
...response.data,
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载概览数据失败:', error);
|
||||
this.error = '加载概览数据失败';
|
||||
} finally {
|
||||
this.overview.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 加载实时数据
|
||||
async loadRealtime() {
|
||||
this.realtime.loading = true;
|
||||
try {
|
||||
const response = await dashboardAPI.getRealtime();
|
||||
if (response.success) {
|
||||
this.realtime = {
|
||||
...this.realtime,
|
||||
...response.data,
|
||||
lastUpdate: new Date(),
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载实时数据失败:', error);
|
||||
this.error = '加载实时数据失败';
|
||||
} finally {
|
||||
this.realtime.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 加载地图数据
|
||||
async loadMapData() {
|
||||
this.mapData.loading = true;
|
||||
try {
|
||||
const response = await dashboardAPI.getMapRegions();
|
||||
if (response.regions) {
|
||||
this.mapData.regions = response.regions;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载地图数据失败:', error);
|
||||
this.error = '加载地图数据失败';
|
||||
} finally {
|
||||
this.mapData.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 选择地图区域
|
||||
async selectRegion(regionId) {
|
||||
try {
|
||||
const response = await dashboardAPI.getRegionDetail(regionId);
|
||||
this.mapData.selectedRegion = response;
|
||||
} catch (error) {
|
||||
console.error('加载区域详情失败:', error);
|
||||
this.error = '加载区域详情失败';
|
||||
}
|
||||
},
|
||||
|
||||
// 加载统计数据
|
||||
async loadStatistics() {
|
||||
this.statistics.loading = true;
|
||||
try {
|
||||
const [cattle, finance, trading, government, mall] = await Promise.all([
|
||||
dashboardAPI.getFarmData(),
|
||||
dashboardAPI.getFinanceData(),
|
||||
dashboardAPI.getTradingData(),
|
||||
dashboardAPI.getGovernmentData(),
|
||||
dashboardAPI.getMallData(),
|
||||
]);
|
||||
|
||||
this.statistics = {
|
||||
cattle: cattle.data,
|
||||
finance: finance.data,
|
||||
trading: trading.data,
|
||||
government: government.data,
|
||||
mall: mall.data,
|
||||
loading: false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败:', error);
|
||||
this.error = '加载统计数据失败';
|
||||
} finally {
|
||||
this.statistics.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 初始化大屏数据
|
||||
async initDashboard() {
|
||||
await Promise.all([
|
||||
this.loadOverview(),
|
||||
this.loadRealtime(),
|
||||
this.loadMapData(),
|
||||
this.loadStatistics(),
|
||||
]);
|
||||
},
|
||||
|
||||
// 定时刷新数据
|
||||
startAutoRefresh(interval = 30000) {
|
||||
// 每30秒刷新一次实时数据
|
||||
setInterval(() => {
|
||||
this.loadRealtime();
|
||||
}, interval);
|
||||
|
||||
// 每5分钟刷新一次统计数据
|
||||
setInterval(() => {
|
||||
this.loadStatistics();
|
||||
}, interval * 10);
|
||||
},
|
||||
|
||||
// 清除错误
|
||||
clearError() {
|
||||
this.error = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
2
admin-system/dashboard/src/store/index.js
Normal file
2
admin-system/dashboard/src/store/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { useAuthStore } from './auth.js';
|
||||
export { useDashboardStore } from './dashboard.js';
|
||||
551
admin-system/dashboard/src/views/CattleManagement.vue
Normal file
551
admin-system/dashboard/src/views/CattleManagement.vue
Normal file
@@ -0,0 +1,551 @@
|
||||
<template>
|
||||
<div class="cattle-management">
|
||||
<a-card title="牛只档案管理" :bordered="false">
|
||||
<!-- 操作按钮 -->
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="showAddModal = true">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加牛只
|
||||
</a-button>
|
||||
<a-button @click="loadCattle">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button @click="exportData">
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-section">
|
||||
<a-row :gutter="16" style="margin-bottom: 24px;">
|
||||
<a-col :span="6">
|
||||
<a-card :bordered="false" class="stat-card">
|
||||
<a-statistic
|
||||
title="总牛只数量"
|
||||
:value="stats.total"
|
||||
suffix="头"
|
||||
:value-style="{ color: '#3f8600' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card :bordered="false" class="stat-card">
|
||||
<a-statistic
|
||||
title="健康牛只"
|
||||
:value="stats.healthy"
|
||||
suffix="头"
|
||||
:value-style="{ color: '#52c41a' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card :bordered="false" class="stat-card">
|
||||
<a-statistic
|
||||
title="平均体重"
|
||||
:value="stats.avgWeight"
|
||||
suffix="kg"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card :bordered="false" class="stat-card">
|
||||
<a-statistic
|
||||
title="本月新增"
|
||||
:value="stats.monthlyNew"
|
||||
suffix="头"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<div class="search-form">
|
||||
<a-form layout="inline" :model="searchForm" @finish="handleSearch">
|
||||
<a-form-item label="耳标号">
|
||||
<a-input v-model:value="searchForm.ear_tag" placeholder="请输入耳标号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="品种">
|
||||
<a-select v-model:value="searchForm.breed" placeholder="请选择品种" style="width: 150px;">
|
||||
<a-select-option value="">全部品种</a-select-option>
|
||||
<a-select-option value="西门塔尔牛">西门塔尔牛</a-select-option>
|
||||
<a-select-option value="安格斯牛">安格斯牛</a-select-option>
|
||||
<a-select-option value="夏洛莱牛">夏洛莱牛</a-select-option>
|
||||
<a-select-option value="利木赞牛">利木赞牛</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="健康状态">
|
||||
<a-select v-model:value="searchForm.health_status" placeholder="请选择状态" style="width: 120px;">
|
||||
<a-select-option value="">全部状态</a-select-option>
|
||||
<a-select-option value="healthy">健康</a-select-option>
|
||||
<a-select-option value="sick">生病</a-select-option>
|
||||
<a-select-option value="quarantine">隔离</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="所有者">
|
||||
<a-input v-model:value="searchForm.owner_name" placeholder="请输入所有者" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">搜索</a-button>
|
||||
<a-button style="margin-left: 8px;" @click="resetSearch">重置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 牛只表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="cattle"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1500 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'ear_tag'">
|
||||
<a-tag color="blue">{{ record.ear_tag }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'health_status'">
|
||||
<a-tag :color="getHealthStatusColor(record.health_status)">
|
||||
{{ getHealthStatusText(record.health_status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'gender'">
|
||||
<a-tag :color="record.gender === 'male' ? 'blue' : 'pink'">
|
||||
{{ record.gender === 'male' ? '公牛' : '母牛' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'age'">
|
||||
{{ calculateAge(record.birth_date) }}个月
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="viewCattle(record)">查看</a-button>
|
||||
<a-button type="link" size="small" @click="editCattle(record)">编辑</a-button>
|
||||
<a-button type="link" size="small" @click="viewFeedingRecords(record)">饲养记录</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这头牛只吗?"
|
||||
@confirm="deleteCattle(record.id)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 添加/编辑牛只模态框 -->
|
||||
<a-modal
|
||||
v-model:open="showAddModal"
|
||||
:title="editingCattle ? '编辑牛只' : '添加牛只'"
|
||||
@ok="handleSaveCattle"
|
||||
@cancel="handleCancel"
|
||||
:confirm-loading="saving"
|
||||
width="800px"
|
||||
>
|
||||
<a-form :model="cattleForm" :rules="rules" ref="cattleFormRef" layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="耳标号" name="ear_tag">
|
||||
<a-input v-model:value="cattleForm.ear_tag" placeholder="请输入耳标号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="名称" name="name">
|
||||
<a-input v-model:value="cattleForm.name" placeholder="请输入牛只名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="品种" name="breed">
|
||||
<a-select v-model:value="cattleForm.breed" placeholder="请选择品种">
|
||||
<a-select-option value="西门塔尔牛">西门塔尔牛</a-select-option>
|
||||
<a-select-option value="安格斯牛">安格斯牛</a-select-option>
|
||||
<a-select-option value="夏洛莱牛">夏洛莱牛</a-select-option>
|
||||
<a-select-option value="利木赞牛">利木赞牛</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="性别" name="gender">
|
||||
<a-radio-group v-model:value="cattleForm.gender">
|
||||
<a-radio value="male">公牛</a-radio>
|
||||
<a-radio value="female">母牛</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="出生日期" name="birth_date">
|
||||
<a-date-picker
|
||||
v-model:value="cattleForm.birth_date"
|
||||
style="width: 100%;"
|
||||
placeholder="请选择出生日期"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="毛色" name="color">
|
||||
<a-input v-model:value="cattleForm.color" placeholder="请输入毛色" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="体重(kg)" name="weight">
|
||||
<a-input-number
|
||||
v-model:value="cattleForm.weight"
|
||||
:min="0"
|
||||
:max="2000"
|
||||
style="width: 100%;"
|
||||
placeholder="请输入体重"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="健康状态" name="health_status">
|
||||
<a-select v-model:value="cattleForm.health_status" placeholder="请选择健康状态">
|
||||
<a-select-option value="healthy">健康</a-select-option>
|
||||
<a-select-option value="sick">生病</a-select-option>
|
||||
<a-select-option value="quarantine">隔离</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="牧场位置" name="farm_location">
|
||||
<a-input v-model:value="cattleForm.farm_location" placeholder="请输入牧场位置" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 查看牛只详情模态框 -->
|
||||
<a-modal
|
||||
v-model:open="showDetailModal"
|
||||
title="牛只详细信息"
|
||||
:footer="null"
|
||||
width="900px"
|
||||
>
|
||||
<div v-if="selectedCattle" class="cattle-detail">
|
||||
<a-descriptions title="基本信息" :column="2" bordered>
|
||||
<a-descriptions-item label="耳标号">
|
||||
<a-tag color="blue">{{ selectedCattle.ear_tag }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="名称">{{ selectedCattle.name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="品种">{{ selectedCattle.breed }}</a-descriptions-item>
|
||||
<a-descriptions-item label="性别">
|
||||
<a-tag :color="selectedCattle.gender === 'male' ? 'blue' : 'pink'">
|
||||
{{ selectedCattle.gender === 'male' ? '公牛' : '母牛' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="出生日期">{{ selectedCattle.birth_date }}</a-descriptions-item>
|
||||
<a-descriptions-item label="年龄">{{ calculateAge(selectedCattle.birth_date) }}个月</a-descriptions-item>
|
||||
<a-descriptions-item label="毛色">{{ selectedCattle.color }}</a-descriptions-item>
|
||||
<a-descriptions-item label="体重">{{ selectedCattle.weight }}kg</a-descriptions-item>
|
||||
<a-descriptions-item label="健康状态">
|
||||
<a-tag :color="getHealthStatusColor(selectedCattle.health_status)">
|
||||
{{ getHealthStatusText(selectedCattle.health_status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="牧场位置">{{ selectedCattle.farm_location }}</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">{{ selectedCattle.created_at }}</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">{{ selectedCattle.updated_at }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { PlusOutlined, ReloadOutlined, ExportOutlined } from '@ant-design/icons-vue';
|
||||
import { cattleAPI } from '../services/api.js';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 响应式数据
|
||||
const cattle = ref([]);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const showAddModal = ref(false);
|
||||
const showDetailModal = ref(false);
|
||||
const editingCattle = ref(null);
|
||||
const selectedCattle = ref(null);
|
||||
const cattleFormRef = ref();
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
total: 0,
|
||||
healthy: 0,
|
||||
avgWeight: 0,
|
||||
monthlyNew: 0,
|
||||
});
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
ear_tag: '',
|
||||
breed: '',
|
||||
health_status: '',
|
||||
owner_name: '',
|
||||
});
|
||||
|
||||
// 牛只表单
|
||||
const cattleForm = reactive({
|
||||
ear_tag: '',
|
||||
name: '',
|
||||
breed: '',
|
||||
gender: '',
|
||||
birth_date: null,
|
||||
color: '',
|
||||
weight: null,
|
||||
health_status: 'healthy',
|
||||
farm_location: '',
|
||||
});
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
});
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{ title: '耳标号', dataIndex: 'ear_tag', key: 'ear_tag', width: 120, fixed: 'left' },
|
||||
{ title: '名称', dataIndex: 'name', key: 'name', width: 100 },
|
||||
{ title: '品种', dataIndex: 'breed', key: 'breed', width: 120 },
|
||||
{ title: '性别', dataIndex: 'gender', key: 'gender', width: 80 },
|
||||
{ title: '年龄', key: 'age', width: 80 },
|
||||
{ title: '体重(kg)', dataIndex: 'weight', key: 'weight', width: 100 },
|
||||
{ title: '毛色', dataIndex: 'color', key: 'color', width: 80 },
|
||||
{ title: '健康状态', dataIndex: 'health_status', key: 'health_status', width: 120 },
|
||||
{ title: '牧场位置', dataIndex: 'farm_location', key: 'farm_location', width: 200 },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 160 },
|
||||
{ title: '操作', key: 'action', width: 250, fixed: 'right' },
|
||||
];
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
ear_tag: [{ required: true, message: '请输入耳标号' }],
|
||||
name: [{ required: true, message: '请输入牛只名称' }],
|
||||
breed: [{ required: true, message: '请选择品种' }],
|
||||
gender: [{ required: true, message: '请选择性别' }],
|
||||
birth_date: [{ required: true, message: '请选择出生日期' }],
|
||||
weight: [{ required: true, message: '请输入体重' }],
|
||||
health_status: [{ required: true, message: '请选择健康状态' }],
|
||||
};
|
||||
|
||||
// 加载牛只列表
|
||||
const loadCattle = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
...searchForm,
|
||||
};
|
||||
|
||||
const response = await cattleAPI.getCattle(params);
|
||||
|
||||
if (response.success) {
|
||||
cattle.value = response.data.cattle || [];
|
||||
pagination.total = response.data.pagination?.total || 0;
|
||||
} else {
|
||||
message.error(response.message || '获取牛只列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取牛只列表失败:', error);
|
||||
message.error('获取牛只列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const response = await cattleAPI.getStatistics();
|
||||
if (response.success) {
|
||||
stats.value = response.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 计算年龄(月份)
|
||||
const calculateAge = (birthDate) => {
|
||||
if (!birthDate) return 0;
|
||||
return dayjs().diff(dayjs(birthDate), 'month');
|
||||
};
|
||||
|
||||
// 获取健康状态颜色
|
||||
const getHealthStatusColor = (status) => {
|
||||
const colors = {
|
||||
healthy: 'green',
|
||||
sick: 'red',
|
||||
quarantine: 'orange',
|
||||
};
|
||||
return colors[status] || 'default';
|
||||
};
|
||||
|
||||
// 获取健康状态文本
|
||||
const getHealthStatusText = (status) => {
|
||||
const texts = {
|
||||
healthy: '健康',
|
||||
sick: '生病',
|
||||
quarantine: '隔离',
|
||||
};
|
||||
return texts[status] || status;
|
||||
};
|
||||
|
||||
// 表格变化处理
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current;
|
||||
pagination.pageSize = pag.pageSize;
|
||||
loadCattle();
|
||||
};
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1;
|
||||
loadCattle();
|
||||
};
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
ear_tag: '',
|
||||
breed: '',
|
||||
health_status: '',
|
||||
owner_name: '',
|
||||
});
|
||||
pagination.current = 1;
|
||||
loadCattle();
|
||||
};
|
||||
|
||||
// 查看牛只详情
|
||||
const viewCattle = (record) => {
|
||||
selectedCattle.value = record;
|
||||
showDetailModal.value = true;
|
||||
};
|
||||
|
||||
// 编辑牛只
|
||||
const editCattle = (record) => {
|
||||
editingCattle.value = record;
|
||||
Object.assign(cattleForm, {
|
||||
...record,
|
||||
birth_date: record.birth_date ? dayjs(record.birth_date) : null,
|
||||
});
|
||||
showAddModal.value = true;
|
||||
};
|
||||
|
||||
// 查看饲养记录
|
||||
const viewFeedingRecords = (record) => {
|
||||
message.info(`查看 ${record.name} 的饲养记录`);
|
||||
// TODO: 实现饲养记录查看
|
||||
};
|
||||
|
||||
// 删除牛只
|
||||
const deleteCattle = async (id) => {
|
||||
try {
|
||||
const response = await cattleAPI.deleteCattle(id);
|
||||
if (response.success) {
|
||||
message.success('删除成功');
|
||||
loadCattle();
|
||||
loadStats();
|
||||
} else {
|
||||
message.error(response.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除牛只失败:', error);
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 保存牛只
|
||||
const handleSaveCattle = async () => {
|
||||
try {
|
||||
await cattleFormRef.value.validate();
|
||||
saving.value = true;
|
||||
|
||||
const formData = {
|
||||
...cattleForm,
|
||||
birth_date: cattleForm.birth_date ? cattleForm.birth_date.format('YYYY-MM-DD') : null,
|
||||
};
|
||||
|
||||
let response;
|
||||
if (editingCattle.value) {
|
||||
response = await cattleAPI.updateCattle(editingCattle.value.id, formData);
|
||||
} else {
|
||||
response = await cattleAPI.createCattle(formData);
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
message.success(editingCattle.value ? '更新成功' : '创建成功');
|
||||
showAddModal.value = false;
|
||||
loadCattle();
|
||||
loadStats();
|
||||
} else {
|
||||
message.error(response.message || '保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存牛只失败:', error);
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
showAddModal.value = false;
|
||||
editingCattle.value = null;
|
||||
cattleFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
// 导出数据
|
||||
const exportData = () => {
|
||||
message.success('导出功能开发中');
|
||||
};
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadCattle();
|
||||
loadStats();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cattle-management {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cattle-detail {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<!-- 临时添加API测试组件 -->
|
||||
<div style="position: fixed; top: 80px; right: 20px; z-index: 9999; width: 350px;">
|
||||
<ApiTest />
|
||||
</div>
|
||||
|
||||
<header class="dashboard-header">
|
||||
<div class="header-decoration"></div>
|
||||
<div class="header-title">
|
||||
@@ -118,12 +123,14 @@
|
||||
import * as echarts from 'echarts'
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import ThreeDMap from '@/components/map/ThreeDMap.vue'
|
||||
import ApiTest from '@/components/ApiTest.vue'
|
||||
import { fetchMapData } from '@/services/dashboard.js'
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
ThreeDMap
|
||||
ThreeDMap,
|
||||
ApiTest
|
||||
},
|
||||
setup() {
|
||||
const currentTime = ref(new Date().toLocaleString())
|
||||
|
||||
@@ -1,130 +1,523 @@
|
||||
<template>
|
||||
<div class="finance-container">
|
||||
<h1>金融服务</h1>
|
||||
<div v-if="loading" class="loading-indicator">数据加载中...</div>
|
||||
<div v-if="error" class="error-message">数据加载失败,请稍后重试。</div>
|
||||
<div v-if="!loading && !error">
|
||||
<div class="loan-section">
|
||||
<h3>贷款数据</h3>
|
||||
<div class="chart-container">
|
||||
<div id="loan-chart" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="insurance-section">
|
||||
<h3>保险数据</h3>
|
||||
<div class="chart-container">
|
||||
<div id="insurance-chart" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="finance-page">
|
||||
<!-- 页面标题和操作按钮 -->
|
||||
<div class="page-header">
|
||||
<a-page-header title="金融服务监管" sub-title="贷款和保险业务管理">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="showAddModal('loan')">
|
||||
<PlusOutlined /> 新增贷款申请
|
||||
</a-button>
|
||||
<a-button @click="showAddModal('insurance')">
|
||||
<SafetyOutlined /> 新增保险申请
|
||||
</a-button>
|
||||
</template>
|
||||
</a-page-header>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-cards">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="贷款申请总数" :value="stats.totalLoans" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="保险申请总数" :value="stats.totalInsurance" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="贷款总金额"
|
||||
:value="stats.totalLoanAmount"
|
||||
suffix="万元"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="保险总金额"
|
||||
:value="stats.totalInsuranceAmount"
|
||||
suffix="万元"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<a-tabs v-model:activeKey="activeTab" class="finance-tabs">
|
||||
<a-tab-pane key="loans" tab="贷款管理">
|
||||
<!-- 贷款搜索表单 -->
|
||||
<a-card class="search-card">
|
||||
<a-form layout="inline" :model="loanSearchForm">
|
||||
<a-form-item label="申请人">
|
||||
<a-input v-model:value="loanSearchForm.applicant" placeholder="请输入申请人姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="贷款类型">
|
||||
<a-select v-model:value="loanSearchForm.loanType" placeholder="请选择贷款类型" style="width: 150px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="cattle">牛只质押贷款</a-select-option>
|
||||
<a-select-option value="farm">牧场贷款</a-select-option>
|
||||
<a-select-option value="equipment">设备贷款</a-select-option>
|
||||
<a-select-option value="operating">经营贷款</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select v-model:value="loanSearchForm.status" placeholder="请选择状态" style="width: 150px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="submitted">已提交</a-select-option>
|
||||
<a-select-option value="under_review">审核中</a-select-option>
|
||||
<a-select-option value="approved">已批准</a-select-option>
|
||||
<a-select-option value="rejected">已拒绝</a-select-option>
|
||||
<a-select-option value="disbursed">已放款</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="searchLoans">
|
||||
<SearchOutlined /> 搜索
|
||||
</a-button>
|
||||
<a-button @click="resetLoanSearch" style="margin-left: 8px">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 贷款列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="loanColumns"
|
||||
:data-source="loans"
|
||||
:loading="loanLoading"
|
||||
:pagination="loanPagination"
|
||||
@change="handleLoanTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getLoanStatusColor(record.status)">
|
||||
{{ getLoanStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="viewLoanDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.status === 'under_review'"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="reviewLoan(record)"
|
||||
>
|
||||
审核
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="insurance" tab="保险管理">
|
||||
<!-- 保险搜索表单 -->
|
||||
<a-card class="search-card">
|
||||
<a-form layout="inline" :model="insuranceSearchForm">
|
||||
<a-form-item label="申请人">
|
||||
<a-input v-model:value="insuranceSearchForm.applicant" placeholder="请输入申请人姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="保险类型">
|
||||
<a-select v-model:value="insuranceSearchForm.insuranceType" placeholder="请选择保险类型" style="width: 150px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="cattle_death">牛只死亡险</a-select-option>
|
||||
<a-select-option value="cattle_health">牛只健康险</a-select-option>
|
||||
<a-select-option value="cattle_theft">牛只盗窃险</a-select-option>
|
||||
<a-select-option value="property">财产险</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select v-model:value="insuranceSearchForm.status" placeholder="请选择状态" style="width: 150px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="applied">已申请</a-select-option>
|
||||
<a-select-option value="underwriting">核保中</a-select-option>
|
||||
<a-select-option value="issued">已出单</a-select-option>
|
||||
<a-select-option value="active">生效中</a-select-option>
|
||||
<a-select-option value="expired">已过期</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="searchInsurance">
|
||||
<SearchOutlined /> 搜索
|
||||
</a-button>
|
||||
<a-button @click="resetInsuranceSearch" style="margin-left: 8px">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 保险列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="insuranceColumns"
|
||||
:data-source="insuranceList"
|
||||
:loading="insuranceLoading"
|
||||
:pagination="insurancePagination"
|
||||
@change="handleInsuranceTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getInsuranceStatusColor(record.status)">
|
||||
{{ getInsuranceStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="viewInsuranceDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.status === 'underwriting'"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="reviewInsurance(record)"
|
||||
>
|
||||
核保
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
import * as echarts from 'echarts';
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { financeAPI } from '@/services/api.js';
|
||||
import {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
SafetyOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
export default {
|
||||
name: 'Finance',
|
||||
components: {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
SafetyOutlined
|
||||
},
|
||||
setup() {
|
||||
const loanData = ref([]);
|
||||
const insuranceData = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
const activeTab = ref('loans');
|
||||
const loanLoading = ref(false);
|
||||
const insuranceLoading = ref(false);
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
totalLoans: 156,
|
||||
totalInsurance: 89,
|
||||
totalLoanAmount: 2850,
|
||||
totalInsuranceAmount: 1260
|
||||
});
|
||||
|
||||
const fetchData = async () => {
|
||||
// 贷款数据
|
||||
const loans = ref([]);
|
||||
const loanSearchForm = reactive({
|
||||
applicant: '',
|
||||
loanType: '',
|
||||
status: ''
|
||||
});
|
||||
const loanPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true
|
||||
});
|
||||
|
||||
// 保险数据
|
||||
const insuranceList = ref([]);
|
||||
const insuranceSearchForm = reactive({
|
||||
applicant: '',
|
||||
insuranceType: '',
|
||||
status: ''
|
||||
});
|
||||
const insurancePagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true
|
||||
});
|
||||
|
||||
// 贷款表格列
|
||||
const loanColumns = [
|
||||
{ title: '申请编号', dataIndex: 'id', key: 'id', width: 120 },
|
||||
{ title: '申请人', dataIndex: 'applicant_name', key: 'applicant_name' },
|
||||
{ title: '贷款类型', dataIndex: 'loan_type', key: 'loan_type' },
|
||||
{ title: '申请金额(万元)', dataIndex: 'loan_amount', key: 'loan_amount' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '申请时间', dataIndex: 'created_at', key: 'created_at' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
];
|
||||
|
||||
// 保险表格列
|
||||
const insuranceColumns = [
|
||||
{ title: '保单号', dataIndex: 'policy_number', key: 'policy_number', width: 120 },
|
||||
{ title: '申请人', dataIndex: 'applicant_name', key: 'applicant_name' },
|
||||
{ title: '保险类型', dataIndex: 'insurance_type', key: 'insurance_type' },
|
||||
{ title: '保险金额(万元)', dataIndex: 'insured_amount', key: 'insured_amount' },
|
||||
{ title: '保费(元)', dataIndex: 'premium', key: 'premium' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '申请时间', dataIndex: 'created_at', key: 'created_at' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
];
|
||||
|
||||
// 加载贷款数据
|
||||
const loadLoans = async () => {
|
||||
loanLoading.value = true;
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = false;
|
||||
const [loanResponse, insuranceResponse] = await Promise.all([
|
||||
axios.get('/api/loan-data'),
|
||||
axios.get('/api/insurance-data')
|
||||
]);
|
||||
loanData.value = loanResponse.data;
|
||||
insuranceData.value = insuranceResponse.data;
|
||||
renderCharts();
|
||||
} catch (err) {
|
||||
error.value = true;
|
||||
console.error('获取数据失败:', err);
|
||||
const params = {
|
||||
page: loanPagination.current,
|
||||
limit: loanPagination.pageSize,
|
||||
...loanSearchForm
|
||||
};
|
||||
const response = await financeAPI.getLoans(params);
|
||||
if (response.success) {
|
||||
loans.value = response.data.loans || [];
|
||||
loanPagination.total = response.data.pagination?.total || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取贷款列表失败:', error);
|
||||
message.error('获取贷款列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loanLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const renderCharts = () => {
|
||||
const loanChart = echarts.init(document.getElementById('loan-chart'));
|
||||
loanChart.setOption({
|
||||
tooltip: {},
|
||||
xAxis: { data: loanData.value.map(item => item.month) },
|
||||
yAxis: {},
|
||||
series: [{ type: 'bar', data: loanData.value.map(item => item.value) }]
|
||||
});
|
||||
// 加载保险数据
|
||||
const loadInsurance = async () => {
|
||||
insuranceLoading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: insurancePagination.current,
|
||||
limit: insurancePagination.pageSize,
|
||||
...insuranceSearchForm
|
||||
};
|
||||
const response = await financeAPI.getInsurance(params);
|
||||
if (response.success) {
|
||||
insuranceList.value = response.data.insurance || [];
|
||||
insurancePagination.total = response.data.pagination?.total || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取保险列表失败:', error);
|
||||
message.error('获取保险列表失败');
|
||||
} finally {
|
||||
insuranceLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const insuranceChart = echarts.init(document.getElementById('insurance-chart'));
|
||||
insuranceChart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
data: insuranceData.value.map(item => item)
|
||||
}]
|
||||
// 贷款状态颜色
|
||||
const getLoanStatusColor = (status) => {
|
||||
const colors = {
|
||||
'submitted': 'blue',
|
||||
'under_review': 'orange',
|
||||
'approved': 'green',
|
||||
'rejected': 'red',
|
||||
'disbursed': 'purple',
|
||||
'completed': 'green',
|
||||
'overdue': 'red'
|
||||
};
|
||||
return colors[status] || 'default';
|
||||
};
|
||||
|
||||
// 贷款状态文本
|
||||
const getLoanStatusText = (status) => {
|
||||
const texts = {
|
||||
'submitted': '已提交',
|
||||
'under_review': '审核中',
|
||||
'approved': '已批准',
|
||||
'rejected': '已拒绝',
|
||||
'disbursed': '已放款',
|
||||
'completed': '已完成',
|
||||
'overdue': '逾期'
|
||||
};
|
||||
return texts[status] || status;
|
||||
};
|
||||
|
||||
// 保险状态颜色
|
||||
const getInsuranceStatusColor = (status) => {
|
||||
const colors = {
|
||||
'applied': 'blue',
|
||||
'underwriting': 'orange',
|
||||
'issued': 'green',
|
||||
'active': 'green',
|
||||
'expired': 'red',
|
||||
'cancelled': 'red'
|
||||
};
|
||||
return colors[status] || 'default';
|
||||
};
|
||||
|
||||
// 保险状态文本
|
||||
const getInsuranceStatusText = (status) => {
|
||||
const texts = {
|
||||
'applied': '已申请',
|
||||
'underwriting': '核保中',
|
||||
'issued': '已出单',
|
||||
'active': '生效中',
|
||||
'expired': '已过期',
|
||||
'cancelled': '已取消'
|
||||
};
|
||||
return texts[status] || status;
|
||||
};
|
||||
|
||||
// 搜索贷款
|
||||
const searchLoans = () => {
|
||||
loanPagination.current = 1;
|
||||
loadLoans();
|
||||
};
|
||||
|
||||
// 重置贷款搜索
|
||||
const resetLoanSearch = () => {
|
||||
Object.assign(loanSearchForm, {
|
||||
applicant: '',
|
||||
loanType: '',
|
||||
status: ''
|
||||
});
|
||||
searchLoans();
|
||||
};
|
||||
|
||||
// 搜索保险
|
||||
const searchInsurance = () => {
|
||||
insurancePagination.current = 1;
|
||||
loadInsurance();
|
||||
};
|
||||
|
||||
// 重置保险搜索
|
||||
const resetInsuranceSearch = () => {
|
||||
Object.assign(insuranceSearchForm, {
|
||||
applicant: '',
|
||||
insuranceType: '',
|
||||
status: ''
|
||||
});
|
||||
searchInsurance();
|
||||
};
|
||||
|
||||
// 表格变化处理
|
||||
const handleLoanTableChange = (pagination) => {
|
||||
loanPagination.current = pagination.current;
|
||||
loanPagination.pageSize = pagination.pageSize;
|
||||
loadLoans();
|
||||
};
|
||||
|
||||
const handleInsuranceTableChange = (pagination) => {
|
||||
insurancePagination.current = pagination.current;
|
||||
insurancePagination.pageSize = pagination.pageSize;
|
||||
loadInsurance();
|
||||
};
|
||||
|
||||
// 显示添加模态框
|
||||
const showAddModal = (type) => {
|
||||
message.info(`添加${type === 'loan' ? '贷款' : '保险'}申请功能开发中`);
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const viewLoanDetail = (record) => {
|
||||
message.info(`查看贷款详情: ${record.id}`);
|
||||
};
|
||||
|
||||
const viewInsuranceDetail = (record) => {
|
||||
message.info(`查看保险详情: ${record.policy_number}`);
|
||||
};
|
||||
|
||||
// 审核
|
||||
const reviewLoan = (record) => {
|
||||
message.info(`审核贷款: ${record.id}`);
|
||||
};
|
||||
|
||||
const reviewInsurance = (record) => {
|
||||
message.info(`核保保险: ${record.policy_number}`);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
loadLoans();
|
||||
loadInsurance();
|
||||
});
|
||||
|
||||
return {
|
||||
loanData,
|
||||
insuranceData,
|
||||
loading,
|
||||
error
|
||||
activeTab,
|
||||
stats,
|
||||
loans,
|
||||
loanLoading,
|
||||
loanSearchForm,
|
||||
loanPagination,
|
||||
loanColumns,
|
||||
insuranceList,
|
||||
insuranceLoading,
|
||||
insuranceSearchForm,
|
||||
insurancePagination,
|
||||
insuranceColumns,
|
||||
loadLoans,
|
||||
loadInsurance,
|
||||
getLoanStatusColor,
|
||||
getLoanStatusText,
|
||||
getInsuranceStatusColor,
|
||||
getInsuranceStatusText,
|
||||
searchLoans,
|
||||
resetLoanSearch,
|
||||
searchInsurance,
|
||||
resetInsuranceSearch,
|
||||
handleLoanTableChange,
|
||||
handleInsuranceTableChange,
|
||||
showAddModal,
|
||||
viewLoanDetail,
|
||||
viewInsuranceDetail,
|
||||
reviewLoan,
|
||||
reviewInsurance
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.finance-container {
|
||||
padding: 20px;
|
||||
.finance-page {
|
||||
padding: 24px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.loan-section,
|
||||
.insurance-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
.page-header {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.loading-indicator,
|
||||
.error-message {
|
||||
.stats-cards {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-cards .ant-card {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
.finance-tabs {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.loan-section,
|
||||
.insurance-section {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.search-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#loan-chart,
|
||||
#insurance-chart {
|
||||
height: 250px;
|
||||
}
|
||||
.search-card .ant-form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,104 +1,647 @@
|
||||
<template>
|
||||
<div class="government-container">
|
||||
<h1>政府平台</h1>
|
||||
<div v-if="loading" class="loading-indicator">数据加载中...</div>
|
||||
<div v-if="error" class="error-message">数据加载失败,请稍后重试。</div>
|
||||
<div v-if="!loading && !error">
|
||||
<div class="policy-section">
|
||||
<h3>政策通知</h3>
|
||||
<ul>
|
||||
<li v-for="(policy, index) in policies" :key="index">
|
||||
{{ policy.title }} - {{ policy.date }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="data-section">
|
||||
<h3>政务数据</h3>
|
||||
<a-table :dataSource="governmentData" :columns="columns" />
|
||||
</div>
|
||||
<div class="government-page">
|
||||
<!-- 页面标题和操作按钮 -->
|
||||
<div class="page-header">
|
||||
<a-page-header title="政府监管" sub-title="检查记录和质量追溯">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="showAddModal('inspection')">
|
||||
<AuditOutlined /> 新增检查
|
||||
</a-button>
|
||||
<a-button @click="showAddModal('trace')">
|
||||
<SearchOutlined /> 质量追溯
|
||||
</a-button>
|
||||
</template>
|
||||
</a-page-header>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-cards">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="总检查次数" :value="stats.totalInspections" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="合规率" :value="stats.complianceRate" suffix="%" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="追溯记录" :value="stats.totalTraces" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="本月检查" :value="stats.monthlyInspections" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<a-tabs v-model:activeKey="activeTab" class="government-tabs">
|
||||
<a-tab-pane key="inspections" tab="检查记录">
|
||||
<!-- 检查搜索表单 -->
|
||||
<a-card class="search-card">
|
||||
<a-form layout="inline" :model="inspectionSearchForm">
|
||||
<a-form-item label="牧场名称">
|
||||
<a-input v-model:value="inspectionSearchForm.farmName" placeholder="请输入牧场名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="检查类型">
|
||||
<a-select v-model:value="inspectionSearchForm.inspectionType" placeholder="请选择检查类型" style="width: 150px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="safety">安全检查</a-select-option>
|
||||
<a-select-option value="health">卫生检查</a-select-option>
|
||||
<a-select-option value="environment">环保检查</a-select-option>
|
||||
<a-select-option value="quality">质量检查</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="检查结果">
|
||||
<a-select v-model:value="inspectionSearchForm.result" placeholder="请选择结果" style="width: 120px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="passed">通过</a-select-option>
|
||||
<a-select-option value="failed">不通过</a-select-option>
|
||||
<a-select-option value="conditional">有条件通过</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="searchInspections">
|
||||
<SearchOutlined /> 搜索
|
||||
</a-button>
|
||||
<a-button @click="resetInspectionSearch" style="margin-left: 8px">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 检查列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="inspectionColumns"
|
||||
:data-source="inspections"
|
||||
:loading="inspectionLoading"
|
||||
:pagination="inspectionPagination"
|
||||
@change="handleInspectionTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'inspection_type'">
|
||||
<a-tag color="blue">
|
||||
{{ getInspectionTypeText(record.inspection_type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'result'">
|
||||
<a-tag :color="getInspectionResultColor(record.result)">
|
||||
{{ getInspectionResultText(record.result) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="viewInspectionDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.result === 'failed'"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="createRectification(record)"
|
||||
>
|
||||
整改通知
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="traces" tab="质量追溯">
|
||||
<!-- 追溯搜索表单 -->
|
||||
<a-card class="search-card">
|
||||
<a-form layout="inline" :model="traceSearchForm">
|
||||
<a-form-item label="追溯编号">
|
||||
<a-input v-model:value="traceSearchForm.traceId" placeholder="请输入追溯编号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="牛只耳标">
|
||||
<a-input v-model:value="traceSearchForm.earTag" placeholder="请输入牛只耳标" />
|
||||
</a-form-item>
|
||||
<a-form-item label="追溯类型">
|
||||
<a-select v-model:value="traceSearchForm.traceType" placeholder="请选择类型" style="width: 150px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="origin">源头追溯</a-select-option>
|
||||
<a-select-option value="feed">饵料追溯</a-select-option>
|
||||
<a-select-option value="medicine">药物追溯</a-select-option>
|
||||
<a-select-option value="transport">运输追溯</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="searchTraces">
|
||||
<SearchOutlined /> 搜索
|
||||
</a-button>
|
||||
<a-button @click="resetTraceSearch" style="margin-left: 8px">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 追溯列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="traceColumns"
|
||||
:data-source="traces"
|
||||
:loading="traceLoading"
|
||||
:pagination="tracePagination"
|
||||
@change="handleTraceTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'trace_type'">
|
||||
<a-tag color="green">
|
||||
{{ getTraceTypeText(record.trace_type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="viewTraceDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button size="small" @click="viewTraceChain(record)">
|
||||
追溯链
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="policies" tab="政策法规">
|
||||
<!-- 政策搜索表单 -->
|
||||
<a-card class="search-card">
|
||||
<a-form layout="inline" :model="policySearchForm">
|
||||
<a-form-item label="政策标题">
|
||||
<a-input v-model:value="policySearchForm.title" placeholder="请输入政策标题" />
|
||||
</a-form-item>
|
||||
<a-form-item label="政策类型">
|
||||
<a-select v-model:value="policySearchForm.policyType" placeholder="请选择类型" style="width: 150px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="industry">行业政策</a-select-option>
|
||||
<a-select-option value="subsidy">补贴政策</a-select-option>
|
||||
<a-select-option value="regulation">监管政策</a-select-option>
|
||||
<a-select-option value="environment">环保政策</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="searchPolicies">
|
||||
<SearchOutlined /> 搜索
|
||||
</a-button>
|
||||
<a-button @click="resetPolicySearch" style="margin-left: 8px">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 政策列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="policyColumns"
|
||||
:data-source="policies"
|
||||
:loading="policyLoading"
|
||||
:pagination="policyPagination"
|
||||
@change="handlePolicyTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'policy_type'">
|
||||
<a-tag color="purple">
|
||||
{{ getPolicyTypeText(record.policy_type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="viewPolicyDetail(record)">
|
||||
查看
|
||||
</a-button>
|
||||
<a-button size="small" @click="downloadPolicy(record)">
|
||||
下载
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { governmentAPI } from '@/services/api.js';
|
||||
import {
|
||||
AuditOutlined,
|
||||
SearchOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
export default {
|
||||
name: 'Government',
|
||||
components: {
|
||||
AuditOutlined,
|
||||
SearchOutlined
|
||||
},
|
||||
setup() {
|
||||
const policies = ref([]);
|
||||
const governmentData = ref([]);
|
||||
const columns = ref([
|
||||
{ title: '指标', dataIndex: 'indicator', key: 'indicator' },
|
||||
{ title: '数值', dataIndex: 'value', key: 'value' },
|
||||
{ title: '单位', dataIndex: 'unit', key: 'unit' },
|
||||
]);
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
const activeTab = ref('inspections');
|
||||
const inspectionLoading = ref(false);
|
||||
const traceLoading = ref(false);
|
||||
const policyLoading = ref(false);
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
totalInspections: 856,
|
||||
complianceRate: 92.5,
|
||||
totalTraces: 1245,
|
||||
monthlyInspections: 128
|
||||
});
|
||||
|
||||
const fetchData = async () => {
|
||||
// 检查数据
|
||||
const inspections = ref([]);
|
||||
const inspectionSearchForm = reactive({
|
||||
farmName: '',
|
||||
inspectionType: '',
|
||||
result: ''
|
||||
});
|
||||
const inspectionPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true
|
||||
});
|
||||
|
||||
// 追溯数据
|
||||
const traces = ref([]);
|
||||
const traceSearchForm = reactive({
|
||||
traceId: '',
|
||||
earTag: '',
|
||||
traceType: ''
|
||||
});
|
||||
const tracePagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true
|
||||
});
|
||||
|
||||
// 政策数据
|
||||
const policies = ref([]);
|
||||
const policySearchForm = reactive({
|
||||
title: '',
|
||||
policyType: ''
|
||||
});
|
||||
const policyPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true
|
||||
});
|
||||
|
||||
// 检查表格列
|
||||
const inspectionColumns = [
|
||||
{ title: '检查编号', dataIndex: 'inspection_id', key: 'inspection_id', width: 120 },
|
||||
{ title: '牧场名称', dataIndex: 'farm_name', key: 'farm_name' },
|
||||
{ title: '检查类型', dataIndex: 'inspection_type', key: 'inspection_type' },
|
||||
{ title: '检查人员', dataIndex: 'inspector_name', key: 'inspector_name' },
|
||||
{ title: '检查结果', dataIndex: 'result', key: 'result' },
|
||||
{ title: '检查时间', dataIndex: 'inspection_date', key: 'inspection_date' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
];
|
||||
|
||||
// 追溯表格列
|
||||
const traceColumns = [
|
||||
{ title: '追溯编号', dataIndex: 'trace_id', key: 'trace_id', width: 150 },
|
||||
{ title: '牛只耳标', dataIndex: 'ear_tag', key: 'ear_tag' },
|
||||
{ title: '追溯类型', dataIndex: 'trace_type', key: 'trace_type' },
|
||||
{ title: '纳入时间', dataIndex: 'record_date', key: 'record_date' },
|
||||
{ title: '操作员', dataIndex: 'operator_name', key: 'operator_name' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
];
|
||||
|
||||
// 政策表格列
|
||||
const policyColumns = [
|
||||
{ title: '政策标题', dataIndex: 'title', key: 'title' },
|
||||
{ title: '政策类型', dataIndex: 'policy_type', key: 'policy_type' },
|
||||
{ title: '发布时间', dataIndex: 'publish_date', key: 'publish_date' },
|
||||
{ title: '生效时间', dataIndex: 'effective_date', key: 'effective_date' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
];
|
||||
|
||||
// 加载检查数据
|
||||
const loadInspections = async () => {
|
||||
inspectionLoading.value = true;
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = false;
|
||||
const [policyResponse, dataResponse] = await Promise.all([
|
||||
axios.get('/api/policies'),
|
||||
axios.get('/api/government-data')
|
||||
]);
|
||||
policies.value = policyResponse.data;
|
||||
governmentData.value = dataResponse.data;
|
||||
} catch (err) {
|
||||
error.value = true;
|
||||
console.error('获取数据失败:', err);
|
||||
const params = {
|
||||
page: inspectionPagination.current,
|
||||
limit: inspectionPagination.pageSize,
|
||||
...inspectionSearchForm
|
||||
};
|
||||
const response = await governmentAPI.getInspections(params);
|
||||
if (response.success) {
|
||||
inspections.value = response.data.inspections || [];
|
||||
inspectionPagination.total = response.data.pagination?.total || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取检查列表失败:', error);
|
||||
message.error('获取检查列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
inspectionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载追溯数据
|
||||
const loadTraces = async () => {
|
||||
traceLoading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: tracePagination.current,
|
||||
limit: tracePagination.pageSize,
|
||||
...traceSearchForm
|
||||
};
|
||||
const response = await governmentAPI.getTraces(params);
|
||||
if (response.success) {
|
||||
traces.value = response.data.traces || [];
|
||||
tracePagination.total = response.data.pagination?.total || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取追溯列表失败:', error);
|
||||
message.error('获取追溯列表失败');
|
||||
} finally {
|
||||
traceLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载政策数据
|
||||
const loadPolicies = async () => {
|
||||
policyLoading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: policyPagination.current,
|
||||
limit: policyPagination.pageSize,
|
||||
...policySearchForm
|
||||
};
|
||||
const response = await governmentAPI.getPolicies(params);
|
||||
if (response.success) {
|
||||
policies.value = response.data.policies || [];
|
||||
policyPagination.total = response.data.pagination?.total || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取政策列表失败:', error);
|
||||
message.error('获取政策列表失败');
|
||||
} finally {
|
||||
policyLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查类型文本
|
||||
const getInspectionTypeText = (type) => {
|
||||
const texts = {
|
||||
'safety': '安全检查',
|
||||
'health': '卫生检查',
|
||||
'environment': '环保检查',
|
||||
'quality': '质量检查'
|
||||
};
|
||||
return texts[type] || type;
|
||||
};
|
||||
|
||||
// 检查结果颜色
|
||||
const getInspectionResultColor = (result) => {
|
||||
const colors = {
|
||||
'passed': 'green',
|
||||
'failed': 'red',
|
||||
'conditional': 'orange'
|
||||
};
|
||||
return colors[result] || 'default';
|
||||
};
|
||||
|
||||
// 检查结果文本
|
||||
const getInspectionResultText = (result) => {
|
||||
const texts = {
|
||||
'passed': '通过',
|
||||
'failed': '不通过',
|
||||
'conditional': '有条件通过'
|
||||
};
|
||||
return texts[result] || result;
|
||||
};
|
||||
|
||||
// 追溯类型文本
|
||||
const getTraceTypeText = (type) => {
|
||||
const texts = {
|
||||
'origin': '源头追溯',
|
||||
'feed': '饵料追溯',
|
||||
'medicine': '药物追溯',
|
||||
'transport': '运输追溯'
|
||||
};
|
||||
return texts[type] || type;
|
||||
};
|
||||
|
||||
// 政策类型文本
|
||||
const getPolicyTypeText = (type) => {
|
||||
const texts = {
|
||||
'industry': '行业政策',
|
||||
'subsidy': '补贴政策',
|
||||
'regulation': '监管政策',
|
||||
'environment': '环保政策'
|
||||
};
|
||||
return texts[type] || type;
|
||||
};
|
||||
|
||||
// 搜索检查
|
||||
const searchInspections = () => {
|
||||
inspectionPagination.current = 1;
|
||||
loadInspections();
|
||||
};
|
||||
|
||||
// 重置检查搜索
|
||||
const resetInspectionSearch = () => {
|
||||
Object.assign(inspectionSearchForm, {
|
||||
farmName: '',
|
||||
inspectionType: '',
|
||||
result: ''
|
||||
});
|
||||
searchInspections();
|
||||
};
|
||||
|
||||
// 搜索追溯
|
||||
const searchTraces = () => {
|
||||
tracePagination.current = 1;
|
||||
loadTraces();
|
||||
};
|
||||
|
||||
// 重置追溯搜索
|
||||
const resetTraceSearch = () => {
|
||||
Object.assign(traceSearchForm, {
|
||||
traceId: '',
|
||||
earTag: '',
|
||||
traceType: ''
|
||||
});
|
||||
searchTraces();
|
||||
};
|
||||
|
||||
// 搜索政策
|
||||
const searchPolicies = () => {
|
||||
policyPagination.current = 1;
|
||||
loadPolicies();
|
||||
};
|
||||
|
||||
// 重置政策搜索
|
||||
const resetPolicySearch = () => {
|
||||
Object.assign(policySearchForm, {
|
||||
title: '',
|
||||
policyType: ''
|
||||
});
|
||||
searchPolicies();
|
||||
};
|
||||
|
||||
// 表格变化处理
|
||||
const handleInspectionTableChange = (pagination) => {
|
||||
inspectionPagination.current = pagination.current;
|
||||
inspectionPagination.pageSize = pagination.pageSize;
|
||||
loadInspections();
|
||||
};
|
||||
|
||||
const handleTraceTableChange = (pagination) => {
|
||||
tracePagination.current = pagination.current;
|
||||
tracePagination.pageSize = pagination.pageSize;
|
||||
loadTraces();
|
||||
};
|
||||
|
||||
const handlePolicyTableChange = (pagination) => {
|
||||
policyPagination.current = pagination.current;
|
||||
policyPagination.pageSize = pagination.pageSize;
|
||||
loadPolicies();
|
||||
};
|
||||
|
||||
// 显示添加模态框
|
||||
const showAddModal = (type) => {
|
||||
message.info(`添加${type === 'inspection' ? '检查' : '追溯'}功能开发中`);
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const viewInspectionDetail = (record) => {
|
||||
message.info(`查看检查详情: ${record.inspection_id}`);
|
||||
};
|
||||
|
||||
const viewTraceDetail = (record) => {
|
||||
message.info(`查看追溯详情: ${record.trace_id}`);
|
||||
};
|
||||
|
||||
const viewPolicyDetail = (record) => {
|
||||
message.info(`查看政策详情: ${record.title}`);
|
||||
};
|
||||
|
||||
// 其他操作
|
||||
const createRectification = (record) => {
|
||||
message.info(`创建整改通知: ${record.inspection_id}`);
|
||||
};
|
||||
|
||||
const viewTraceChain = (record) => {
|
||||
message.info(`查看追溯链: ${record.trace_id}`);
|
||||
};
|
||||
|
||||
const downloadPolicy = (record) => {
|
||||
message.info(`下载政策文件: ${record.title}`);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
loadInspections();
|
||||
loadTraces();
|
||||
loadPolicies();
|
||||
});
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
stats,
|
||||
inspections,
|
||||
inspectionLoading,
|
||||
inspectionSearchForm,
|
||||
inspectionPagination,
|
||||
inspectionColumns,
|
||||
traces,
|
||||
traceLoading,
|
||||
traceSearchForm,
|
||||
tracePagination,
|
||||
traceColumns,
|
||||
policies,
|
||||
governmentData,
|
||||
columns,
|
||||
loading,
|
||||
error
|
||||
policyLoading,
|
||||
policySearchForm,
|
||||
policyPagination,
|
||||
policyColumns,
|
||||
loadInspections,
|
||||
loadTraces,
|
||||
loadPolicies,
|
||||
getInspectionTypeText,
|
||||
getInspectionResultColor,
|
||||
getInspectionResultText,
|
||||
getTraceTypeText,
|
||||
getPolicyTypeText,
|
||||
searchInspections,
|
||||
resetInspectionSearch,
|
||||
searchTraces,
|
||||
resetTraceSearch,
|
||||
searchPolicies,
|
||||
resetPolicySearch,
|
||||
handleInspectionTableChange,
|
||||
handleTraceTableChange,
|
||||
handlePolicyTableChange,
|
||||
showAddModal,
|
||||
viewInspectionDetail,
|
||||
viewTraceDetail,
|
||||
viewPolicyDetail,
|
||||
createRectification,
|
||||
viewTraceChain,
|
||||
downloadPolicy
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.government-container {
|
||||
padding: 20px;
|
||||
.government-page {
|
||||
padding: 24px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.policy-section,
|
||||
.data-section {
|
||||
margin-bottom: 20px;
|
||||
.page-header {
|
||||
background: white;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.loading-indicator,
|
||||
.error-message {
|
||||
.stats-cards {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-cards .ant-card {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
.government-tabs {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.policy-section ul,
|
||||
.data-section {
|
||||
padding: 10px;
|
||||
}
|
||||
.search-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.data-section .ant-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.search-card .ant-form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
284
admin-system/dashboard/src/views/Login.vue
Normal file
284
admin-system/dashboard/src/views/Login.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<h1>锡林郭勒盟智慧养殖平台</h1>
|
||||
<p>数字化管理系统</p>
|
||||
</div>
|
||||
|
||||
<a-form
|
||||
:model="loginForm"
|
||||
:rules="rules"
|
||||
@finish="handleLogin"
|
||||
class="login-form"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item name="username" label="用户名">
|
||||
<a-input
|
||||
v-model:value="loginForm.username"
|
||||
placeholder="请输入用户名"
|
||||
size="large"
|
||||
:prefix="renderIcon('user')"
|
||||
>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="password" label="密码">
|
||||
<a-input-password
|
||||
v-model:value="loginForm.password"
|
||||
placeholder="请输入密码"
|
||||
size="large"
|
||||
:prefix="renderIcon('lock')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="loginForm.remember">
|
||||
记住登录状态
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
block
|
||||
>
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<div class="demo-accounts">
|
||||
<h4>演示账户</h4>
|
||||
<div class="account-list">
|
||||
<div
|
||||
v-for="account in demoAccounts"
|
||||
:key="account.username"
|
||||
@click="setDemoAccount(account)"
|
||||
class="account-item"
|
||||
>
|
||||
<span class="username">{{ account.username }}</span>
|
||||
<span class="role">{{ account.role }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>© 2024 锡林郭勒盟智慧养殖平台. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, h, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
|
||||
import { useAuthStore } from '../store/auth.js';
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 表单数据
|
||||
const loginForm = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
});
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, message: '用户名至少3个字符', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码至少6个字符', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 演示账户
|
||||
const demoAccounts = [
|
||||
{ username: 'admin', password: '123456', role: '系统管理员' },
|
||||
{ username: 'farmer001', password: '123456', role: '养殖户' },
|
||||
{ username: 'banker001', password: '123456', role: '银行职员' },
|
||||
{ username: 'insurer001', password: '123456', role: '保险员' },
|
||||
{ username: 'inspector001', password: '123456', role: '政府检查员' },
|
||||
{ username: 'trader001', password: '123456', role: '交易员' },
|
||||
];
|
||||
|
||||
// 渲染图标
|
||||
const renderIcon = (type) => {
|
||||
const icons = {
|
||||
user: UserOutlined,
|
||||
lock: LockOutlined,
|
||||
};
|
||||
return h(icons[type]);
|
||||
};
|
||||
|
||||
// 设置演示账户
|
||||
const setDemoAccount = (account) => {
|
||||
loginForm.value.username = account.username;
|
||||
loginForm.value.password = account.password;
|
||||
message.info(`已填入${account.role}演示账户信息`);
|
||||
};
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = async () => {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const result = await authStore.login(loginForm.value);
|
||||
|
||||
if (result.success) {
|
||||
message.success('登录成功!');
|
||||
|
||||
// 跳转到首页
|
||||
router.push('/');
|
||||
} else {
|
||||
message.error(result.message || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error);
|
||||
message.error('登录失败,请稍后重试');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时检查是否已登录
|
||||
onMounted(() => {
|
||||
if (authStore.isAuthenticated) {
|
||||
router.push('/');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
color: #2c3e50;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #7f8c8d;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.demo-accounts {
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.demo-accounts h4 {
|
||||
color: #34495e;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.account-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.account-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.account-item:hover {
|
||||
background: #e3f2fd;
|
||||
border-color: #2196f3;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.account-item .username {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.account-item .role {
|
||||
font-size: 10px;
|
||||
color: #7f8c8d;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-footer p {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 480px) {
|
||||
.login-box {
|
||||
padding: 30px 20px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.account-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
692
admin-system/dashboard/src/views/MallManagement.vue
Normal file
692
admin-system/dashboard/src/views/MallManagement.vue
Normal file
@@ -0,0 +1,692 @@
|
||||
<template>
|
||||
<div class="mall-page">
|
||||
<!-- 页面标题和操作按钮 -->
|
||||
<div class="page-header">
|
||||
<a-page-header title="商城管理" sub-title="商品和订单管理">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="showAddModal('product')">
|
||||
<ShopOutlined /> 新增商品
|
||||
</a-button>
|
||||
<a-button @click="showAddModal('order')">
|
||||
<ShoppingCartOutlined /> 新增订单
|
||||
</a-button>
|
||||
</template>
|
||||
</a-page-header>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-cards">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="商品总数" :value="stats.totalProducts" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="订单总数" :value="stats.totalOrders" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="销售额"
|
||||
:value="stats.totalSales"
|
||||
suffix="万元"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="今日订单" :value="stats.todayOrders" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<a-tabs v-model:activeKey="activeTab" class="mall-tabs">
|
||||
<a-tab-pane key="products" tab="商品管理">
|
||||
<!-- 商品搜索表单 -->
|
||||
<a-card class="search-card">
|
||||
<a-form layout="inline" :model="productSearchForm">
|
||||
<a-form-item label="商品名称">
|
||||
<a-input v-model:value="productSearchForm.name" placeholder="请输入商品名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="商品分类">
|
||||
<a-select v-model:value="productSearchForm.category" placeholder="请选择分类" style="width: 150px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="beef">牛肉制品</a-select-option>
|
||||
<a-select-option value="dairy">乳制品</a-select-option>
|
||||
<a-select-option value="snacks">特产零食</a-select-option>
|
||||
<a-select-option value="equipment">养殖设备</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="商品状态">
|
||||
<a-select v-model:value="productSearchForm.status" placeholder="请选择状态" style="width: 120px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="active">上架</a-select-option>
|
||||
<a-select-option value="inactive">下架</a-select-option>
|
||||
<a-select-option value="draft">草稿</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="searchProducts">
|
||||
<SearchOutlined /> 搜索
|
||||
</a-button>
|
||||
<a-button @click="resetProductSearch" style="margin-left: 8px">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="productColumns"
|
||||
:data-source="products"
|
||||
:loading="productLoading"
|
||||
:pagination="productPagination"
|
||||
@change="handleProductTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image
|
||||
:width="50"
|
||||
:height="50"
|
||||
:src="record.image_url || '/placeholder.jpg'"
|
||||
:preview="true"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'category'">
|
||||
<a-tag color="blue">
|
||||
{{ getProductCategoryText(record.category) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getProductStatusColor(record.status)">
|
||||
{{ getProductStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'featured'">
|
||||
<a-tag :color="record.featured ? 'gold' : 'default'">
|
||||
{{ record.featured ? '推荐' : '普通' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="viewProductDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="editProduct(record)"
|
||||
>
|
||||
编辑
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="orders" tab="订单管理">
|
||||
<!-- 订单搜索表单 -->
|
||||
<a-card class="search-card">
|
||||
<a-form layout="inline" :model="orderSearchForm">
|
||||
<a-form-item label="订单编号">
|
||||
<a-input v-model:value="orderSearchForm.orderNumber" placeholder="请输入订单编号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="买家姓名">
|
||||
<a-input v-model:value="orderSearchForm.buyerName" placeholder="请输入买家姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="订单状态">
|
||||
<a-select v-model:value="orderSearchForm.status" placeholder="请选择状态" style="width: 150px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="pending">待付款</a-select-option>
|
||||
<a-select-option value="paid">已付款</a-select-option>
|
||||
<a-select-option value="shipped">已发货</a-select-option>
|
||||
<a-select-option value="delivered">已送达</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
<a-select-option value="cancelled">已取消</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="searchOrders">
|
||||
<SearchOutlined /> 搜索
|
||||
</a-button>
|
||||
<a-button @click="resetOrderSearch" style="margin-left: 8px">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="orderColumns"
|
||||
:data-source="orders"
|
||||
:loading="orderLoading"
|
||||
:pagination="orderPagination"
|
||||
@change="handleOrderTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getOrderStatusColor(record.status)">
|
||||
{{ getOrderStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="viewOrderDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.status === 'pending'"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="confirmOrder(record)"
|
||||
>
|
||||
确认订单
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.status === 'paid'"
|
||||
size="small"
|
||||
@click="shipOrder(record)"
|
||||
>
|
||||
发货
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="reviews" tab="评价管理">
|
||||
<!-- 评价搜索表单 -->
|
||||
<a-card class="search-card">
|
||||
<a-form layout="inline" :model="reviewSearchForm">
|
||||
<a-form-item label="商品名称">
|
||||
<a-input v-model:value="reviewSearchForm.productName" placeholder="请输入商品名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="评分">
|
||||
<a-select v-model:value="reviewSearchForm.rating" placeholder="请选择评分" style="width: 120px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="5">5星</a-select-option>
|
||||
<a-select-option value="4">4星</a-select-option>
|
||||
<a-select-option value="3">3星</a-select-option>
|
||||
<a-select-option value="2">2星</a-select-option>
|
||||
<a-select-option value="1">1星</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="searchReviews">
|
||||
<SearchOutlined /> 搜索
|
||||
</a-button>
|
||||
<a-button @click="resetReviewSearch" style="margin-left: 8px">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 评价列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="reviewColumns"
|
||||
:data-source="reviews"
|
||||
:loading="reviewLoading"
|
||||
:pagination="reviewPagination"
|
||||
@change="handleReviewTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'rating'">
|
||||
<a-rate :value="record.rating" disabled />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="viewReviewDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button size="small" @click="replyReview(record)">
|
||||
回复
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { mallAPI } from '@/services/api.js';
|
||||
import {
|
||||
ShopOutlined,
|
||||
ShoppingCartOutlined,
|
||||
SearchOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
export default {
|
||||
name: 'MallManagement',
|
||||
components: {
|
||||
ShopOutlined,
|
||||
ShoppingCartOutlined,
|
||||
SearchOutlined
|
||||
},
|
||||
setup() {
|
||||
const activeTab = ref('products');
|
||||
const productLoading = ref(false);
|
||||
const orderLoading = ref(false);
|
||||
const reviewLoading = ref(false);
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
totalProducts: 456,
|
||||
totalOrders: 1289,
|
||||
totalSales: 3650,
|
||||
todayOrders: 28
|
||||
});
|
||||
|
||||
// 商品数据
|
||||
const products = ref([]);
|
||||
const productSearchForm = reactive({
|
||||
name: '',
|
||||
category: '',
|
||||
status: ''
|
||||
});
|
||||
const productPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true
|
||||
});
|
||||
|
||||
// 订单数据
|
||||
const orders = ref([]);
|
||||
const orderSearchForm = reactive({
|
||||
orderNumber: '',
|
||||
buyerName: '',
|
||||
status: ''
|
||||
});
|
||||
const orderPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true
|
||||
});
|
||||
|
||||
// 评价数据
|
||||
const reviews = ref([]);
|
||||
const reviewSearchForm = reactive({
|
||||
productName: '',
|
||||
rating: ''
|
||||
});
|
||||
const reviewPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true
|
||||
});
|
||||
|
||||
// 商品表格列
|
||||
const productColumns = [
|
||||
{ title: '商品图片', key: 'image', width: 80 },
|
||||
{ title: '商品名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '分类', dataIndex: 'category', key: 'category' },
|
||||
{ title: '价格(元)', dataIndex: 'price', key: 'price' },
|
||||
{ title: '库存', dataIndex: 'stock', key: 'stock' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '推荐', dataIndex: 'featured', key: 'featured' },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
];
|
||||
|
||||
// 订单表格列
|
||||
const orderColumns = [
|
||||
{ title: '订单编号', dataIndex: 'order_number', key: 'order_number', width: 150 },
|
||||
{ title: '买家', dataIndex: 'buyer_name', key: 'buyer_name' },
|
||||
{ title: '商品名称', dataIndex: 'product_name', key: 'product_name' },
|
||||
{ title: '数量', dataIndex: 'quantity', key: 'quantity' },
|
||||
{ title: '总金额(元)', dataIndex: 'total_amount', key: 'total_amount' },
|
||||
{ title: '订单状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '下单时间', dataIndex: 'created_at', key: 'created_at' },
|
||||
{ title: '操作', key: 'action', width: 180 }
|
||||
];
|
||||
|
||||
// 评价表格列
|
||||
const reviewColumns = [
|
||||
{ title: '商品名称', dataIndex: 'product_name', key: 'product_name' },
|
||||
{ title: '评价人', dataIndex: 'reviewer_name', key: 'reviewer_name' },
|
||||
{ title: '评分', dataIndex: 'rating', key: 'rating' },
|
||||
{ title: '评价内容', dataIndex: 'content', key: 'content' },
|
||||
{ title: '评价时间', dataIndex: 'created_at', key: 'created_at' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
];
|
||||
|
||||
// 加载商品数据
|
||||
const loadProducts = async () => {
|
||||
productLoading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: productPagination.current,
|
||||
limit: productPagination.pageSize,
|
||||
...productSearchForm
|
||||
};
|
||||
const response = await mallAPI.getProducts(params);
|
||||
if (response.success) {
|
||||
products.value = response.data.products || [];
|
||||
productPagination.total = response.data.pagination?.total || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取商品列表失败:', error);
|
||||
message.error('获取商品列表失败');
|
||||
} finally {
|
||||
productLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载订单数据
|
||||
const loadOrders = async () => {
|
||||
orderLoading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: orderPagination.current,
|
||||
limit: orderPagination.pageSize,
|
||||
...orderSearchForm
|
||||
};
|
||||
const response = await mallAPI.getOrders(params);
|
||||
if (response.success) {
|
||||
orders.value = response.data.orders || [];
|
||||
orderPagination.total = response.data.pagination?.total || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取订单列表失败:', error);
|
||||
message.error('获取订单列表失败');
|
||||
} finally {
|
||||
orderLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载评价数据
|
||||
const loadReviews = async () => {
|
||||
reviewLoading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: reviewPagination.current,
|
||||
limit: reviewPagination.pageSize,
|
||||
...reviewSearchForm
|
||||
};
|
||||
const response = await mallAPI.getReviews(params);
|
||||
if (response.success) {
|
||||
reviews.value = response.data.reviews || [];
|
||||
reviewPagination.total = response.data.pagination?.total || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取评价列表失败:', error);
|
||||
message.error('获取评价列表失败');
|
||||
} finally {
|
||||
reviewLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 商品分类文本
|
||||
const getProductCategoryText = (category) => {
|
||||
const texts = {
|
||||
'beef': '牛肉制品',
|
||||
'dairy': '乳制品',
|
||||
'snacks': '特产零食',
|
||||
'equipment': '养殖设备'
|
||||
};
|
||||
return texts[category] || category;
|
||||
};
|
||||
|
||||
// 商品状态颜色
|
||||
const getProductStatusColor = (status) => {
|
||||
const colors = {
|
||||
'active': 'green',
|
||||
'inactive': 'red',
|
||||
'draft': 'orange'
|
||||
};
|
||||
return colors[status] || 'default';
|
||||
};
|
||||
|
||||
// 商品状态文本
|
||||
const getProductStatusText = (status) => {
|
||||
const texts = {
|
||||
'active': '上架',
|
||||
'inactive': '下架',
|
||||
'draft': '草稿'
|
||||
};
|
||||
return texts[status] || status;
|
||||
};
|
||||
|
||||
// 订单状态颜色
|
||||
const getOrderStatusColor = (status) => {
|
||||
const colors = {
|
||||
'pending': 'orange',
|
||||
'paid': 'blue',
|
||||
'shipped': 'purple',
|
||||
'delivered': 'cyan',
|
||||
'completed': 'green',
|
||||
'cancelled': 'red'
|
||||
};
|
||||
return colors[status] || 'default';
|
||||
};
|
||||
|
||||
// 订单状态文本
|
||||
const getOrderStatusText = (status) => {
|
||||
const texts = {
|
||||
'pending': '待付款',
|
||||
'paid': '已付款',
|
||||
'shipped': '已发货',
|
||||
'delivered': '已送达',
|
||||
'completed': '已完成',
|
||||
'cancelled': '已取消'
|
||||
};
|
||||
return texts[status] || status;
|
||||
};
|
||||
|
||||
// 搜索商品
|
||||
const searchProducts = () => {
|
||||
productPagination.current = 1;
|
||||
loadProducts();
|
||||
};
|
||||
|
||||
// 重置商品搜索
|
||||
const resetProductSearch = () => {
|
||||
Object.assign(productSearchForm, {
|
||||
name: '',
|
||||
category: '',
|
||||
status: ''
|
||||
});
|
||||
searchProducts();
|
||||
};
|
||||
|
||||
// 搜索订单
|
||||
const searchOrders = () => {
|
||||
orderPagination.current = 1;
|
||||
loadOrders();
|
||||
};
|
||||
|
||||
// 重置订单搜索
|
||||
const resetOrderSearch = () => {
|
||||
Object.assign(orderSearchForm, {
|
||||
orderNumber: '',
|
||||
buyerName: '',
|
||||
status: ''
|
||||
});
|
||||
searchOrders();
|
||||
};
|
||||
|
||||
// 搜索评价
|
||||
const searchReviews = () => {
|
||||
reviewPagination.current = 1;
|
||||
loadReviews();
|
||||
};
|
||||
|
||||
// 重置评价搜索
|
||||
const resetReviewSearch = () => {
|
||||
Object.assign(reviewSearchForm, {
|
||||
productName: '',
|
||||
rating: ''
|
||||
});
|
||||
searchReviews();
|
||||
};
|
||||
|
||||
// 表格变化处理
|
||||
const handleProductTableChange = (pagination) => {
|
||||
productPagination.current = pagination.current;
|
||||
productPagination.pageSize = pagination.pageSize;
|
||||
loadProducts();
|
||||
};
|
||||
|
||||
const handleOrderTableChange = (pagination) => {
|
||||
orderPagination.current = pagination.current;
|
||||
orderPagination.pageSize = pagination.pageSize;
|
||||
loadOrders();
|
||||
};
|
||||
|
||||
const handleReviewTableChange = (pagination) => {
|
||||
reviewPagination.current = pagination.current;
|
||||
reviewPagination.pageSize = pagination.pageSize;
|
||||
loadReviews();
|
||||
};
|
||||
|
||||
// 显示添加模态框
|
||||
const showAddModal = (type) => {
|
||||
message.info(`添加${type === 'product' ? '商品' : '订单'}功能开发中`);
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const viewProductDetail = (record) => {
|
||||
message.info(`查看商品详情: ${record.name}`);
|
||||
};
|
||||
|
||||
const viewOrderDetail = (record) => {
|
||||
message.info(`查看订单详情: ${record.order_number}`);
|
||||
};
|
||||
|
||||
const viewReviewDetail = (record) => {
|
||||
message.info(`查看评价详情: ${record.product_name}`);
|
||||
};
|
||||
|
||||
// 其他操作
|
||||
const editProduct = (record) => {
|
||||
message.info(`编辑商品: ${record.name}`);
|
||||
};
|
||||
|
||||
const confirmOrder = (record) => {
|
||||
message.info(`确认订单: ${record.order_number}`);
|
||||
};
|
||||
|
||||
const shipOrder = (record) => {
|
||||
message.info(`发货订单: ${record.order_number}`);
|
||||
};
|
||||
|
||||
const replyReview = (record) => {
|
||||
message.info(`回复评价: ${record.product_name}`);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadProducts();
|
||||
loadOrders();
|
||||
loadReviews();
|
||||
});
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
stats,
|
||||
products,
|
||||
productLoading,
|
||||
productSearchForm,
|
||||
productPagination,
|
||||
productColumns,
|
||||
orders,
|
||||
orderLoading,
|
||||
orderSearchForm,
|
||||
orderPagination,
|
||||
orderColumns,
|
||||
reviews,
|
||||
reviewLoading,
|
||||
reviewSearchForm,
|
||||
reviewPagination,
|
||||
reviewColumns,
|
||||
loadProducts,
|
||||
loadOrders,
|
||||
loadReviews,
|
||||
getProductCategoryText,
|
||||
getProductStatusColor,
|
||||
getProductStatusText,
|
||||
getOrderStatusColor,
|
||||
getOrderStatusText,
|
||||
searchProducts,
|
||||
resetProductSearch,
|
||||
searchOrders,
|
||||
resetOrderSearch,
|
||||
searchReviews,
|
||||
resetReviewSearch,
|
||||
handleProductTableChange,
|
||||
handleOrderTableChange,
|
||||
handleReviewTableChange,
|
||||
showAddModal,
|
||||
viewProductDetail,
|
||||
viewOrderDetail,
|
||||
viewReviewDetail,
|
||||
editProduct,
|
||||
confirmOrder,
|
||||
shipOrder,
|
||||
replyReview
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mall-page {
|
||||
padding: 24px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: white;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-cards .ant-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mall-tabs {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-card .ant-form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,478 +1,557 @@
|
||||
<template>
|
||||
<div class="trade-container">
|
||||
<h1>交易统计</h1>
|
||||
<div v-if="loading" class="loading-indicator">
|
||||
<div class="loading-spinner"></div>
|
||||
数据加载中...
|
||||
</div>
|
||||
<div v-if="error" class="error-message">数据加载失败,请稍后重试。</div>
|
||||
<div v-if="!loading && !error" class="trade-content">
|
||||
<!-- 牛只交易量统计 -->
|
||||
<div class="volume-section card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">牛只交易量统计</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="volume-content">
|
||||
<div class="volume-cards">
|
||||
<div class="volume-card card" v-for="(item, index) in volumeData" :key="index">
|
||||
<div class="card-body">
|
||||
<div class="card-title">{{ item.title }}</div>
|
||||
<div class="card-value">{{ item.value }}</div>
|
||||
<div class="card-change" :class="item.change > 0 ? 'positive' : 'negative'">
|
||||
{{ item.change > 0 ? '↑' : '↓' }} {{ Math.abs(item.change) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="volume-chart">
|
||||
<div ref="volumeChart" class="chart-placeholder"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 价格趋势和区域分布 -->
|
||||
<div class="price-section card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">价格趋势和区域分布</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="price-content">
|
||||
<div class="trend-chart card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">价格趋势</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div ref="trendChart" class="chart-placeholder"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="distribution-chart card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">区域价格分布</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div ref="distributionChart" class="chart-placeholder"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 交易类型分析 -->
|
||||
<div class="type-section card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">交易类型分析</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="type-content">
|
||||
<div class="type-chart">
|
||||
<div ref="typeChart" class="chart-placeholder"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 交易排行榜 -->
|
||||
<div class="ranking-section card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">交易排行榜</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="ranking-content">
|
||||
<div class="farm-ranking card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">热门牧场</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<a-table :dataSource="farmRankingData" :columns="farmRankingColumns" :pagination="false" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="trader-ranking card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">活跃交易员</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<a-table :dataSource="traderRankingData" :columns="traderRankingColumns" :pagination="false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="trade-page">
|
||||
<!-- 页面标题和操作按钮 -->
|
||||
<div class="page-header">
|
||||
<a-page-header title="交易管理" sub-title="交易记录和合同管理">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="showAddModal('transaction')">
|
||||
<PlusOutlined /> 新增交易
|
||||
</a-button>
|
||||
<a-button @click="showAddModal('contract')">
|
||||
<FileTextOutlined /> 新增合同
|
||||
</a-button>
|
||||
</template>
|
||||
</a-page-header>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-cards">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="总交易量" :value="stats.totalTransactions" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="有效合同" :value="stats.totalContracts" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="交易总金额"
|
||||
:value="stats.totalAmount"
|
||||
suffix="万元"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="今日交易"
|
||||
:value="stats.todayTransactions"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<a-tabs v-model:activeKey="activeTab" class="trade-tabs">
|
||||
<a-tab-pane key="transactions" tab="交易记录">
|
||||
<!-- 交易搜索表单 -->
|
||||
<a-card class="search-card">
|
||||
<a-form layout="inline" :model="transactionSearchForm">
|
||||
<a-form-item label="交易编号">
|
||||
<a-input v-model:value="transactionSearchForm.transactionNumber" placeholder="请输入交易编号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="交易类型">
|
||||
<a-select v-model:value="transactionSearchForm.transactionType" placeholder="请选择交易类型" style="width: 150px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="cattle_sale">牛只销售</a-select-option>
|
||||
<a-select-option value="feed_purchase">饵料采购</a-select-option>
|
||||
<a-select-option value="equipment_sale">设备销售</a-select-option>
|
||||
<a-select-option value="service">服务</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="交易状态">
|
||||
<a-select v-model:value="transactionSearchForm.status" placeholder="请选择状态" style="width: 150px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="pending">待处理</a-select-option>
|
||||
<a-select-option value="confirmed">已确认</a-select-option>
|
||||
<a-select-option value="in_progress">进行中</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
<a-select-option value="cancelled">已取消</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="searchTransactions">
|
||||
<SearchOutlined /> 搜索
|
||||
</a-button>
|
||||
<a-button @click="resetTransactionSearch" style="margin-left: 8px">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 交易列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="transactionColumns"
|
||||
:data-source="transactions"
|
||||
:loading="transactionLoading"
|
||||
:pagination="transactionPagination"
|
||||
@change="handleTransactionTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'transaction_type'">
|
||||
<a-tag color="blue">
|
||||
{{ getTransactionTypeText(record.transaction_type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getTransactionStatusColor(record.status)">
|
||||
{{ getTransactionStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="viewTransactionDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.status === 'pending'"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="confirmTransaction(record)"
|
||||
>
|
||||
确认
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="contracts" tab="合同管理">
|
||||
<!-- 合同搜索表单 -->
|
||||
<a-card class="search-card">
|
||||
<a-form layout="inline" :model="contractSearchForm">
|
||||
<a-form-item label="合同编号">
|
||||
<a-input v-model:value="contractSearchForm.contractNumber" placeholder="请输入合同编号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="合同类型">
|
||||
<a-select v-model:value="contractSearchForm.contractType" placeholder="请选择合同类型" style="width: 150px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="sale">销售合同</a-select-option>
|
||||
<a-select-option value="purchase">采购合同</a-select-option>
|
||||
<a-select-option value="service">服务合同</a-select-option>
|
||||
<a-select-option value="lease">租赁合同</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="合同状态">
|
||||
<a-select v-model:value="contractSearchForm.status" placeholder="请选择状态" style="width: 150px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="draft">草稿</a-select-option>
|
||||
<a-select-option value="pending">待签署</a-select-option>
|
||||
<a-select-option value="signed">已签署</a-select-option>
|
||||
<a-select-option value="executing">执行中</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
<a-select-option value="terminated">已终止</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="searchContracts">
|
||||
<SearchOutlined /> 搜索
|
||||
</a-button>
|
||||
<a-button @click="resetContractSearch" style="margin-left: 8px">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 合同列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="contractColumns"
|
||||
:data-source="contracts"
|
||||
:loading="contractLoading"
|
||||
:pagination="contractPagination"
|
||||
@change="handleContractTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'contract_type'">
|
||||
<a-tag color="green">
|
||||
{{ getContractTypeText(record.contract_type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getContractStatusColor(record.status)">
|
||||
{{ getContractStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="viewContractDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.status === 'pending'"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="signContract(record)"
|
||||
>
|
||||
签署
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { tradingAPI } from '@/services/api.js';
|
||||
import {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
FileTextOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
export default {
|
||||
name: 'Trade',
|
||||
components: {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
FileTextOutlined
|
||||
},
|
||||
setup() {
|
||||
const volumeData = ref([]);
|
||||
const farmRankingData = ref([]);
|
||||
const traderRankingData = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
const volumeChart = ref(null);
|
||||
const trendChart = ref(null);
|
||||
const distributionChart = ref(null);
|
||||
const typeChart = ref(null);
|
||||
const activeTab = ref('transactions');
|
||||
const transactionLoading = ref(false);
|
||||
const contractLoading = ref(false);
|
||||
|
||||
let volumeChartInstance = null;
|
||||
let trendChartInstance = null;
|
||||
let distributionChartInstance = null;
|
||||
let typeChartInstance = null;
|
||||
|
||||
// 交易量数据
|
||||
volumeData.value = [
|
||||
{ title: '今日交易量', value: '1,245头', change: 5.2 },
|
||||
{ title: '本月交易量', value: '38,650头', change: 8.7 },
|
||||
{ title: '年度交易量', value: '420,860头', change: 12.3 }
|
||||
];
|
||||
|
||||
// 热门牧场排行榜列定义
|
||||
const farmRankingColumns = ref([
|
||||
{ title: '排名', dataIndex: 'rank', key: 'rank' },
|
||||
{ title: '牧场名称', dataIndex: 'farm', key: 'farm' },
|
||||
{ title: '交易量', dataIndex: 'volume', key: 'volume' },
|
||||
{ title: '交易额', dataIndex: 'amount', key: 'amount' },
|
||||
]);
|
||||
|
||||
// 热门牧场排行榜数据
|
||||
farmRankingData.value = [
|
||||
{ key: '1', rank: '1', farm: '锡市牧场A', volume: '2,450头', amount: '¥1,245万' },
|
||||
{ key: '2', rank: '2', farm: '东乌旗牧场B', volume: '1,980头', amount: '¥980万' },
|
||||
{ key: '3', rank: '3', farm: '西乌旗牧场C', volume: '1,650头', amount: '¥820万' },
|
||||
{ key: '4', rank: '4', farm: '镶黄旗牧场D', volume: '1,320头', amount: '¥650万' },
|
||||
{ key: '5', rank: '5', farm: '正蓝旗牧场E', volume: '1,150头', amount: '¥580万' },
|
||||
];
|
||||
|
||||
// 活跃交易员排行榜列定义
|
||||
const traderRankingColumns = ref([
|
||||
{ title: '排名', dataIndex: 'rank', key: 'rank' },
|
||||
{ title: '交易员', dataIndex: 'trader', key: 'trader' },
|
||||
{ title: '交易数', dataIndex: 'count', key: 'count' },
|
||||
{ title: '交易额', dataIndex: 'amount', key: 'amount' },
|
||||
]);
|
||||
|
||||
// 活跃交易员排行榜数据
|
||||
traderRankingData.value = [
|
||||
{ key: '1', rank: '1', trader: '张三', count: '126笔', amount: '¥860万' },
|
||||
{ key: '2', rank: '2', trader: '李四', count: '98笔', amount: '¥620万' },
|
||||
{ key: '3', rank: '3', trader: '王五', count: '85笔', amount: '¥540万' },
|
||||
{ key: '4', rank: '4', trader: '赵六', count: '72笔', amount: '¥420万' },
|
||||
{ key: '5', rank: '5', trader: '孙七', count: '65笔', amount: '¥380万' },
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
totalTransactions: 2456,
|
||||
totalContracts: 189,
|
||||
totalAmount: 8650,
|
||||
todayTransactions: 45
|
||||
});
|
||||
|
||||
// 交易数据
|
||||
const transactions = ref([]);
|
||||
const transactionSearchForm = reactive({
|
||||
transactionNumber: '',
|
||||
transactionType: '',
|
||||
status: ''
|
||||
});
|
||||
const transactionPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true
|
||||
});
|
||||
|
||||
// 合同数据
|
||||
const contracts = ref([]);
|
||||
const contractSearchForm = reactive({
|
||||
contractNumber: '',
|
||||
contractType: '',
|
||||
status: ''
|
||||
});
|
||||
const contractPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true
|
||||
});
|
||||
|
||||
// 交易表格列
|
||||
const transactionColumns = [
|
||||
{ title: '交易编号', dataIndex: 'transaction_number', key: 'transaction_number', width: 150 },
|
||||
{ title: '交易类型', dataIndex: 'transaction_type', key: 'transaction_type' },
|
||||
{ title: '买方', dataIndex: 'buyer_name', key: 'buyer_name' },
|
||||
{ title: '卖方', dataIndex: 'seller_name', key: 'seller_name' },
|
||||
{ title: '交易金额(万元)', dataIndex: 'total_amount', key: 'total_amount' },
|
||||
{ title: '交易状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '交易时间', dataIndex: 'created_at', key: 'created_at' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
];
|
||||
|
||||
// 初始化交易量图表
|
||||
const initVolumeChart = () => {
|
||||
if (volumeChart.value) {
|
||||
volumeChartInstance = echarts.init(volumeChart.value);
|
||||
volumeChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [32000, 35000, 38000, 40000, 42000, 45000],
|
||||
type: 'bar',
|
||||
itemStyle: { color: '#4CAF50' }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化价格趋势图表
|
||||
const initTrendChart = () => {
|
||||
if (trendChart.value) {
|
||||
trendChartInstance = echarts.init(trendChart.value);
|
||||
trendChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [28000, 29500, 31000, 30500, 32000, 33500],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: { color: '#2196F3' }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化区域分布图表
|
||||
const initDistributionChart = () => {
|
||||
if (distributionChart.value) {
|
||||
distributionChartInstance = echarts.init(distributionChart.value);
|
||||
distributionChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
bottom: '0'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
data: [
|
||||
{ value: 35, name: '锡市' },
|
||||
{ value: 25, name: '东乌旗' },
|
||||
{ value: 20, name: '西乌旗' },
|
||||
{ value: 10, name: '镶黄旗' },
|
||||
{ value: 10, name: '其他' }
|
||||
],
|
||||
itemStyle: {
|
||||
color: function(params) {
|
||||
const colorList = ['#4CAF50', '#2196F3', '#FF9800', '#f44336', '#9C27B0'];
|
||||
return colorList[params.dataIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化交易类型图表
|
||||
const initTypeChart = () => {
|
||||
if (typeChart.value) {
|
||||
typeChartInstance = echarts.init(typeChart.value);
|
||||
typeChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['活牛交易', '牛肉制品']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '活牛交易',
|
||||
type: 'bar',
|
||||
stack: '总量',
|
||||
data: [28000, 30000, 32000, 31000, 33000, 35000],
|
||||
itemStyle: { color: '#4CAF50' }
|
||||
},
|
||||
{
|
||||
name: '牛肉制品',
|
||||
type: 'bar',
|
||||
stack: '总量',
|
||||
data: [4000, 5000, 6000, 5500, 7000, 8000],
|
||||
itemStyle: { color: '#2196F3' }
|
||||
}
|
||||
]
|
||||
});
|
||||
// 合同表格列
|
||||
const contractColumns = [
|
||||
{ title: '合同编号', dataIndex: 'contract_number', key: 'contract_number', width: 150 },
|
||||
{ title: '合同类型', dataIndex: 'contract_type', key: 'contract_type' },
|
||||
{ title: '甲方', dataIndex: 'party_a_name', key: 'party_a_name' },
|
||||
{ title: '乙方', dataIndex: 'party_b_name', key: 'party_b_name' },
|
||||
{ title: '合同金额(万元)', dataIndex: 'contract_amount', key: 'contract_amount' },
|
||||
{ title: '合同状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '签署时间', dataIndex: 'signed_date', key: 'signed_date' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
];
|
||||
|
||||
// 加载交易数据
|
||||
const loadTransactions = async () => {
|
||||
transactionLoading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: transactionPagination.current,
|
||||
limit: transactionPagination.pageSize,
|
||||
...transactionSearchForm
|
||||
};
|
||||
const response = await tradingAPI.getTransactions(params);
|
||||
if (response.success) {
|
||||
transactions.value = response.data.transactions || [];
|
||||
transactionPagination.total = response.data.pagination?.total || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取交易列表失败:', error);
|
||||
message.error('获取交易列表失败');
|
||||
} finally {
|
||||
transactionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口大小改变时重绘图表
|
||||
const resizeCharts = () => {
|
||||
if (volumeChartInstance) volumeChartInstance.resize();
|
||||
if (trendChartInstance) trendChartInstance.resize();
|
||||
if (distributionChartInstance) distributionChartInstance.resize();
|
||||
if (typeChartInstance) typeChartInstance.resize();
|
||||
// 加载合同数据
|
||||
const loadContracts = async () => {
|
||||
contractLoading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: contractPagination.current,
|
||||
limit: contractPagination.pageSize,
|
||||
...contractSearchForm
|
||||
};
|
||||
const response = await tradingAPI.getContracts(params);
|
||||
if (response.success) {
|
||||
contracts.value = response.data.contracts || [];
|
||||
contractPagination.total = response.data.pagination?.total || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取合同列表失败:', error);
|
||||
message.error('获取合同列表失败');
|
||||
} finally {
|
||||
contractLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 交易类型文本
|
||||
const getTransactionTypeText = (type) => {
|
||||
const texts = {
|
||||
'cattle_sale': '牛只销售',
|
||||
'feed_purchase': '饲料采购',
|
||||
'equipment_sale': '设备销售',
|
||||
'service': '服务'
|
||||
};
|
||||
return texts[type] || type;
|
||||
};
|
||||
|
||||
// 交易状态颜色
|
||||
const getTransactionStatusColor = (status) => {
|
||||
const colors = {
|
||||
'pending': 'orange',
|
||||
'confirmed': 'blue',
|
||||
'in_progress': 'purple',
|
||||
'completed': 'green',
|
||||
'cancelled': 'red',
|
||||
'refunded': 'red'
|
||||
};
|
||||
return colors[status] || 'default';
|
||||
};
|
||||
|
||||
// 交易状态文本
|
||||
const getTransactionStatusText = (status) => {
|
||||
const texts = {
|
||||
'pending': '待处理',
|
||||
'confirmed': '已确认',
|
||||
'in_progress': '进行中',
|
||||
'completed': '已完成',
|
||||
'cancelled': '已取消',
|
||||
'refunded': '已退款'
|
||||
};
|
||||
return texts[status] || status;
|
||||
};
|
||||
|
||||
// 合同类型文本
|
||||
const getContractTypeText = (type) => {
|
||||
const texts = {
|
||||
'sale': '销售合同',
|
||||
'purchase': '采购合同',
|
||||
'service': '服务合同',
|
||||
'lease': '租赁合同'
|
||||
};
|
||||
return texts[type] || type;
|
||||
};
|
||||
|
||||
// 合同状态颜色
|
||||
const getContractStatusColor = (status) => {
|
||||
const colors = {
|
||||
'draft': 'default',
|
||||
'pending': 'orange',
|
||||
'signed': 'blue',
|
||||
'executing': 'purple',
|
||||
'completed': 'green',
|
||||
'terminated': 'red'
|
||||
};
|
||||
return colors[status] || 'default';
|
||||
};
|
||||
|
||||
// 合同状态文本
|
||||
const getContractStatusText = (status) => {
|
||||
const texts = {
|
||||
'draft': '草稿',
|
||||
'pending': '待签署',
|
||||
'signed': '已签署',
|
||||
'executing': '执行中',
|
||||
'completed': '已完成',
|
||||
'terminated': '已终止'
|
||||
};
|
||||
return texts[status] || status;
|
||||
};
|
||||
|
||||
// 搜索交易
|
||||
const searchTransactions = () => {
|
||||
transactionPagination.current = 1;
|
||||
loadTransactions();
|
||||
};
|
||||
|
||||
// 重置交易搜索
|
||||
const resetTransactionSearch = () => {
|
||||
Object.assign(transactionSearchForm, {
|
||||
transactionNumber: '',
|
||||
transactionType: '',
|
||||
status: ''
|
||||
});
|
||||
searchTransactions();
|
||||
};
|
||||
|
||||
// 搜索合同
|
||||
const searchContracts = () => {
|
||||
contractPagination.current = 1;
|
||||
loadContracts();
|
||||
};
|
||||
|
||||
// 重置合同搜索
|
||||
const resetContractSearch = () => {
|
||||
Object.assign(contractSearchForm, {
|
||||
contractNumber: '',
|
||||
contractType: '',
|
||||
status: ''
|
||||
});
|
||||
searchContracts();
|
||||
};
|
||||
|
||||
// 表格变化处理
|
||||
const handleTransactionTableChange = (pagination) => {
|
||||
transactionPagination.current = pagination.current;
|
||||
transactionPagination.pageSize = pagination.pageSize;
|
||||
loadTransactions();
|
||||
};
|
||||
|
||||
const handleContractTableChange = (pagination) => {
|
||||
contractPagination.current = pagination.current;
|
||||
contractPagination.pageSize = pagination.pageSize;
|
||||
loadContracts();
|
||||
};
|
||||
|
||||
// 显示添加模态框
|
||||
const showAddModal = (type) => {
|
||||
message.info(`添加${type === 'transaction' ? '交易' : '合同'}功能开发中`);
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const viewTransactionDetail = (record) => {
|
||||
message.info(`查看交易详情: ${record.transaction_number}`);
|
||||
};
|
||||
|
||||
const viewContractDetail = (record) => {
|
||||
message.info(`查看合同详情: ${record.contract_number}`);
|
||||
};
|
||||
|
||||
// 确认交易
|
||||
const confirmTransaction = (record) => {
|
||||
message.info(`确认交易: ${record.transaction_number}`);
|
||||
};
|
||||
|
||||
// 签署合同
|
||||
const signContract = (record) => {
|
||||
message.info(`签署合同: ${record.contract_number}`);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loading.value = false;
|
||||
initVolumeChart();
|
||||
initTrendChart();
|
||||
initDistributionChart();
|
||||
initTypeChart();
|
||||
window.addEventListener('resize', resizeCharts);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resizeCharts);
|
||||
if (volumeChartInstance) volumeChartInstance.dispose();
|
||||
if (trendChartInstance) trendChartInstance.dispose();
|
||||
if (distributionChartInstance) distributionChartInstance.dispose();
|
||||
if (typeChartInstance) typeChartInstance.dispose();
|
||||
loadTransactions();
|
||||
loadContracts();
|
||||
});
|
||||
|
||||
return {
|
||||
volumeData,
|
||||
farmRankingData,
|
||||
traderRankingData,
|
||||
farmRankingColumns,
|
||||
traderRankingColumns,
|
||||
loading,
|
||||
error,
|
||||
volumeChart,
|
||||
trendChart,
|
||||
distributionChart,
|
||||
typeChart
|
||||
activeTab,
|
||||
stats,
|
||||
transactions,
|
||||
transactionLoading,
|
||||
transactionSearchForm,
|
||||
transactionPagination,
|
||||
transactionColumns,
|
||||
contracts,
|
||||
contractLoading,
|
||||
contractSearchForm,
|
||||
contractPagination,
|
||||
contractColumns,
|
||||
loadTransactions,
|
||||
loadContracts,
|
||||
getTransactionTypeText,
|
||||
getTransactionStatusColor,
|
||||
getTransactionStatusText,
|
||||
getContractTypeText,
|
||||
getContractStatusColor,
|
||||
getContractStatusText,
|
||||
searchTransactions,
|
||||
resetTransactionSearch,
|
||||
searchContracts,
|
||||
resetContractSearch,
|
||||
handleTransactionTableChange,
|
||||
handleContractTableChange,
|
||||
showAddModal,
|
||||
viewTransactionDetail,
|
||||
viewContractDetail,
|
||||
confirmTransaction,
|
||||
signContract
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trade-container {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(135deg, #0f2027, #20555d, #2c5364);
|
||||
.trade-page {
|
||||
padding: 24px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.trade-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
.page-header {
|
||||
background: white;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.volume-section,
|
||||
.price-section,
|
||||
.type-section,
|
||||
.ranking-section {
|
||||
box-shadow: var(--box-shadow);
|
||||
border-radius: var(--border-radius-lg);
|
||||
.stats-cards {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.volume-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.volume-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.volume-card {
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.card-change {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card-change.positive {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.card-change.negative {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.price-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.type-content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(76, 175, 80, 0.3);
|
||||
border-top: 4px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 20px;
|
||||
.stats-cards .ant-card {
|
||||
text-align: center;
|
||||
color: var(--danger-color);
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.trade-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.volume-content,
|
||||
.price-content,
|
||||
.ranking-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
height: 250px;
|
||||
}
|
||||
.trade-tabs {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-card .ant-form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
345
admin-system/dashboard/src/views/UserManagement.vue
Normal file
345
admin-system/dashboard/src/views/UserManagement.vue
Normal file
@@ -0,0 +1,345 @@
|
||||
<template>
|
||||
<div class="user-management">
|
||||
<a-card title="用户管理" :bordered="false">
|
||||
<!-- 操作按钮 -->
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="showAddModal = true">
|
||||
<template #icon><UserAddOutlined /></template>
|
||||
添加用户
|
||||
</a-button>
|
||||
<a-button @click="loadUsers">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<div class="search-form">
|
||||
<a-form layout="inline" :model="searchForm" @finish="handleSearch">
|
||||
<a-form-item label="用户名">
|
||||
<a-input v-model:value="searchForm.username" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="用户类型">
|
||||
<a-select v-model:value="searchForm.user_type" placeholder="请选择用户类型" style="width: 120px;">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="admin">管理员</a-select-option>
|
||||
<a-select-option value="farmer">养殖户</a-select-option>
|
||||
<a-select-option value="banker">银行职员</a-select-option>
|
||||
<a-select-option value="insurer">保险员</a-select-option>
|
||||
<a-select-option value="government">政府人员</a-select-option>
|
||||
<a-select-option value="trader">交易员</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">搜索</a-button>
|
||||
<a-button style="margin-left: 8px;" @click="resetSearch">重置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="users"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'user_type'">
|
||||
<a-tag :color="getUserTypeColor(record.user_type)">
|
||||
{{ getUserTypeText(record.user_type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
||||
{{ record.status === 1 ? '正常' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="editUser(record)">编辑</a-button>
|
||||
<a-button type="link" size="small" @click="viewUser(record)">查看</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这个用户吗?"
|
||||
@confirm="deleteUser(record.id)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 添加/编辑用户模态框 -->
|
||||
<a-modal
|
||||
v-model:open="showAddModal"
|
||||
:title="editingUser ? '编辑用户' : '添加用户'"
|
||||
@ok="handleSaveUser"
|
||||
@cancel="handleCancel"
|
||||
:confirm-loading="saving"
|
||||
>
|
||||
<a-form :model="userForm" :rules="rules" ref="userFormRef" layout="vertical">
|
||||
<a-form-item label="用户名" name="username">
|
||||
<a-input v-model:value="userForm.username" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="真实姓名" name="real_name">
|
||||
<a-input v-model:value="userForm.real_name" placeholder="请输入真实姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input v-model:value="userForm.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号" name="phone">
|
||||
<a-input v-model:value="userForm.phone" placeholder="请输入手机号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="用户类型" name="user_type">
|
||||
<a-select v-model:value="userForm.user_type" placeholder="请选择用户类型">
|
||||
<a-select-option value="admin">管理员</a-select-option>
|
||||
<a-select-option value="farmer">养殖户</a-select-option>
|
||||
<a-select-option value="banker">银行职员</a-select-option>
|
||||
<a-select-option value="insurer">保险员</a-select-option>
|
||||
<a-select-option value="government">政府人员</a-select-option>
|
||||
<a-select-option value="trader">交易员</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="!editingUser" label="密码" name="password">
|
||||
<a-input-password v-model:value="userForm.password" placeholder="请输入密码" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { UserAddOutlined, ReloadOutlined } from '@ant-design/icons-vue';
|
||||
import { userAPI } from '../services/api.js';
|
||||
|
||||
// 响应式数据
|
||||
const users = ref([]);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const showAddModal = ref(false);
|
||||
const editingUser = ref(null);
|
||||
const userFormRef = ref();
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
username: '',
|
||||
user_type: '',
|
||||
});
|
||||
|
||||
// 用户表单
|
||||
const userForm = reactive({
|
||||
username: '',
|
||||
real_name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
user_type: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
});
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{ title: '真实姓名', dataIndex: 'real_name', key: 'real_name' },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
||||
{ title: '手机号', dataIndex: 'phone', key: 'phone' },
|
||||
{ title: '用户类型', dataIndex: 'user_type', key: 'user_type' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 160 },
|
||||
{ title: '操作', key: 'action', width: 200 },
|
||||
];
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名' }],
|
||||
real_name: [{ required: true, message: '请输入真实姓名' }],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '邮箱格式不正确' },
|
||||
],
|
||||
phone: [{ required: true, message: '请输入手机号' }],
|
||||
user_type: [{ required: true, message: '请选择用户类型' }],
|
||||
password: [{ required: true, message: '请输入密码', min: 6 }],
|
||||
};
|
||||
|
||||
// 加载用户列表
|
||||
const loadUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
...searchForm,
|
||||
};
|
||||
|
||||
const response = await userAPI.getUsers(params);
|
||||
|
||||
if (response.success) {
|
||||
users.value = response.data.users || [];
|
||||
pagination.total = response.data.pagination?.total || 0;
|
||||
} else {
|
||||
message.error(response.message || '获取用户列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error);
|
||||
message.error('获取用户列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 表格变化处理
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current;
|
||||
pagination.pageSize = pag.pageSize;
|
||||
loadUsers();
|
||||
};
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1;
|
||||
loadUsers();
|
||||
};
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
username: '',
|
||||
user_type: '',
|
||||
});
|
||||
pagination.current = 1;
|
||||
loadUsers();
|
||||
};
|
||||
|
||||
// 获取用户类型颜色
|
||||
const getUserTypeColor = (type) => {
|
||||
const colors = {
|
||||
admin: 'red',
|
||||
farmer: 'green',
|
||||
banker: 'blue',
|
||||
insurer: 'orange',
|
||||
government: 'purple',
|
||||
trader: 'cyan',
|
||||
};
|
||||
return colors[type] || 'default';
|
||||
};
|
||||
|
||||
// 获取用户类型文本
|
||||
const getUserTypeText = (type) => {
|
||||
const texts = {
|
||||
admin: '管理员',
|
||||
farmer: '养殖户',
|
||||
banker: '银行职员',
|
||||
insurer: '保险员',
|
||||
government: '政府人员',
|
||||
trader: '交易员',
|
||||
};
|
||||
return texts[type] || type;
|
||||
};
|
||||
|
||||
// 编辑用户
|
||||
const editUser = (record) => {
|
||||
editingUser.value = record;
|
||||
Object.assign(userForm, {
|
||||
username: record.username,
|
||||
real_name: record.real_name,
|
||||
email: record.email,
|
||||
phone: record.phone,
|
||||
user_type: record.user_type,
|
||||
password: '',
|
||||
});
|
||||
showAddModal.value = true;
|
||||
};
|
||||
|
||||
// 查看用户
|
||||
const viewUser = (record) => {
|
||||
message.info(`查看用户: ${record.real_name}`);
|
||||
};
|
||||
|
||||
// 删除用户
|
||||
const deleteUser = async (id) => {
|
||||
try {
|
||||
const response = await userAPI.deleteUser(id);
|
||||
if (response.success) {
|
||||
message.success('删除成功');
|
||||
loadUsers();
|
||||
} else {
|
||||
message.error(response.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error);
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 保存用户
|
||||
const handleSaveUser = async () => {
|
||||
try {
|
||||
await userFormRef.value.validate();
|
||||
saving.value = true;
|
||||
|
||||
let response;
|
||||
if (editingUser.value) {
|
||||
response = await userAPI.updateUser(editingUser.value.id, userForm);
|
||||
} else {
|
||||
response = await userAPI.createUser(userForm);
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
message.success(editingUser.value ? '更新成功' : '创建成功');
|
||||
showAddModal.value = false;
|
||||
loadUsers();
|
||||
} else {
|
||||
message.error(response.message || '保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存用户失败:', error);
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
showAddModal.value = false;
|
||||
editingUser.value = null;
|
||||
userFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadUsers();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-management {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user