fix:合同功能的完善,完成了合同的功能,可以通过时间段进行合同的查询,在新增或者修改合同的时候,项目和部门以及人员可以调用接口进行查询
This commit is contained in:
parent
80f174e9a4
commit
80353e44bf
|
@ -3,9 +3,10 @@ import http from '@/utils/http'
|
|||
|
||||
const BASE_URL = '/user'
|
||||
|
||||
/** @desc 查询用户列表 */
|
||||
/** @desc 分页查询用户列表 */
|
||||
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 查询所有用户列表 */
|
||||
|
|
|
@ -70,6 +70,6 @@ declare global {
|
|||
// for type re-export
|
||||
declare global {
|
||||
// @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')
|
||||
}
|
||||
|
|
|
@ -7,8 +7,68 @@ export {}
|
|||
|
||||
declare module 'vue' {
|
||||
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']
|
||||
ParentView: typeof import('./../components/ParentView/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
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-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="projectName" label="项目名称">
|
||||
<a-input v-model="contractData.projectName" />
|
||||
<a-form-item field="projectId" label="项目">
|
||||
<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-col>
|
||||
</a-row>
|
||||
|
@ -66,8 +74,27 @@
|
|||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="productService" label="产品或服务">
|
||||
<a-input v-model="contractData.productService" />
|
||||
<a-form-item field="departmentId" label="销售部门">
|
||||
<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-col>
|
||||
</a-row>
|
||||
|
@ -88,6 +115,10 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
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'
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -100,6 +131,138 @@ const emit = defineEmits<{
|
|||
|
||||
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变化更新内部数据
|
||||
watch(
|
||||
() => props.contractData,
|
||||
|
|
|
@ -55,11 +55,9 @@
|
|||
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-link @click="viewDetail(record)">详情</a-link>
|
||||
<a-link v-if="record.contractStatus === '未确认'" @click="editRecord(record)">编辑</a-link>
|
||||
<a-link v-if="record.contractStatus === '待审批'" @click="approveContract(record)">审批</a-link>
|
||||
<a-link @click="viewPayment(record)">收款记录</a-link>
|
||||
<a-link v-if="record.contractStatus !== '已签署' && record.contractStatus !== '已完成'" @click="deleteContract(record)">删除</a-link>
|
||||
<a-link @click="viewDetail(record)">查看</a-link>
|
||||
<a-link @click="editRecord(record)">编辑</a-link>
|
||||
<a-link status="danger" @click="deleteContract(record)">删除</a-link>
|
||||
</a-space>
|
||||
</template>
|
||||
</GiTable>
|
||||
|
@ -89,6 +87,21 @@
|
|||
@update:contract-data="handleContractDataUpdate"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
@ -135,25 +148,16 @@ interface ContractItem {
|
|||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
contractName: '',
|
||||
contractCode: '',
|
||||
client: '',
|
||||
status: '',
|
||||
signDate: '',
|
||||
signDateRange: [] as [string, string] | [],
|
||||
page: 1,
|
||||
size: 10,
|
||||
})
|
||||
|
||||
// 查询条件配置
|
||||
const queryFormColumns = [
|
||||
{
|
||||
field: 'contractName',
|
||||
label: '合同名称',
|
||||
type: 'input' as const,
|
||||
props: {
|
||||
placeholder: '请输入合同名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'client',
|
||||
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 = {
|
||||
page: searchForm.page,
|
||||
pageSize: searchForm.size,
|
||||
contractName: searchForm.contractName,
|
||||
code: searchForm.contractCode,
|
||||
customer: searchForm.client,
|
||||
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)
|
||||
|
||||
if (response.code === 200) {
|
||||
// 过滤出类型为"收入合同"的数据
|
||||
// 过滤出类型为"支出合同"的数据
|
||||
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,
|
||||
pendingAmount: (item.amount || 0) - (item.receivedAmount || 0),
|
||||
}))
|
||||
|
||||
pagination.total = Number.parseInt(response.total) || 0
|
||||
// 更新分页总数(前端过滤后以过滤结果数为准)
|
||||
pagination.total = dataList.value.length
|
||||
} else {
|
||||
Message.error(response.msg || '获取合同列表失败')
|
||||
dataList.value = []
|
||||
|
@ -277,11 +307,10 @@ const search = async () => {
|
|||
|
||||
const reset = () => {
|
||||
Object.assign(searchForm, {
|
||||
contractName: '',
|
||||
contractCode: '',
|
||||
client: '',
|
||||
status: '',
|
||||
signDate: '',
|
||||
signDateRange: [],
|
||||
page: 1,
|
||||
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 = () => {
|
||||
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 = () => {
|
||||
|
@ -339,7 +491,12 @@ const closeEditModal = () => {
|
|||
}
|
||||
|
||||
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 () => {
|
||||
|
@ -391,28 +548,29 @@ const handleEditSubmit = async () => {
|
|||
}
|
||||
|
||||
|
||||
// 删除合同
|
||||
const deleteContract = async (record: ContractItem) => {
|
||||
try {
|
||||
await Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除合同 "${record.projectName}" 吗?`,
|
||||
})
|
||||
|
||||
const response = await http.delete(`/contract/${record.contractId}`)
|
||||
if (response.code === 200) {
|
||||
Message.success('合同删除成功')
|
||||
search() // 刷新列表
|
||||
} else {
|
||||
Message.error(response.msg || '合同删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
// 用户取消删除或请求失败
|
||||
if (error !== 'cancel') {
|
||||
console.error('合同删除失败:', error)
|
||||
Message.error('合同删除失败')
|
||||
}
|
||||
}
|
||||
// 删除合同:只有在确认框点击确定后才执行删除
|
||||
const deleteContract = (record: ContractItem) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除合同 "${record.projectName}" 吗?`,
|
||||
async onOk() {
|
||||
try {
|
||||
const response = await http.del(`/contract/${record.contractId}`)
|
||||
if (response.code === 200) {
|
||||
Message.success('合同删除成功')
|
||||
search()
|
||||
return true
|
||||
} else {
|
||||
Message.error(response.msg || '合同删除失败')
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('合同删除失败:', error)
|
||||
Message.error('合同删除失败')
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 显示合同详情弹窗
|
||||
|
|
|
@ -7,8 +7,16 @@
|
|||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="projectName" label="项目名称">
|
||||
<a-input v-model="contractData.projectName" />
|
||||
<a-form-item field="projectId" label="项目">
|
||||
<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-col>
|
||||
</a-row>
|
||||
|
@ -66,8 +74,27 @@
|
|||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="productService" label="产品或服务">
|
||||
<a-input v-model="contractData.productService" />
|
||||
<a-form-item field="departmentId" label="销售部门">
|
||||
<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-col>
|
||||
</a-row>
|
||||
|
@ -88,6 +115,10 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
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'
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -100,6 +131,134 @@ const emit = defineEmits<{
|
|||
|
||||
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变化更新内部数据
|
||||
watch(
|
||||
() => props.contractData,
|
||||
|
|
|
@ -55,11 +55,9 @@
|
|||
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-link @click="viewDetail(record)">详情</a-link>
|
||||
<a-link v-if="record.contractStatus === '未确认'" @click="editRecord(record)">编辑</a-link>
|
||||
<a-link v-if="record.contractStatus === '待审批'" @click="approveContract(record)">审批</a-link>
|
||||
<a-link @click="viewPayment(record)">收款记录</a-link>
|
||||
<a-link v-if="record.contractStatus !== '已签署' && record.contractStatus !== '已完成'" @click="deleteContract(record)">删除</a-link>
|
||||
<a-link @click="viewDetail(record)">查看</a-link>
|
||||
<a-link @click="editRecord(record)">编辑</a-link>
|
||||
<a-link status="danger" @click="deleteContract(record)">删除</a-link>
|
||||
</a-space>
|
||||
</template>
|
||||
</GiTable>
|
||||
|
@ -391,28 +389,29 @@ const handleEditSubmit = async () => {
|
|||
}
|
||||
|
||||
|
||||
// 删除合同
|
||||
const deleteContract = async (record: ContractItem) => {
|
||||
try {
|
||||
await Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除合同 "${record.projectName}" 吗?`,
|
||||
})
|
||||
|
||||
const response = await http.delete(`/contract/${record.contractId}`)
|
||||
if (response.code === 200) {
|
||||
Message.success('合同删除成功')
|
||||
search() // 刷新列表
|
||||
} else {
|
||||
Message.error(response.msg || '合同删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
// 用户取消删除或请求失败
|
||||
if (error !== 'cancel') {
|
||||
console.error('合同删除失败:', error)
|
||||
Message.error('合同删除失败')
|
||||
}
|
||||
}
|
||||
// 删除合同:只有在确认框点击确定后才执行删除
|
||||
const deleteContract = (record: ContractItem) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除合同 "${record.projectName}" 吗?`,
|
||||
async onOk() {
|
||||
try {
|
||||
const response = await http.del(`/contract/${record.contractId}`)
|
||||
if (response.code === 200) {
|
||||
Message.success('合同删除成功')
|
||||
search()
|
||||
return true
|
||||
} else {
|
||||
Message.error(response.msg || '合同删除失败')
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('合同删除失败:', error)
|
||||
Message.error('合同删除失败')
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 显示合同详情弹窗
|
||||
|
|
Loading…
Reference in New Issue