add 项目利润结算

pull/853/head
wersd 2025-08-28 00:12:36 +08:00
parent ad6d91f78c
commit 6b0dd8728b
6 changed files with 584 additions and 92 deletions

View File

@ -3,27 +3,36 @@ import request from '@/config/axios'
// 项目利润结算 VO
export interface ProjectSettlementVO {
id: number; // 结算记录ID
projectId: number; // 关联项目ID
projectId: number; // 项目ID
projectName: string; // 项目名称
piles: string; // 枪数
settlementPeriod: string; // 结算周期(如:2023-Q1)
period: number; // 总期数
settlementDate: Date; // 结算日期
actualRevenue: number; // 实际营收
governmentSubsidy: number; // 政府补贴收入
otherIncome: number; // 其他项目收入
totalIncome: number; // 总收入
incomeRemark: string; // 收入备注
electricityCost: number; // 电费成本
venueRent: number; // 场地租金
venueDeposit: number; // 项目押金
projectPromotion: number; // 项目推介费
equipmentUpgrade: number; // 设备更新购置费
managementCost: number; // 管理费用
taxCost: number; // 税收
startDate: string | Date | number[]; // 结算开始时间
endDate: string | Date; // 结算结束时间
settlementDate: string | Date | number[]; // 结算日期
previousLossCarryover: number; // 上期亏损转结
grossProfit: number; // 毛利润
netProfit: number; // 净利润
expenseRemark: string; // 支出备注
settlementStatus: number; // 结算状态
proofAttachment: string; // 结算凭证附件
remark: string; // 结算备注
actualRevenue?: number; // 实际营收
governmentSubsidy?: number; // 政府补贴收入
otherIncome?: number; // 其他项目收入
totalIncome?: number; // 总收入
incomeRemark?: string; // 收入备注
electricityCost?: number; // 电费成本
venueRent?: number; // 场地租金
venueDeposit?: number; // 项目押金
projectPromotion?: number; // 项目推介费
equipmentUpgrade?: number; // 设备更新购置费
managementCost?: number; // 管理费用
taxCost?: number; // 税收
otherExpenses?: number; // 其他支出
totalExpenses?: number; // 总支出
shareAmount?: number; // 分成金额
expenseRemark?: string; // 支出备注
proofAttachment?: string; // 结算凭证附件
remark?: string; // 结算备注
}
// 查询项目利润结算分页
@ -55,3 +64,8 @@ export const deleteProjectSettlement = async (id: number) => {
export const exportProjectSettlement = async (params: any) => {
return await request.download({ url: `/crm/project-settlement/export-excel`, params })
}
// 导出项目利润结算明细 Excel
export const exportProjectSettlementDetail = async (params: any) => {
return await request.download({ url: `/crm/receivable/export-settlement-excel`, params })
}

View File

@ -0,0 +1,399 @@
<template>
<div class="el-quarter-picker">
<el-popover
v-model:visible="visible"
:disabled="!canPopover"
:tabindex="null"
placement="bottom-start"
transition="el-zoom-in-top"
width="300"
trigger="click">
<div class="el-date-picker el-quarter-popover-content">
<div class="el-picker-panel__body">
<div class="el-date-picker__header el-date-picker__header--bordered" style="margin:0px; line-height:30px">
<button
type="button"
@click="clickLast"
aria-label="前一年"
class="el-picker-panel__icon-btn el-date-picker__prev-btn el-icon-d-arrow-left"></button>
<span role="button" class="el-date-picker__header-label" @click="clickYear">{{ title }}</span>
<button
type="button"
@click="clickNext"
aria-label="后一年"
class="el-picker-panel__icon-btn el-date-picker__next-btn el-icon-d-arrow-right"></button>
</div>
<div class="el-picker-panel__content" style="margin:0px; width:100%">
<table class="el-month-table" style="">
<tbody>
<tr v-for="line in lineCount" :key="line">
<td v-for="index in (line * 4 <= viewList.length ? 4 : viewList.length - (line - 1) * 4)" :key="index" :class="{ today: viewList[(line - 1) * 4 + index - 1].current, current: viewList[(line - 1) * 4 + index - 1].active }">
<div><a class="cell" @click="clickItem(viewList[(line - 1) * 4 + index - 1])">{{ viewList[(line - 1) * 4 + index - 1].label }}</a></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<template #reference>
<el-input
@change="changeText"
@mouseenter="mouseEnter"
@mouseleave="mouseLeave"
:placeholder="placeholder"
v-model="text"
:size="size"
:readonly="!canEdit"
:disabled="disabled">
<template #prefix>
<i class="el-input__icon el-icon-date"></i>
</template>
<template #suffix>
<i class="el-input__icon el-icon-circle-close" v-show="showClear" style="cursor:pointer" @click.stop="clear"></i>
</template>
</el-input>
</template>
</el-popover>
</div>
</template>
<script>
export default {
name: 'ElQuarterPicker',
props: {
placeholder: {
type: String,
default: ''
},
size: {
type: String,
default: ''
},
readonly: {
type: Boolean,
default: false
},
clearable: {
type: Boolean,
default: true
},
editable: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
},
format: {
type: String,
default: 'yyyy年第Q季度'
},
valueFormat: {
type: String,
default: 'yyyy-qq'
},
modelValue: {
type: String,
default: ''
}
},
data() {
return {
visible: false,
showClear: false, //
canEdit: true, //
canPopover: true, //
text: '', //
viewType: 1, // 12
viewYear: 0, //
viewList: [], //
lineCount: 0, //
title: '', //
data: [0, 0] // -
}
},
watch: {
modelValue(val) {
this.changeValue(val)
},
readonly(val) {
this.canEdit = !val && this.editable
this.canPopover = !this.disabled && !val
},
editable(val) {
this.canEdit = !this.readonly && val
},
disabled(val) {
this.canPopover = !val && !this.readonly
}
},
mounted() {
this.changeValue(this.modelValue)
//
this.canEdit = !this.readonly && this.editable
this.canPopover = !this.disabled && !this.readonly
// ()
document.onkeydown = (event) => {
if (this.visible) {
const data = [this.data[0], this.data[1]]
if (data[0] < 1 || data[1] < 1) {
//
const curDate = new Date()
data[0] = curDate.getFullYear()
data[1] = parseInt(curDate.getMonth() / 3) + 1
}
if (event.code === 'ArrowLeft') {
//
if (data[1] === 1) {
data[0] = data[0] - 1
data[1] = 4
} else {
data[1] = data[1] - 1
}
} else if (event.code === 'ArrowRight') {
//
if (data[1] === 4) {
data[0] = data[0] + 1
data[1] = 1
} else {
data[1] = data[1] + 1
}
} else if (event.code === 'ArrowUp') {
//
data[0] = data[0] - 1
} else if (event.code === 'ArrowDown') {
//
data[0] = data[0] + 1
} else {
return
}
//
if (data[0] < 1000 || data[0] > 9999) {
return
}
this.data = data
this.viewType = 1
this.viewYear = data[0]
this.$emit('update:modelValue', this.formatTo(data, this.valueFormat))
}
}
},
unmounted() {
document.onkeydown = null
},
methods: {
//
changeText() {
if (this.checkFormat(this.format, this.text)) {
//
this.formatFrom(this.text, this.format)
this.$emit('update:modelValue', this.formatTo(this.data, this.valueFormat))
} else {
//
if (this.data[0] < 1 || this.data[1] < 1) {
this.text = ''
} else {
this.text = this.formatTo(this.data, this.format)
}
}
this.visible = false
},
//
mouseEnter() {
if (!this.disabled && !this.readonly && this.clearable && this.text !== '') {
this.showClear = true
}
},
//
mouseLeave() {
if (!this.disabled && this.clearable) {
this.showClear = false
}
},
//
clear() {
this.showClear = false
this.visible = false
this.$emit('update:modelValue', '')
},
//
changeValue(val) {
this.viewType = 1
if (val) {
//
this.formatFrom(val, this.valueFormat)
this.text = this.formatTo(this.data, this.format)
this.viewYear = this.data[0]
} else {
this.text = ''
this.data = [0, 0]
this.viewYear = new Date().getFullYear()
}
this.initView()
},
//
initView() {
const list = []
const curDate = new Date()
const curYear = curDate.getFullYear()
const curQuarter = parseInt(curDate.getMonth() / 3) + 1
if (this.viewType === 1) {
let index = 0
for (const i of '一二三四') {
index++
const item = { label: '第' + i + '季度', year: this.viewYear, quarter: index, current: false, active: false }
if (this.viewYear === curYear && index === curQuarter) {
item.current = true
} else if (this.viewYear === this.data[0] && index === this.data[1]) {
item.active = true
}
list.push(item)
}
this.title = this.viewYear + ' 年'
} else {
const start = parseInt(this.viewYear / 10) * 10
this.viewYear = start
for (let i = 0; i < 10; i++) {
const year = start + i
const item = { label: year + '', year: year, current: false, active: false }
if (year === curYear) {
item.current = true
} else if (year === this.data[0]) {
item.active = true
}
list.push(item)
}
this.title = start + ' 年 - ' + (start + 9) + ' 年'
}
this.viewList = list
this.lineCount = parseInt(list.length / 4)
if (list.length % 4 > 0) {
this.lineCount++
}
},
//
checkFormat(pattern, val) {
//
let text = ''
for (const char of pattern) {
const dict = '\\^$.+?*[]{}!'
if (dict.indexOf(char) === -1) {
text += char
} else {
text += '\\' + char
}
}
text = text.replace('yyyy', '[1-9]\\d{3}')
text = text.replace('qq', '0[1-4]')
text = text.replace('q', '[1-4]')
text = text.replace('Q', '[一二三四]')
text = '^' + text + '$'
const patt = new RegExp(text)
return patt.test(val)
},
//
formatTo(data, pattern) {
let text = pattern.replace('yyyy', '' + data[0])
text = text.replace('qq', '0' + data[1])
text = text.replace('q', '' + data[1])
text = text.replace('Q', '一二三四'.substr(data[1] - 1, 1))
return text
},
//
formatFrom(str, pattern) {
const year = this.findText(str, pattern, 'yyyy')
const quarter = this.findText(str, pattern, ['qq', 'q', 'Q'])
this.data = [year, quarter]
},
//
findText(str, pattern, find) {
if (find instanceof Array) {
for (const f of find) {
const val = this.findText(str, pattern, f)
if (val !== -1) {
return val
}
}
return -1
}
const index = pattern.indexOf(find)
if (index === -1) {
return index
}
const val = str.substr(index, find.length)
if (find === 'Q') {
return '一二三四'.indexOf(val) + 1
} else {
return parseInt(val)
}
},
//
clickYear() {
if (this.viewType !== 1) {
return
}
//
this.viewType = 2
this.initView()
},
//
clickItem(item) {
if (this.viewType === 1) {
//
this.$emit('update:modelValue', this.formatTo([item.year, item.quarter], this.valueFormat))
this.visible = false
} else {
//
this.viewType = 1
this.viewYear = item.year
this.initView()
}
},
//
clickLast() {
if (this.viewYear > 1000) {
if (this.viewType === 1) {
this.viewYear--
this.initView()
} else {
this.viewYear = this.viewYear - 10
this.initView()
}
}
},
//
clickNext() {
if (this.viewYear < 9999) {
if (this.viewType === 1) {
this.viewYear++
this.initView()
} else {
this.viewYear = this.viewYear + 10
this.initView()
}
}
}
}
}
</script>
<style>
.el-quarter-picker {
width: 220px;
display: inline-block;
}
.el-quarter-popover-content {
max-width: 100%;
word-break: break-all;
box-sizing: border-box;
}
</style>

View File

@ -599,6 +599,17 @@ const remainingRouter: AppRouteRecordRaw[] = [
activeMenu: '/crm/station-site'
},
component: () => import('@/views/crm/stationsite/detail/index.vue')
},
{
path: 'project/settlement/:id',
name: 'CrmProjectSettlementDetail',
meta: {
title: '利润结算详情',
noCache: true,
hidden: true,
activeMenu: '/crm/project/settlement'
},
component: () => import('@/views/crm/project/settlement/detail/index.vue')
}
]
},

View File

@ -8,7 +8,6 @@
v-loading="formLoading"
>
<el-row>
{{ formData }}
<el-col :span="12">
<el-form-item label="项目名称" prop="deptId">
<el-select

View File

@ -1,5 +1,5 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<Dialog :title="dialogTitle" v-model="dialogVisible" width="40%">
<el-form
ref="formRef"
:model="formData"
@ -7,47 +7,79 @@
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="关联项目ID" prop="projectId">
<el-select
v-model="formData.projectId"
filterable
placeholder="请选择关联项目"
class="!w-100%"
>
<el-option
v-for="item in projectOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="结算周期(如:2023-Q1)" prop="settlementPeriod">
<el-input v-model="formData.settlementPeriod" placeholder="请输入结算周期(如:2023-Q1)" />
</el-form-item>
<el-form-item label="总期数" prop="period">
<el-input v-model.number="formData.period" placeholder="请输入总期数" type="number" />
</el-form-item>
<el-form-item label="结算日期" prop="settlementDate">
<el-date-picker
v-model="formData.settlementDate"
type="date"
value-format="x"
placeholder="选择结算日期"
/>
</el-form-item>
<el-form-item label="收入备注" prop="incomeRemark">
<el-input v-model="formData.incomeRemark" placeholder="请输入收入备注" />
</el-form-item>
<el-form-item label="支出备注" prop="expenseRemark">
<el-input v-model="formData.expenseRemark" placeholder="请输入支出备注" />
</el-form-item>
<el-form-item label="结算凭证附件" prop="proofAttachment">
<UploadFile v-model="formData.proofAttachment" />
</el-form-item>
<el-form-item label="结算备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入结算备注" />
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="项目" prop="projectId">
<el-select
v-model="formData.projectId"
filterable
placeholder="请选择关联项目"
class="!w-100%"
>
<el-option
v-for="item in projectOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结算周期" prop="settlementPeriod">
<el-quarter-picker v-model="formData.settlementPeriod" placeholder="选择季度" value-format="yyyy-q" clearable style="width: 100%"
popper-class="full-width-datepicker" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item label="结算日期" prop="settlementDate">
<el-date-picker
v-model="formData.settlementDate"
type="date"
placeholder="选择结算日期"
style="width: 100%"
popper-class="full-width-datepicker"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="结算开始时间" prop="startDate">
<el-date-picker
v-model="formData.startDate"
type="date"
placeholder="选择结算开始时间"
style="width: 100%"
popper-class="full-width-datepicker"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="结算结束时间" prop="endDate">
<el-date-picker
v-model="formData.endDate"
type="date"
placeholder="选择结算结束时间"
style="width: 100%"
popper-class="full-width-datepicker"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="收入备注" prop="incomeRemark">
<el-input v-model="formData.incomeRemark" placeholder="请输入收入备注" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="支出备注" prop="expenseRemark">
<el-input v-model="formData.expenseRemark" placeholder="请输入支出备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
@ -58,6 +90,7 @@
<script setup lang="ts">
import * as ProjectSettlementApi from '@/api/crm/project/settlement'
import * as ProjectApi from '@/api/crm/project'
import ElQuarterPicker from '@/components/ElQuarterPicker/index.vue'
/** 项目利润结算 表单 */
defineOptions({ name: 'ProjectSettlementForm' })
@ -74,6 +107,8 @@ const formData = ref({
projectId: undefined,
settlementPeriod: undefined,
period: undefined,
startDate: undefined,
endDate: undefined,
settlementDate: undefined,
incomeRemark: undefined,
expenseRemark: undefined,
@ -81,8 +116,8 @@ const formData = ref({
remark: undefined,
})
const formRules = reactive({
projectId: [{ required: true, message: '关联项目ID不能为空', trigger: 'blur' }],
settlementPeriod: [{ required: true, message: '结算周期(如:2023-Q1)不能为空', trigger: 'blur' }],
projectId: [{ required: true, message: '项目不能为空', trigger: 'blur' }],
settlementPeriod: [{ required: true, message: '结算周期不能为空', trigger: 'blur' }],
settlementDate: [{ required: true, message: '结算日期不能为空', trigger: 'blur' }],
})
const formRef = ref() // Ref
@ -139,6 +174,8 @@ const resetForm = () => {
projectId: undefined,
settlementPeriod: undefined,
period: undefined,
startDate: undefined,
endDate: undefined,
settlementDate: undefined,
incomeRemark: undefined,
expenseRemark: undefined,

View File

@ -8,7 +8,7 @@
:inline="true"
label-width="68px"
>
<el-form-item label="项目" prop="projectId">
<el-form-item labssel="项目" prop="projectId">
<el-select
v-model="queryParams.projectId"
clearable
@ -23,19 +23,18 @@
/>
</el-select>
</el-form-item>
<el-form-item label="结算周期(如:2023-Q1)" prop="settlementPeriod">
<el-input
v-model="queryParams.settlementPeriod"
placeholder="请输入结算周期(如:2023-Q1)"
<el-form-item label="结算周期" prop="settlementPeriod">
<el-quarter-picker
v-model="queryParams.settlementPeriod"
placeholder="选择季度"
value-format="yyyy-q"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="结算日期" prop="settlementDate">
<el-date-picker
v-model="queryParams.settlementDate"
value-format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
@ -53,17 +52,6 @@
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
@ -91,18 +79,39 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="项目名称" align="center" prop="projectName" />
<el-table-column label="枪数" align="center" prop="settlementPeriod" />
<el-table-column label="结算周期(如:2023-Q1)" align="center" prop="settlementPeriod" />
<el-table-column label="结算开始时间" align="center" prop="settlementDate" />
<el-table-column label="结算结束时间" align="center" prop="settlementDate" />
<el-table-column label="结算日期" align="center" prop="settlementDate" />
<el-table-column label="项目名称" align="center" prop="projectName">
<template #default="scope">
<el-link type="primary" @click="goDetail(scope.row)">{{ scope.row.projectName }}</el-link>
</template>
</el-table-column>
<el-table-column label="枪数" align="center" prop="piles" />
<el-table-column label="结算周期" align="center" prop="settlementPeriod" >
<template #default="scope">
{{ formatQuarterPeriod(scope.row.settlementPeriod) }}
</template>
</el-table-column>
<el-table-column label="结算开始时间" align="center" prop="startDate">
<template #default="scope">
{{ scope.row.startDate ? formatDate(scope.row.startDate, 'YYYY-MM-DD') : '' }}
</template>
</el-table-column>
<el-table-column label="结算结束时间" align="center" prop="endDate">
<template #default="scope">
{{ scope.row.endDate ? formatDate(scope.row.endDate, 'YYYY-MM-DD') : '' }}
</template>
</el-table-column>
<el-table-column label="结算日期" align="center" prop="settlementDate">
<template #default="scope">
{{ scope.row.settlementDate ? formatDate(scope.row.settlementDate, 'YYYY-MM-DD') : '' }}
</template>
</el-table-column>
<el-table-column label="总期数" align="center" prop="period" />
<el-table-column label="毛利润" align="center" prop="settlementDate" />
<el-table-column label="单枪毛利润" align="center" prop="settlementDate" />
<el-table-column label="净利润" align="center" prop="settlementDate" />
<el-table-column label="毛利润" align="center" prop="grossProfit" />
<el-table-column label="上期亏损转结" align="center" prop="previousLossCarryover" />
<el-table-column label="净利润" align="center" prop="netProfit" />
<el-table-column label="结算状态" align="center" prop="settlementStatus" />
<el-table-column label="操作" align="center" min-width="120px">
<el-table-column label="操作" align="center" min-width="180px" fixed="right">
<template #default="scope">
<el-button
link
@ -137,11 +146,23 @@
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import { formatDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as ProjectSettlementApi from '@/api/crm/project/settlement'
import * as ProjectApi from '@/api/crm/project'
import ProjectSettlementForm from './ProjectSettlementForm.vue'
import ElQuarterPicker from '@/components/ElQuarterPicker/index.vue'
// '2024-1' => '2024'
function formatQuarterPeriod(val?: string) {
if (!val) return ''
const match = val.match(/^(\d{4})-(\d)$/)
if (!match) return val
const year = match[1]
const quarter = match[2]
const quarterMap = { '1': '第一季度', '2': '第二季度', '3': '第三季度', '4': '第四季度' }
return `${year}${quarterMap[quarter] || quarter}`
}
/** 项目利润结算 列表 */
defineOptions({ name: 'CrmProjectSettlement' })
@ -223,6 +244,17 @@ const handleExport = async () => {
}
}
/** 跳转到项目结算明细页面 */
const router = useRouter()
function goDetail(row: ProjectSettlementApi.ProjectSettlementVO) {
router.push({
name: 'CrmProjectSettlementDetail',
params: { id: row.id }
})
}
/** 初始化 **/
onMounted(async () => {
await getList()