✨ feat: 财务报表初版
This commit is contained in:
44
gear-ui3/src/api/oa/requirement.js
Normal file
44
gear-ui3/src/api/oa/requirement.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询OA 需求列表
|
||||
export function listRequirements(query) {
|
||||
return request({
|
||||
url: '/oa/requirements/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询OA 需求详细
|
||||
export function getRequirements(requirementId) {
|
||||
return request({
|
||||
url: '/oa/requirements/' + requirementId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增OA 需求
|
||||
export function addRequirements(data) {
|
||||
return request({
|
||||
url: '/oa/requirements',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改OA 需求
|
||||
export function updateRequirements(data) {
|
||||
return request({
|
||||
url: '/oa/requirements',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除OA 需求
|
||||
export function delRequirements(requirementId) {
|
||||
return request({
|
||||
url: '/oa/requirements/' + requirementId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
248
gear-ui3/src/views/finance/dashboard/components/TimeFilter.vue
Normal file
248
gear-ui3/src/views/finance/dashboard/components/TimeFilter.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row :gutter="15">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="时间粒度">
|
||||
<el-select
|
||||
v-model="timeGranularity"
|
||||
@change="handleGranularityChange"
|
||||
clearable
|
||||
>
|
||||
<el-option label="按周" value="week"></el-option>
|
||||
<el-option label="按月" value="month"></el-option>
|
||||
<el-option label="按年" value="year"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="dateLabel">
|
||||
<template v-if="timeGranularity === 'week'">
|
||||
<el-date-picker
|
||||
v-model="startDate"
|
||||
type="date"
|
||||
placeholder="选择起始日期"
|
||||
value-format="YYYY-MM-DD 00:00:00"
|
||||
@change="handleStartDateChange"
|
||||
></el-date-picker>
|
||||
</template>
|
||||
|
||||
<template v-if="timeGranularity === 'month'">
|
||||
<el-date-picker
|
||||
v-model="startDate"
|
||||
type="month"
|
||||
placeholder="选择起始月份"
|
||||
value-format="YYYY-MM-01 00:00:00"
|
||||
@change="handleStartDateChange"
|
||||
></el-date-picker>
|
||||
</template>
|
||||
|
||||
<template v-if="timeGranularity === 'year'">
|
||||
<el-date-picker
|
||||
v-model="startDate"
|
||||
type="year"
|
||||
placeholder="选择起始年份"
|
||||
value-format="YYYY-01-01 00:00:00"
|
||||
@change="handleStartDateChange"
|
||||
></el-date-picker>
|
||||
</template>
|
||||
|
||||
<template v-if="!timeGranularity">
|
||||
<div class="placeholder-text">请先选择时间粒度</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6" class="filter-button">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleQuery"
|
||||
:disabled="!timeGranularity || !startDate"
|
||||
>
|
||||
查询
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
// 时间粒度:week, month, year
|
||||
const timeGranularity = ref('week');
|
||||
// 起始日期
|
||||
const startDate = ref('');
|
||||
// 结束日期(自动计算)
|
||||
const endDate = ref('');
|
||||
|
||||
// 计算日期标签文本
|
||||
const dateLabel = computed(() => {
|
||||
if (timeGranularity.value === 'week') return '选择周';
|
||||
if (timeGranularity.value === 'month') return '选择月';
|
||||
if (timeGranularity.value === 'year') return '选择年';
|
||||
return '时间选择';
|
||||
});
|
||||
|
||||
// 时间粒度变化处理
|
||||
const handleGranularityChange = (val) => {
|
||||
// 重置日期选择
|
||||
startDate.value = '';
|
||||
endDate.value = '';
|
||||
|
||||
// 如果选择了粒度,设置默认起始时间
|
||||
if (val) {
|
||||
setDefaultStartDate(val);
|
||||
}
|
||||
};
|
||||
|
||||
// 设置默认起始时间
|
||||
const setDefaultStartDate = (granularity) => {
|
||||
const now = new Date();
|
||||
|
||||
// 统一格式化为YYYY-MM-DD 00:00:00
|
||||
if (granularity === 'week') {
|
||||
// 默认当前周的周一
|
||||
const day = now.getDay() || 7; // 转换为周一为1,周日为7
|
||||
const monday = new Date(now);
|
||||
monday.setDate(now.getDate() - (day - 1));
|
||||
startDate.value = `${monday.getFullYear()}-${(monday.getMonth() + 1).toString().padStart(2, '0')}-${monday.getDate().toString().padStart(2, '0')} 00:00:00`;
|
||||
calculateEndDate();
|
||||
} else if (granularity === 'month') {
|
||||
// 默认当前月
|
||||
startDate.value = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-01 00:00:00`;
|
||||
calculateEndDate();
|
||||
} else if (granularity === 'year') {
|
||||
// 默认当前年
|
||||
startDate.value = `${now.getFullYear()}-01-01 00:00:00`;
|
||||
calculateEndDate();
|
||||
}
|
||||
};
|
||||
|
||||
// 起始日期变化处理
|
||||
const handleStartDateChange = () => {
|
||||
if (startDate.value) {
|
||||
calculateEndDate();
|
||||
} else {
|
||||
endDate.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 计算结束日期
|
||||
const calculateEndDate = () => {
|
||||
if (!timeGranularity.value || !startDate.value) return;
|
||||
|
||||
if (timeGranularity.value === 'week') {
|
||||
// 周粒度:起始日期 + 6天(周日)
|
||||
const start = new Date(startDate.value);
|
||||
const end = new Date(start);
|
||||
end.setDate(start.getDate() + 6);
|
||||
endDate.value = formatDate(end, 'date');
|
||||
} else if (timeGranularity.value === 'month') {
|
||||
// 月粒度:下个月第一天
|
||||
const [year, month] = startDate.value.split('-').map(Number);
|
||||
const end = new Date(year, month + 1, 0);
|
||||
endDate.value = formatDate(end, 'month');
|
||||
} else if (timeGranularity.value === 'year') {
|
||||
// 年粒度:下一年第一天
|
||||
const year = parseInt(startDate.value);
|
||||
const end = new Date(year + 1, 0, 1);
|
||||
endDate.value = formatDate(end, 'year');
|
||||
}
|
||||
|
||||
// 通知父组件日期已变更
|
||||
emit('dateChange', getTimeParams());
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date, type) => {
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
|
||||
if (type === 'date') return `${year}-${month}-${day}`;
|
||||
if (type === 'month') return `${year}-${month}`;
|
||||
if (type === 'year') return `${year}`;
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
// 获取时间参数
|
||||
const getTimeParams = () => {
|
||||
let endTime = '';
|
||||
if (timeGranularity.value === 'week') {
|
||||
endTime = `${endDate.value} 23:59:59`;
|
||||
} else if (timeGranularity.value === 'month') {
|
||||
endTime = `${endDate.value}-01 00:00:00`;
|
||||
} else if (timeGranularity.value === 'year') {
|
||||
endTime = `${endDate.value}-01-01 00:00:00`;
|
||||
}
|
||||
return {
|
||||
startTime: startDate.value,
|
||||
endTime: endTime,
|
||||
}
|
||||
};
|
||||
|
||||
// 查询按钮点击
|
||||
const handleQuery = () => {
|
||||
if (!timeGranularity.value) {
|
||||
ElMessage.warning('请选择时间粒度');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startDate.value) {
|
||||
ElMessage.warning('请选择起始时间');
|
||||
return;
|
||||
}
|
||||
|
||||
emit('query', getTimeParams());
|
||||
};
|
||||
|
||||
// 重置按钮点击
|
||||
const handleReset = () => {
|
||||
timeGranularity.value = '';
|
||||
startDate.value = '';
|
||||
endDate.value = '';
|
||||
emit('reset');
|
||||
};
|
||||
|
||||
// 定义组件事件
|
||||
const emit = defineEmits(['query', 'reset', 'dateChange']);
|
||||
|
||||
// 初始化默认值
|
||||
watch(timeGranularity, (newVal) => {
|
||||
if (newVal && !startDate.value) {
|
||||
setDefaultStartDate(newVal);
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 初始时间设置为今天,触发一次qeury
|
||||
onMounted(() => {
|
||||
startDate.value = new Date().toISOString().split('T')[0] + ' 00:00:00';
|
||||
handleQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.time-filter-card {
|
||||
padding: 15px 0;
|
||||
|
||||
.date-separator {
|
||||
margin: 0 10px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.filter-button {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
padding-bottom: 22px;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
line-height: 40px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
913
gear-ui3/src/views/finance/dashboard/index.vue
Normal file
913
gear-ui3/src/views/finance/dashboard/index.vue
Normal file
@@ -0,0 +1,913 @@
|
||||
<template>
|
||||
<div class="finance-analysis-page">
|
||||
<el-row :gutter="20" class="content-container">
|
||||
<!-- 1. 时间选择区 - 使用封装的组件 -->
|
||||
<el-col :span="24" class="time-filter">
|
||||
<TimeFilter
|
||||
@query="fetchData"
|
||||
@reset="handleFilterReset"
|
||||
@dateChange="handleDateChange"
|
||||
/>
|
||||
</el-col>
|
||||
|
||||
<!-- 2. 指标卡区域 -->
|
||||
<el-col :span="24" class="indicator-cards">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-card class="indicator-card total-income">
|
||||
<div class="card-header">
|
||||
<span>总收入</span>
|
||||
<i class="el-icon-arrow-up"></i>
|
||||
</div>
|
||||
<div class="card-value">{{ totalIncome | formatCurrency }}</div>
|
||||
<!-- <div class="card-desc">较上期 <span class="rise">+5.2%</span></div> -->
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="indicator-card total-expense">
|
||||
<div class="card-header">
|
||||
<span>总支出</span>
|
||||
<i class="el-icon-arrow-down"></i>
|
||||
</div>
|
||||
<div class="card-value">{{ totalExpense | formatCurrency }}</div>
|
||||
<!-- <div class="card-desc">较上期 <span class="drop">-2.8%</span></div> -->
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="indicator-card net-cashflow">
|
||||
<div class="card-header">
|
||||
<span>净现金流</span>
|
||||
<i class="el-icon-refresh"></i>
|
||||
</div>
|
||||
<div class="card-value">{{ netCashflow | formatCurrency }}</div>
|
||||
<!-- <div class="card-desc">较上期 <span class="rise">+8.0%</span></div> -->
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="indicator-card outstanding-receivable">
|
||||
<div class="card-header">
|
||||
<span>未结清应收</span>
|
||||
<i class="el-icon-info"></i>
|
||||
</div>
|
||||
<div class="card-value">{{ outstandingReceivable | formatCurrency }}</div>
|
||||
<div class="card-desc">共 {{ receivableCount }} 笔未结清</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
<!-- 3. echarts图表区域 -->
|
||||
<el-col :span="24" class="charts-container">
|
||||
<!-- 收入支出趋势图 -->
|
||||
<el-row :gutter="20" class="chart-row">
|
||||
<el-col :span="24">
|
||||
<el-card class="chart-card">
|
||||
<div slot="header" class="chart-header">
|
||||
<span>收入支出趋势</span>
|
||||
</div>
|
||||
<div class="chart-content">
|
||||
<div ref="trendChart" class="chart-wrapper"></div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 按订单和客户/供应商分析 -->
|
||||
<el-row :gutter="20" class="chart-row">
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<div slot="header" class="chart-header">
|
||||
<span>按订单区分的收支</span>
|
||||
</div>
|
||||
<div class="chart-content">
|
||||
<div ref="orderChart" class="chart-wrapper"></div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<div slot="header" class="chart-header">
|
||||
<span>按客户区分的收入</span>
|
||||
</div>
|
||||
<div class="chart-content">
|
||||
<div ref="customerChart" class="chart-wrapper"></div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="chart-row">
|
||||
<el-col :span="24">
|
||||
<el-card class="chart-card">
|
||||
<div slot="header" class="chart-header">
|
||||
<span>按供应商区分的支出</span>
|
||||
</div>
|
||||
<div class="chart-content">
|
||||
<div ref="supplierChart" class="chart-wrapper"></div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
<!-- 4. 回款(应收未收)任务信息表格 -->
|
||||
<el-col :span="24" class="table-container">
|
||||
<el-card>
|
||||
<div slot="header" class="table-header">
|
||||
<span>回款(应收未收)任务信息</span>
|
||||
</div>
|
||||
<el-table
|
||||
:data="receivableTasks"
|
||||
border
|
||||
style="width: 100%"
|
||||
v-loading="tableLoading"
|
||||
>
|
||||
<el-table-column
|
||||
prop="receivableId"
|
||||
label="应收ID"
|
||||
width="180"
|
||||
align="center"
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
prop="customerName"
|
||||
label="客户名称"
|
||||
width="150"
|
||||
align="center"
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
prop="orderId"
|
||||
label="订单ID"
|
||||
width="180"
|
||||
align="center"
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
prop="dueDate"
|
||||
label="到期日期"
|
||||
width="150"
|
||||
align="center"
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
prop="amount"
|
||||
label="总金额"
|
||||
width="120"
|
||||
align="center"
|
||||
:formatter="formatCurrency"
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
prop="paidAmount"
|
||||
label="已付金额"
|
||||
width="120"
|
||||
align="center"
|
||||
:formatter="formatCurrency"
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
prop="balanceAmount"
|
||||
label="未付金额"
|
||||
width="120"
|
||||
align="center"
|
||||
:formatter="formatCurrency"
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
prop="status"
|
||||
label="状态"
|
||||
width="100"
|
||||
align="center"
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
align="center"
|
||||
></el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="currentPage"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="receivableTotal"
|
||||
></el-pagination>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import * as echarts from 'echarts';
|
||||
import { listReceivable } from '@/api/finance/receivable';
|
||||
import { listPayable } from '@/api/finance/payable';
|
||||
import TimeFilter from './components/TimeFilter.vue'; // 引入封装的时间筛选组件
|
||||
|
||||
// 分页相关
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
|
||||
// 数据相关
|
||||
const receivableData = ref([]); // 应收数据
|
||||
const payableData = ref([]); // 应付数据
|
||||
const receivableTasks = ref([]); // 回款任务数据
|
||||
const receivableTotal = ref(0); // 回款任务总数
|
||||
const tableLoading = ref(false);
|
||||
const currentTimeParams = ref({}); // 当前时间参数
|
||||
|
||||
// 指标数据
|
||||
const totalIncome = ref(0);
|
||||
const totalExpense = ref(0);
|
||||
const netCashflow = ref(0);
|
||||
const outstandingReceivable = ref(0);
|
||||
const receivableCount = ref(0);
|
||||
|
||||
// 图表实例
|
||||
const trendChart = ref(null);
|
||||
const orderChart = ref(null);
|
||||
const customerChart = ref(null);
|
||||
const supplierChart = ref(null);
|
||||
const trendChartInstance = ref(null);
|
||||
const orderChartInstance = ref(null);
|
||||
const customerChartInstance = ref(null);
|
||||
const supplierChartInstance = ref(null);
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 初始化图表
|
||||
initCharts();
|
||||
});
|
||||
|
||||
// 初始化图表
|
||||
const initCharts = () => {
|
||||
if (trendChart.value) {
|
||||
trendChartInstance.value = echarts.init(trendChart.value);
|
||||
}
|
||||
if (orderChart.value) {
|
||||
orderChartInstance.value = echarts.init(orderChart.value);
|
||||
}
|
||||
if (customerChart.value) {
|
||||
customerChartInstance.value = echarts.init(customerChart.value);
|
||||
}
|
||||
if (supplierChart.value) {
|
||||
supplierChartInstance.value = echarts.init(supplierChart.value);
|
||||
}
|
||||
|
||||
// 监听窗口大小变化,调整图表
|
||||
window.addEventListener('resize', () => {
|
||||
trendChartInstance.value?.resize();
|
||||
orderChartInstance.value?.resize();
|
||||
customerChartInstance.value?.resize();
|
||||
supplierChartInstance.value?.resize();
|
||||
});
|
||||
};
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async (timeParams) => {
|
||||
tableLoading.value = true;
|
||||
currentTimeParams.value = timeParams || currentTimeParams.value;
|
||||
|
||||
try {
|
||||
// 获取应收数据
|
||||
const receivableRes = await listReceivable({ pageSize: 9999, pageNum: 1 });
|
||||
receivableData.value = receivableRes.rows || [];
|
||||
receivableTasks.value = receivableData.value.filter(item => item.status === '未结清');
|
||||
receivableTotal.value = receivableRes.total || 0;
|
||||
|
||||
// 获取应付数据
|
||||
const payableRes = await listPayable({ pageSize: 9999, pageNum: 1 });
|
||||
payableData.value = payableRes.rows || [];
|
||||
|
||||
// 处理数据并更新指标
|
||||
processData();
|
||||
|
||||
// 更新图表
|
||||
updateCharts();
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
ElMessage.error('获取数据失败,请重试');
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理数据计算指标
|
||||
const processData = () => {
|
||||
// 计算总收入(应收金额总和)
|
||||
totalIncome.value = receivableData.value.reduce((sum, item) => {
|
||||
return sum + parseFloat(item.amount || 0);
|
||||
}, 0);
|
||||
|
||||
// 计算总支出(应付金额总和)
|
||||
totalExpense.value = payableData.value.reduce((sum, item) => {
|
||||
return sum + parseFloat(item.amount || 0);
|
||||
}, 0);
|
||||
|
||||
// 计算净现金流
|
||||
netCashflow.value = totalIncome.value - totalExpense.value;
|
||||
|
||||
// 计算未结清应收总额和数量
|
||||
const outstanding = receivableData.value.filter(item => item.status === '未结清');
|
||||
outstandingReceivable.value = outstanding.reduce((sum, item) => {
|
||||
return sum + parseFloat(item.balanceAmount || 0);
|
||||
}, 0);
|
||||
receivableCount.value = outstanding.length;
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
const updateCharts = () => {
|
||||
updateTrendChart();
|
||||
updateOrderChart();
|
||||
updateCustomerChart();
|
||||
updateSupplierChart();
|
||||
};
|
||||
|
||||
// 更新收入支出趋势图
|
||||
const updateTrendChart = () => {
|
||||
// 按时间粒度处理数据
|
||||
const timeGroups = groupDataByTime();
|
||||
|
||||
// 准备图表数据
|
||||
const xAxisData = Object.keys(timeGroups);
|
||||
const incomeData = xAxisData.map(key => timeGroups[key].income);
|
||||
const expenseData = xAxisData.map(key => timeGroups[key].expense);
|
||||
|
||||
// 设置图表配置
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: '{b}<br/>收入: {c0} 元<br/>支出: {c1} 元'
|
||||
},
|
||||
legend: {
|
||||
data: ['收入', '支出'],
|
||||
top: 0
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLabel: {
|
||||
rotate: 45,
|
||||
interval: 0
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '金额 (元)',
|
||||
axisLabel: {
|
||||
formatter: '{value}'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '收入',
|
||||
type: 'bar',
|
||||
data: incomeData,
|
||||
itemStyle: {
|
||||
color: '#4e79a7'
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '支出',
|
||||
type: 'bar',
|
||||
data: expenseData,
|
||||
itemStyle: {
|
||||
color: '#e15759'
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
trendChartInstance.value.setOption(option);
|
||||
};
|
||||
|
||||
// 更新按订单区分的收支图表
|
||||
const updateOrderChart = () => {
|
||||
// 按订单ID合并数据
|
||||
const orderMap = {};
|
||||
|
||||
// 处理应收数据
|
||||
receivableData.value.forEach(item => {
|
||||
if (!orderMap[item.orderId]) {
|
||||
orderMap[item.orderId] = {
|
||||
income: 0,
|
||||
expense: 0
|
||||
};
|
||||
}
|
||||
orderMap[item.orderId].income += parseFloat(item.amount || 0);
|
||||
});
|
||||
|
||||
// 处理应付数据
|
||||
payableData.value.forEach(item => {
|
||||
if (!orderMap[item.orderId]) {
|
||||
orderMap[item.orderId] = {
|
||||
income: 0,
|
||||
expense: 0
|
||||
};
|
||||
}
|
||||
orderMap[item.orderId].expense += parseFloat(item.amount || 0);
|
||||
});
|
||||
|
||||
// 转换为图表数据
|
||||
const orderList = Object.entries(orderMap)
|
||||
.map(([orderId, data]) => ({
|
||||
orderId,
|
||||
income: data.income,
|
||||
expense: data.expense,
|
||||
net: data.income - data.expense
|
||||
}))
|
||||
.sort((a, b) => b.net - a.net)
|
||||
.slice(0, 10); // 只展示前10个订单
|
||||
|
||||
const xAxisData = orderList.map(item => `订单 ${item.orderId.slice(-6)}`);
|
||||
const incomeData = orderList.map(item => item.income);
|
||||
const expenseData = orderList.map(item => item.expense);
|
||||
|
||||
// 设置图表配置
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['收入', '支出'],
|
||||
top: 0
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLabel: {
|
||||
rotate: 45,
|
||||
interval: 0
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '金额 (元)'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '收入',
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: incomeData,
|
||||
itemStyle: {
|
||||
color: '#4e79a7'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '支出',
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: expenseData.map(v => -v), // 支出用负值表示
|
||||
itemStyle: {
|
||||
color: '#e15759'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
orderChartInstance.value.setOption(option);
|
||||
};
|
||||
|
||||
// 更新按客户区分的收入图表
|
||||
const updateCustomerChart = () => {
|
||||
// 按客户分组计算收入
|
||||
const customerMap = {};
|
||||
|
||||
receivableData.value.forEach(item => {
|
||||
if (!customerMap[item.customerId]) {
|
||||
customerMap[item.customerId] = {
|
||||
name: item.customerName,
|
||||
amount: 0
|
||||
};
|
||||
}
|
||||
customerMap[item.customerId].amount += parseFloat(item.amount || 0);
|
||||
});
|
||||
|
||||
// 转换为图表数据并排序
|
||||
const customerList = Object.values(customerMap)
|
||||
.sort((a, b) => b.amount - a.amount)
|
||||
.slice(0, 10); // 只展示前10个客户
|
||||
|
||||
// 设置图表配置
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 10,
|
||||
top: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '客户收入',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: customerList.map(item => ({
|
||||
name: item.name,
|
||||
value: item.amount
|
||||
}))
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
customerChartInstance.value.setOption(option);
|
||||
};
|
||||
|
||||
// 更新按供应商区分的支出图表
|
||||
const updateSupplierChart = () => {
|
||||
// 按供应商分组计算支出
|
||||
const supplierMap = {};
|
||||
|
||||
payableData.value.forEach(item => {
|
||||
if (!supplierMap[item.supplierId]) {
|
||||
supplierMap[item.supplierId] = {
|
||||
name: item.supplierName,
|
||||
amount: 0
|
||||
};
|
||||
}
|
||||
supplierMap[item.supplierId].amount += parseFloat(item.amount || 0);
|
||||
});
|
||||
|
||||
// 转换为图表数据并排序
|
||||
const supplierList = Object.values(supplierMap)
|
||||
.sort((a, b) => b.amount - a.amount)
|
||||
.slice(0, 10); // 只展示前10个供应商
|
||||
|
||||
// 设置图表配置
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '金额 (元)'
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: supplierList.map(item => item.name),
|
||||
axisLabel: {
|
||||
interval: 0
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '支出金额',
|
||||
type: 'bar',
|
||||
data: supplierList.map(item => item.amount),
|
||||
itemStyle: {
|
||||
color: '#e15759'
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
supplierChartInstance.value.setOption(option);
|
||||
};
|
||||
|
||||
// 按时间粒度分组数据
|
||||
const groupDataByTime = () => {
|
||||
const timeGroups = {};
|
||||
const granularity = currentTimeParams.value.timeGranularity || 'month';
|
||||
|
||||
// 处理应收数据
|
||||
receivableData.value.forEach(item => {
|
||||
const date = new Date(item.dueDate);
|
||||
const timeKey = getTimeKey(date, granularity);
|
||||
|
||||
if (!timeGroups[timeKey]) {
|
||||
timeGroups[timeKey] = {
|
||||
income: 0,
|
||||
expense: 0
|
||||
};
|
||||
}
|
||||
|
||||
timeGroups[timeKey].income += parseFloat(item.amount || 0);
|
||||
});
|
||||
|
||||
// 处理应付数据
|
||||
payableData.value.forEach(item => {
|
||||
const date = new Date(item.dueDate);
|
||||
const timeKey = getTimeKey(date, granularity);
|
||||
|
||||
if (!timeGroups[timeKey]) {
|
||||
timeGroups[timeKey] = {
|
||||
income: 0,
|
||||
expense: 0
|
||||
};
|
||||
}
|
||||
|
||||
timeGroups[timeKey].expense += parseFloat(item.amount || 0);
|
||||
});
|
||||
|
||||
// 排序时间分组
|
||||
return Object.keys(timeGroups).sort().reduce((obj, key) => {
|
||||
obj[key] = timeGroups[key];
|
||||
return obj;
|
||||
}, {});
|
||||
};
|
||||
|
||||
// 根据时间粒度生成时间键
|
||||
const getTimeKey = (date, granularity) => {
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
|
||||
if (granularity === 'year') {
|
||||
return `${year}`;
|
||||
} else if (granularity === 'month') {
|
||||
return `${year}-${month.toString().padStart(2, '0')}`;
|
||||
} else {
|
||||
// 按周分组 (ISO周数)
|
||||
const week = getWeekNumber(date);
|
||||
return `${year}-W${week.toString().padStart(2, '0')}`;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取ISO周数
|
||||
const getWeekNumber = (date) => {
|
||||
const temp = new Date(date.getTime());
|
||||
temp.setHours(0, 0, 0, 0);
|
||||
// 把周一作为一周的第一天
|
||||
temp.setDate(temp.getDate() + 3 - (temp.getDay() + 6) % 7);
|
||||
const firstDay = new Date(temp.getFullYear(), 0, 4);
|
||||
return Math.round((temp - firstDay) / (7 * 24 * 60 * 60 * 1000)) + 1;
|
||||
};
|
||||
|
||||
// 格式化金额
|
||||
const formatCurrency = (row, column, value) => {
|
||||
if (!value) return '0.00';
|
||||
return parseFloat(value).toFixed(2);
|
||||
};
|
||||
|
||||
// 事件处理函数
|
||||
const handleDateChange = (timeParams) => {
|
||||
currentTimeParams.value = timeParams;
|
||||
};
|
||||
|
||||
const handleFilterReset = () => {
|
||||
currentPage.value = 1;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.finance-analysis-page {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
color: #1f2d3d;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #8392a5;
|
||||
}
|
||||
}
|
||||
|
||||
.content-container {
|
||||
.time-filter {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.indicator-cards {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.indicator-card {
|
||||
height: 100%;
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
color: #8392a5;
|
||||
font-size: 14px;
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: #8392a5;
|
||||
|
||||
.rise {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.drop {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -30px;
|
||||
top: -30px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
.total-income {
|
||||
&::after {
|
||||
background-color: #4e79a7;
|
||||
}
|
||||
}
|
||||
|
||||
.total-expense {
|
||||
&::after {
|
||||
background-color: #e15759;
|
||||
}
|
||||
}
|
||||
|
||||
.net-cashflow {
|
||||
&::after {
|
||||
background-color: #59a14f;
|
||||
}
|
||||
}
|
||||
|
||||
.outstanding-receivable {
|
||||
&::after {
|
||||
background-color: #9c755f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
height: 100%;
|
||||
|
||||
.chart-header {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #1f2d3d;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
.table-header {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #1f2d3d;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式调整
|
||||
@media (max-width: 1200px) {
|
||||
.charts-container {
|
||||
.chart-wrapper {
|
||||
height: 350px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.indicator-cards {
|
||||
.el-col {
|
||||
&:nth-child(1), &:nth-child(2) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
.chart-row {
|
||||
.el-col {
|
||||
&:nth-child(1) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.indicator-cards {
|
||||
.el-col {
|
||||
&:nth-child(3) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
.chart-wrapper {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
552
gear-ui3/src/views/info/requirement/index.vue
Normal file
552
gear-ui3/src/views/info/requirement/index.vue
Normal file
@@ -0,0 +1,552 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item label="需求标题" prop="title">
|
||||
<el-input
|
||||
v-model="queryParams.title"
|
||||
placeholder="请输入需求标题"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="需求方" prop="requesterId">
|
||||
<el-select
|
||||
v-model="queryParams.requesterId"
|
||||
placeholder="请选择需求方"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 180px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in ownerOptions"
|
||||
:key="item.userId"
|
||||
:label="item.nickName"
|
||||
:value="item.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="ownerId">
|
||||
<el-select
|
||||
v-model="queryParams.ownerId"
|
||||
placeholder="请选择负责人"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 180px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in ownerOptions"
|
||||
:key="item.userId"
|
||||
:label="item.nickName"
|
||||
:value="item.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联订单" prop="orderId">
|
||||
<el-input
|
||||
v-model="queryParams.orderId"
|
||||
placeholder="请输入关联订单"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="需求描述" prop="description">
|
||||
<el-input
|
||||
v-model="queryParams.description"
|
||||
placeholder="请输入需求描述"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="截止日期" prop="deadline">
|
||||
<el-date-picker clearable
|
||||
v-model="queryParams.deadline"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="请选择截止日期">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="完成情况" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择完成情况" clearable style="width: 120px">
|
||||
<el-option :value="undefined" label="全部" />
|
||||
<el-option :value="0" label="未完成" />
|
||||
<el-option :value="1" label="已完成" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="Plus"
|
||||
size="mini"
|
||||
@click="handleAdd"
|
||||
>新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
icon="Edit"
|
||||
size="mini"
|
||||
:disabled="single"
|
||||
@click="handleUpdate"
|
||||
>修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="Delete"
|
||||
size="mini"
|
||||
:disabled="multiple"
|
||||
@click="handleDelete"
|
||||
>删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="Download"
|
||||
size="mini"
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<!-- <el-col :span="2">
|
||||
<el-button
|
||||
:type="onlyOwnerMe ? 'primary' : 'default'"
|
||||
icon="User"
|
||||
size="mini"
|
||||
@click="toggleOnlyOwnerMe"
|
||||
>只看发布给我的</el-button>
|
||||
</el-col>
|
||||
<el-col :span="2">
|
||||
<el-button
|
||||
:type="onlyRequesterMe ? 'primary' : 'default'"
|
||||
icon="Solid"
|
||||
size="mini"
|
||||
@click="toggleOnlyRequesterMe"
|
||||
>只看我发布的</el-button>
|
||||
</el-col> -->
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="requirementsList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="需求标题" align="center" prop="title" />
|
||||
<el-table-column label="需求方" align="center" prop="requesterNickName" />
|
||||
<el-table-column label="负责人" align="center" prop="ownerNickName" />
|
||||
<el-table-column label="关联订单" align="center" prop="orderCode" />
|
||||
<el-table-column label="需求描述" align="center" prop="description" />
|
||||
<el-table-column label="截止日期" align="center" prop="deadline" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.deadline, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余时间" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.status!==1"
|
||||
:type="getRemainTagType(scope.row.deadline)"
|
||||
:style="getRemainTagStyle(scope.row.deadline)"
|
||||
>
|
||||
{{ getRemainText(scope.row.deadline) }}
|
||||
</el-tag>
|
||||
<el-tag v-else>
|
||||
完成
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预览图" align="center" prop="accessory">
|
||||
<template #default="scope">
|
||||
<image-preview :src="scope.row.image" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="需求状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.status == 0" type="info">未完成</el-tag>
|
||||
<el-tag v-else-if="scope.row.status == 1" type="success">已完成</el-tag>
|
||||
<el-tag v-else type="warning">未知</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-button size="mini" type="text" icon="Check" @click="handleComplete(scope.row)" v-if="scope.row.status!==1">完成</el-button>
|
||||
<el-button size="mini" type="text" icon="Edit" @click="handleUpdate(scope.row)" v-if="scope.row.status!==1">修改</el-button>
|
||||
<el-button size="mini" type="text" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
|
||||
<el-button size="mini" type="text" icon="View" @click="showDetail(scope.row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<!-- 添加或修改OA 需求对话框 -->
|
||||
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="需求标题" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入需求标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="ownerId">
|
||||
<el-select
|
||||
v-model="form.ownerId"
|
||||
placeholder="请选择负责人"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in ownerOptions"
|
||||
:key="item.userId"
|
||||
:label="item.nickName"
|
||||
:value="item.userId"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联订单" prop="orderId">
|
||||
<el-input v-model="form.orderId" placeholder="请输入关联订单" />
|
||||
</el-form-item>
|
||||
<el-form-item label="需求描述" prop="description">
|
||||
<el-input v-model="form.description" type="textarea" placeholder="请输入需求描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="截止日期" prop="deadline">
|
||||
<el-date-picker clearable
|
||||
v-model="form.deadline"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择截止日期">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="预览图" prop="image">
|
||||
<image-upload v-model="form.image" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" prop="accessory">
|
||||
<file-upload v-model="form.accessory" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 需求详情对话框 -->
|
||||
<el-dialog title="需求详情" v-model="detailDialog" width="600px" append-to-body>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="需求标题">{{ detailRow.title }}</el-descriptions-item>
|
||||
<el-descriptions-item label="需求方">{{ detailRow.requesterNickName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">{{ detailRow.ownerNickName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="关联项目">{{ detailRow.projectName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="需求描述">{{ detailRow.description }}</el-descriptions-item>
|
||||
<el-descriptions-item label="截止日期">{{ detailRow.deadline }}</el-descriptions-item>
|
||||
<el-descriptions-item label="剩余时间">{{ getRemainText(detailRow.deadline) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="需求状态">
|
||||
<el-tag :type="getStatusTagType(detailRow.status)">
|
||||
{{ detailRow.status == 1 ? '已完成' : '未完成' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">{{ detailRow.remark }}</el-descriptions-item>
|
||||
<el-descriptions-item label="附件">
|
||||
<!-- <FilePreview :value="detailRow.accessory" /> -->
|
||||
<file-upload v-model="detailRow.accessory" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="detailDialog = false">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addRequirements, delRequirements, getRequirements, listRequirements, updateRequirements } from "@/api/oa/requirement";
|
||||
import { listUser } from "@/api/system/user";
|
||||
import useUserStore from "@/store/modules/user";
|
||||
|
||||
export default {
|
||||
name: "Requirements",
|
||||
setup() {
|
||||
const userStore = useUserStore();
|
||||
console.log(userStore);
|
||||
return {
|
||||
userStore
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 按钮loading
|
||||
buttonLoading: false,
|
||||
// 遮罩层
|
||||
loading: true,
|
||||
// 选中数组
|
||||
ids: [],
|
||||
// 非单个禁用
|
||||
single: true,
|
||||
// 非多个禁用
|
||||
multiple: true,
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
// 总条数
|
||||
total: 0,
|
||||
// OA 需求表格数据
|
||||
requirementsList: [],
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 负责人选项
|
||||
ownerOptions: [],
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
title: undefined,
|
||||
requesterId: undefined,
|
||||
ownerId: undefined,
|
||||
orderId: undefined,
|
||||
description: undefined,
|
||||
deadline: undefined,
|
||||
status: 0,
|
||||
accessory: undefined,
|
||||
},
|
||||
// 表单参数
|
||||
form: {},
|
||||
// 表单校验
|
||||
rules: {
|
||||
title: [
|
||||
{ required: true, message: "需求标题不能为空", trigger: "blur" }
|
||||
],
|
||||
ownerId: [
|
||||
{ required: true, message: "请选择负责人", trigger: "change" }
|
||||
],
|
||||
description: [
|
||||
{ required: true, message: "需求描述不能为空", trigger: "blur" }
|
||||
],
|
||||
deadline: [
|
||||
{ required: true, message: "请选择截止日期", trigger: "change" }
|
||||
]
|
||||
},
|
||||
detailDialog: false,
|
||||
detailRow: {},
|
||||
onlyOwnerMe: false,
|
||||
onlyRequesterMe: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
this.getUsers();
|
||||
},
|
||||
methods: {
|
||||
/** 获取用户列表 */
|
||||
getUsers() {
|
||||
listUser({}).then(response => {
|
||||
this.ownerOptions = response.rows;
|
||||
});
|
||||
},
|
||||
/** 查询OA 需求列表 */
|
||||
getList() {
|
||||
this.loading = true;
|
||||
listRequirements(this.queryParams).then(response => {
|
||||
this.requirementsList = response.rows;
|
||||
this.total = response.total;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
// 取消按钮
|
||||
cancel() {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
// 表单重置
|
||||
reset() {
|
||||
this.form = {
|
||||
requirementId: undefined,
|
||||
title: undefined,
|
||||
requesterId: undefined,
|
||||
ownerId: undefined,
|
||||
orderId: undefined,
|
||||
description: undefined,
|
||||
deadline: undefined,
|
||||
status: 0,
|
||||
remark: undefined,
|
||||
accessory: undefined,
|
||||
createBy: undefined,
|
||||
createTime: undefined,
|
||||
updateBy: undefined,
|
||||
updateTime: undefined,
|
||||
delFlag: undefined,
|
||||
image: undefined,
|
||||
};
|
||||
this.resetForm("form");
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1;
|
||||
this.getList();
|
||||
},
|
||||
/** 重置按钮操作 */
|
||||
resetQuery() {
|
||||
this.resetForm("queryForm");
|
||||
this.handleQuery();
|
||||
},
|
||||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.requirementId)
|
||||
this.single = selection.length!==1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
handleAdd() {
|
||||
this.reset();
|
||||
this.form.requesterId = this.userStore.id;
|
||||
this.open = true;
|
||||
this.title = "添加OA 需求";
|
||||
},
|
||||
handleComplete(row){
|
||||
this.$modal.confirm('确认要完成'+row.title+'的需求内容吗?').then(() => {
|
||||
row.status = 1;
|
||||
updateRequirements(row).then(response => {
|
||||
this.$modal.msgSuccess("操作成功")
|
||||
this.getList()
|
||||
})
|
||||
})
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate(row) {
|
||||
this.loading = true;
|
||||
this.reset();
|
||||
const requirementId = row.requirementId || this.ids
|
||||
getRequirements(requirementId).then(response => {
|
||||
this.loading = false;
|
||||
this.form = response.data;
|
||||
this.open = true;
|
||||
this.title = "修改OA 需求";
|
||||
});
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
this.buttonLoading = true;
|
||||
if (this.form.requirementId != null) {
|
||||
updateRequirements(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
} else {
|
||||
addRequirements(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const requirementIds = row.requirementId || this.ids;
|
||||
this.$modal.confirm('是否确认删除OA 需求编号为"' + requirementIds + '"的数据项?').then(() => {
|
||||
this.loading = true;
|
||||
return delRequirements(requirementIds);
|
||||
}).then(() => {
|
||||
this.loading = false;
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
/** 导出按钮操作 */
|
||||
handleExport() {
|
||||
this.download('oa/requirements/export', {
|
||||
...this.queryParams
|
||||
}, `requirements_${new Date().getTime()}.xlsx`)
|
||||
},
|
||||
getRemainText(deadline) {
|
||||
if (!deadline) return '无截止日期';
|
||||
const now = new Date();
|
||||
const end = new Date(deadline);
|
||||
now.setHours(0,0,0,0);
|
||||
end.setHours(0,0,0,0);
|
||||
const diff = Math.floor((end - now) / (1000 * 60 * 60 * 24));
|
||||
if (diff < 0) return '已超时';
|
||||
if (diff < 3) return `剩余${diff}天`;
|
||||
if (diff < 5) return `剩余${diff}天`;
|
||||
return `剩余${diff}天`;
|
||||
},
|
||||
getRemainTagType(deadline) {
|
||||
if (!deadline) return 'info';
|
||||
const now = new Date();
|
||||
const end = new Date(deadline);
|
||||
now.setHours(0,0,0,0);
|
||||
end.setHours(0,0,0,0);
|
||||
const diff = Math.floor((end - now) / (1000 * 60 * 60 * 24));
|
||||
if (diff < 0) return 'danger'; // 深红
|
||||
if (diff < 3) return 'danger'; // 浅红
|
||||
if (diff < 5) return 'warning'; // 橙黄色
|
||||
return 'success'; // 充裕
|
||||
},
|
||||
getStatusTagType(status) {
|
||||
return status == 1 ? 'success' : 'info';
|
||||
},
|
||||
getRemainTagStyle(deadline) {
|
||||
if (!deadline) return '';
|
||||
const now = new Date();
|
||||
const end = new Date(deadline);
|
||||
now.setHours(0,0,0,0);
|
||||
end.setHours(0,0,0,0);
|
||||
const diff = Math.floor((end - now) / (1000 * 60 * 60 * 24));
|
||||
if (diff < 0) return 'background:#b71c1c;color:#fff;border:none;'; // 深红
|
||||
if (diff < 3) return 'background:#ffcccc;color:#b71c1c;border:none;'; // 浅红
|
||||
return '';
|
||||
},
|
||||
showDetail(row) {
|
||||
this.detailRow = row;
|
||||
this.detailDialog = true;
|
||||
},
|
||||
toggleOnlyOwnerMe() {
|
||||
if (this.onlyOwnerMe) {
|
||||
this.onlyOwnerMe = false;
|
||||
this.queryParams.ownerId = undefined;
|
||||
} else {
|
||||
this.onlyOwnerMe = true;
|
||||
this.onlyRequesterMe = false;
|
||||
this.queryParams.ownerId = this.userStore.id;
|
||||
this.queryParams.requesterId = undefined;
|
||||
}
|
||||
this.handleQuery();
|
||||
},
|
||||
toggleOnlyRequesterMe() {
|
||||
if (this.onlyRequesterMe) {
|
||||
this.onlyRequesterMe = false;
|
||||
this.queryParams.requesterId = undefined;
|
||||
} else {
|
||||
this.onlyRequesterMe = true;
|
||||
this.onlyOwnerMe = false;
|
||||
this.queryParams.requesterId = this.userStore.id;
|
||||
this.queryParams.ownerId = undefined;
|
||||
}
|
||||
this.handleQuery();
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -73,7 +73,7 @@
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="customerList" @selection-change="handleSelectionChange" :customColumns="customColumns">
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="Download" size="small" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="journalList" @selection-change="handleSelectionChange">
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="payableList" @selection-change="handleSelectionChange">
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="receivableList" @selection-change="handleSelectionChange">
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="payableList" @selection-change="handleSelectionChange">
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="receivableList" @selection-change="handleSelectionChange">
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="stockLogList" @selection-change="handleSelectionChange">
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
|
||||
<el-table v-loading="loading" :data="stockList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="stockIoList" @selection-change="handleSelectionChange">
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
<el-table v-loading="loading" :data="manualList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="Download" size="mini" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="stockIoList" @selection-change="handleSelectionChange">
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
@click="toggleExpandAll"
|
||||
>展开/折叠</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table
|
||||
|
||||
Reference in New Issue
Block a user