Industrial-image-management.../src/views/project-management/contract/expense-contract/ContractEdit.vue

286 lines
9.5 KiB
Vue

<template>
<a-form :model="contractData" layout="vertical">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="code" label="合同编号">
<a-input v-model="contractData.code" />
</a-form-item>
</a-col>
<a-col :span="12">
<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>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="customer" label="客户名称">
<a-input v-model="contractData.customer" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="amount" label="合同金额">
<a-input-number v-model="contractData.amount" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="accountNumber" label="收款账号">
<a-input v-model="contractData.accountNumber" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="contractStatus" label="合同状态">
<a-select v-model="contractData.contractStatus">
<a-option value="未确认">未确认</a-option>
<a-option value="待审批">待审批</a-option>
<a-option value="已签署">已签署</a-option>
<a-option value="执行中">执行中</a-option>
<a-option value="已完成">已完成</a-option>
<a-option value="已终止">已终止</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="signDate" label="签订日期">
<a-date-picker v-model="contractData.signDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="performanceDeadline" label="履约期限">
<a-date-picker v-model="contractData.performanceDeadline" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="paymentDate" label="付款日期">
<a-date-picker v-model="contractData.paymentDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<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>
<a-form-item field="paymentAddress" label="付款地址">
<a-input v-model="contractData.paymentAddress" />
</a-form-item>
<a-form-item field="notes" label="备注">
<a-textarea v-model="contractData.notes" />
</a-form-item>
<a-form-item field="contractText" label="合同内容">
<a-textarea v-model="contractData.contractText" />
</a-form-item>
</a-form>
</template>
<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<{
contractData: ContractItem
}>()
const emit = defineEmits<{
(e: 'update:contractData', data: ContractItem): void
}>()
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,
(newVal) => {
if (newVal) {
contractData.value = { ...newVal }
}
},
{ immediate: true },
)
// 监听内部数据变化并触发更新事件
watch(
contractData,
(newVal) => {
emit('update:contractData', newVal)
},
{ deep: true },
)
</script>