feat: 移除现有前端
This commit is contained in:
parent
61821ccd55
commit
4b001d23a4
|
|
@ -1,2 +0,0 @@
|
|||
# 开发环境配置
|
||||
VITE_API_BASE_URL=http://localhost:8000/api
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
# 生产环境配置
|
||||
VITE_API_BASE_URL=/api
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
# 新材料数据库管理系统 - 前端
|
||||
|
||||
## 项目介绍
|
||||
|
||||
新材料数据库管理系统前端,基于Vue 3 + Element Plus构建。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vue 3 - 渐进式JavaScript框架
|
||||
- Vite - 新一代前端构建工具
|
||||
- Element Plus - 基于Vue 3的组件库
|
||||
- Vue Router - Vue.js官方路由
|
||||
- Pinia - Vue状态管理库
|
||||
- Axios - HTTP客户端
|
||||
- ECharts - 数据可视化库
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## 开发模式
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 生产构建
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 预览生产构建
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── public/ # 静态资源
|
||||
├── src/
|
||||
│ ├── api/ # API接口
|
||||
│ ├── assets/ # 资源文件
|
||||
│ ├── components/ # 公共组件
|
||||
│ ├── layout/ # 布局组件
|
||||
│ ├── router/ # 路由配置
|
||||
│ ├── stores/ # 状态管理
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── views/ # 页面组件
|
||||
│ ├── App.vue # 根组件
|
||||
│ └── main.js # 入口文件
|
||||
├── index.html # HTML模板
|
||||
├── package.json # 项目配置
|
||||
└── vite.config.js # Vite配置
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
- 开发环境: `.env.development`
|
||||
- 生产环境: `.env.production`
|
||||
|
||||
## 功能模块
|
||||
|
||||
- 用户认证与授权
|
||||
- 材料库管理
|
||||
- 工厂库管理
|
||||
- 数据统计与分析
|
||||
- 字典管理
|
||||
- 用户管理
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>新材料数据库管理系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
"name": "new-materials-frontend",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"axios": "^1.5.0",
|
||||
"echarts": "^5.4.3",
|
||||
"element-plus": "^2.3.14",
|
||||
"pinia": "^2.1.6",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"sass": "^1.78.0",
|
||||
"vite": "^4.4.9"
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -1,30 +0,0 @@
|
|||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时检查用户登录状态
|
||||
userStore.checkAuth()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* @param {Object} credentials - 登录凭证 { username, password }
|
||||
* @returns {Promise} - 返回包含token和用户信息的Promise
|
||||
*/
|
||||
export function login(credentials) {
|
||||
return request({
|
||||
url: '/auth/login/',
|
||||
method: 'post',
|
||||
data: credentials
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
* @returns {Promise} - 返回用户信息的Promise
|
||||
*/
|
||||
export function getCurrentUser() {
|
||||
return request({
|
||||
url: '/auth/user/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
* @param {String} refreshToken - 刷新令牌
|
||||
* @returns {Promise} - 返回新token的Promise
|
||||
*/
|
||||
export function refreshToken(refreshToken) {
|
||||
return request({
|
||||
url: '/auth/token/refresh/',
|
||||
method: 'post',
|
||||
data: { refresh: refreshToken }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} - 返回用户列表的Promise
|
||||
*/
|
||||
export function getUserList(params) {
|
||||
return request({
|
||||
url: '/auth/users/',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
* @param {Object} data - 用户数据
|
||||
* @returns {Promise} - 返回创建的用户信息的Promise
|
||||
*/
|
||||
export function createUser(data) {
|
||||
return request({
|
||||
url: '/auth/users/',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户详情
|
||||
* @param {Number} id - 用户ID
|
||||
* @returns {Promise} - 返回用户详情的Promise
|
||||
*/
|
||||
export function getUserDetail(id) {
|
||||
return request({
|
||||
url: `/auth/users/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
* @param {Number} id - 用户ID
|
||||
* @param {Object} data - 更新的用户数据
|
||||
* @returns {Promise} - 返回更新后的用户信息的Promise
|
||||
*/
|
||||
export function updateUser(id, data) {
|
||||
return request({
|
||||
url: `/auth/users/${id}/`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
* @param {Number} id - 用户ID
|
||||
* @returns {Promise} - 返回删除结果的Promise
|
||||
*/
|
||||
export function deleteUser(id) {
|
||||
return request({
|
||||
url: `/auth/users/${id}/`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取字典列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} - 返回字典列表的Promise
|
||||
*/
|
||||
export function getDictionaryList(params) {
|
||||
return request({
|
||||
url: '/dictionary/',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分组字典
|
||||
* @returns {Promise} - 返回分组字典的Promise
|
||||
*/
|
||||
export function getDictionaryGrouped() {
|
||||
return request({
|
||||
url: '/dictionary/grouped/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典详情
|
||||
* @param {Number} id - 字典ID
|
||||
* @returns {Promise} - 返回字典详情的Promise
|
||||
*/
|
||||
export function getDictionaryDetail(id) {
|
||||
return request({
|
||||
url: `/dictionary/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建字典
|
||||
* @param {Object} data - 字典数据
|
||||
* @returns {Promise} - 返回创建的字典信息的Promise
|
||||
*/
|
||||
export function createDictionary(data) {
|
||||
return request({
|
||||
url: '/dictionary/',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字典信息
|
||||
* @param {Number} id - 字典ID
|
||||
* @param {Object} data - 更新的字典数据
|
||||
* @returns {Promise} - 返回更新后的字典信息的Promise
|
||||
*/
|
||||
export function updateDictionary(id, data) {
|
||||
return request({
|
||||
url: `/dictionary/${id}/`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典
|
||||
* @param {Number} id - 字典ID
|
||||
* @returns {Promise} - 返回删除结果的Promise
|
||||
*/
|
||||
export function deleteDictionary(id) {
|
||||
return request({
|
||||
url: `/dictionary/${id}/`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取工厂列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} - 返回工厂列表的Promise
|
||||
*/
|
||||
export function getFactoryList(params) {
|
||||
return request({
|
||||
url: '/factory/',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取简化工厂列表(用于下拉选择)
|
||||
* @returns {Promise} - 返回简化工厂列表的Promise
|
||||
*/
|
||||
export function getFactoryListSimple() {
|
||||
return request({
|
||||
url: '/factory/simple/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工厂详情
|
||||
* @param {Number} id - 工厂ID
|
||||
* @returns {Promise} - 返回工厂详情的Promise
|
||||
*/
|
||||
export function getFactoryDetail(id) {
|
||||
return request({
|
||||
url: `/factory/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工厂
|
||||
* @param {Object} data - 工厂数据
|
||||
* @returns {Promise} - 返回创建的工厂信息的Promise
|
||||
*/
|
||||
export function createFactory(data) {
|
||||
return request({
|
||||
url: '/factory/',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新工厂信息
|
||||
* @param {Number} id - 工厂ID
|
||||
* @param {Object} data - 更新的工厂数据
|
||||
* @returns {Promise} - 返回更新后的工厂信息的Promise
|
||||
*/
|
||||
export function updateFactory(id, data) {
|
||||
return request({
|
||||
url: `/factory/${id}/`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除工厂
|
||||
* @param {Number} id - 工厂ID
|
||||
* @returns {Promise} - 返回删除结果的Promise
|
||||
*/
|
||||
export function deleteFactory(id) {
|
||||
return request({
|
||||
url: `/factory/${id}/`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取材料列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} - 返回材料列表的Promise
|
||||
*/
|
||||
export function getMaterialList(params) {
|
||||
return request({
|
||||
url: '/material/',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取材料详情
|
||||
* @param {Number} id - 材料ID
|
||||
* @returns {Promise} - 返回材料详情的Promise
|
||||
*/
|
||||
export function getMaterialDetail(id) {
|
||||
return request({
|
||||
url: `/material/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建材料
|
||||
* @param {Object} data - 材料数据
|
||||
* @returns {Promise} - 返回创建的材料信息的Promise
|
||||
*/
|
||||
export function createMaterial(data) {
|
||||
return request({
|
||||
url: '/material/',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新材料信息
|
||||
* @param {Number} id - 材料ID
|
||||
* @param {Object} data - 更新的材料数据
|
||||
* @returns {Promise} - 返回更新后的材料信息的Promise
|
||||
*/
|
||||
export function updateMaterial(id, data) {
|
||||
return request({
|
||||
url: `/material/${id}/`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除材料
|
||||
* @param {Number} id - 材料ID
|
||||
* @returns {Promise} - 返回删除结果的Promise
|
||||
*/
|
||||
export function deleteMaterial(id) {
|
||||
return request({
|
||||
url: `/material/${id}/`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交材料审核
|
||||
* @param {Number} id - 材料ID
|
||||
* @returns {Promise} - 返回提交结果的Promise
|
||||
*/
|
||||
export function submitMaterial(id) {
|
||||
return request({
|
||||
url: `/material/${id}/submit/`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核通过材料
|
||||
* @param {Number} id - 材料ID
|
||||
* @returns {Promise} - 返回审核结果的Promise
|
||||
*/
|
||||
export function approveMaterial(id) {
|
||||
return request({
|
||||
url: `/material/${id}/approve/`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核拒绝材料
|
||||
* @param {Number} id - 材料ID
|
||||
* @returns {Promise} - 返回审核结果的Promise
|
||||
*/
|
||||
export function rejectMaterial(id) {
|
||||
return request({
|
||||
url: `/material/${id}/reject/`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取数据总览统计
|
||||
* @returns {Promise} - 返回数据总览统计的Promise
|
||||
*/
|
||||
export function getOverviewStatistics() {
|
||||
return request({
|
||||
url: '/statistics/overview/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取材料库统计
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} - 返回材料库统计的Promise
|
||||
*/
|
||||
export function getMaterialStatistics(params) {
|
||||
return request({
|
||||
url: '/statistics/materials/',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工厂库统计
|
||||
* @returns {Promise} - 返回工厂库统计的Promise
|
||||
*/
|
||||
export function getFactoryStatistics() {
|
||||
return request({
|
||||
url: '/statistics/factories/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
/* 全局样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Element Plus 样式覆盖 */
|
||||
.el-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
.el-card__header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
font-size: 14px;
|
||||
|
||||
th {
|
||||
background-color: #f5f7fa;
|
||||
color: #606266;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
.el-pagination__sizes {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
border-radius: 8px;
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-divider__text {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="isEdit ? '编辑字典' : '新增字典'"
|
||||
:visible="visible"
|
||||
width="600px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="字典类型" prop="type">
|
||||
<el-input v-model="form.type" placeholder="请输入字典类型" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="字典名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入字典名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="字典值" prop="value">
|
||||
<el-input v-model="form.value" placeholder="请输入字典值" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="9999" controls-position="right" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { createDictionary, updateDictionary } from '@/api/dictionary'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
dictionary: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
|
||||
const formRef = ref(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
const isEdit = computed(() => !!props.dictionary)
|
||||
|
||||
const form = reactive({
|
||||
type: '',
|
||||
name: '',
|
||||
value: '',
|
||||
sort: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
type: [
|
||||
{ required: true, message: '请输入字典类型', trigger: 'blur' }
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入字典名称', trigger: 'blur' }
|
||||
],
|
||||
value: [
|
||||
{ required: true, message: '请输入字典值', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听dictionary变化,填充表单
|
||||
watch(() => props.dictionary, (newVal) => {
|
||||
if (newVal) {
|
||||
Object.assign(form, {
|
||||
type: newVal.type || '',
|
||||
name: newVal.name || '',
|
||||
value: newVal.value || '',
|
||||
sort: newVal.sort || 0,
|
||||
remark: newVal.remark || ''
|
||||
})
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
type: '',
|
||||
name: '',
|
||||
value: '',
|
||||
sort: 0,
|
||||
remark: ''
|
||||
})
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
resetForm()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
const formData = { ...form }
|
||||
|
||||
if (isEdit.value) {
|
||||
await updateDictionary(props.dictionary.id, formData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createDictionary(formData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 样式可以根据需要添加
|
||||
</style>
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="isEdit ? '编辑工厂' : '新增工厂'"
|
||||
:visible="visible"
|
||||
width="600px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="经销商名称" prop="dealer_name">
|
||||
<el-input v-model="form.dealer_name" placeholder="请输入经销商名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="产品分类" prop="product_category">
|
||||
<el-input v-model="form.product_category" placeholder="请输入产品分类" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="工厂全称" prop="factory_name">
|
||||
<el-input v-model="form.factory_name" placeholder="请输入工厂全称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="工厂简称" prop="factory_short_name">
|
||||
<el-input v-model="form.factory_short_name" placeholder="请输入工厂简称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="省份" prop="province">
|
||||
<el-input v-model="form.province" placeholder="请输入省份" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="城市" prop="city">
|
||||
<el-input v-model="form.city" placeholder="请输入城市" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="区" prop="district">
|
||||
<el-input v-model="form.district" placeholder="请输入区" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="详细地址" prop="address">
|
||||
<el-input
|
||||
v-model="form.address"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入详细地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="官网链接" prop="website">
|
||||
<el-input v-model="form.website" placeholder="请输入官网链接" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { createFactory, updateFactory } from '@/api/factory'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
factory: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
|
||||
const formRef = ref(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
const isEdit = computed(() => !!props.factory)
|
||||
|
||||
const form = reactive({
|
||||
dealer_name: '',
|
||||
product_category: '',
|
||||
factory_name: '',
|
||||
factory_short_name: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
address: '',
|
||||
website: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
dealer_name: [
|
||||
{ required: true, message: '请输入经销商名称', trigger: 'blur' }
|
||||
],
|
||||
factory_name: [
|
||||
{ required: true, message: '请输入工厂全称', trigger: 'blur' }
|
||||
],
|
||||
factory_short_name: [
|
||||
{ required: true, message: '请输入工厂简称', trigger: 'blur' }
|
||||
],
|
||||
province: [
|
||||
{ required: true, message: '请输入省份', trigger: 'blur' }
|
||||
],
|
||||
city: [
|
||||
{ required: true, message: '请输入城市', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听factory变化,填充表单
|
||||
watch(() => props.factory, (newVal) => {
|
||||
if (newVal) {
|
||||
Object.assign(form, newVal)
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
dealer_name: '',
|
||||
product_category: '',
|
||||
factory_name: '',
|
||||
factory_short_name: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
address: '',
|
||||
website: ''
|
||||
})
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
resetForm()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updateFactory(props.factory.id, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createFactory(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 样式可以根据需要添加
|
||||
</style>
|
||||
|
|
@ -1,440 +0,0 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="isEdit ? '编辑材料' : '新增材料'"
|
||||
:visible="visible"
|
||||
width="80%"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-divider content-position="left">基本信息</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="材料名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入材料名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="专业类别" prop="major_category">
|
||||
<el-select v-model="form.major_category" placeholder="请选择专业类别" style="width: 100%">
|
||||
<el-option label="建筑" value="architecture" />
|
||||
<el-option label="景观" value="landscape" />
|
||||
<el-option label="设备" value="equipment" />
|
||||
<el-option label="装修" value="decoration" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="材料分类" prop="material_category">
|
||||
<el-input v-model="form.material_category" placeholder="请输入材料分类" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="材料子类" prop="material_subcategory">
|
||||
<el-input v-model="form.material_subcategory" placeholder="请输入材料子类" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="规格型号" prop="spec">
|
||||
<el-input v-model="form.spec" placeholder="请输入规格型号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="符合标准" prop="standard">
|
||||
<el-input v-model="form.standard" placeholder="请输入符合标准" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">应用场景</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="应用场景" prop="application_scene">
|
||||
<el-select v-model="form.application_scene" placeholder="请选择应用场景" clearable style="width: 100%">
|
||||
<el-option label="府系" value="fu" />
|
||||
<el-option label="境系" value="jing" />
|
||||
<el-option label="城系" value="cheng" />
|
||||
<el-option label="住系" value="zhu" />
|
||||
<el-option label="保障房" value="affordable" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="应用场景说明" prop="application_desc">
|
||||
<el-input
|
||||
v-model="form.application_desc"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入应用场景说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">替代材料</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="替代材料类型" prop="replace_type">
|
||||
<el-select v-model="form.replace_type" placeholder="请选择替代材料类型" clearable style="width: 100%">
|
||||
<el-option label="平替" value="alternative" />
|
||||
<el-option label="新研发" value="new_development" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="竞争优势" prop="advantage">
|
||||
<el-select v-model="form.advantage" placeholder="请选择竞争优势" clearable style="width: 100%">
|
||||
<el-option label="品质" value="quality" />
|
||||
<el-option label="成本" value="cost" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="优势说明" prop="advantage_desc">
|
||||
<el-input
|
||||
v-model="form.advantage_desc"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入优势说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="成本对比百分数" prop="cost_compare">
|
||||
<el-input-number
|
||||
v-model="form.cost_compare"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="成本说明" prop="cost_desc">
|
||||
<el-input
|
||||
v-model="form.cost_desc"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入成本说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">案例与宣传</el-divider>
|
||||
|
||||
<el-form-item label="案例" prop="cases">
|
||||
<el-input
|
||||
v-model="form.cases"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入案例"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="宣传页图片" prop="brochure">
|
||||
<el-upload
|
||||
class="brochure-uploader"
|
||||
action="/api/upload/"
|
||||
:show-file-list="false"
|
||||
:on-success="handleUploadSuccess"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<img v-if="form.brochure_url" :src="form.brochure_url" class="brochure" />
|
||||
<el-icon v-else class="brochure-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">星级评价</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="质量提升等级" prop="quality_level">
|
||||
<el-rate v-model="form.quality_level" :max="3" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item label="耐久可靠等级" prop="durability_level">
|
||||
<el-rate v-model="form.durability_level" :max="3" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item label="环保健康等级" prop="eco_level">
|
||||
<el-rate v-model="form.eco_level" :max="3" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="循环低碳等级" prop="carbon_level">
|
||||
<el-rate v-model="form.carbon_level" :max="3" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item label="总评分等级" prop="score_level">
|
||||
<el-rate v-model="form.score_level" :max="3" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">施工与限制</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="连接方式" prop="connection_method">
|
||||
<el-input v-model="form.connection_method" placeholder="请输入连接方式" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="施工工艺" prop="construction_method">
|
||||
<el-input v-model="form.construction_method" placeholder="请输入施工工艺" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="限制条件" prop="limit_condition">
|
||||
<el-input
|
||||
v-model="form.limit_condition"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入限制条件"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { createMaterial, updateMaterial } from '@/api/material'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
material: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
|
||||
const userStore = useUserStore()
|
||||
const formRef = ref(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
const isEdit = computed(() => !!props.material)
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
major_category: '',
|
||||
material_category: '',
|
||||
material_subcategory: '',
|
||||
spec: '',
|
||||
standard: '',
|
||||
application_scene: '',
|
||||
application_desc: '',
|
||||
replace_type: '',
|
||||
advantage: '',
|
||||
advantage_desc: '',
|
||||
cost_compare: null,
|
||||
cost_desc: '',
|
||||
cases: '',
|
||||
brochure: null,
|
||||
brochure_url: '',
|
||||
quality_level: null,
|
||||
durability_level: null,
|
||||
eco_level: null,
|
||||
carbon_level: null,
|
||||
score_level: null,
|
||||
connection_method: '',
|
||||
construction_method: '',
|
||||
limit_condition: '',
|
||||
factory: userStore.factoryId
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入材料名称', trigger: 'blur' }
|
||||
],
|
||||
major_category: [
|
||||
{ required: true, message: '请选择专业类别', trigger: 'change' }
|
||||
],
|
||||
material_category: [
|
||||
{ required: true, message: '请输入材料分类', trigger: 'blur' }
|
||||
],
|
||||
material_subcategory: [
|
||||
{ required: true, message: '请输入材料子类', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
name: '',
|
||||
major_category: '',
|
||||
material_category: '',
|
||||
material_subcategory: '',
|
||||
spec: '',
|
||||
standard: '',
|
||||
application_scene: '',
|
||||
application_desc: '',
|
||||
replace_type: '',
|
||||
advantage: '',
|
||||
advantage_desc: '',
|
||||
cost_compare: null,
|
||||
cost_desc: '',
|
||||
cases: '',
|
||||
brochure: null,
|
||||
brochure_url: '',
|
||||
quality_level: null,
|
||||
durability_level: null,
|
||||
eco_level: null,
|
||||
carbon_level: null,
|
||||
score_level: null,
|
||||
connection_method: '',
|
||||
construction_method: '',
|
||||
limit_condition: '',
|
||||
factory: userStore.factoryId
|
||||
})
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 监听material变化,填充表单
|
||||
watch(() => props.material, (newVal) => {
|
||||
if (newVal) {
|
||||
Object.assign(form, newVal)
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
resetForm()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
const formData = { ...form }
|
||||
|
||||
// 处理星级评价,如果没有选择则设为null
|
||||
['quality_level', 'durability_level', 'eco_level', 'carbon_level', 'score_level'].forEach(field => {
|
||||
if (formData[field] === 0) {
|
||||
formData[field] = null
|
||||
}
|
||||
})
|
||||
|
||||
if (isEdit.value) {
|
||||
await updateMaterial(props.material.id, formData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createMaterial(formData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 上传前校验
|
||||
const beforeUpload = (file) => {
|
||||
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isJPG) {
|
||||
ElMessage.error('上传图片只能是 JPG/PNG 格式!')
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('上传图片大小不能超过 2MB!')
|
||||
}
|
||||
return isJPG && isLt2M
|
||||
}
|
||||
|
||||
// 上传成功
|
||||
const handleUploadSuccess = (response) => {
|
||||
form.brochure = response.url
|
||||
form.brochure_url = response.url
|
||||
ElMessage.success('上传成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.brochure-uploader {
|
||||
.brochure {
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409EFF;
|
||||
}
|
||||
}
|
||||
|
||||
.brochure-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
line-height: 178px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="isEdit ? '编辑用户' : '新增用户'"
|
||||
:visible="visible"
|
||||
width="600px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="form.username" placeholder="请输入用户名" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password" v-if="!isEdit">
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="确认密码" prop="password_confirm" v-if="!isEdit">
|
||||
<el-input v-model="form.password_confirm" type="password" placeholder="请再次输入密码" show-password />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="姓" prop="first_name">
|
||||
<el-input v-model="form.first_name" placeholder="请输入姓" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="名" prop="last_name">
|
||||
<el-input v-model="form.last_name" placeholder="请输入名" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色" prop="role">
|
||||
<el-select v-model="form.role" placeholder="请选择角色" style="width: 100%">
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="普通账号" value="user" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="所属工厂" prop="factory" v-if="form.role === 'user'">
|
||||
<el-select v-model="form.factory" placeholder="请选择所属工厂" style="width: 100%" filterable>
|
||||
<el-option
|
||||
v-for="factory in factoryList"
|
||||
:key="factory.id"
|
||||
:label="factory.factory_name"
|
||||
:value="factory.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="is_active">
|
||||
<el-switch v-model="form.is_active" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { createUser, updateUser } from '@/api/auth'
|
||||
import { getFactoryListSimple } from '@/api/factory'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
|
||||
const formRef = ref(null)
|
||||
const submitting = ref(false)
|
||||
const factoryList = ref([])
|
||||
|
||||
const isEdit = computed(() => !!props.user)
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
password_confirm: '',
|
||||
email: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
role: 'user',
|
||||
factory: null,
|
||||
phone: '',
|
||||
is_active: true
|
||||
})
|
||||
|
||||
const validatePasswordConfirm = (rule, value, callback) => {
|
||||
if (value !== form.password) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
password_confirm: [
|
||||
{ required: true, message: '请再次输入密码', trigger: 'blur' },
|
||||
{ validator: validatePasswordConfirm, trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
|
||||
],
|
||||
role: [
|
||||
{ required: true, message: '请选择角色', trigger: 'change' }
|
||||
],
|
||||
factory: [
|
||||
{ required: true, message: '请选择所属工厂', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听user变化,填充表单
|
||||
watch(() => props.user, (newVal) => {
|
||||
if (newVal) {
|
||||
Object.assign(form, newVal)
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 监听角色变化,清空工厂选择
|
||||
watch(() => form.role, (newVal) => {
|
||||
if (newVal === 'admin') {
|
||||
form.factory = null
|
||||
}
|
||||
})
|
||||
|
||||
// 加载工厂列表
|
||||
const loadFactoryList = async () => {
|
||||
try {
|
||||
factoryList.value = await getFactoryListSimple()
|
||||
} catch (error) {
|
||||
console.error('加载工厂列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
username: '',
|
||||
password: '',
|
||||
password_confirm: '',
|
||||
email: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
role: 'user',
|
||||
factory: null,
|
||||
phone: '',
|
||||
is_active: true
|
||||
})
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
resetForm()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
const formData = { ...form }
|
||||
|
||||
// 移除不需要提交的字段
|
||||
if (isEdit.value) {
|
||||
delete formData.password
|
||||
delete formData.password_confirm
|
||||
} else {
|
||||
delete formData.password_confirm
|
||||
}
|
||||
|
||||
// 如果是管理员,清空工厂字段
|
||||
if (formData.role === 'admin') {
|
||||
formData.factory = null
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await updateUser(props.user.id, formData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createUser(formData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadFactoryList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 样式可以根据需要添加
|
||||
</style>
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
<template>
|
||||
<el-container class="main-layout">
|
||||
<el-aside width="200px">
|
||||
<div class="logo">
|
||||
<h2>新材料数据库</h2>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:router="true"
|
||||
class="sidebar-menu"
|
||||
background-color="#304156"
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#409EFF"
|
||||
>
|
||||
<el-menu-item index="/dashboard" v-if="isAdmin">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<span>数据总览</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/materials">
|
||||
<el-icon><Box /></el-icon>
|
||||
<span>材料库</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/factories">
|
||||
<el-icon><OfficeBuilding /></el-icon>
|
||||
<span>工厂库</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/users" v-if="isAdmin">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>用户管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/dictionaries" v-if="isAdmin">
|
||||
<el-icon><Notebook /></el-icon>
|
||||
<span>字典管理</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<el-container>
|
||||
<el-header>
|
||||
<div class="header-content">
|
||||
<div class="breadcrumb">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-if="currentRoute.meta.title">
|
||||
{{ currentRoute.meta.title }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="el-dropdown-link">
|
||||
{{ userStore.userInfo?.username }}
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-main>
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const activeMenu = computed(() => route.path)
|
||||
const currentRoute = computed(() => route)
|
||||
const isAdmin = computed(() => userStore.isAdmin)
|
||||
|
||||
const handleCommand = (command) => {
|
||||
if (command === 'logout') {
|
||||
userStore.logout()
|
||||
ElMessage.success('已退出登录')
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-layout {
|
||||
height: 100vh;
|
||||
|
||||
.el-aside {
|
||||
background-color: #304156;
|
||||
color: #fff;
|
||||
transition: width 0.3s;
|
||||
|
||||
.logo {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
border-bottom: 1px solid #1f2d3d;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border-right: none;
|
||||
height: calc(100vh - 50px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.el-header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
|
||||
.header-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.user-info {
|
||||
.el-dropdown-link {
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-main {
|
||||
background-color: #f0f2f5;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './assets/styles/main.scss'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus, { locale: zhCn })
|
||||
|
||||
app.mount('#app')
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/Login.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layout/MainLayout.vue'),
|
||||
redirect: '/dashboard',
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/Dashboard.vue'),
|
||||
meta: { title: '数据总览', requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'materials',
|
||||
name: 'Materials',
|
||||
component: () => import('@/views/Materials.vue'),
|
||||
meta: { title: '材料库', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'materials/:id',
|
||||
name: 'MaterialDetail',
|
||||
component: () => import('@/views/MaterialDetail.vue'),
|
||||
meta: { title: '材料详情', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'factories',
|
||||
name: 'Factories',
|
||||
component: () => import('@/views/Factories.vue'),
|
||||
meta: { title: '工厂库', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'factories/:id',
|
||||
name: 'FactoryDetail',
|
||||
component: () => import('@/views/FactoryDetail.vue'),
|
||||
meta: { title: '工厂详情', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'Users',
|
||||
component: () => import('@/views/Users.vue'),
|
||||
meta: { title: '用户管理', requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'dictionaries',
|
||||
name: 'Dictionaries',
|
||||
component: () => import('@/views/Dictionaries.vue'),
|
||||
meta: { title: '字典管理', requiresAuth: true, requiresAdmin: true }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 检查是否需要登录
|
||||
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
|
||||
ElMessage.warning('请先登录')
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要管理员权限
|
||||
if (to.meta.requiresAdmin && !userStore.isAdmin) {
|
||||
ElMessage.warning('需要管理员权限')
|
||||
next('/dashboard')
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { login as loginApi, getCurrentUser as getCurrentUserApi } from '@/api/auth'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
token: localStorage.getItem('token') || '',
|
||||
refreshToken: localStorage.getItem('refreshToken') || '',
|
||||
userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null'),
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isLoggedIn: (state) => !!state.token,
|
||||
isAdmin: (state) => state.userInfo && state.userInfo.role === 'admin',
|
||||
factoryId: (state) => state.userInfo ? state.userInfo.factory : null,
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 登录
|
||||
async login(credentials) {
|
||||
try {
|
||||
const response = await loginApi(credentials)
|
||||
const { access, refresh, user } = response
|
||||
|
||||
this.token = access
|
||||
this.refreshToken = refresh
|
||||
this.userInfo = user
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('token', access)
|
||||
localStorage.setItem('refreshToken', refresh)
|
||||
localStorage.setItem('userInfo', JSON.stringify(user))
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
return user
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '登录失败')
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 登出
|
||||
logout() {
|
||||
this.token = ''
|
||||
this.refreshToken = ''
|
||||
this.userInfo = null
|
||||
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('refreshToken')
|
||||
localStorage.removeItem('userInfo')
|
||||
},
|
||||
|
||||
// 检查认证状态
|
||||
async checkAuth() {
|
||||
if (this.token) {
|
||||
try {
|
||||
const user = await getCurrentUserApi()
|
||||
this.userInfo = user
|
||||
localStorage.setItem('userInfo', JSON.stringify(user))
|
||||
} catch (error) {
|
||||
this.logout()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 更新用户信息
|
||||
updateUserInfo(userInfo) {
|
||||
this.userInfo = { ...this.userInfo, ...userInfo }
|
||||
localStorage.setItem('userInfo', JSON.stringify(this.userInfo))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import router from '@/router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
const userStore = useUserStore()
|
||||
if (userStore.token) {
|
||||
config.headers['Authorization'] = `Bearer ${userStore.token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
console.error('请求错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
|
||||
// 如果响应的状态码不是200,则判断为错误
|
||||
if (response.status !== 200) {
|
||||
ElMessage({
|
||||
message: res.message || '请求失败',
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
return Promise.reject(new Error(res.message || '请求失败'))
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error('响应错误:', error)
|
||||
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
|
||||
if (status === 401) {
|
||||
// 未授权,跳转到登录页
|
||||
const userStore = useUserStore()
|
||||
userStore.logout()
|
||||
router.push('/login')
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
} else if (status === 403) {
|
||||
ElMessage.error('没有权限访问该资源')
|
||||
} else if (status === 404) {
|
||||
ElMessage.error('请求的资源不存在')
|
||||
} else if (status === 500) {
|
||||
ElMessage.error('服务器错误')
|
||||
} else {
|
||||
ElMessage.error(data.detail || '请求失败')
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('网络错误,请检查网络连接')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
||||
|
|
@ -1,654 +0,0 @@
|
|||
<template>
|
||||
<div class="dashboard">
|
||||
<el-row :gutter="20">
|
||||
<!-- 统计卡片 -->
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card stat-card-hover" shadow="hover">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon stat-icon-pulse" style="background-color: #409EFF;">
|
||||
<el-icon :size="30"><Box /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value stat-value-animate">{{ overviewData.total_materials || 0 }}</div>
|
||||
<div class="stat-label">材料总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card stat-card-hover" shadow="hover">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon stat-icon-pulse" style="background-color: #67C23A;">
|
||||
<el-icon :size="30"><Grid /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value stat-value-animate">{{ overviewData.total_material_categories || 0 }}</div>
|
||||
<div class="stat-label">材料种类</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card stat-card-hover" shadow="hover">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon stat-icon-pulse" style="background-color: #E6A23C;">
|
||||
<el-icon :size="30"><OfficeBuilding /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value stat-value-animate">{{ overviewData.total_brands || 0 }}</div>
|
||||
<div class="stat-label">品牌数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card stat-card-hover" shadow="hover">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon stat-icon-pulse" style="background-color: #F56C6C;">
|
||||
<el-icon :size="30"><Document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value stat-value-animate">{{ overviewData.cases_list?.length || 0 }}</div>
|
||||
<div class="stat-label">应用案例</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="charts-row">
|
||||
<!-- 按专业类别的材料数量分布 -->
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>按专业类别的材料数量分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="majorCategoryChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 按材料子类的材料数量分布 -->
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>按材料子类的材料数量分布(TOP10)</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="materialSubcategoryChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="charts-row">
|
||||
<!-- 按所属品牌的材料数量分布 -->
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>按所属品牌的材料数量分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="brandChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 按地区的工厂数量分布 -->
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>按地区的工厂数量分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="regionChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="charts-row">
|
||||
<!-- 应用案例列表 -->
|
||||
<el-col :span="24">
|
||||
<el-card class="case-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>应用案例列表</span>
|
||||
<el-tag type="info" size="small">共 {{ overviewData.cases_list?.length || 0 }} 条</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
:data="overviewData.cases_list || []"
|
||||
stripe
|
||||
class="case-table"
|
||||
:header-cell-style="{background:'#f5f7fa', color:'#606266'}"
|
||||
:row-style="{height: '60px'}"
|
||||
:cell-style="{padding: '12px 0'}"
|
||||
>
|
||||
<el-table-column prop="name" label="材料名称" width="200">
|
||||
<template #default="scope">
|
||||
<div class="material-name">{{ scope.row.name }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="factory__factory_name" label="所属品牌" width="200">
|
||||
<template #default="scope">
|
||||
<el-tag size="small" type="primary">{{ scope.row.factory__factory_name }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cases" label="案例说明" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<div class="case-description">{{ scope.row.cases }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { getOverviewStatistics } from '@/api/statistics'
|
||||
|
||||
// 图表实例
|
||||
let majorCategoryChartInstance = null
|
||||
let materialSubcategoryChartInstance = null
|
||||
let brandChartInstance = null
|
||||
let regionChartInstance = null
|
||||
|
||||
// 图表容器引用
|
||||
const majorCategoryChart = ref(null)
|
||||
const materialSubcategoryChart = ref(null)
|
||||
const brandChart = ref(null)
|
||||
const regionChart = ref(null)
|
||||
|
||||
// 数据总览数据
|
||||
const overviewData = ref({
|
||||
total_materials: 0,
|
||||
total_material_categories: 0,
|
||||
total_brands: 0,
|
||||
major_category_stats: [],
|
||||
material_subcategory_stats: [],
|
||||
brand_stats: [],
|
||||
region_stats: [],
|
||||
cases_list: []
|
||||
})
|
||||
|
||||
// 自动刷新定时器
|
||||
let refreshTimer = null
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const data = await getOverviewStatistics()
|
||||
overviewData.value = data
|
||||
|
||||
// 更新图表
|
||||
updateCharts(data)
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initCharts = () => {
|
||||
majorCategoryChartInstance = echarts.init(majorCategoryChart.value)
|
||||
materialSubcategoryChartInstance = echarts.init(materialSubcategoryChart.value)
|
||||
brandChartInstance = echarts.init(brandChart.value)
|
||||
regionChartInstance = echarts.init(regionChart.value)
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
const updateCharts = (data) => {
|
||||
// 按专业类别的材料数量分布 - 饼图
|
||||
majorCategoryChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor: '#eee',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
textStyle: {
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '专业类别',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: data.major_category_stats.map(item => ({
|
||||
value: item.count,
|
||||
name: item.major_category
|
||||
}))
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 按材料子类的材料数量分布 - 柱状图
|
||||
materialSubcategoryChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor: '#eee',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#ddd'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#f0f0f0'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: data.material_subcategory_stats.map(item => item.material_subcategory),
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '材料数量',
|
||||
type: 'bar',
|
||||
data: data.material_subcategory_stats.map(item => item.count),
|
||||
itemStyle: {
|
||||
borderRadius: [0, 4, 4, 0],
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#83bff6' },
|
||||
{ offset: 0.5, color: '#188df0' },
|
||||
{ offset: 1, color: '#188df0' }
|
||||
])
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#2378f7' },
|
||||
{ offset: 0.7, color: '#2378f7' },
|
||||
{ offset: 1, color: '#83bff6' }
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 按所属品牌的材料数量分布 - 柱状图
|
||||
brandChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor: '#eee',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.brand_stats.map(item => item.factory__factory_name),
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: 30,
|
||||
color: '#666'
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#ddd'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#f0f0f0'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '材料数量',
|
||||
type: 'bar',
|
||||
data: data.brand_stats.map(item => item.count),
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#67C23A' },
|
||||
{ offset: 1, color: '#85CE61' }
|
||||
])
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#529B2E' },
|
||||
{ offset: 1, color: '#67C23A' }
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 按地区的工厂数量分布 - 柱状图
|
||||
regionChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor: '#eee',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.region_stats.map(item => `${item.province} ${item.city}`),
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: 30,
|
||||
color: '#666'
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#ddd'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#f0f0f0'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '工厂数量',
|
||||
type: 'bar',
|
||||
data: data.region_stats.map(item => item.count),
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#E6A23C' },
|
||||
{ offset: 1, color: '#F3D19E' }
|
||||
])
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#CF8E2F' },
|
||||
{ offset: 1, color: '#E6A23C' }
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
// 窗口大小变化时重新调整图表大小
|
||||
const handleResize = () => {
|
||||
majorCategoryChartInstance && majorCategoryChartInstance.resize()
|
||||
materialSubcategoryChartInstance && materialSubcategoryChartInstance.resize()
|
||||
brandChartInstance && brandChartInstance.resize()
|
||||
regionChartInstance && regionChartInstance.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化图表
|
||||
initCharts()
|
||||
|
||||
// 加载数据
|
||||
loadData()
|
||||
|
||||
// 设置自动刷新,每10秒刷新一次
|
||||
refreshTimer = setInterval(loadData, 10000)
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 清除定时器
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
}
|
||||
|
||||
// 销毁图表实例
|
||||
majorCategoryChartInstance && majorCategoryChartInstance.dispose()
|
||||
materialSubcategoryChartInstance && materialSubcategoryChartInstance.dispose()
|
||||
brandChartInstance && brandChartInstance.dispose()
|
||||
regionChartInstance && regionChartInstance.dispose()
|
||||
|
||||
// 移除窗口大小变化监听
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #EBEEF5;
|
||||
|
||||
&.stat-card-hover:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
margin-right: 20px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.stat-icon-pulse:hover {
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 5px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.stat-value-animate {
|
||||
animation: fadeInUp 0.5s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
margin-top: 20px;
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.case-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.case-table {
|
||||
.material-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.case-description {
|
||||
color: #606266;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
:deep(.el-table__row) {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:hover > td) {
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
70% {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
<template>
|
||||
<div class="dictionaries">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>字典管理</span>
|
||||
<el-button type="primary" @click="handleAdd">新增字典</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="字典类型">
|
||||
<el-input v-model="searchForm.type" placeholder="请输入字典类型" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="字典名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入字典名称" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 字典表格 -->
|
||||
<el-table :data="tableData" stripe v-loading="loading">
|
||||
<el-table-column prop="type" label="字典类型" />
|
||||
<el-table-column prop="name" label="字典名称" />
|
||||
<el-table-column prop="value" label="字典值" />
|
||||
<el-table-column prop="created_at" label="创建时间">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 字典表单对话框 -->
|
||||
<DictionaryForm
|
||||
v-if="showForm"
|
||||
:visible="showForm"
|
||||
:dictionary="currentDictionary"
|
||||
@close="showForm = false"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { getDictionaryList, deleteDictionary } from '@/api/dictionary'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import DictionaryForm from '@/components/DictionaryForm.vue'
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
type: '',
|
||||
name: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表单对话框
|
||||
const showForm = ref(false)
|
||||
const currentDictionary = ref(null)
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.size,
|
||||
...searchForm
|
||||
}
|
||||
const data = await getDictionaryList(params)
|
||||
tableData.value = data.results || data
|
||||
pagination.total = data.count || data.length
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
type: '',
|
||||
name: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
currentDictionary.value = null
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
currentDictionary.value = { ...row }
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该字典吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteDictionary(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dictionaries {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,235 +0,0 @@
|
|||
<template>
|
||||
<div class="factories">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>工厂库</span>
|
||||
<el-button type="primary" @click="handleAdd" v-if="isAdmin">新增工厂</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="工厂名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入工厂名称" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="省份">
|
||||
<el-input v-model="searchForm.province" placeholder="请输入省份" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="城市">
|
||||
<el-input v-model="searchForm.city" placeholder="请输入城市" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 工厂表格 -->
|
||||
<el-table :data="tableData" stripe v-loading="loading">
|
||||
<el-table-column prop="factory_name" label="工厂全称" />
|
||||
<el-table-column prop="factory_short_name" label="工厂简称" />
|
||||
<el-table-column prop="dealer_name" label="经销商名称" />
|
||||
<el-table-column prop="province" label="省份" />
|
||||
<el-table-column prop="city" label="城市" />
|
||||
<el-table-column prop="material_count" label="材料数量" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">查看</el-button>
|
||||
<el-button link type="primary" @click="handleEdit(row)" v-if="canEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)" v-if="canDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 工厂表单对话框 -->
|
||||
<FactoryForm
|
||||
v-if="showForm"
|
||||
:visible="showForm"
|
||||
:factory="currentFactory"
|
||||
@close="showForm = false"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getFactoryList, deleteFactory } from '@/api/factory'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import FactoryForm from '@/components/FactoryForm.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const isAdmin = computed(() => userStore.isAdmin)
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
province: '',
|
||||
city: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表单对话框
|
||||
const showForm = ref(false)
|
||||
const currentFactory = ref(null)
|
||||
|
||||
// 判断是否可以编辑
|
||||
const canEdit = (row) => {
|
||||
if (isAdmin.value) return true
|
||||
return userStore.factoryId === row.id
|
||||
}
|
||||
|
||||
// 判断是否可以删除
|
||||
const canDelete = (row) => {
|
||||
if (isAdmin.value) return true
|
||||
return userStore.factoryId === row.id
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.size,
|
||||
...searchForm
|
||||
}
|
||||
const data = await getFactoryList(params)
|
||||
tableData.value = data.results || data
|
||||
pagination.total = data.count || data.length
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
name: '',
|
||||
province: '',
|
||||
city: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
currentFactory.value = null
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 查看
|
||||
const handleView = (row) => {
|
||||
router.push(`/factories/${row.id}`)
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
currentFactory.value = { ...row }
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该工厂吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteFactory(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.factories {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
<template>
|
||||
<div class="factory-detail">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-button link @click="handleBack">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
<div class="header-actions">
|
||||
<el-button v-if="canEdit" type="primary" @click="handleEdit">编辑</el-button>
|
||||
<el-button v-if="canDelete" type="danger" @click="handleDelete">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="经销商名称">{{ factory.dealer_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="产品分类">{{ factory.product_category || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工厂全称">{{ factory.factory_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工厂简称">{{ factory.factory_short_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="省份">{{ factory.province }}</el-descriptions-item>
|
||||
<el-descriptions-item label="城市">{{ factory.city }}</el-descriptions-item>
|
||||
<el-descriptions-item label="区">{{ factory.district || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="官网链接">
|
||||
<a v-if="factory.website" :href="factory.website" target="_blank">{{ factory.website }}</a>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider>详细地址</el-divider>
|
||||
<div class="content-text">{{ factory.address || '暂无' }}</div>
|
||||
|
||||
<el-divider>其他信息</el-divider>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="材料数量">{{ factory.material_count || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(factory.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(factory.updated_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider>材料列表</el-divider>
|
||||
<el-table :data="factory.materials || []" stripe>
|
||||
<el-table-column prop="name" label="材料名称" />
|
||||
<el-table-column prop="major_category_display" label="专业类别" />
|
||||
<el-table-column prop="material_category" label="材料分类" />
|
||||
<el-table-column prop="material_subcategory" label="材料子类" />
|
||||
<el-table-column prop="status_display" label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ row.status_display }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleViewMaterial(row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getFactoryDetail, deleteFactory } from '@/api/factory'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const factory = ref({})
|
||||
const loading = ref(false)
|
||||
|
||||
const isAdmin = computed(() => userStore.isAdmin)
|
||||
|
||||
// 判断是否可以编辑
|
||||
const canEdit = computed(() => {
|
||||
if (isAdmin.value) return true
|
||||
return userStore.factoryId === factory.value.id
|
||||
})
|
||||
|
||||
// 判断是否可以删除
|
||||
const canDelete = computed(() => {
|
||||
if (isAdmin.value) return true
|
||||
return userStore.factoryId === factory.value.id
|
||||
})
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status) => {
|
||||
const statusMap = {
|
||||
draft: 'info',
|
||||
pending: 'warning',
|
||||
approved: 'success'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const id = route.params.id
|
||||
factory.value = await getFactoryDetail(id)
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = () => {
|
||||
router.push(`/factories/${factory.value.id}/edit`)
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该工厂吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteFactory(factory.value.id)
|
||||
ElMessage.success('删除成功')
|
||||
router.push('/factories')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查看材料
|
||||
const handleViewMaterial = (row) => {
|
||||
router.push(`/materials/${row.id}`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.factory-detail {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-text {
|
||||
padding: 10px 0;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
<template>
|
||||
<div class="login-container">
|
||||
<el-card class="login-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>新材料数据库管理系统</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
label-width="80px"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="请输入用户名"
|
||||
prefix-icon="User"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
prefix-icon="Lock"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
style="width: 100%"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const loginFormRef = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const loginRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
await loginFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
try {
|
||||
await userStore.login(loginForm)
|
||||
|
||||
// 根据用户角色跳转到不同页面
|
||||
if (userStore.isAdmin) {
|
||||
router.push('/dashboard')
|
||||
} else {
|
||||
router.push('/materials')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,324 +0,0 @@
|
|||
<template>
|
||||
<div class="material-detail">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-button link @click="handleBack">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
<div class="header-actions">
|
||||
<el-button v-if="canEdit" type="primary" @click="handleEdit">编辑</el-button>
|
||||
<el-button v-if="canSubmit" type="warning" @click="handleSubmit">提交审核</el-button>
|
||||
<el-button v-if="canApprove" type="success" @click="handleApprove">审核通过</el-button>
|
||||
<el-button v-if="canReject" type="danger" @click="handleReject">审核拒绝</el-button>
|
||||
<el-button v-if="canDelete" type="danger" @click="handleDelete">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="材料名称">{{ material.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="专业类别">{{ material.major_category_display }}</el-descriptions-item>
|
||||
<el-descriptions-item label="材料分类">{{ material.material_category }}</el-descriptions-item>
|
||||
<el-descriptions-item label="材料子类">{{ material.material_subcategory }}</el-descriptions-item>
|
||||
<el-descriptions-item label="规格型号">{{ material.spec || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="符合标准">{{ material.standard || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="应用场景">{{ material.application_scene_display || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属品牌">{{ material.factory_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusType(material.status)">
|
||||
{{ material.status_display }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider>应用场景说明</el-divider>
|
||||
<div class="content-text">{{ material.application_desc || '暂无' }}</div>
|
||||
|
||||
<el-divider>替代材料信息</el-divider>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="替代材料类型">{{ material.replace_type_display || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="竞争优势">{{ material.advantage_display || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="content-text">{{ material.advantage_desc || '暂无' }}</div>
|
||||
|
||||
<el-divider>成本信息</el-divider>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="成本对比百分数">
|
||||
{{ material.cost_compare ? `${material.cost_compare}%` : '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="content-text">{{ material.cost_desc || '暂无' }}</div>
|
||||
|
||||
<el-divider>案例</el-divider>
|
||||
<div class="content-text">{{ material.cases || '暂无' }}</div>
|
||||
|
||||
<el-divider>宣传页图片</el-divider>
|
||||
<div v-if="material.brochure_url" class="brochure-container">
|
||||
<el-image
|
||||
:src="material.brochure_url"
|
||||
fit="contain"
|
||||
:preview-src-list="[material.brochure_url]"
|
||||
style="max-width: 100%; max-height: 500px;"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="content-text">暂无</div>
|
||||
|
||||
<el-divider>星级评价</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<div class="star-item">
|
||||
<span class="star-label">质量提升等级:</span>
|
||||
<el-rate v-model="material.quality_level" disabled :max="3" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="star-item">
|
||||
<span class="star-label">耐久可靠等级:</span>
|
||||
<el-rate v-model="material.durability_level" disabled :max="3" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="star-item">
|
||||
<span class="star-label">环保健康等级:</span>
|
||||
<el-rate v-model="material.eco_level" disabled :max="3" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="8">
|
||||
<div class="star-item">
|
||||
<span class="star-label">循环低碳等级:</span>
|
||||
<el-rate v-model="material.carbon_level" disabled :max="3" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="star-item">
|
||||
<span class="star-label">总评分等级:</span>
|
||||
<el-rate v-model="material.score_level" disabled :max="3" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider>施工与限制</el-divider>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="连接方式">{{ material.connection_method || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="施工工艺">{{ material.construction_method || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="content-text">{{ material.limit_condition || '暂无' }}</div>
|
||||
|
||||
<el-divider>其他信息</el-divider>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(material.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(material.updated_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getMaterialDetail, deleteMaterial, submitMaterial, approveMaterial, rejectMaterial } from '@/api/material'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const material = ref({})
|
||||
const loading = ref(false)
|
||||
|
||||
const isAdmin = computed(() => userStore.isAdmin)
|
||||
|
||||
// 判断是否可以编辑
|
||||
const canEdit = computed(() => {
|
||||
if (isAdmin.value) return true
|
||||
return material.value.status === 'draft' && userStore.factoryId === material.value.factory
|
||||
})
|
||||
|
||||
// 判断是否可以提交
|
||||
const canSubmit = computed(() => {
|
||||
return material.value.status === 'draft' && (isAdmin.value || userStore.factoryId === material.value.factory)
|
||||
})
|
||||
|
||||
// 判断是否可以审核
|
||||
const canApprove = computed(() => {
|
||||
return isAdmin.value && material.value.status === 'pending'
|
||||
})
|
||||
|
||||
// 判断是否可以拒绝
|
||||
const canReject = computed(() => {
|
||||
return isAdmin.value && material.value.status === 'pending'
|
||||
})
|
||||
|
||||
// 判断是否可以删除
|
||||
const canDelete = computed(() => {
|
||||
if (isAdmin.value) return true
|
||||
return material.value.status === 'draft' && userStore.factoryId === material.value.factory
|
||||
})
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status) => {
|
||||
const statusMap = {
|
||||
draft: 'info',
|
||||
pending: 'warning',
|
||||
approved: 'success'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const id = route.params.id
|
||||
material.value = await getMaterialDetail(id)
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = () => {
|
||||
router.push(`/materials/${material.value.id}/edit`)
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认提交该材料进行审核吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await submitMaterial(material.value.id)
|
||||
ElMessage.success('提交成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('提交失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 审核通过
|
||||
const handleApprove = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认审核通过该材料吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await approveMaterial(material.value.id)
|
||||
ElMessage.success('审核通过')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('审核失败:', error)
|
||||
ElMessage.error('审核失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 审核拒绝
|
||||
const handleReject = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认拒绝该材料吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await rejectMaterial(material.value.id)
|
||||
ElMessage.success('已拒绝')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该材料吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteMaterial(material.value.id)
|
||||
ElMessage.success('删除成功')
|
||||
router.push('/materials')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.material-detail {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-text {
|
||||
padding: 10px 0;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.brochure-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.star-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.star-label {
|
||||
margin-right: 10px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,338 +0,0 @@
|
|||
<template>
|
||||
<div class="materials">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>材料库</span>
|
||||
<el-button type="primary" @click="handleAdd">新增材料</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="材料名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入材料名称" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="专业类别">
|
||||
<el-select v-model="searchForm.major_category" placeholder="请选择专业类别" clearable>
|
||||
<el-option label="建筑" value="architecture" />
|
||||
<el-option label="景观" value="landscape" />
|
||||
<el-option label="设备" value="equipment" />
|
||||
<el-option label="装修" value="decoration" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="创建中" value="draft" />
|
||||
<el-option label="待审核" value="pending" />
|
||||
<el-option label="已审核" value="approved" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 材料表格 -->
|
||||
<el-table :data="tableData" stripe v-loading="loading">
|
||||
<el-table-column prop="name" label="材料名称" />
|
||||
<el-table-column prop="major_category_display" label="专业类别" />
|
||||
<el-table-column prop="material_category" label="材料分类" />
|
||||
<el-table-column prop="material_subcategory" label="材料子类" />
|
||||
<el-table-column prop="factory_name" label="所属品牌" />
|
||||
<el-table-column prop="status_display" label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ row.status_display }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">查看</el-button>
|
||||
<el-button link type="primary" @click="handleEdit(row)" v-if="canEdit(row)">编辑</el-button>
|
||||
<el-button link type="primary" @click="handleSubmit(row)" v-if="canSubmit(row)">提交</el-button>
|
||||
<el-button link type="success" @click="handleApprove(row)" v-if="canApprove(row)">通过</el-button>
|
||||
<el-button link type="danger" @click="handleReject(row)" v-if="canReject(row)">拒绝</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)" v-if="canDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 材料表单对话框 -->
|
||||
<MaterialForm
|
||||
v-if="showForm"
|
||||
:visible="showForm"
|
||||
:material="currentMaterial"
|
||||
@close="showForm = false"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getMaterialList, deleteMaterial, submitMaterial, approveMaterial, rejectMaterial } from '@/api/material'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import MaterialForm from '@/components/MaterialForm.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const isAdmin = computed(() => userStore.isAdmin)
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
major_category: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表单对话框
|
||||
const showForm = ref(false)
|
||||
const currentMaterial = ref(null)
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status) => {
|
||||
const statusMap = {
|
||||
draft: 'info',
|
||||
pending: 'warning',
|
||||
approved: 'success'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 判断是否可以编辑
|
||||
const canEdit = (row) => {
|
||||
if (isAdmin.value) return true
|
||||
return row.status === 'draft' && userStore.factoryId === row.factory
|
||||
}
|
||||
|
||||
// 判断是否可以提交
|
||||
const canSubmit = (row) => {
|
||||
return row.status === 'draft' && (isAdmin.value || userStore.factoryId === row.factory)
|
||||
}
|
||||
|
||||
// 判断是否可以审核
|
||||
const canApprove = (row) => {
|
||||
return isAdmin.value && row.status === 'pending'
|
||||
}
|
||||
|
||||
// 判断是否可以拒绝
|
||||
const canReject = (row) => {
|
||||
return isAdmin.value && row.status === 'pending'
|
||||
}
|
||||
|
||||
// 判断是否可以删除
|
||||
const canDelete = (row) => {
|
||||
if (isAdmin.value) return true
|
||||
return row.status === 'draft' && userStore.factoryId === row.factory
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.size,
|
||||
...searchForm
|
||||
}
|
||||
const data = await getMaterialList(params)
|
||||
tableData.value = data.results || data
|
||||
pagination.total = data.count || data.length
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
name: '',
|
||||
major_category: '',
|
||||
status: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
currentMaterial.value = null
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 查看
|
||||
const handleView = (row) => {
|
||||
router.push(`/materials/${row.id}`)
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
currentMaterial.value = { ...row }
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认提交该材料进行审核吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await submitMaterial(row.id)
|
||||
ElMessage.success('提交成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('提交失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 审核通过
|
||||
const handleApprove = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认审核通过该材料吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await approveMaterial(row.id)
|
||||
ElMessage.success('审核通过')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('审核失败:', error)
|
||||
ElMessage.error('审核失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 审核拒绝
|
||||
const handleReject = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认拒绝该材料吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await rejectMaterial(row.id)
|
||||
ElMessage.success('已拒绝')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该材料吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteMaterial(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.materials {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,240 +0,0 @@
|
|||
<template>
|
||||
<div class="users">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户管理</span>
|
||||
<el-button type="primary" @click="handleAdd">新增用户</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="searchForm.username" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="searchForm.role" placeholder="请选择角色" clearable>
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="普通账号" value="user" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<el-table :data="tableData" stripe v-loading="loading">
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="first_name" label="姓" />
|
||||
<el-table-column prop="last_name" label="名" />
|
||||
<el-table-column prop="role_display" label="角色">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.role === 'admin' ? 'danger' : 'primary'">
|
||||
{{ row.role === 'admin' ? '管理员' : '普通账号' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="factory_name" label="所属工厂" />
|
||||
<el-table-column prop="phone" label="手机号" />
|
||||
<el-table-column prop="is_active" label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_active ? 'success' : 'info'">
|
||||
{{ row.is_active ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">查看</el-button>
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)" v-if="canDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户表单对话框 -->
|
||||
<UserForm
|
||||
v-if="showForm"
|
||||
:visible="showForm"
|
||||
:user="currentUser"
|
||||
@close="showForm = false"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getUserList, deleteUser } from '@/api/auth'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import UserForm from '@/components/UserForm.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const currentUserId = computed(() => userStore.userInfo?.id)
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
username: '',
|
||||
role: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表单对话框
|
||||
const showForm = ref(false)
|
||||
const currentUser = ref(null)
|
||||
|
||||
// 判断是否可以删除
|
||||
const canDelete = (row) => {
|
||||
// 不能删除自己
|
||||
return row.id !== currentUserId.value
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.size,
|
||||
...searchForm
|
||||
}
|
||||
const data = await getUserList(params)
|
||||
tableData.value = data.results || data
|
||||
pagination.total = data.count || data.length
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
username: '',
|
||||
role: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
currentUser.value = null
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 查看
|
||||
const handleView = (row) => {
|
||||
router.push(`/users/${row.id}`)
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
currentUser.value = { ...row }
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该用户吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteUser(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.users {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: 'modern-compiler' // 使用现代 Sass API 以消除弃用警告
|
||||
}
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue