fix:合同功能的完善,完成了合同的功能,可以通过时间段进行合同的查询,在新增或者修改合同的时候,项目和部门以及人员可以调用接口进行查询
This commit is contained in:
parent
80f174e9a4
commit
80353e44bf
|
@ -3,9 +3,10 @@ import http from '@/utils/http'
|
||||||
|
|
||||||
const BASE_URL = '/user'
|
const BASE_URL = '/user'
|
||||||
|
|
||||||
/** @desc 查询用户列表 */
|
/** @desc 分页查询用户列表 */
|
||||||
export function listUser(query: T.UserPageQuery) {
|
export function listUser(query: T.UserPageQuery) {
|
||||||
return http.get<PageRes<T.UserResp[]>>(`${BASE_URL}`, query)
|
// 后端分页接口为 /user/page
|
||||||
|
return http.get<PageRes<T.UserResp[]>>(`${BASE_URL}/page`, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @desc 查询所有用户列表 */
|
/** @desc 查询所有用户列表 */
|
||||||
|
|
|
@ -70,6 +70,6 @@ declare global {
|
||||||
// for type re-export
|
// for type re-export
|
||||||
declare global {
|
declare global {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||||
import('vue')
|
import('vue')
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,68 @@ export {}
|
||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
ApprovalAssistant: typeof import('./../components/ApprovalAssistant/index.vue')['default']
|
||||||
|
Avatar: typeof import('./../components/Avatar/index.vue')['default']
|
||||||
|
Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
|
||||||
|
CellCopy: typeof import('./../components/CellCopy/index.vue')['default']
|
||||||
|
Chart: typeof import('./../components/Chart/index.vue')['default']
|
||||||
|
ColumnSetting: typeof import('./../components/GiTable/src/components/ColumnSetting.vue')['default']
|
||||||
|
CronForm: typeof import('./../components/GenCron/CronForm/index.vue')['default']
|
||||||
|
CronModal: typeof import('./../components/GenCron/CronModal/index.vue')['default']
|
||||||
|
DateRangePicker: typeof import('./../components/DateRangePicker/index.vue')['default']
|
||||||
|
DayForm: typeof import('./../components/GenCron/CronForm/component/day-form.vue')['default']
|
||||||
|
FilePreview: typeof import('./../components/FilePreview/index.vue')['default']
|
||||||
|
GiCellAvatar: typeof import('./../components/GiCell/GiCellAvatar.vue')['default']
|
||||||
|
GiCellGender: typeof import('./../components/GiCell/GiCellGender.vue')['default']
|
||||||
|
GiCellStatus: typeof import('./../components/GiCell/GiCellStatus.vue')['default']
|
||||||
|
GiCellTag: typeof import('./../components/GiCell/GiCellTag.vue')['default']
|
||||||
|
GiCellTags: typeof import('./../components/GiCell/GiCellTags.vue')['default']
|
||||||
|
GiCodeView: typeof import('./../components/GiCodeView/index.vue')['default']
|
||||||
|
GiDot: typeof import('./../components/GiDot/index.tsx')['default']
|
||||||
|
GiEditTable: typeof import('./../components/GiEditTable/GiEditTable.vue')['default']
|
||||||
|
GiFooter: typeof import('./../components/GiFooter/index.vue')['default']
|
||||||
|
GiForm: typeof import('./../components/GiForm/src/GiForm.vue')['default']
|
||||||
|
GiIconBox: typeof import('./../components/GiIconBox/index.vue')['default']
|
||||||
|
GiIconSelector: typeof import('./../components/GiIconSelector/index.vue')['default']
|
||||||
|
GiIframe: typeof import('./../components/GiIframe/index.vue')['default']
|
||||||
|
GiOption: typeof import('./../components/GiOption/index.vue')['default']
|
||||||
|
GiOptionItem: typeof import('./../components/GiOptionItem/index.vue')['default']
|
||||||
|
GiPageLayout: typeof import('./../components/GiPageLayout/index.vue')['default']
|
||||||
|
GiSpace: typeof import('./../components/GiSpace/index.vue')['default']
|
||||||
|
GiSplitButton: typeof import('./../components/GiSplitButton/index.vue')['default']
|
||||||
|
GiSplitPane: typeof import('./../components/GiSplitPane/index.vue')['default']
|
||||||
|
GiSplitPaneFlexibleBox: typeof import('./../components/GiSplitPane/components/GiSplitPaneFlexibleBox.vue')['default']
|
||||||
|
GiSvgIcon: typeof import('./../components/GiSvgIcon/index.vue')['default']
|
||||||
|
GiTable: typeof import('./../components/GiTable/src/GiTable.vue')['default']
|
||||||
|
GiTag: typeof import('./../components/GiTag/index.tsx')['default']
|
||||||
|
GiThemeBtn: typeof import('./../components/GiThemeBtn/index.vue')['default']
|
||||||
|
HourForm: typeof import('./../components/GenCron/CronForm/component/hour-form.vue')['default']
|
||||||
|
Icon403: typeof import('./../components/icons/Icon403.vue')['default']
|
||||||
|
Icon404: typeof import('./../components/icons/Icon404.vue')['default']
|
||||||
|
Icon500: typeof import('./../components/icons/Icon500.vue')['default']
|
||||||
|
IconBorders: typeof import('./../components/icons/IconBorders.vue')['default']
|
||||||
|
IconTableSize: typeof import('./../components/icons/IconTableSize.vue')['default']
|
||||||
|
IconTreeAdd: typeof import('./../components/icons/IconTreeAdd.vue')['default']
|
||||||
|
IconTreeReduce: typeof import('./../components/icons/IconTreeReduce.vue')['default']
|
||||||
|
ImageImport: typeof import('./../components/ImageImport/index.vue')['default']
|
||||||
|
ImageImportWizard: typeof import('./../components/ImageImportWizard/index.vue')['default']
|
||||||
|
IndustrialImageList: typeof import('./../components/IndustrialImageList/index.vue')['default']
|
||||||
|
JsonPretty: typeof import('./../components/JsonPretty/index.vue')['default']
|
||||||
|
MinuteForm: typeof import('./../components/GenCron/CronForm/component/minute-form.vue')['default']
|
||||||
|
MonthForm: typeof import('./../components/GenCron/CronForm/component/month-form.vue')['default']
|
||||||
NotificationCenter: typeof import('./../components/NotificationCenter/index.vue')['default']
|
NotificationCenter: typeof import('./../components/NotificationCenter/index.vue')['default']
|
||||||
|
ParentView: typeof import('./../components/ParentView/index.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default']
|
||||||
|
SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default']
|
||||||
|
TextCopy: typeof import('./../components/TextCopy/index.vue')['default']
|
||||||
|
TurbineGrid: typeof import('./../components/TurbineGrid/index.vue')['default']
|
||||||
|
UserSelect: typeof import('./../components/UserSelect/index.vue')['default']
|
||||||
|
Verify: typeof import('./../components/Verify/index.vue')['default']
|
||||||
|
VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default']
|
||||||
|
VerifySlide: typeof import('./../components/Verify/Verify/VerifySlide.vue')['default']
|
||||||
|
WeekForm: typeof import('./../components/GenCron/CronForm/component/week-form.vue')['default']
|
||||||
|
YearForm: typeof import('./../components/GenCron/CronForm/component/year-form.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,16 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item field="projectName" label="项目名称">
|
<a-form-item field="projectId" label="项目">
|
||||||
<a-input v-model="contractData.projectName" />
|
<a-select v-model="contractData.projectId"
|
||||||
|
:options="projectOptions"
|
||||||
|
:loading="projectLoading"
|
||||||
|
:virtual-list-props="virtualListProps"
|
||||||
|
placeholder="请选择项目"
|
||||||
|
allow-search allow-clear
|
||||||
|
@focus="handleProjectFocus"
|
||||||
|
@dropdown-visible-change="handleProjectDropdown"
|
||||||
|
@search="handleProjectSearch" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
@ -66,8 +74,27 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item field="productService" label="产品或服务">
|
<a-form-item field="departmentId" label="销售部门">
|
||||||
<a-input v-model="contractData.productService" />
|
<a-tree-select v-model="contractData.departmentId"
|
||||||
|
:data="deptTree"
|
||||||
|
:loading="deptLoading"
|
||||||
|
placeholder="请选择部门"
|
||||||
|
allow-search allow-clear
|
||||||
|
@dropdown-visible-change="handleDeptDropdown"
|
||||||
|
@focus="handleDeptFocus"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="salespersonId" label="销售人员">
|
||||||
|
<a-select v-model="contractData.salespersonId"
|
||||||
|
:options="userOptions"
|
||||||
|
:loading="userLoading"
|
||||||
|
:virtual-list-props="userVirtualListProps"
|
||||||
|
placeholder="请选择业务员"
|
||||||
|
allow-search allow-clear
|
||||||
|
@dropdown-visible-change="handleUserDropdown"
|
||||||
|
@search="handleUserSearch" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
@ -88,6 +115,10 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import { listProject, type ProjectResp } from '@/apis/project'
|
||||||
|
import { getDeptTree } from '@/apis/system/dept'
|
||||||
|
import { listUser } from '@/apis/system/user'
|
||||||
import type { ContractItem } from './index.vue'
|
import type { ContractItem } from './index.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -100,6 +131,138 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const contractData = ref({ ...props.contractData })
|
const contractData = ref({ ...props.contractData })
|
||||||
|
|
||||||
|
// 项目选项与加载(支持无限滚动)
|
||||||
|
const projectOptions = ref<{ label: string; value: string }[]>([])
|
||||||
|
const projectLoading = ref(false)
|
||||||
|
const projPage = ref(1)
|
||||||
|
const pageSize = 20
|
||||||
|
const hasMore = ref(true)
|
||||||
|
const currentKeyword = ref('')
|
||||||
|
let searchTimer: number | undefined
|
||||||
|
|
||||||
|
const appendProjects = (list: ProjectResp[]) => {
|
||||||
|
const items = list.map(p => ({ label: p.projectName, value: String((p as any).projectId ?? (p as any).id) }))
|
||||||
|
projectOptions.value = projectOptions.value.concat(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractList = (data: any): ProjectResp[] => {
|
||||||
|
if (Array.isArray(data)) return data as ProjectResp[]
|
||||||
|
if (Array.isArray(data?.list)) return data.list as ProjectResp[]
|
||||||
|
if (Array.isArray(data?.rows)) return data.rows as ProjectResp[]
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadProjects = async (keyword = currentKeyword.value, reset = false) => {
|
||||||
|
try {
|
||||||
|
if (reset) {
|
||||||
|
projPage.value = 1
|
||||||
|
hasMore.value = true
|
||||||
|
projectOptions.value = []
|
||||||
|
}
|
||||||
|
if (!hasMore.value) return
|
||||||
|
projectLoading.value = true
|
||||||
|
const resp = await listProject({ page: projPage.value, size: pageSize, projectName: keyword } as any)
|
||||||
|
const list = extractList((resp as any).data)
|
||||||
|
appendProjects(list)
|
||||||
|
if (list.length < pageSize) hasMore.value = false
|
||||||
|
projPage.value += 1
|
||||||
|
} catch (e) {
|
||||||
|
Message.error('获取项目列表失败')
|
||||||
|
hasMore.value = false
|
||||||
|
} finally {
|
||||||
|
projectLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const virtualListProps = {
|
||||||
|
height: 240,
|
||||||
|
onReachBottom: () => {
|
||||||
|
if (hasMore.value && !projectLoading.value) loadProjects()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProjectFocus = () => {
|
||||||
|
if (projectOptions.value.length === 0) loadProjects('', true)
|
||||||
|
}
|
||||||
|
const handleProjectDropdown = (visible: boolean) => {
|
||||||
|
if (visible && projectOptions.value.length === 0) loadProjects('', true)
|
||||||
|
}
|
||||||
|
const handleProjectSearch = (val: string) => {
|
||||||
|
if (searchTimer) window.clearTimeout(searchTimer)
|
||||||
|
currentKeyword.value = val || ''
|
||||||
|
searchTimer = window.setTimeout(() => loadProjects(currentKeyword.value, true), 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 部门树
|
||||||
|
const deptTree = ref([] as any[])
|
||||||
|
const deptLoading = ref(false)
|
||||||
|
const loadDept = async () => {
|
||||||
|
try {
|
||||||
|
deptLoading.value = true
|
||||||
|
const res = await getDeptTree()
|
||||||
|
const data = (res as any).data || []
|
||||||
|
// 转换为 a-tree-select 结构
|
||||||
|
const toTree = (arr: any[]): any[] => arr.map(i => ({
|
||||||
|
key: String(i.deptId),
|
||||||
|
title: i.deptName,
|
||||||
|
value: String(i.deptId),
|
||||||
|
children: Array.isArray(i.children) ? toTree(i.children) : [],
|
||||||
|
}))
|
||||||
|
deptTree.value = toTree(Array.isArray(data) ? data : (data.list || data.rows || []))
|
||||||
|
} finally {
|
||||||
|
deptLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleDeptDropdown = (visible: boolean) => { if (visible && deptTree.value.length === 0) loadDept() }
|
||||||
|
const handleDeptFocus = () => { if (deptTree.value.length === 0) loadDept() }
|
||||||
|
|
||||||
|
// 用户列表(分页 + 搜索 + 无限滚动)
|
||||||
|
const userOptions = ref<{ label: string; value: string }[]>([])
|
||||||
|
const userLoading = ref(false)
|
||||||
|
const userPage = ref(1)
|
||||||
|
const userHasMore = ref(true)
|
||||||
|
const userPageSize = 20
|
||||||
|
const userKeyword = ref('')
|
||||||
|
|
||||||
|
const appendUsers = (rows: any[]) => {
|
||||||
|
userOptions.value = userOptions.value.concat(
|
||||||
|
rows.map(u => ({ label: u.name || u.nickname || u.username, value: String(u.userId || u.id) }))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const extractUsers = (data: any): any[] => {
|
||||||
|
if (Array.isArray(data)) return data
|
||||||
|
if (Array.isArray(data?.rows)) return data.rows
|
||||||
|
if (Array.isArray(data?.list)) return data.list
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const loadUsers = async (reset = false) => {
|
||||||
|
try {
|
||||||
|
if (reset) { userOptions.value = []; userPage.value = 1; userHasMore.value = true }
|
||||||
|
if (!userHasMore.value) return
|
||||||
|
userLoading.value = true
|
||||||
|
const resp = await listUser({ page: userPage.value, pageSize: userPageSize, name: userKeyword.value } as any)
|
||||||
|
const rows = extractUsers((resp as any).data)
|
||||||
|
appendUsers(rows)
|
||||||
|
if (rows.length < userPageSize) userHasMore.value = false
|
||||||
|
userPage.value += 1
|
||||||
|
} finally {
|
||||||
|
userLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const userVirtualListProps = {
|
||||||
|
height: 240,
|
||||||
|
onReachBottom: () => { if (userHasMore.value && !userLoading.value) loadUsers() }
|
||||||
|
}
|
||||||
|
const handleUserDropdown = (visible: boolean) => { if (visible && userOptions.value.length === 0) { userKeyword.value=''; loadUsers(true) } }
|
||||||
|
const handleUserSearch = (val: string) => {
|
||||||
|
if (searchTimer) window.clearTimeout(searchTimer)
|
||||||
|
userKeyword.value = val || ''
|
||||||
|
searchTimer = window.setTimeout(() => loadUsers(true), 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化:首次展开项目/部门/用户时加载
|
||||||
|
// 不在 mounted 直接加载,减少无效请求
|
||||||
|
|
||||||
// 监听props变化更新内部数据
|
// 监听props变化更新内部数据
|
||||||
watch(
|
watch(
|
||||||
() => props.contractData,
|
() => props.contractData,
|
||||||
|
|
|
@ -55,11 +55,9 @@
|
||||||
|
|
||||||
<template #action="{ record }">
|
<template #action="{ record }">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-link @click="viewDetail(record)">详情</a-link>
|
<a-link @click="viewDetail(record)">查看</a-link>
|
||||||
<a-link v-if="record.contractStatus === '未确认'" @click="editRecord(record)">编辑</a-link>
|
<a-link @click="editRecord(record)">编辑</a-link>
|
||||||
<a-link v-if="record.contractStatus === '待审批'" @click="approveContract(record)">审批</a-link>
|
<a-link status="danger" @click="deleteContract(record)">删除</a-link>
|
||||||
<a-link @click="viewPayment(record)">收款记录</a-link>
|
|
||||||
<a-link v-if="record.contractStatus !== '已签署' && record.contractStatus !== '已完成'" @click="deleteContract(record)">删除</a-link>
|
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</GiTable>
|
</GiTable>
|
||||||
|
@ -89,6 +87,21 @@
|
||||||
@update:contract-data="handleContractDataUpdate"
|
@update:contract-data="handleContractDataUpdate"
|
||||||
/>
|
/>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 新建合同弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="showAddModal"
|
||||||
|
title="新建合同"
|
||||||
|
:width="800"
|
||||||
|
@cancel="closeAddModal"
|
||||||
|
@before-ok="handleAddSubmit"
|
||||||
|
>
|
||||||
|
<ContractEdit
|
||||||
|
v-if="showAddModal"
|
||||||
|
:contract-data="newContractData"
|
||||||
|
@update:contract-data="handleNewContractDataUpdate"
|
||||||
|
/>
|
||||||
|
</a-modal>
|
||||||
</GiPageLayout>
|
</GiPageLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -135,25 +148,16 @@ interface ContractItem {
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
contractName: '',
|
|
||||||
contractCode: '',
|
contractCode: '',
|
||||||
client: '',
|
client: '',
|
||||||
status: '',
|
status: '',
|
||||||
signDate: '',
|
signDateRange: [] as [string, string] | [],
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 10,
|
size: 10,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 查询条件配置
|
// 查询条件配置
|
||||||
const queryFormColumns = [
|
const queryFormColumns = [
|
||||||
{
|
|
||||||
field: 'contractName',
|
|
||||||
label: '合同名称',
|
|
||||||
type: 'input' as const,
|
|
||||||
props: {
|
|
||||||
placeholder: '请输入合同名称',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: 'client',
|
field: 'client',
|
||||||
label: '客户',
|
label: '客户',
|
||||||
|
@ -178,6 +182,16 @@ const queryFormColumns = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'signDateRange',
|
||||||
|
label: '签署时间',
|
||||||
|
type: 'range-picker' as const,
|
||||||
|
props: {
|
||||||
|
placeholder: ['开始时间', '结束时间'],
|
||||||
|
showTime: true,
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// 表格列配置
|
// 表格列配置
|
||||||
|
@ -209,27 +223,43 @@ const fetchContractList = async () => {
|
||||||
const params = {
|
const params = {
|
||||||
page: searchForm.page,
|
page: searchForm.page,
|
||||||
pageSize: searchForm.size,
|
pageSize: searchForm.size,
|
||||||
contractName: searchForm.contractName,
|
|
||||||
code: searchForm.contractCode,
|
code: searchForm.contractCode,
|
||||||
customer: searchForm.client,
|
customer: searchForm.client,
|
||||||
contractStatus: searchForm.status,
|
contractStatus: searchForm.status,
|
||||||
signDate: searchForm.signDate,
|
signDateStart: Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange[0] : undefined,
|
||||||
|
signDateEnd: Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange[1] : undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await http.get('/contract/list', params)
|
const response = await http.get('/contract/list', params)
|
||||||
|
|
||||||
if (response.code === 200) {
|
if (response.code === 200) {
|
||||||
// 过滤出类型为"收入合同"的数据
|
// 过滤出类型为"支出合同"的数据
|
||||||
const allContracts = response.rows || []
|
const allContracts = response.rows || []
|
||||||
const revenueContracts = allContracts.filter((item: ContractItem) => item.type === '支出合同')
|
let filtered = allContracts.filter((item: ContractItem) => item.type === '支出合同')
|
||||||
|
|
||||||
|
// 如果后端未按时间段过滤,则在前端按签署时间范围再次过滤
|
||||||
|
const range = Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange : null
|
||||||
|
if (range) {
|
||||||
|
const [start, end] = range
|
||||||
|
const startTime = new Date(start as any).getTime()
|
||||||
|
const endTime = new Date(end as any).getTime()
|
||||||
|
if (!Number.isNaN(startTime) && !Number.isNaN(endTime)) {
|
||||||
|
filtered = filtered.filter((item: ContractItem) => {
|
||||||
|
if (!item.signDate) return false
|
||||||
|
const t = new Date(item.signDate as any).getTime()
|
||||||
|
return !Number.isNaN(t) && t >= startTime && t <= endTime
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 计算未收款金额
|
// 计算未收款金额
|
||||||
dataList.value = revenueContracts.map((item: ContractItem) => ({
|
dataList.value = filtered.map((item: ContractItem) => ({
|
||||||
...item,
|
...item,
|
||||||
pendingAmount: (item.amount || 0) - (item.receivedAmount || 0),
|
pendingAmount: (item.amount || 0) - (item.receivedAmount || 0),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
pagination.total = Number.parseInt(response.total) || 0
|
// 更新分页总数(前端过滤后以过滤结果数为准)
|
||||||
|
pagination.total = dataList.value.length
|
||||||
} else {
|
} else {
|
||||||
Message.error(response.msg || '获取合同列表失败')
|
Message.error(response.msg || '获取合同列表失败')
|
||||||
dataList.value = []
|
dataList.value = []
|
||||||
|
@ -277,11 +307,10 @@ const search = async () => {
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
Object.assign(searchForm, {
|
Object.assign(searchForm, {
|
||||||
contractName: '',
|
|
||||||
contractCode: '',
|
contractCode: '',
|
||||||
client: '',
|
client: '',
|
||||||
status: '',
|
status: '',
|
||||||
signDate: '',
|
signDateRange: [],
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 10,
|
size: 10,
|
||||||
})
|
})
|
||||||
|
@ -305,8 +334,131 @@ const onPageSizeChange = (size: number) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 操作方法
|
// 操作方法
|
||||||
|
const showAddModal = ref(false)
|
||||||
|
const newContractData = ref<ContractItem>({
|
||||||
|
contractId: '',
|
||||||
|
customer: '',
|
||||||
|
code: '',
|
||||||
|
projectId: '',
|
||||||
|
type: '支出合同',
|
||||||
|
productService: '',
|
||||||
|
paymentDate: null,
|
||||||
|
performanceDeadline: null,
|
||||||
|
paymentAddress: '',
|
||||||
|
amount: 0,
|
||||||
|
accountNumber: '',
|
||||||
|
notes: '',
|
||||||
|
contractStatus: '未确认',
|
||||||
|
contractText: '',
|
||||||
|
projectName: '',
|
||||||
|
salespersonName: null,
|
||||||
|
salespersonDeptName: '',
|
||||||
|
settlementAmount: null,
|
||||||
|
receivedAmount: null,
|
||||||
|
contractStatusLabel: null,
|
||||||
|
createBy: null,
|
||||||
|
updateBy: null,
|
||||||
|
createTime: '',
|
||||||
|
updateTime: '',
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
signDate: '',
|
||||||
|
duration: '',
|
||||||
|
})
|
||||||
|
|
||||||
const openAddModal = () => {
|
const openAddModal = () => {
|
||||||
Message.info('新建合同功能开发中...')
|
// 重置默认值
|
||||||
|
newContractData.value = {
|
||||||
|
contractId: '',
|
||||||
|
customer: '',
|
||||||
|
code: '',
|
||||||
|
projectId: '',
|
||||||
|
type: '支出合同',
|
||||||
|
productService: '',
|
||||||
|
paymentDate: null,
|
||||||
|
performanceDeadline: null,
|
||||||
|
paymentAddress: '',
|
||||||
|
amount: 0,
|
||||||
|
accountNumber: '',
|
||||||
|
notes: '',
|
||||||
|
contractStatus: '未确认',
|
||||||
|
contractText: '',
|
||||||
|
projectName: '',
|
||||||
|
salespersonName: null,
|
||||||
|
salespersonDeptName: '',
|
||||||
|
settlementAmount: null,
|
||||||
|
receivedAmount: null,
|
||||||
|
contractStatusLabel: null,
|
||||||
|
createBy: null,
|
||||||
|
updateBy: null,
|
||||||
|
createTime: '',
|
||||||
|
updateTime: '',
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
signDate: '',
|
||||||
|
duration: '',
|
||||||
|
}
|
||||||
|
showAddModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeAddModal = () => {
|
||||||
|
showAddModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewContractDataUpdate = (data: ContractItem) => {
|
||||||
|
// 不更换引用,避免子组件watch到props变更导致重渲染丢失焦点
|
||||||
|
Object.assign(newContractData.value, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const payload: any = {
|
||||||
|
accountNumber: newContractData.value.accountNumber || '',
|
||||||
|
amount: newContractData.value.amount || 0,
|
||||||
|
code: newContractData.value.code || '',
|
||||||
|
contractStatus: newContractData.value.contractStatus || '',
|
||||||
|
contractText: newContractData.value.contractText || '',
|
||||||
|
customer: newContractData.value.customer || '',
|
||||||
|
departmentId: (newContractData.value as any).departmentId || '',
|
||||||
|
duration: newContractData.value.duration || '',
|
||||||
|
notes: newContractData.value.notes || '',
|
||||||
|
paymentAddress: newContractData.value.paymentAddress || '',
|
||||||
|
paymentDate: newContractData.value.paymentDate || null,
|
||||||
|
performanceDeadline: newContractData.value.performanceDeadline || null,
|
||||||
|
productService: newContractData.value.productService || '',
|
||||||
|
projectId: newContractData.value.projectId || '',
|
||||||
|
salespersonId: (newContractData.value as any).salespersonId || '',
|
||||||
|
signDate: newContractData.value.signDate || null,
|
||||||
|
type: newContractData.value.type || '支出合同',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有合同ID则不传
|
||||||
|
if (!newContractData.value.contractId) delete payload.contractId
|
||||||
|
|
||||||
|
const response = await http.post('/contract', payload)
|
||||||
|
|
||||||
|
if ((response as any).status === 200 && response.code === 200) {
|
||||||
|
Message.success('新建合同成功')
|
||||||
|
closeAddModal()
|
||||||
|
search()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 某些接口只返回 code/msg
|
||||||
|
if (response.code === 200) {
|
||||||
|
Message.success('新建合同成功')
|
||||||
|
closeAddModal()
|
||||||
|
search()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
Message.error(response.msg || '新建合同失败')
|
||||||
|
return false
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('新建合同失败:', error)
|
||||||
|
Message.error('新建合同失败: ' + (error?.message || '请稍后再试'))
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportContract = () => {
|
const exportContract = () => {
|
||||||
|
@ -339,7 +491,12 @@ const closeEditModal = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleContractDataUpdate = (data: ContractItem) => {
|
const handleContractDataUpdate = (data: ContractItem) => {
|
||||||
editedContractData.value = data
|
// 避免替换引用导致子组件重建,保持编辑输入流畅
|
||||||
|
if (!editedContractData.value) {
|
||||||
|
editedContractData.value = { ...(data as any) } as ContractItem
|
||||||
|
} else {
|
||||||
|
Object.assign(editedContractData.value, data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditSubmit = async () => {
|
const handleEditSubmit = async () => {
|
||||||
|
@ -391,28 +548,29 @@ const handleEditSubmit = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 删除合同
|
// 删除合同:只有在确认框点击确定后才执行删除
|
||||||
const deleteContract = async (record: ContractItem) => {
|
const deleteContract = (record: ContractItem) => {
|
||||||
try {
|
Modal.confirm({
|
||||||
await Modal.confirm({
|
title: '确认删除',
|
||||||
title: '确认删除',
|
content: `确定要删除合同 "${record.projectName}" 吗?`,
|
||||||
content: `确定要删除合同 "${record.projectName}" 吗?`,
|
async onOk() {
|
||||||
})
|
try {
|
||||||
|
const response = await http.del(`/contract/${record.contractId}`)
|
||||||
const response = await http.delete(`/contract/${record.contractId}`)
|
if (response.code === 200) {
|
||||||
if (response.code === 200) {
|
Message.success('合同删除成功')
|
||||||
Message.success('合同删除成功')
|
search()
|
||||||
search() // 刷新列表
|
return true
|
||||||
} else {
|
} else {
|
||||||
Message.error(response.msg || '合同删除失败')
|
Message.error(response.msg || '合同删除失败')
|
||||||
}
|
return false
|
||||||
} catch (error) {
|
}
|
||||||
// 用户取消删除或请求失败
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
console.error('合同删除失败:', error)
|
||||||
console.error('合同删除失败:', error)
|
Message.error('合同删除失败')
|
||||||
Message.error('合同删除失败')
|
return false
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示合同详情弹窗
|
// 显示合同详情弹窗
|
||||||
|
|
|
@ -7,8 +7,16 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item field="projectName" label="项目名称">
|
<a-form-item field="projectId" label="项目">
|
||||||
<a-input v-model="contractData.projectName" />
|
<a-select v-model="contractData.projectId"
|
||||||
|
:options="projectOptions"
|
||||||
|
:loading="projectLoading"
|
||||||
|
:virtual-list-props="virtualListProps"
|
||||||
|
placeholder="请选择项目"
|
||||||
|
allow-search allow-clear
|
||||||
|
@focus="handleProjectFocus"
|
||||||
|
@dropdown-visible-change="handleProjectDropdown"
|
||||||
|
@search="handleProjectSearch" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
@ -66,8 +74,27 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item field="productService" label="产品或服务">
|
<a-form-item field="departmentId" label="销售部门">
|
||||||
<a-input v-model="contractData.productService" />
|
<a-tree-select v-model="contractData.departmentId"
|
||||||
|
:data="deptTree"
|
||||||
|
:loading="deptLoading"
|
||||||
|
placeholder="请选择部门"
|
||||||
|
allow-search allow-clear
|
||||||
|
@dropdown-visible-change="handleDeptDropdown"
|
||||||
|
@focus="handleDeptFocus"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="salespersonId" label="销售人员">
|
||||||
|
<a-select v-model="contractData.salespersonId"
|
||||||
|
:options="userOptions"
|
||||||
|
:loading="userLoading"
|
||||||
|
:virtual-list-props="userVirtualListProps"
|
||||||
|
placeholder="请选择业务员"
|
||||||
|
allow-search allow-clear
|
||||||
|
@dropdown-visible-change="handleUserDropdown"
|
||||||
|
@search="handleUserSearch" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
@ -88,6 +115,10 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import { listProject, type ProjectResp } from '@/apis/project'
|
||||||
|
import { getDeptTree } from '@/apis/system/dept'
|
||||||
|
import { listUser } from '@/apis/system/user'
|
||||||
import type { ContractItem } from './index.vue'
|
import type { ContractItem } from './index.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -100,6 +131,134 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const contractData = ref({ ...props.contractData })
|
const contractData = ref({ ...props.contractData })
|
||||||
|
|
||||||
|
// 项目选项与加载(支持无限滚动)
|
||||||
|
const projectOptions = ref<{ label: string; value: string }[]>([])
|
||||||
|
const projectLoading = ref(false)
|
||||||
|
const projPage = ref(1)
|
||||||
|
const pageSize = 20
|
||||||
|
const hasMore = ref(true)
|
||||||
|
const currentKeyword = ref('')
|
||||||
|
let searchTimer: number | undefined
|
||||||
|
|
||||||
|
const appendProjects = (list: ProjectResp[]) => {
|
||||||
|
const items = list.map(p => ({ label: p.projectName, value: String((p as any).projectId ?? (p as any).id) }))
|
||||||
|
projectOptions.value = projectOptions.value.concat(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractList = (data: any): ProjectResp[] => {
|
||||||
|
if (Array.isArray(data)) return data as ProjectResp[]
|
||||||
|
if (Array.isArray(data?.list)) return data.list as ProjectResp[]
|
||||||
|
if (Array.isArray(data?.rows)) return data.rows as ProjectResp[]
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadProjects = async (keyword = currentKeyword.value, reset = false) => {
|
||||||
|
try {
|
||||||
|
if (reset) {
|
||||||
|
projPage.value = 1
|
||||||
|
hasMore.value = true
|
||||||
|
projectOptions.value = []
|
||||||
|
}
|
||||||
|
if (!hasMore.value) return
|
||||||
|
projectLoading.value = true
|
||||||
|
const resp = await listProject({ page: projPage.value, size: pageSize, projectName: keyword } as any)
|
||||||
|
const list = extractList((resp as any).data)
|
||||||
|
appendProjects(list)
|
||||||
|
if (list.length < pageSize) hasMore.value = false
|
||||||
|
projPage.value += 1
|
||||||
|
} catch (e) {
|
||||||
|
Message.error('获取项目列表失败')
|
||||||
|
hasMore.value = false
|
||||||
|
} finally {
|
||||||
|
projectLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const virtualListProps = {
|
||||||
|
height: 240,
|
||||||
|
onReachBottom: () => {
|
||||||
|
if (hasMore.value && !projectLoading.value) loadProjects()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProjectFocus = () => {
|
||||||
|
if (projectOptions.value.length === 0) loadProjects('', true)
|
||||||
|
}
|
||||||
|
const handleProjectDropdown = (visible: boolean) => {
|
||||||
|
if (visible && projectOptions.value.length === 0) loadProjects('', true)
|
||||||
|
}
|
||||||
|
const handleProjectSearch = (val: string) => {
|
||||||
|
if (searchTimer) window.clearTimeout(searchTimer)
|
||||||
|
currentKeyword.value = val || ''
|
||||||
|
searchTimer = window.setTimeout(() => loadProjects(currentKeyword.value, true), 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 部门树
|
||||||
|
const deptTree = ref([] as any[])
|
||||||
|
const deptLoading = ref(false)
|
||||||
|
const loadDept = async () => {
|
||||||
|
try {
|
||||||
|
deptLoading.value = true
|
||||||
|
const res = await getDeptTree()
|
||||||
|
const data = (res as any).data || []
|
||||||
|
const toTree = (arr: any[]): any[] => arr.map(i => ({
|
||||||
|
key: String(i.deptId),
|
||||||
|
title: i.deptName,
|
||||||
|
value: String(i.deptId),
|
||||||
|
children: Array.isArray(i.children) ? toTree(i.children) : [],
|
||||||
|
}))
|
||||||
|
deptTree.value = toTree(Array.isArray(data) ? data : (data.list || data.rows || []))
|
||||||
|
} finally {
|
||||||
|
deptLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleDeptDropdown = (visible: boolean) => { if (visible && deptTree.value.length === 0) loadDept() }
|
||||||
|
const handleDeptFocus = () => { if (deptTree.value.length === 0) loadDept() }
|
||||||
|
|
||||||
|
// 用户列表(分页 + 搜索 + 无限滚动)
|
||||||
|
const userOptions = ref<{ label: string; value: string }[]>([])
|
||||||
|
const userLoading = ref(false)
|
||||||
|
const userPage = ref(1)
|
||||||
|
const userHasMore = ref(true)
|
||||||
|
const userPageSize = 20
|
||||||
|
const userKeyword = ref('')
|
||||||
|
|
||||||
|
const appendUsers = (rows: any[]) => {
|
||||||
|
userOptions.value = userOptions.value.concat(
|
||||||
|
rows.map(u => ({ label: u.name || u.nickname || u.username, value: String(u.userId || u.id) }))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const extractUsers = (data: any): any[] => {
|
||||||
|
if (Array.isArray(data)) return data
|
||||||
|
if (Array.isArray(data?.rows)) return data.rows
|
||||||
|
if (Array.isArray(data?.list)) return data.list
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const loadUsers = async (reset = false) => {
|
||||||
|
try {
|
||||||
|
if (reset) { userOptions.value = []; userPage.value = 1; userHasMore.value = true }
|
||||||
|
if (!userHasMore.value) return
|
||||||
|
userLoading.value = true
|
||||||
|
const resp = await listUser({ page: userPage.value, pageSize: userPageSize, name: userKeyword.value } as any)
|
||||||
|
const rows = extractUsers((resp as any).data)
|
||||||
|
appendUsers(rows)
|
||||||
|
if (rows.length < userPageSize) userHasMore.value = false
|
||||||
|
userPage.value += 1
|
||||||
|
} finally {
|
||||||
|
userLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const userVirtualListProps = {
|
||||||
|
height: 240,
|
||||||
|
onReachBottom: () => { if (userHasMore.value && !userLoading.value) loadUsers() }
|
||||||
|
}
|
||||||
|
const handleUserDropdown = (visible: boolean) => { if (visible && userOptions.value.length === 0) { userKeyword.value=''; loadUsers(true) } }
|
||||||
|
const handleUserSearch = (val: string) => {
|
||||||
|
if (searchTimer) window.clearTimeout(searchTimer)
|
||||||
|
userKeyword.value = val || ''
|
||||||
|
searchTimer = window.setTimeout(() => loadUsers(true), 300)
|
||||||
|
}
|
||||||
|
|
||||||
// 监听props变化更新内部数据
|
// 监听props变化更新内部数据
|
||||||
watch(
|
watch(
|
||||||
() => props.contractData,
|
() => props.contractData,
|
||||||
|
|
|
@ -55,11 +55,9 @@
|
||||||
|
|
||||||
<template #action="{ record }">
|
<template #action="{ record }">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-link @click="viewDetail(record)">详情</a-link>
|
<a-link @click="viewDetail(record)">查看</a-link>
|
||||||
<a-link v-if="record.contractStatus === '未确认'" @click="editRecord(record)">编辑</a-link>
|
<a-link @click="editRecord(record)">编辑</a-link>
|
||||||
<a-link v-if="record.contractStatus === '待审批'" @click="approveContract(record)">审批</a-link>
|
<a-link status="danger" @click="deleteContract(record)">删除</a-link>
|
||||||
<a-link @click="viewPayment(record)">收款记录</a-link>
|
|
||||||
<a-link v-if="record.contractStatus !== '已签署' && record.contractStatus !== '已完成'" @click="deleteContract(record)">删除</a-link>
|
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</GiTable>
|
</GiTable>
|
||||||
|
@ -391,28 +389,29 @@ const handleEditSubmit = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 删除合同
|
// 删除合同:只有在确认框点击确定后才执行删除
|
||||||
const deleteContract = async (record: ContractItem) => {
|
const deleteContract = (record: ContractItem) => {
|
||||||
try {
|
Modal.confirm({
|
||||||
await Modal.confirm({
|
title: '确认删除',
|
||||||
title: '确认删除',
|
content: `确定要删除合同 "${record.projectName}" 吗?`,
|
||||||
content: `确定要删除合同 "${record.projectName}" 吗?`,
|
async onOk() {
|
||||||
})
|
try {
|
||||||
|
const response = await http.del(`/contract/${record.contractId}`)
|
||||||
const response = await http.delete(`/contract/${record.contractId}`)
|
if (response.code === 200) {
|
||||||
if (response.code === 200) {
|
Message.success('合同删除成功')
|
||||||
Message.success('合同删除成功')
|
search()
|
||||||
search() // 刷新列表
|
return true
|
||||||
} else {
|
} else {
|
||||||
Message.error(response.msg || '合同删除失败')
|
Message.error(response.msg || '合同删除失败')
|
||||||
}
|
return false
|
||||||
} catch (error) {
|
}
|
||||||
// 用户取消删除或请求失败
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
console.error('合同删除失败:', error)
|
||||||
console.error('合同删除失败:', error)
|
Message.error('合同删除失败')
|
||||||
Message.error('合同删除失败')
|
return false
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示合同详情弹窗
|
// 显示合同详情弹窗
|
||||||
|
|
Loading…
Reference in New Issue