更新项目文件结构,统一文档风格

This commit is contained in:
ylweng
2025-09-04 01:39:31 +08:00
parent 216cf80eab
commit 3ae7b4db8c
45 changed files with 17218 additions and 642 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

15
admin-system/dashboard/dist/index.html vendored Normal file
View 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>

View File

@@ -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;

View 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>

View 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>

View File

@@ -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')

View File

@@ -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

View 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 };

View File

@@ -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 {};

View 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');
},
},
});

View 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;
},
},
});

View File

@@ -0,0 +1,2 @@
export { useAuthStore } from './auth.js';
export { useDashboardStore } from './dashboard.js';

View 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>

View File

@@ -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())

View File

@@ -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>

View File

@@ -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>

View 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>&copy; 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>

View 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>

View File

@@ -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>

View 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>