feat: 页面优化

This commit is contained in:
caoqianming 2026-03-10 14:30:35 +08:00
parent adedaecf29
commit 61821ccd55
6 changed files with 2830 additions and 50 deletions

2371
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,17 +8,17 @@
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"pinia": "^2.1.6",
"@element-plus/icons-vue": "^2.1.0",
"axios": "^1.5.0",
"element-plus": "^2.3.14",
"echarts": "^5.4.3",
"@element-plus/icons-vue": "^2.1.0"
"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",
"vite": "^4.4.9",
"sass": "^1.66.1"
"sass": "^1.78.0",
"vite": "^4.4.9"
}
}

View File

@ -0,0 +1,152 @@
<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

@ -302,15 +302,6 @@ const rules = {
]
}
// material
watch(() => props.material, (newVal) => {
if (newVal) {
Object.assign(form, newVal)
} else {
resetForm()
}
}, { immediate: true })
//
const resetForm = () => {
Object.assign(form, {
@ -343,6 +334,15 @@ const resetForm = () => {
formRef.value?.clearValidate()
}
// material
watch(() => props.material, (newVal) => {
if (newVal) {
Object.assign(form, newVal)
} else {
resetForm()
}
}, { immediate: true })
//
const handleClose = () => {
resetForm()

View File

@ -3,13 +3,13 @@
<el-row :gutter="20">
<!-- 统计卡片 -->
<el-col :span="6">
<el-card class="stat-card">
<el-card class="stat-card stat-card-hover" shadow="hover">
<div class="stat-content">
<div class="stat-icon" style="background-color: #409EFF;">
<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">{{ overviewData.total_materials || 0 }}</div>
<div class="stat-value stat-value-animate">{{ overviewData.total_materials || 0 }}</div>
<div class="stat-label">材料总数</div>
</div>
</div>
@ -17,13 +17,13 @@
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<el-card class="stat-card stat-card-hover" shadow="hover">
<div class="stat-content">
<div class="stat-icon" style="background-color: #67C23A;">
<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">{{ overviewData.total_material_categories || 0 }}</div>
<div class="stat-value stat-value-animate">{{ overviewData.total_material_categories || 0 }}</div>
<div class="stat-label">材料种类</div>
</div>
</div>
@ -31,13 +31,13 @@
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<el-card class="stat-card stat-card-hover" shadow="hover">
<div class="stat-content">
<div class="stat-icon" style="background-color: #E6A23C;">
<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">{{ overviewData.total_brands || 0 }}</div>
<div class="stat-value stat-value-animate">{{ overviewData.total_brands || 0 }}</div>
<div class="stat-label">品牌数</div>
</div>
</div>
@ -45,13 +45,13 @@
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<el-card class="stat-card stat-card-hover" shadow="hover">
<div class="stat-content">
<div class="stat-icon" style="background-color: #F56C6C;">
<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">{{ overviewData.cases_list?.length || 0 }}</div>
<div class="stat-value stat-value-animate">{{ overviewData.cases_list?.length || 0 }}</div>
<div class="stat-label">应用案例</div>
</div>
</div>
@ -114,16 +114,36 @@
<el-row :gutter="20" class="charts-row">
<!-- 应用案例列表 -->
<el-col :span="24">
<el-card>
<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>
<el-table-column prop="name" label="材料名称" />
<el-table-column prop="factory__factory_name" label="所属品牌" />
<el-table-column prop="cases" label="案例说明" show-overflow-tooltip />
<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>
@ -190,28 +210,55 @@ const updateCharts = (data) => {
majorCategoryChartInstance.setOption({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
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'
left: 'left',
textStyle: {
color: '#666'
}
},
series: [
{
name: '专业类别',
type: 'pie',
radius: '50%',
data: data.major_category_stats.map(item => ({
value: item.count,
name: item.major_category
})),
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
}))
}
]
})
@ -222,6 +269,12 @@ const updateCharts = (data) => {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#eee',
borderWidth: 1,
textStyle: {
color: '#333'
}
},
grid: {
@ -231,17 +284,53 @@ const updateCharts = (data) => {
containLabel: true
},
xAxis: {
type: 'value'
type: 'value',
axisLine: {
lineStyle: {
color: '#ddd'
}
},
splitLine: {
lineStyle: {
color: '#f0f0f0'
}
}
},
yAxis: {
type: 'category',
data: data.material_subcategory_stats.map(item => item.material_subcategory)
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)
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' }
])
}
}
}
]
})
@ -252,6 +341,12 @@ const updateCharts = (data) => {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#eee',
borderWidth: 1,
textStyle: {
color: '#333'
}
},
grid: {
@ -265,17 +360,49 @@ const updateCharts = (data) => {
data: data.brand_stats.map(item => item.factory__factory_name),
axisLabel: {
interval: 0,
rotate: 30
rotate: 30,
color: '#666'
},
axisLine: {
lineStyle: {
color: '#ddd'
}
},
axisTick: {
show: false
}
},
yAxis: {
type: 'value'
type: 'value',
axisLine: {
show: false
},
splitLine: {
lineStyle: {
color: '#f0f0f0'
}
}
},
series: [
{
name: '材料数量',
type: 'bar',
data: data.brand_stats.map(item => item.count)
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' }
])
}
}
}
]
})
@ -286,6 +413,12 @@ const updateCharts = (data) => {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#eee',
borderWidth: 1,
textStyle: {
color: '#333'
}
},
grid: {
@ -299,17 +432,49 @@ const updateCharts = (data) => {
data: data.region_stats.map(item => `${item.province} ${item.city}`),
axisLabel: {
interval: 0,
rotate: 30
rotate: 30,
color: '#666'
},
axisLine: {
lineStyle: {
color: '#ddd'
}
},
axisTick: {
show: false
}
},
yAxis: {
type: 'value'
type: 'value',
axisLine: {
show: false
},
splitLine: {
lineStyle: {
color: '#f0f0f0'
}
}
},
series: [
{
name: '工厂数量',
type: 'bar',
data: data.region_stats.map(item => item.count)
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' }
])
}
}
}
]
})
@ -356,8 +521,24 @@ onBeforeUnmount(() => {
<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;
@ -372,6 +553,11 @@ onBeforeUnmount(() => {
align-items: center;
color: #fff;
margin-right: 20px;
transition: all 0.3s ease;
&.stat-icon-pulse:hover {
animation: pulse 1.5s infinite;
}
}
.stat-info {
@ -382,6 +568,11 @@ onBeforeUnmount(() => {
font-weight: bold;
color: #303133;
margin-bottom: 5px;
transition: all 0.3s ease;
&.stat-value-animate {
animation: fadeInUp 0.5s ease;
}
}
.stat-label {
@ -399,6 +590,65 @@ onBeforeUnmount(() => {
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

@ -10,6 +10,13 @@ export default defineConfig({
'@': resolve(__dirname, 'src')
}
},
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler' // 使用现代 Sass API 以消除弃用警告
}
}
},
server: {
port: 5173,
proxy: {