286 lines
9.5 KiB
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>
|