feat: 移除现有前端

This commit is contained in:
caoqianming 2026-03-10 15:30:26 +08:00
parent 61821ccd55
commit 4b001d23a4
33 changed files with 0 additions and 6797 deletions

View File

@ -1,2 +0,0 @@
# 开发环境配置
VITE_API_BASE_URL=http://localhost:8000/api

View File

@ -1,2 +0,0 @@
# 生产环境配置
VITE_API_BASE_URL=/api

24
frontend/.gitignore vendored
View File

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

View File

@ -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`
## 功能模块
- 用户认证与授权
- 材料库管理
- 工厂库管理
- 数据统计与分析
- 字典管理
- 用户管理

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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