commits
This commit is contained in:
1 parent
b103192fac
commit
0f961789dc
538 files changed
+128210
-128008
No files matched your search
@@ -1,329 +1,329 @@
|
||||
<template>
|
||||
<div class="erp-dashboard">
|
||||
<!-- 第一行统计卡片 -->
|
||||
<el-row :gutter="16" class="stat-row">
|
||||
<el-col :span="4" v-for="(item, index) in firstRowStats" :key="'first-' + index">
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-label">{{ item.label }}</span>
|
||||
<el-icon class="stat-icon"><Info-Filled /></el-icon>
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
<span class="currency">¥</span>
|
||||
<span class="number">{{ formatMoney(item.value) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 第二行统计卡片 -->
|
||||
<el-row :gutter="16" class="stat-row" style="margin-top: 16px;">
|
||||
<el-col :span="4" v-for="(item, index) in secondRowStats" :key="'second-' + index">
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-label">{{ item.label }}</span>
|
||||
<el-icon class="stat-icon"><Info-Filled /></el-icon>
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
<span class="currency">¥</span>
|
||||
<span class="number">{{ formatMoney(item.value) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="16" class="chart-row" style="margin-top: 16px;">
|
||||
<el-col :span="8">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">销售统计</div>
|
||||
<div ref="salesChartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">零售统计</div>
|
||||
<div ref="retailChartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">采购统计</div>
|
||||
<div ref="purchaseChartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, shallowRef, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
|
||||
// 统计卡片数据 - 第一行
|
||||
const firstRowStats = ref([
|
||||
{ label: '今日销售', value: 0 },
|
||||
{ label: '今日零售', value: 0 },
|
||||
{ label: '今日采购', value: 0 },
|
||||
{ label: '本月累计销售', value: 0 },
|
||||
{ label: '本月累计零售', value: 0 },
|
||||
{ label: '本月累计采购', value: 0 },
|
||||
]);
|
||||
|
||||
// 统计卡片数据 - 第二行
|
||||
const secondRowStats = ref([
|
||||
{ label: '昨日销售', value: 0 },
|
||||
{ label: '昨日零售', value: 0 },
|
||||
{ label: '昨日采购', value: 0 },
|
||||
{ label: '今年累计销售', value: 0 },
|
||||
{ label: '今年累计零售', value: 0 },
|
||||
{ label: '今年累计采购', value: 0 },
|
||||
]);
|
||||
|
||||
// 图表实例
|
||||
const salesChartRef = ref<HTMLElement | null>(null);
|
||||
const retailChartRef = ref<HTMLElement | null>(null);
|
||||
const purchaseChartRef = ref<HTMLElement | null>(null);
|
||||
const salesChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
const retailChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
const purchaseChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (value: number): string => {
|
||||
return value.toLocaleString();
|
||||
};
|
||||
|
||||
// 生成最近6个月的月份标签
|
||||
const getMonthLabels = (): string[] => {
|
||||
const labels: string[] = [];
|
||||
const now = new Date();
|
||||
for (let i = 5; i >= 0; i--) {
|
||||
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
labels.push(`${year}-${month}`);
|
||||
}
|
||||
return labels;
|
||||
};
|
||||
|
||||
// 初始化图表通用配置
|
||||
const initChart = (refEl: HTMLElement, title: string, color: string) => {
|
||||
const chart = echarts.init(refEl);
|
||||
const months = getMonthLabels();
|
||||
|
||||
chart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'cross' }
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: months,
|
||||
axisLine: { lineStyle: { color: '#E0E6ED' } },
|
||||
axisLabel: { color: '#606266', fontSize: 11 }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: '#F2F6FC' } },
|
||||
axisLabel: { color: '#606266', fontSize: 11 }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: title,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: { color: color, width: 2 },
|
||||
itemStyle: { color: color },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: color + '40' },
|
||||
{ offset: 1, color: color + '10' }
|
||||
])
|
||||
},
|
||||
data: [0, 0, 0, 0, 0, 0]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return chart;
|
||||
};
|
||||
|
||||
// 初始化所有图表
|
||||
const initCharts = () => {
|
||||
if (salesChartRef.value) {
|
||||
salesChartInstance.value = initChart(salesChartRef.value, '销售', '#409EFF');
|
||||
}
|
||||
if (retailChartRef.value) {
|
||||
retailChartInstance.value = initChart(retailChartRef.value, '零售', '#67C23A');
|
||||
}
|
||||
if (purchaseChartRef.value) {
|
||||
purchaseChartInstance.value = initChart(purchaseChartRef.value, '采购', '#E6A23C');
|
||||
}
|
||||
};
|
||||
|
||||
// 加载数据(模拟接口调用)
|
||||
const loadData = async () => {
|
||||
// TODO: 调用接口获取真实数据
|
||||
// const res = await getErpDashboard();
|
||||
|
||||
// 模拟数据更新
|
||||
firstRowStats.value = [
|
||||
{ label: '今日销售', value: 12580 },
|
||||
{ label: '今日零售', value: 8560 },
|
||||
{ label: '今日采购', value: 23400 },
|
||||
{ label: '本月累计销售', value: 358600 },
|
||||
{ label: '本月累计零售', value: 245800 },
|
||||
{ label: '本月累计采购', value: 568900 },
|
||||
];
|
||||
|
||||
secondRowStats.value = [
|
||||
{ label: '昨日销售', value: 15230 },
|
||||
{ label: '昨日零售', value: 9870 },
|
||||
{ label: '昨日采购', value: 18900 },
|
||||
{ label: '今年累计销售', value: 2856000 },
|
||||
{ label: '今年累计零售', value: 1985000 },
|
||||
{ label: '今年累计采购', value: 4568000 },
|
||||
];
|
||||
|
||||
// 更新图表数据
|
||||
updateCharts();
|
||||
};
|
||||
|
||||
// 更新图表数据
|
||||
const updateCharts = () => {
|
||||
const salesData = [28000, 32000, 35000, 38000, 42000, 358600];
|
||||
const retailData = [18000, 21000, 22000, 25000, 28000, 245800];
|
||||
const purchaseData = [45000, 48000, 52000, 55000, 58000, 568900];
|
||||
|
||||
if (salesChartInstance.value) {
|
||||
salesChartInstance.value.setOption({ series: [{ data: salesData }] });
|
||||
}
|
||||
if (retailChartInstance.value) {
|
||||
retailChartInstance.value.setOption({ series: [{ data: retailData }] });
|
||||
}
|
||||
if (purchaseChartInstance.value) {
|
||||
purchaseChartInstance.value.setOption({ series: [{ data: purchaseData }] });
|
||||
}
|
||||
};
|
||||
|
||||
// 自适应窗口大小
|
||||
const handleResize = () => {
|
||||
salesChartInstance.value?.resize();
|
||||
retailChartInstance.value?.resize();
|
||||
purchaseChartInstance.value?.resize();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
initCharts();
|
||||
await nextTick();
|
||||
await loadData();
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
salesChartInstance.value?.dispose();
|
||||
retailChartInstance.value?.dispose();
|
||||
purchaseChartInstance.value?.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.erp-dashboard {
|
||||
background-color: #f5f7fa;
|
||||
min-height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
.el-col {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.stat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 14px;
|
||||
color: #c0c4cc;
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.currency {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
.el-col {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
height: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chart-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="erp-dashboard">
|
||||
<!-- 第一行统计卡片 -->
|
||||
<el-row :gutter="16" class="stat-row">
|
||||
<el-col :span="4" v-for="(item, index) in firstRowStats" :key="'first-' + index">
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-label">{{ item.label }}</span>
|
||||
<el-icon class="stat-icon"><Info-Filled /></el-icon>
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
<span class="currency">¥</span>
|
||||
<span class="number">{{ formatMoney(item.value) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 第二行统计卡片 -->
|
||||
<el-row :gutter="16" class="stat-row" style="margin-top: 16px;">
|
||||
<el-col :span="4" v-for="(item, index) in secondRowStats" :key="'second-' + index">
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-label">{{ item.label }}</span>
|
||||
<el-icon class="stat-icon"><Info-Filled /></el-icon>
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
<span class="currency">¥</span>
|
||||
<span class="number">{{ formatMoney(item.value) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="16" class="chart-row" style="margin-top: 16px;">
|
||||
<el-col :span="8">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">销售统计</div>
|
||||
<div ref="salesChartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">零售统计</div>
|
||||
<div ref="retailChartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">采购统计</div>
|
||||
<div ref="purchaseChartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, shallowRef, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
|
||||
// 统计卡片数据 - 第一行
|
||||
const firstRowStats = ref([
|
||||
{ label: '今日销售', value: 0 },
|
||||
{ label: '今日零售', value: 0 },
|
||||
{ label: '今日采购', value: 0 },
|
||||
{ label: '本月累计销售', value: 0 },
|
||||
{ label: '本月累计零售', value: 0 },
|
||||
{ label: '本月累计采购', value: 0 },
|
||||
]);
|
||||
|
||||
// 统计卡片数据 - 第二行
|
||||
const secondRowStats = ref([
|
||||
{ label: '昨日销售', value: 0 },
|
||||
{ label: '昨日零售', value: 0 },
|
||||
{ label: '昨日采购', value: 0 },
|
||||
{ label: '今年累计销售', value: 0 },
|
||||
{ label: '今年累计零售', value: 0 },
|
||||
{ label: '今年累计采购', value: 0 },
|
||||
]);
|
||||
|
||||
// 图表实例
|
||||
const salesChartRef = ref<HTMLElement | null>(null);
|
||||
const retailChartRef = ref<HTMLElement | null>(null);
|
||||
const purchaseChartRef = ref<HTMLElement | null>(null);
|
||||
const salesChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
const retailChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
const purchaseChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (value: number): string => {
|
||||
return value.toLocaleString();
|
||||
};
|
||||
|
||||
// 生成最近6个月的月份标签
|
||||
const getMonthLabels = (): string[] => {
|
||||
const labels: string[] = [];
|
||||
const now = new Date();
|
||||
for (let i = 5; i >= 0; i--) {
|
||||
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
labels.push(`${year}-${month}`);
|
||||
}
|
||||
return labels;
|
||||
};
|
||||
|
||||
// 初始化图表通用配置
|
||||
const initChart = (refEl: HTMLElement, title: string, color: string) => {
|
||||
const chart = echarts.init(refEl);
|
||||
const months = getMonthLabels();
|
||||
|
||||
chart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'cross' }
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: months,
|
||||
axisLine: { lineStyle: { color: '#E0E6ED' } },
|
||||
axisLabel: { color: '#606266', fontSize: 11 }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: '#F2F6FC' } },
|
||||
axisLabel: { color: '#606266', fontSize: 11 }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: title,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: { color: color, width: 2 },
|
||||
itemStyle: { color: color },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: color + '40' },
|
||||
{ offset: 1, color: color + '10' }
|
||||
])
|
||||
},
|
||||
data: [0, 0, 0, 0, 0, 0]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return chart;
|
||||
};
|
||||
|
||||
// 初始化所有图表
|
||||
const initCharts = () => {
|
||||
if (salesChartRef.value) {
|
||||
salesChartInstance.value = initChart(salesChartRef.value, '销售', '#409EFF');
|
||||
}
|
||||
if (retailChartRef.value) {
|
||||
retailChartInstance.value = initChart(retailChartRef.value, '零售', '#67C23A');
|
||||
}
|
||||
if (purchaseChartRef.value) {
|
||||
purchaseChartInstance.value = initChart(purchaseChartRef.value, '采购', '#E6A23C');
|
||||
}
|
||||
};
|
||||
|
||||
// 加载数据(模拟接口调用)
|
||||
const loadData = async () => {
|
||||
// TODO: 调用接口获取真实数据
|
||||
// const res = await getErpDashboard();
|
||||
|
||||
// 模拟数据更新
|
||||
firstRowStats.value = [
|
||||
{ label: '今日销售', value: 12580 },
|
||||
{ label: '今日零售', value: 8560 },
|
||||
{ label: '今日采购', value: 23400 },
|
||||
{ label: '本月累计销售', value: 358600 },
|
||||
{ label: '本月累计零售', value: 245800 },
|
||||
{ label: '本月累计采购', value: 568900 },
|
||||
];
|
||||
|
||||
secondRowStats.value = [
|
||||
{ label: '昨日销售', value: 15230 },
|
||||
{ label: '昨日零售', value: 9870 },
|
||||
{ label: '昨日采购', value: 18900 },
|
||||
{ label: '今年累计销售', value: 2856000 },
|
||||
{ label: '今年累计零售', value: 1985000 },
|
||||
{ label: '今年累计采购', value: 4568000 },
|
||||
];
|
||||
|
||||
// 更新图表数据
|
||||
updateCharts();
|
||||
};
|
||||
|
||||
// 更新图表数据
|
||||
const updateCharts = () => {
|
||||
const salesData = [28000, 32000, 35000, 38000, 42000, 358600];
|
||||
const retailData = [18000, 21000, 22000, 25000, 28000, 245800];
|
||||
const purchaseData = [45000, 48000, 52000, 55000, 58000, 568900];
|
||||
|
||||
if (salesChartInstance.value) {
|
||||
salesChartInstance.value.setOption({ series: [{ data: salesData }] });
|
||||
}
|
||||
if (retailChartInstance.value) {
|
||||
retailChartInstance.value.setOption({ series: [{ data: retailData }] });
|
||||
}
|
||||
if (purchaseChartInstance.value) {
|
||||
purchaseChartInstance.value.setOption({ series: [{ data: purchaseData }] });
|
||||
}
|
||||
};
|
||||
|
||||
// 自适应窗口大小
|
||||
const handleResize = () => {
|
||||
salesChartInstance.value?.resize();
|
||||
retailChartInstance.value?.resize();
|
||||
purchaseChartInstance.value?.resize();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
initCharts();
|
||||
await nextTick();
|
||||
await loadData();
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
salesChartInstance.value?.dispose();
|
||||
retailChartInstance.value?.dispose();
|
||||
purchaseChartInstance.value?.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.erp-dashboard {
|
||||
background-color: #f5f7fa;
|
||||
min-height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
.el-col {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.stat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 14px;
|
||||
color: #c0c4cc;
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.currency {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
.el-col {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
height: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chart-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,122 +1,122 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="修改密码" width="420px" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||||
<el-form-item label="用户">
|
||||
<span>{{ currentUser.name || currentUser.account || "-" }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="新密码" prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
placeholder="请输入新密码"
|
||||
show-password
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="form.confirmPassword"
|
||||
type="password"
|
||||
placeholder="请再次输入新密码"
|
||||
show-password
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||
import { changePassword } from "@/api/user";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "submit"): void;
|
||||
}>();
|
||||
|
||||
const visible = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const currentUser = ref<any>({});
|
||||
|
||||
const form = reactive({
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
const validateConfirmPassword = (_rule: any, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value) {
|
||||
callback(new Error("请再次输入新密码"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (value !== form.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
const rules: FormRules = {
|
||||
password: [
|
||||
{ required: true, message: "请输入新密码", trigger: "blur" },
|
||||
{ min: 6, message: "密码长度不能少于 6 位", trigger: "blur" },
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: "请再次输入新密码", trigger: "blur" },
|
||||
{ validator: validateConfirmPassword, trigger: "blur" },
|
||||
],
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.password = "";
|
||||
form.confirmPassword = "";
|
||||
formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
const openChangePass = (row: any) => {
|
||||
currentUser.value = row || {};
|
||||
resetForm();
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!currentUser.value?.id) {
|
||||
ElMessage.error("用户 ID 不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
await formRef.value?.validate();
|
||||
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
await changePassword(currentUser.value.id, {
|
||||
newPassword: form.password,
|
||||
});
|
||||
|
||||
ElMessage.success("密码修改成功");
|
||||
visible.value = false;
|
||||
emit("submit");
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || e?.message || "密码修改失败");
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
openChangePass,
|
||||
open: openChangePass,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="修改密码" width="420px" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||||
<el-form-item label="用户">
|
||||
<span>{{ currentUser.name || currentUser.account || "-" }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="新密码" prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
placeholder="请输入新密码"
|
||||
show-password
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="form.confirmPassword"
|
||||
type="password"
|
||||
placeholder="请再次输入新密码"
|
||||
show-password
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||
import { changePassword } from "@/api/user";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "submit"): void;
|
||||
}>();
|
||||
|
||||
const visible = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const currentUser = ref<any>({});
|
||||
|
||||
const form = reactive({
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
const validateConfirmPassword = (_rule: any, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value) {
|
||||
callback(new Error("请再次输入新密码"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (value !== form.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
const rules: FormRules = {
|
||||
password: [
|
||||
{ required: true, message: "请输入新密码", trigger: "blur" },
|
||||
{ min: 6, message: "密码长度不能少于 6 位", trigger: "blur" },
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: "请再次输入新密码", trigger: "blur" },
|
||||
{ validator: validateConfirmPassword, trigger: "blur" },
|
||||
],
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.password = "";
|
||||
form.confirmPassword = "";
|
||||
formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
const openChangePass = (row: any) => {
|
||||
currentUser.value = row || {};
|
||||
resetForm();
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!currentUser.value?.id) {
|
||||
ElMessage.error("用户 ID 不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
await formRef.value?.validate();
|
||||
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
await changePassword(currentUser.value.id, {
|
||||
newPassword: form.password,
|
||||
});
|
||||
|
||||
ElMessage.success("密码修改成功");
|
||||
visible.value = false;
|
||||
emit("submit");
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || e?.message || "密码修改失败");
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
openChangePass,
|
||||
open: openChangePass,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,97 +1,97 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" title="查看用户" width="500px">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="ID">{{ detail.id || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="账号">{{ detail.account || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ detail.name || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ genderText }}</el-descriptions-item>
|
||||
<el-descriptions-item label="生日">{{ detail.birthday || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="隶属单位">{{ detail.affiliate_unit_name || detail.affiliate_unit || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="部门">{{ detail.department_name || detail.department || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="职位">{{ detail.position || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="学历">{{ detail.education || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="民族">{{ detail.nation || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机">{{ detail.phone || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="微信">{{ detail.wechat || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ detail.email || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="家庭住址">{{ detail.home_address || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="账号状态">
|
||||
<el-tag :type="statusValue === 1 ? 'success' : 'danger'">
|
||||
{{ statusText }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { getEmployeeDetail } from "@/api/erp";
|
||||
|
||||
const visible = ref(false);
|
||||
const detail = ref<any>({});
|
||||
|
||||
const genderValue = computed(() => {
|
||||
const value = detail.value.sex ?? detail.value.gender ?? 0;
|
||||
return Number(value);
|
||||
});
|
||||
|
||||
const genderText = computed(() => {
|
||||
if (genderValue.value === 1) {
|
||||
return "男";
|
||||
}
|
||||
if (genderValue.value === 2) {
|
||||
return "女";
|
||||
}
|
||||
return "未知";
|
||||
});
|
||||
|
||||
const statusValue = computed(() => {
|
||||
const value = detail.value.account_status ?? detail.value.status ?? 0;
|
||||
return Number(value);
|
||||
});
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (statusValue.value === 1) {
|
||||
return "启用";
|
||||
}
|
||||
if (statusValue.value === 2) {
|
||||
return "离职";
|
||||
}
|
||||
return "禁用";
|
||||
});
|
||||
|
||||
const loadUserData = async (user: any) => {
|
||||
try {
|
||||
const userId = typeof user === "number" ? user : user?.id || user?.userId;
|
||||
if (!userId) {
|
||||
throw new Error("未提供有效的用户 ID");
|
||||
}
|
||||
|
||||
const res = await getEmployeeDetail(userId);
|
||||
detail.value = res.data || res || {};
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
|
||||
ElMessage.error(errorMsg);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const openView = async (user: any) => {
|
||||
visible.value = true;
|
||||
await loadUserData(user);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
loadUserData,
|
||||
openView,
|
||||
open: openView,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<template>
|
||||
<el-drawer v-model="visible" title="查看用户" width="500px">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="ID">{{ detail.id || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="账号">{{ detail.account || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ detail.name || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ genderText }}</el-descriptions-item>
|
||||
<el-descriptions-item label="生日">{{ detail.birthday || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="隶属单位">{{ detail.affiliate_unit_name || detail.affiliate_unit || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="部门">{{ detail.department_name || detail.department || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="职位">{{ detail.position || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="学历">{{ detail.education || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="民族">{{ detail.nation || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机">{{ detail.phone || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="微信">{{ detail.wechat || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ detail.email || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="家庭住址">{{ detail.home_address || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="账号状态">
|
||||
<el-tag :type="statusValue === 1 ? 'success' : 'danger'">
|
||||
{{ statusText }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { getEmployeeDetail } from "@/api/erp";
|
||||
|
||||
const visible = ref(false);
|
||||
const detail = ref<any>({});
|
||||
|
||||
const genderValue = computed(() => {
|
||||
const value = detail.value.sex ?? detail.value.gender ?? 0;
|
||||
return Number(value);
|
||||
});
|
||||
|
||||
const genderText = computed(() => {
|
||||
if (genderValue.value === 1) {
|
||||
return "男";
|
||||
}
|
||||
if (genderValue.value === 2) {
|
||||
return "女";
|
||||
}
|
||||
return "未知";
|
||||
});
|
||||
|
||||
const statusValue = computed(() => {
|
||||
const value = detail.value.account_status ?? detail.value.status ?? 0;
|
||||
return Number(value);
|
||||
});
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (statusValue.value === 1) {
|
||||
return "启用";
|
||||
}
|
||||
if (statusValue.value === 2) {
|
||||
return "离职";
|
||||
}
|
||||
return "禁用";
|
||||
});
|
||||
|
||||
const loadUserData = async (user: any) => {
|
||||
try {
|
||||
const userId = typeof user === "number" ? user : user?.id || user?.userId;
|
||||
if (!userId) {
|
||||
throw new Error("未提供有效的用户 ID");
|
||||
}
|
||||
|
||||
const res = await getEmployeeDetail(userId);
|
||||
detail.value = res.data || res || {};
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
|
||||
ElMessage.error(errorMsg);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const openView = async (user: any) => {
|
||||
visible.value = true;
|
||||
await loadUserData(user);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
loadUserData,
|
||||
openView,
|
||||
open: openView,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -1,201 +1,201 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>用户管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAddUser">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加用户
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<el-table :data="users" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" align="center" fixed="left" width="80" />
|
||||
<el-table-column prop="account" label="账号" align="center" width="120" />
|
||||
<el-table-column prop="name" label="姓名" align="center" width="120">
|
||||
<template #default="scope">
|
||||
<span class="name-link" @click="handlePreview(scope.row)">{{ scope.row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="隶属单位" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.affiliate_unit_name || scope.row.affiliate_unit || "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="部门" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.department_name || scope.row.department || "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="position" label="职位" align="center" />
|
||||
<el-table-column prop="account_status" label="账号状态" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusValue(scope.row) === 1 ? 'success' : 'danger'">{{
|
||||
getStatusValue(scope.row) === 1 ? "启用" : getStatusValue(scope.row) === 2 ? "离职" : "禁用"
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
||||
<template #default="scope"><!-- -->
|
||||
<el-button size="small" @click="handlePreview(scope.row)">查看</el-button>
|
||||
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="warning" @click="handleChangePassword(scope.row)">
|
||||
修改密码
|
||||
</el-button>
|
||||
<el-button v-if="scope.row.username !== 'admin' && scope.row.id !== 1" size="small" type="danger"
|
||||
@click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-bar">
|
||||
<el-pagination :current-page="page" :page-size="pageSize" :total="total" @current-change="handlePageChange"
|
||||
layout="total, prev, pager, next" />
|
||||
</div>
|
||||
|
||||
<!-- 查看弹窗 -->
|
||||
<View ref="viewRef" />
|
||||
|
||||
<!-- 修改密码弹窗 -->
|
||||
<ChangePass ref="changePassRef" @submit="fetchData" />
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<Edit ref="editRef" @submit="fetchData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||
import { getEmployeeList, deleteEmployee } from "@/api/erp";
|
||||
import Edit from "./components/edit.vue";
|
||||
import View from "./components/view.vue";
|
||||
import ChangePass from "./components/changepass.vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
const users = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const editRef = ref<any>(null);
|
||||
const viewRef = ref<any>(null);
|
||||
const changePassRef = ref<any>(null);
|
||||
|
||||
const getStatusValue = (row: any) => {
|
||||
const status = row.account_status ?? row.status ?? 0;
|
||||
return Number(status);
|
||||
};
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const tenantId = (authStore.user as any)?.tid;
|
||||
|
||||
// console.log(tenantId);
|
||||
|
||||
// 获取列表数据
|
||||
const fetchData = async (currentTenantId = tenantId) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getEmployeeList(currentTenantId);
|
||||
users.value = res.data || res || [];
|
||||
total.value = users.value.length;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || "获取列表失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchData(tenantId);
|
||||
};
|
||||
|
||||
// 添加用户
|
||||
const handleAddUser = () => {
|
||||
editRef.value?.openAdd();
|
||||
};
|
||||
|
||||
// 编辑用户
|
||||
const handleEdit = (row: any) => {
|
||||
editRef.value?.openEdit(row);
|
||||
};
|
||||
|
||||
const handleChangePassword = (row: any) => {
|
||||
changePassRef.value?.openChangePass(row);
|
||||
};
|
||||
|
||||
// 查看
|
||||
const handlePreview = (row: any) => {
|
||||
viewRef.value?.openView(row);
|
||||
};
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除用户 "${row.name}" 吗?`, "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
await deleteEmployee(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
fetchData(tenantId);
|
||||
} catch (e: any) {
|
||||
if (e !== "cancel") {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (val: number) => {
|
||||
page.value = val;
|
||||
fetchData(tenantId);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData(tenantId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-alert__title) {
|
||||
color: #f56c6c !important;
|
||||
}
|
||||
|
||||
.name-link {
|
||||
color: #3973ff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>用户管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAddUser">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加用户
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<el-table :data="users" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" align="center" fixed="left" width="80" />
|
||||
<el-table-column prop="account" label="账号" align="center" width="120" />
|
||||
<el-table-column prop="name" label="姓名" align="center" width="120">
|
||||
<template #default="scope">
|
||||
<span class="name-link" @click="handlePreview(scope.row)">{{ scope.row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="隶属单位" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.affiliate_unit_name || scope.row.affiliate_unit || "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="部门" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.department_name || scope.row.department || "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="position" label="职位" align="center" />
|
||||
<el-table-column prop="account_status" label="账号状态" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusValue(scope.row) === 1 ? 'success' : 'danger'">{{
|
||||
getStatusValue(scope.row) === 1 ? "启用" : getStatusValue(scope.row) === 2 ? "离职" : "禁用"
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
||||
<template #default="scope"><!-- -->
|
||||
<el-button size="small" @click="handlePreview(scope.row)">查看</el-button>
|
||||
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="warning" @click="handleChangePassword(scope.row)">
|
||||
修改密码
|
||||
</el-button>
|
||||
<el-button v-if="scope.row.username !== 'admin' && scope.row.id !== 1" size="small" type="danger"
|
||||
@click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-bar">
|
||||
<el-pagination :current-page="page" :page-size="pageSize" :total="total" @current-change="handlePageChange"
|
||||
layout="total, prev, pager, next" />
|
||||
</div>
|
||||
|
||||
<!-- 查看弹窗 -->
|
||||
<View ref="viewRef" />
|
||||
|
||||
<!-- 修改密码弹窗 -->
|
||||
<ChangePass ref="changePassRef" @submit="fetchData" />
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<Edit ref="editRef" @submit="fetchData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||
import { getEmployeeList, deleteEmployee } from "@/api/erp";
|
||||
import Edit from "./components/edit.vue";
|
||||
import View from "./components/view.vue";
|
||||
import ChangePass from "./components/changepass.vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
const users = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const editRef = ref<any>(null);
|
||||
const viewRef = ref<any>(null);
|
||||
const changePassRef = ref<any>(null);
|
||||
|
||||
const getStatusValue = (row: any) => {
|
||||
const status = row.account_status ?? row.status ?? 0;
|
||||
return Number(status);
|
||||
};
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const tenantId = (authStore.user as any)?.tid;
|
||||
|
||||
// console.log(tenantId);
|
||||
|
||||
// 获取列表数据
|
||||
const fetchData = async (currentTenantId = tenantId) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getEmployeeList(currentTenantId);
|
||||
users.value = res.data || res || [];
|
||||
total.value = users.value.length;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || "获取列表失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchData(tenantId);
|
||||
};
|
||||
|
||||
// 添加用户
|
||||
const handleAddUser = () => {
|
||||
editRef.value?.openAdd();
|
||||
};
|
||||
|
||||
// 编辑用户
|
||||
const handleEdit = (row: any) => {
|
||||
editRef.value?.openEdit(row);
|
||||
};
|
||||
|
||||
const handleChangePassword = (row: any) => {
|
||||
changePassRef.value?.openChangePass(row);
|
||||
};
|
||||
|
||||
// 查看
|
||||
const handlePreview = (row: any) => {
|
||||
viewRef.value?.openView(row);
|
||||
};
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除用户 "${row.name}" 吗?`, "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
await deleteEmployee(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
fetchData(tenantId);
|
||||
} catch (e: any) {
|
||||
if (e !== "cancel") {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (val: number) => {
|
||||
page.value = val;
|
||||
fetchData(tenantId);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData(tenantId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-alert__title) {
|
||||
color: #f56c6c !important;
|
||||
}
|
||||
|
||||
.name-link {
|
||||
color: #3973ff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
||||
@@ -1,252 +1,252 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="isEdit ? '编辑组织' : '添加组织'" width="500px" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
|
||||
<el-form-item label="上级组织" prop="parent_id">
|
||||
<el-tree-select v-model="formData.parent_id" :data="extendedTreeData"
|
||||
:props="props.treeProps || defaultTreeProps" check-strictly placeholder="请选择上级组织" clearable
|
||||
style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="组织名称" prop="org_name">
|
||||
<el-input v-model="formData.org_name" placeholder="请输入组织名称,例如:连云港云泽集团" />
|
||||
</el-form-item>
|
||||
<el-form-item label="组织编码" prop="org_code">
|
||||
<el-input v-model="formData.org_code" placeholder="请输入组织编码,例如:LYG-YZ-GROUP" />
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="leader_id">
|
||||
<el-select v-model="formData.leader_id" placeholder="请选择负责人" filterable clearable style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in leaderList"
|
||||
:key="item.id"
|
||||
:label="item.name || item.account || String(item.id)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否是公司" prop="is_company">
|
||||
<el-radio-group v-model="formData.is_company">
|
||||
<el-radio :label="1">是</el-radio>
|
||||
<el-radio :label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { createOrganization, editOrganization } from '@/api/erp'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { getTenantUsers } from '@/api/user'
|
||||
|
||||
interface TreeNode {
|
||||
id: number
|
||||
org_name: string
|
||||
org_code?: string
|
||||
parent_id?: number
|
||||
parent_name?: string
|
||||
leader_id?: number
|
||||
sort?: number
|
||||
status?: number
|
||||
is_company?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
treeData: TreeNode[]
|
||||
treeProps?: {
|
||||
label: string
|
||||
children: string
|
||||
value: string
|
||||
}
|
||||
}>()
|
||||
|
||||
// 默认 treeProps
|
||||
const defaultTreeProps = {
|
||||
label: 'org_name',
|
||||
children: 'children',
|
||||
value: 'id'
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const isEdit = ref(false)
|
||||
const authStore = useAuthStore()
|
||||
const tenantId = (authStore.user as any)?.tid
|
||||
const leaderList = ref<Array<{ id: number; name?: string; account?: string }>>([])
|
||||
const formData = reactive({
|
||||
id: 0,
|
||||
parent_id: undefined as number | undefined,
|
||||
org_name: '',
|
||||
org_code: '',
|
||||
leader_id: undefined as number | undefined,
|
||||
sort: 0,
|
||||
is_company: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules: FormRules = {
|
||||
org_name: [{ required: true, message: '请输入组织名称', trigger: 'blur' }],
|
||||
org_code: [{ required: true, message: '请输入组织编码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const treeOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
org_name: '顶级组织', // 显示名称
|
||||
children: props.treeData // 将原有的树结构作为其子节点(或者并列,取决于你的业务)
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 如果你希望“顶级组织”是作为一个独立的根选项,而不是所有节点的父级:
|
||||
const extendedTreeData = computed(() => {
|
||||
const rootNode = { id: 0, org_name: '顶级组织' }
|
||||
return [rootNode, ...props.treeData]
|
||||
})
|
||||
|
||||
const getLeaderList = async () => {
|
||||
if (!tenantId) {
|
||||
leaderList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const res: any = await getTenantUsers(tenantId)
|
||||
const responseData = res?.data?.code !== undefined ? res.data : res
|
||||
|
||||
if (responseData?.code === 200) {
|
||||
const list = responseData.data?.list || responseData.data || []
|
||||
leaderList.value = list.map((item: any) => ({
|
||||
...item,
|
||||
id: Number(item.id)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
const open = (data?: TreeNode, parentId?: number) => {
|
||||
getLeaderList()
|
||||
if (data) {
|
||||
// 编辑:只拷贝需要的字段,避免把 children 等多余字段带到表单
|
||||
isEdit.value = true
|
||||
formData.id = data.id
|
||||
formData.parent_id = data.parent_id ?? 0
|
||||
formData.org_name = data.org_name
|
||||
formData.org_code = data.org_code || ''
|
||||
formData.leader_id = data.leader_id === undefined || data.leader_id === null ? undefined : Number(data.leader_id)
|
||||
formData.is_company = data.is_company ?? 0
|
||||
formData.sort = data.sort ?? 0
|
||||
formData.status = data.status ?? 1
|
||||
formData.remark = data.remark || ''
|
||||
} else {
|
||||
// 新增
|
||||
isEdit.value = false
|
||||
formData.id = 0
|
||||
formData.parent_id = parentId ?? 0
|
||||
formData.org_name = ''
|
||||
formData.org_code = ''
|
||||
formData.leader_id = undefined
|
||||
formData.is_company = 0
|
||||
formData.sort = 0
|
||||
formData.status = 1
|
||||
formData.remark = ''
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
let res
|
||||
|
||||
|
||||
// 只允许提交到后台的字段白名单
|
||||
const allowedKeys = ['parent_id', 'org_name', 'org_code', 'leader_id', 'sort', 'is_company', 'status', 'remark']
|
||||
|
||||
if (isEdit.value && formData.id) {
|
||||
// 编辑:使用普通对象提交
|
||||
const submitData: Record<string, any> = {}
|
||||
allowedKeys.forEach(key => {
|
||||
const value = (formData as any)[key]
|
||||
if (value !== undefined && value !== null) {
|
||||
submitData[key] = value
|
||||
}
|
||||
})
|
||||
if (tenantId) {
|
||||
submitData.tid = tenantId
|
||||
}
|
||||
res = await editOrganization(formData.id, submitData)
|
||||
} else {
|
||||
// 新增:使用 FormData 以兼容后端 multipart/form-data
|
||||
const submitData = new FormData()
|
||||
allowedKeys.forEach(key => {
|
||||
const value = (formData as any)[key]
|
||||
if (value !== undefined && value !== null) {
|
||||
submitData.append(key, String(value))
|
||||
}
|
||||
})
|
||||
if (tenantId) {
|
||||
submitData.append('tid', String(tenantId))
|
||||
}
|
||||
res = await createOrganization(submitData)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(isEdit.value ? '编辑成功' : '添加成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.error(res.msg || (isEdit.value ? '编辑失败' : '添加失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getLeaderList()
|
||||
})
|
||||
|
||||
defineExpose({ open })
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="isEdit ? '编辑组织' : '添加组织'" width="500px" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
|
||||
<el-form-item label="上级组织" prop="parent_id">
|
||||
<el-tree-select v-model="formData.parent_id" :data="extendedTreeData"
|
||||
:props="props.treeProps || defaultTreeProps" check-strictly placeholder="请选择上级组织" clearable
|
||||
style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="组织名称" prop="org_name">
|
||||
<el-input v-model="formData.org_name" placeholder="请输入组织名称,例如:连云港云泽集团" />
|
||||
</el-form-item>
|
||||
<el-form-item label="组织编码" prop="org_code">
|
||||
<el-input v-model="formData.org_code" placeholder="请输入组织编码,例如:LYG-YZ-GROUP" />
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="leader_id">
|
||||
<el-select v-model="formData.leader_id" placeholder="请选择负责人" filterable clearable style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in leaderList"
|
||||
:key="item.id"
|
||||
:label="item.name || item.account || String(item.id)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否是公司" prop="is_company">
|
||||
<el-radio-group v-model="formData.is_company">
|
||||
<el-radio :label="1">是</el-radio>
|
||||
<el-radio :label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { createOrganization, editOrganization } from '@/api/erp'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { getTenantUsers } from '@/api/user'
|
||||
|
||||
interface TreeNode {
|
||||
id: number
|
||||
org_name: string
|
||||
org_code?: string
|
||||
parent_id?: number
|
||||
parent_name?: string
|
||||
leader_id?: number
|
||||
sort?: number
|
||||
status?: number
|
||||
is_company?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
treeData: TreeNode[]
|
||||
treeProps?: {
|
||||
label: string
|
||||
children: string
|
||||
value: string
|
||||
}
|
||||
}>()
|
||||
|
||||
// 默认 treeProps
|
||||
const defaultTreeProps = {
|
||||
label: 'org_name',
|
||||
children: 'children',
|
||||
value: 'id'
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const isEdit = ref(false)
|
||||
const authStore = useAuthStore()
|
||||
const tenantId = (authStore.user as any)?.tid
|
||||
const leaderList = ref<Array<{ id: number; name?: string; account?: string }>>([])
|
||||
const formData = reactive({
|
||||
id: 0,
|
||||
parent_id: undefined as number | undefined,
|
||||
org_name: '',
|
||||
org_code: '',
|
||||
leader_id: undefined as number | undefined,
|
||||
sort: 0,
|
||||
is_company: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules: FormRules = {
|
||||
org_name: [{ required: true, message: '请输入组织名称', trigger: 'blur' }],
|
||||
org_code: [{ required: true, message: '请输入组织编码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const treeOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
org_name: '顶级组织', // 显示名称
|
||||
children: props.treeData // 将原有的树结构作为其子节点(或者并列,取决于你的业务)
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 如果你希望“顶级组织”是作为一个独立的根选项,而不是所有节点的父级:
|
||||
const extendedTreeData = computed(() => {
|
||||
const rootNode = { id: 0, org_name: '顶级组织' }
|
||||
return [rootNode, ...props.treeData]
|
||||
})
|
||||
|
||||
const getLeaderList = async () => {
|
||||
if (!tenantId) {
|
||||
leaderList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const res: any = await getTenantUsers(tenantId)
|
||||
const responseData = res?.data?.code !== undefined ? res.data : res
|
||||
|
||||
if (responseData?.code === 200) {
|
||||
const list = responseData.data?.list || responseData.data || []
|
||||
leaderList.value = list.map((item: any) => ({
|
||||
...item,
|
||||
id: Number(item.id)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
const open = (data?: TreeNode, parentId?: number) => {
|
||||
getLeaderList()
|
||||
if (data) {
|
||||
// 编辑:只拷贝需要的字段,避免把 children 等多余字段带到表单
|
||||
isEdit.value = true
|
||||
formData.id = data.id
|
||||
formData.parent_id = data.parent_id ?? 0
|
||||
formData.org_name = data.org_name
|
||||
formData.org_code = data.org_code || ''
|
||||
formData.leader_id = data.leader_id === undefined || data.leader_id === null ? undefined : Number(data.leader_id)
|
||||
formData.is_company = data.is_company ?? 0
|
||||
formData.sort = data.sort ?? 0
|
||||
formData.status = data.status ?? 1
|
||||
formData.remark = data.remark || ''
|
||||
} else {
|
||||
// 新增
|
||||
isEdit.value = false
|
||||
formData.id = 0
|
||||
formData.parent_id = parentId ?? 0
|
||||
formData.org_name = ''
|
||||
formData.org_code = ''
|
||||
formData.leader_id = undefined
|
||||
formData.is_company = 0
|
||||
formData.sort = 0
|
||||
formData.status = 1
|
||||
formData.remark = ''
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
let res
|
||||
|
||||
|
||||
// 只允许提交到后台的字段白名单
|
||||
const allowedKeys = ['parent_id', 'org_name', 'org_code', 'leader_id', 'sort', 'is_company', 'status', 'remark']
|
||||
|
||||
if (isEdit.value && formData.id) {
|
||||
// 编辑:使用普通对象提交
|
||||
const submitData: Record<string, any> = {}
|
||||
allowedKeys.forEach(key => {
|
||||
const value = (formData as any)[key]
|
||||
if (value !== undefined && value !== null) {
|
||||
submitData[key] = value
|
||||
}
|
||||
})
|
||||
if (tenantId) {
|
||||
submitData.tid = tenantId
|
||||
}
|
||||
res = await editOrganization(formData.id, submitData)
|
||||
} else {
|
||||
// 新增:使用 FormData 以兼容后端 multipart/form-data
|
||||
const submitData = new FormData()
|
||||
allowedKeys.forEach(key => {
|
||||
const value = (formData as any)[key]
|
||||
if (value !== undefined && value !== null) {
|
||||
submitData.append(key, String(value))
|
||||
}
|
||||
})
|
||||
if (tenantId) {
|
||||
submitData.append('tid', String(tenantId))
|
||||
}
|
||||
res = await createOrganization(submitData)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(isEdit.value ? '编辑成功' : '添加成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.error(res.msg || (isEdit.value ? '编辑失败' : '添加失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getLeaderList()
|
||||
})
|
||||
|
||||
defineExpose({ open })
|
||||
|
||||
</script>
|
||||
@@ -1,358 +1,358 @@
|
||||
<template>
|
||||
<div class="organization-container">
|
||||
<el-row :gutter="16">
|
||||
<!-- 左侧:组织架构树 -->
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="tree-card" v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>组织架构</span>
|
||||
<el-button type="primary" link @click="handleAddRoot">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="treeData"
|
||||
:props="treeProps"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:expand-on-click-node="false"
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="tree-node">
|
||||
<span class="node-label">{{ node.label }}</span>
|
||||
<div class="node-actions" @click.stop>
|
||||
<el-button link type="primary" size="small" @click="handleAdd(data)">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" @click="handleEdit(data)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(data)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧:详情面板 -->
|
||||
<el-col :span="16">
|
||||
<el-card shadow="hover" class="detail-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ detailTitle }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="selectedNode" class="detail-content">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="组织名称">
|
||||
{{ selectedNode.org_name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="组织编码">
|
||||
{{ selectedNode.org_code || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上级组织">
|
||||
{{ selectedNode.parent_name || '顶级组织' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">
|
||||
{{ selectedNode.leader_name || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="是否是公司">
|
||||
<el-tag :type="selectedNode.is_company === 1 ? 'primary' : 'info'">
|
||||
{{ selectedNode.is_company === 1 ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="selectedNode.status === 1 ? 'success' : 'danger'">
|
||||
{{ selectedNode.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ selectedNode.remark || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="detail-actions">
|
||||
<el-button type="primary" @click="handleEdit(selectedNode)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleDelete(selectedNode)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else description="请选择左侧组织节点查看详情" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<OrganizationEdit
|
||||
ref="editRef"
|
||||
:tree-data="treeData"
|
||||
:tree-props="treeProps"
|
||||
@success="handleSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import OrganizationEdit from './components/edit.vue'
|
||||
import { getOrganizationList, getOrganizationDetail, deleteOrganization } from '@/api/erp'
|
||||
|
||||
// 树形结构数据(与后台字段对应)
|
||||
interface TreeNode {
|
||||
id: number
|
||||
tid?: number
|
||||
org_name: string
|
||||
org_code?: string
|
||||
parent_id?: number
|
||||
parent_name?: string
|
||||
leader_id?: number
|
||||
leader_name?: string
|
||||
is_company?: number
|
||||
sort?: number
|
||||
status?: number
|
||||
remark?: string
|
||||
children?: TreeNode[]
|
||||
}
|
||||
|
||||
// 树形数据
|
||||
const treeData = ref<TreeNode[]>([])
|
||||
|
||||
const treeProps = {
|
||||
label: 'org_name',
|
||||
children: 'children',
|
||||
value: 'id'
|
||||
}
|
||||
|
||||
const treeRef = ref()
|
||||
const editRef = ref()
|
||||
const selectedNode = ref<TreeNode | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
// 将扁平列表构建为树形结构
|
||||
const buildTree = (list: any[]): TreeNode[] => {
|
||||
const nodeMap = new Map<number, TreeNode>()
|
||||
const roots: TreeNode[] = []
|
||||
|
||||
list.forEach(item => {
|
||||
const node: TreeNode = {
|
||||
id: item.id,
|
||||
tid: item.tid,
|
||||
org_name: item.org_name,
|
||||
org_code: item.org_code,
|
||||
parent_id: item.parent_id,
|
||||
parent_name: item.parent_name,
|
||||
leader_id: item.leader_id,
|
||||
leader_name: item.leader_name,
|
||||
is_company: item.is_company,
|
||||
sort: item.sort,
|
||||
status: item.status,
|
||||
remark: item.remark,
|
||||
children: []
|
||||
}
|
||||
nodeMap.set(node.id, node)
|
||||
})
|
||||
|
||||
nodeMap.forEach(node => {
|
||||
if (node.parent_id && nodeMap.has(node.parent_id)) {
|
||||
const parent = nodeMap.get(node.parent_id)!
|
||||
if (!parent.children) {
|
||||
parent.children = []
|
||||
}
|
||||
parent.children.push(node)
|
||||
} else {
|
||||
roots.push(node)
|
||||
}
|
||||
})
|
||||
|
||||
return roots
|
||||
}
|
||||
|
||||
// 获取组织机构详情
|
||||
const loadOrganizationDetail = async (id: number) => {
|
||||
try {
|
||||
const res = await getOrganizationDetail(id)
|
||||
if (res.code === 200) {
|
||||
const data = res.data || null
|
||||
if (data) {
|
||||
selectedNode.value = {
|
||||
id: data.id,
|
||||
tid: data.tid,
|
||||
org_name: data.org_name,
|
||||
org_code: data.org_code,
|
||||
parent_id: data.parent_id,
|
||||
parent_name: data.parent_name,
|
||||
leader_id: data.leader_id,
|
||||
leader_name: data.leader_name,
|
||||
is_company: data.is_company,
|
||||
sort: data.sort,
|
||||
status: data.status,
|
||||
remark: data.remark,
|
||||
children: []
|
||||
}
|
||||
} else {
|
||||
selectedNode.value = null
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取组织机构详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取组织机构详情失败:', error)
|
||||
ElMessage.error('获取组织机构详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取组织机构列表
|
||||
const fetchOrganizationList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getOrganizationList()
|
||||
if (res.code === 200) {
|
||||
const list = res.data || []
|
||||
treeData.value = buildTree(list)
|
||||
|
||||
// 如果当前没有选中的节点且有数据,默认加载第一个节点详情
|
||||
if (!selectedNode.value && list.length) {
|
||||
await loadOrganizationDetail(list[0].id)
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取组织机构列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取组织机构列表失败:', error)
|
||||
ElMessage.error('获取组织机构列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 详情标题
|
||||
const detailTitle = computed(() => {
|
||||
return selectedNode.value ? `部门详情 - ${selectedNode.value.org_name}` : '部门详情'
|
||||
})
|
||||
|
||||
// 点击节点,加载详情
|
||||
const handleNodeClick = (data: TreeNode) => {
|
||||
loadOrganizationDetail(data.id)
|
||||
}
|
||||
|
||||
// 添加根节点
|
||||
const handleAddRoot = () => {
|
||||
editRef.value?.open(undefined, undefined)
|
||||
}
|
||||
|
||||
// 添加子节点
|
||||
const handleAdd = (data: TreeNode) => {
|
||||
editRef.value?.open(undefined, data.id)
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (data: TreeNode) => {
|
||||
editRef.value?.open(data)
|
||||
}
|
||||
|
||||
// 提交成功回调,重新加载列表
|
||||
const handleSuccess = () => {
|
||||
fetchOrganizationList()
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = (data: TreeNode) => {
|
||||
ElMessageBox.confirm(`确定要删除部门「${data.org_name}」吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await deleteOrganization(data.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchOrganizationList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrganizationList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.organization-container {
|
||||
// padding: 16px;
|
||||
}
|
||||
|
||||
.tree-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding-right: 8px;
|
||||
|
||||
.node-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.node-actions {
|
||||
display: none;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&:hover .node-actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
.el-descriptions {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="organization-container">
|
||||
<el-row :gutter="16">
|
||||
<!-- 左侧:组织架构树 -->
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="tree-card" v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>组织架构</span>
|
||||
<el-button type="primary" link @click="handleAddRoot">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="treeData"
|
||||
:props="treeProps"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:expand-on-click-node="false"
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="tree-node">
|
||||
<span class="node-label">{{ node.label }}</span>
|
||||
<div class="node-actions" @click.stop>
|
||||
<el-button link type="primary" size="small" @click="handleAdd(data)">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" @click="handleEdit(data)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(data)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧:详情面板 -->
|
||||
<el-col :span="16">
|
||||
<el-card shadow="hover" class="detail-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ detailTitle }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="selectedNode" class="detail-content">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="组织名称">
|
||||
{{ selectedNode.org_name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="组织编码">
|
||||
{{ selectedNode.org_code || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上级组织">
|
||||
{{ selectedNode.parent_name || '顶级组织' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">
|
||||
{{ selectedNode.leader_name || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="是否是公司">
|
||||
<el-tag :type="selectedNode.is_company === 1 ? 'primary' : 'info'">
|
||||
{{ selectedNode.is_company === 1 ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="selectedNode.status === 1 ? 'success' : 'danger'">
|
||||
{{ selectedNode.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ selectedNode.remark || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="detail-actions">
|
||||
<el-button type="primary" @click="handleEdit(selectedNode)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleDelete(selectedNode)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else description="请选择左侧组织节点查看详情" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<OrganizationEdit
|
||||
ref="editRef"
|
||||
:tree-data="treeData"
|
||||
:tree-props="treeProps"
|
||||
@success="handleSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import OrganizationEdit from './components/edit.vue'
|
||||
import { getOrganizationList, getOrganizationDetail, deleteOrganization } from '@/api/erp'
|
||||
|
||||
// 树形结构数据(与后台字段对应)
|
||||
interface TreeNode {
|
||||
id: number
|
||||
tid?: number
|
||||
org_name: string
|
||||
org_code?: string
|
||||
parent_id?: number
|
||||
parent_name?: string
|
||||
leader_id?: number
|
||||
leader_name?: string
|
||||
is_company?: number
|
||||
sort?: number
|
||||
status?: number
|
||||
remark?: string
|
||||
children?: TreeNode[]
|
||||
}
|
||||
|
||||
// 树形数据
|
||||
const treeData = ref<TreeNode[]>([])
|
||||
|
||||
const treeProps = {
|
||||
label: 'org_name',
|
||||
children: 'children',
|
||||
value: 'id'
|
||||
}
|
||||
|
||||
const treeRef = ref()
|
||||
const editRef = ref()
|
||||
const selectedNode = ref<TreeNode | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
// 将扁平列表构建为树形结构
|
||||
const buildTree = (list: any[]): TreeNode[] => {
|
||||
const nodeMap = new Map<number, TreeNode>()
|
||||
const roots: TreeNode[] = []
|
||||
|
||||
list.forEach(item => {
|
||||
const node: TreeNode = {
|
||||
id: item.id,
|
||||
tid: item.tid,
|
||||
org_name: item.org_name,
|
||||
org_code: item.org_code,
|
||||
parent_id: item.parent_id,
|
||||
parent_name: item.parent_name,
|
||||
leader_id: item.leader_id,
|
||||
leader_name: item.leader_name,
|
||||
is_company: item.is_company,
|
||||
sort: item.sort,
|
||||
status: item.status,
|
||||
remark: item.remark,
|
||||
children: []
|
||||
}
|
||||
nodeMap.set(node.id, node)
|
||||
})
|
||||
|
||||
nodeMap.forEach(node => {
|
||||
if (node.parent_id && nodeMap.has(node.parent_id)) {
|
||||
const parent = nodeMap.get(node.parent_id)!
|
||||
if (!parent.children) {
|
||||
parent.children = []
|
||||
}
|
||||
parent.children.push(node)
|
||||
} else {
|
||||
roots.push(node)
|
||||
}
|
||||
})
|
||||
|
||||
return roots
|
||||
}
|
||||
|
||||
// 获取组织机构详情
|
||||
const loadOrganizationDetail = async (id: number) => {
|
||||
try {
|
||||
const res = await getOrganizationDetail(id)
|
||||
if (res.code === 200) {
|
||||
const data = res.data || null
|
||||
if (data) {
|
||||
selectedNode.value = {
|
||||
id: data.id,
|
||||
tid: data.tid,
|
||||
org_name: data.org_name,
|
||||
org_code: data.org_code,
|
||||
parent_id: data.parent_id,
|
||||
parent_name: data.parent_name,
|
||||
leader_id: data.leader_id,
|
||||
leader_name: data.leader_name,
|
||||
is_company: data.is_company,
|
||||
sort: data.sort,
|
||||
status: data.status,
|
||||
remark: data.remark,
|
||||
children: []
|
||||
}
|
||||
} else {
|
||||
selectedNode.value = null
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取组织机构详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取组织机构详情失败:', error)
|
||||
ElMessage.error('获取组织机构详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取组织机构列表
|
||||
const fetchOrganizationList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getOrganizationList()
|
||||
if (res.code === 200) {
|
||||
const list = res.data || []
|
||||
treeData.value = buildTree(list)
|
||||
|
||||
// 如果当前没有选中的节点且有数据,默认加载第一个节点详情
|
||||
if (!selectedNode.value && list.length) {
|
||||
await loadOrganizationDetail(list[0].id)
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取组织机构列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取组织机构列表失败:', error)
|
||||
ElMessage.error('获取组织机构列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 详情标题
|
||||
const detailTitle = computed(() => {
|
||||
return selectedNode.value ? `部门详情 - ${selectedNode.value.org_name}` : '部门详情'
|
||||
})
|
||||
|
||||
// 点击节点,加载详情
|
||||
const handleNodeClick = (data: TreeNode) => {
|
||||
loadOrganizationDetail(data.id)
|
||||
}
|
||||
|
||||
// 添加根节点
|
||||
const handleAddRoot = () => {
|
||||
editRef.value?.open(undefined, undefined)
|
||||
}
|
||||
|
||||
// 添加子节点
|
||||
const handleAdd = (data: TreeNode) => {
|
||||
editRef.value?.open(undefined, data.id)
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (data: TreeNode) => {
|
||||
editRef.value?.open(data)
|
||||
}
|
||||
|
||||
// 提交成功回调,重新加载列表
|
||||
const handleSuccess = () => {
|
||||
fetchOrganizationList()
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = (data: TreeNode) => {
|
||||
ElMessageBox.confirm(`确定要删除部门「${data.org_name}」吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await deleteOrganization(data.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchOrganizationList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrganizationList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.organization-container {
|
||||
// padding: 16px;
|
||||
}
|
||||
|
||||
.tree-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding-right: 8px;
|
||||
|
||||
.node-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.node-actions {
|
||||
display: none;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&:hover .node-actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
.el-descriptions {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user