|
|
|
|
@ -49,6 +49,19 @@
|
|
|
|
|
:value="f.name"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-checkbox
|
|
|
|
|
:model-value="item.value_transform === 'json_field'"
|
|
|
|
|
@change="(checked) => toggleJsonFieldTransform(item, checked)"
|
|
|
|
|
>
|
|
|
|
|
JSON取字段
|
|
|
|
|
</el-checkbox>
|
|
|
|
|
<el-input
|
|
|
|
|
v-if="item.value_transform === 'json_field'"
|
|
|
|
|
v-model="item.json_field"
|
|
|
|
|
placeholder="id"
|
|
|
|
|
class="w-120"
|
|
|
|
|
/>
|
|
|
|
|
<el-tag v-if="isJsonCandidate(item, oaFieldsMain)" size="small" type="warning" effect="plain">疑似JSON</el-tag>
|
|
|
|
|
<el-button type="danger" link @click="removeContractField(idx)">移除</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<el-button type="primary" link @click="addContractField">新增映射</el-button>
|
|
|
|
|
@ -80,6 +93,19 @@
|
|
|
|
|
:value="f.name"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-checkbox
|
|
|
|
|
:model-value="item.value_transform === 'json_field'"
|
|
|
|
|
@change="(checked) => toggleJsonFieldTransform(item, checked)"
|
|
|
|
|
>
|
|
|
|
|
JSON取字段
|
|
|
|
|
</el-checkbox>
|
|
|
|
|
<el-input
|
|
|
|
|
v-if="item.value_transform === 'json_field'"
|
|
|
|
|
v-model="item.json_field"
|
|
|
|
|
placeholder="id"
|
|
|
|
|
class="w-120"
|
|
|
|
|
/>
|
|
|
|
|
<el-tag v-if="isJsonCandidate(item, oaFieldsSub)" size="small" type="warning" effect="plain">疑似JSON</el-tag>
|
|
|
|
|
<el-button type="danger" link @click="removePayplanField(idx)">移除</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<el-button type="primary" link @click="addPayplanField">新增映射</el-button>
|
|
|
|
|
@ -89,16 +115,175 @@
|
|
|
|
|
<el-form-item>
|
|
|
|
|
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
|
|
|
|
|
<el-button @click="loadData">重置</el-button>
|
|
|
|
|
<el-button type="warning" @click="openSimulationDialog">模拟</el-button>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="simulationDialogVisible"
|
|
|
|
|
title="合同映射模拟"
|
|
|
|
|
width="82%"
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
>
|
|
|
|
|
<el-alert
|
|
|
|
|
title="模拟只读取当前页面设置和所选OA流程生成合同草稿,不会创建合同,不会写入付款计划。"
|
|
|
|
|
type="info"
|
|
|
|
|
show-icon
|
|
|
|
|
:closable="false"
|
|
|
|
|
class="mb-16"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<el-form label-width="160px">
|
|
|
|
|
<el-form-item label="合同流程">
|
|
|
|
|
<div class="simulation-flow-picker">
|
|
|
|
|
<el-select
|
|
|
|
|
v-model="simulationFlowId"
|
|
|
|
|
placeholder="选择一个合同流程"
|
|
|
|
|
filterable
|
|
|
|
|
remote
|
|
|
|
|
clearable
|
|
|
|
|
reserve-keyword
|
|
|
|
|
:remote-method="searchSimulationFlows"
|
|
|
|
|
:loading="simulationFlowsLoading"
|
|
|
|
|
class="simulation-flow-select"
|
|
|
|
|
@focus="loadSimulationFlows()"
|
|
|
|
|
>
|
|
|
|
|
<el-option
|
|
|
|
|
v-for="flow in simulationFlows"
|
|
|
|
|
:key="flow.id"
|
|
|
|
|
:label="flow.label"
|
|
|
|
|
:value="flow.id"
|
|
|
|
|
>
|
|
|
|
|
<div class="flow-option">
|
|
|
|
|
<span>{{ flow.label }}</span>
|
|
|
|
|
<small>{{ flow.created_at || '' }}</small>
|
|
|
|
|
</div>
|
|
|
|
|
</el-option>
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-button type="primary" :loading="simulationLoading" :disabled="!simulationFlowId" @click="runSimulation">
|
|
|
|
|
生成模拟
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
<template v-if="simulationResult">
|
|
|
|
|
<el-divider content-position="left">合同签订表单预览</el-divider>
|
|
|
|
|
<el-form :model="simulationContract" label-width="150px" disabled class="simulation-contract-form">
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="合同编号">
|
|
|
|
|
<el-input :model-value="simulationContract.contract_no || ''" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="合同名称">
|
|
|
|
|
<el-input :model-value="simulationContract.title || ''" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-form-item label="合同主要内容">
|
|
|
|
|
<el-input :model-value="simulationContract.main_content || ''" type="textarea" :rows="3" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="甲方">
|
|
|
|
|
<el-input :model-value="simulationContract.party_a || ''" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="乙方">
|
|
|
|
|
<el-input :model-value="simulationContract.party_b || ''" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="合同金额(元)">
|
|
|
|
|
<el-input :model-value="formatAmount(simulationContract.amount_total)" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="项目预算金额(元)">
|
|
|
|
|
<el-input :model-value="formatAmount(simulationContract.budget_amount)" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="金额类型">
|
|
|
|
|
<el-input :model-value="getAmountTypeLabel(simulationContract.amount_type)" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="合同类型">
|
|
|
|
|
<el-input :model-value="simulationContract.contract_type || simulationContract.contract_type_id || ''" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="签订日期">
|
|
|
|
|
<el-input :model-value="simulationContract.sign_date || ''" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="申请日期">
|
|
|
|
|
<el-input :model-value="simulationContract.apply_date || ''" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="合同履行期">
|
|
|
|
|
<el-input :model-value="simulationContract.perform_period || ''" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="付款方式">
|
|
|
|
|
<el-input :model-value="simulationContract.pay_method || ''" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-form-item label="合同盖章版本附件">
|
|
|
|
|
<el-input :model-value="formatAttachmentValue(simulationContract.attachment_id)" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="备注">
|
|
|
|
|
<el-input :model-value="simulationContract.remark || ''" type="textarea" :rows="2" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
<el-divider content-position="left">付款计划预览</el-divider>
|
|
|
|
|
<el-table :data="simulationPayPlans" border size="small" class="mb-16">
|
|
|
|
|
<el-table-column prop="phase_no" label="期次" width="100" />
|
|
|
|
|
<el-table-column prop="due_date" label="计划付款日" width="150" />
|
|
|
|
|
<el-table-column prop="amount_plan" label="计划金额" width="160">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
{{ formatAmount(row.amount_plan) }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
<el-divider content-position="left">映射结果明细</el-divider>
|
|
|
|
|
<el-table :data="simulationMappingRows" border size="small">
|
|
|
|
|
<el-table-column prop="targetLabel" label="合同字段" width="180" />
|
|
|
|
|
<el-table-column prop="sourceLabel" label="OA字段" width="180" />
|
|
|
|
|
<el-table-column prop="value" label="模拟值" show-overflow-tooltip />
|
|
|
|
|
</el-table>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
<el-button @click="simulationDialogVisible = false">关闭</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, watch } from 'vue'
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
import { contractSettingsAPI } from '@/utils/api'
|
|
|
|
|
import { contractAPI, contractSettingsAPI } from '@/utils/api'
|
|
|
|
|
import { Setting } from '@element-plus/icons-vue'
|
|
|
|
|
|
|
|
|
|
const form = ref(getEmptyForm())
|
|
|
|
|
@ -110,6 +295,30 @@ const oaFieldsSub = ref([])
|
|
|
|
|
const oaFieldsLoading = ref(false)
|
|
|
|
|
const contractFields = ref([])
|
|
|
|
|
const payplanFields = ref([])
|
|
|
|
|
const simulationDialogVisible = ref(false)
|
|
|
|
|
const simulationFlows = ref([])
|
|
|
|
|
const simulationFlowsLoading = ref(false)
|
|
|
|
|
const simulationFlowId = ref('')
|
|
|
|
|
const simulationLoading = ref(false)
|
|
|
|
|
const simulationResult = ref(null)
|
|
|
|
|
const simulationContract = ref({})
|
|
|
|
|
const simulationPayPlans = ref([])
|
|
|
|
|
const simulationMappingRows = ref([])
|
|
|
|
|
|
|
|
|
|
function normalizeNullableId(value) {
|
|
|
|
|
if (value === null || value === undefined || value === '') {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
const id = Number(value)
|
|
|
|
|
return Number.isNaN(id) ? value : id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeModelOptions(list) {
|
|
|
|
|
return (Array.isArray(list) ? list : []).map((item) => ({
|
|
|
|
|
...item,
|
|
|
|
|
id: normalizeNullableId(item.id)
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getEmptyForm() {
|
|
|
|
|
return {
|
|
|
|
|
@ -154,14 +363,142 @@ function removePayplanField(idx) {
|
|
|
|
|
form.value.payplan_field_mapping.splice(idx, 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleJsonFieldTransform(item, checked) {
|
|
|
|
|
if (checked) {
|
|
|
|
|
item.value_transform = 'json_field'
|
|
|
|
|
if (!item.json_field) {
|
|
|
|
|
item.json_field = 'id'
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
item.value_transform = ''
|
|
|
|
|
item.json_field = ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSelectedOaField(item, fields) {
|
|
|
|
|
return fields.find((field) => field.name === item.oa_field)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isJsonCandidate(item, fields) {
|
|
|
|
|
const field = getSelectedOaField(item, fields)
|
|
|
|
|
return ['file', 'image', 'upload', 'relation'].includes(field?.type)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatAmount(amount) {
|
|
|
|
|
if (amount === null || amount === undefined || amount === '') {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
const num = Number(amount)
|
|
|
|
|
if (Number.isNaN(num)) {
|
|
|
|
|
return String(amount)
|
|
|
|
|
}
|
|
|
|
|
return num.toLocaleString('zh-CN', {
|
|
|
|
|
minimumFractionDigits: 2,
|
|
|
|
|
maximumFractionDigits: 2
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getAmountTypeLabel(value) {
|
|
|
|
|
if (value === 'fixed') return '闭口合同(金额确定)'
|
|
|
|
|
if (value === 'open') return '框架协议/开口合同(金额不确定)'
|
|
|
|
|
return value || ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatAttachmentValue(value) {
|
|
|
|
|
if (value === null || value === undefined || value === '') {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
return value.join(', ')
|
|
|
|
|
}
|
|
|
|
|
if (typeof value === 'object') {
|
|
|
|
|
return value.name || value.original_name || value.id || JSON.stringify(value)
|
|
|
|
|
}
|
|
|
|
|
return `附件ID:${value}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getFieldLabel(fields, fieldName) {
|
|
|
|
|
const field = fields.find((item) => item.field === fieldName || item.name === fieldName)
|
|
|
|
|
return field?.label || fieldName || ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildSimulationMappingRows(contract) {
|
|
|
|
|
return (Array.isArray(form.value.contract_field_mapping) ? form.value.contract_field_mapping : [])
|
|
|
|
|
.filter((item) => item.budget_field || item.oa_field)
|
|
|
|
|
.map((item) => ({
|
|
|
|
|
targetLabel: getFieldLabel(contractFields.value, item.budget_field),
|
|
|
|
|
sourceLabel: getFieldLabel(oaFieldsMain.value, item.oa_field),
|
|
|
|
|
value: contract?.[item.budget_field] ?? ''
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadSimulationFlows(keyword = '') {
|
|
|
|
|
if (!form.value.oa_custom_model_id) {
|
|
|
|
|
simulationFlows.value = []
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
simulationFlowsLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const res = await contractSettingsAPI.getOaFlows({
|
|
|
|
|
custom_model_id: form.value.oa_custom_model_id,
|
|
|
|
|
keyword,
|
|
|
|
|
limit: 30
|
|
|
|
|
})
|
|
|
|
|
simulationFlows.value = res?.data || res || []
|
|
|
|
|
} catch (e) {
|
|
|
|
|
simulationFlows.value = []
|
|
|
|
|
} finally {
|
|
|
|
|
simulationFlowsLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function searchSimulationFlows(keyword) {
|
|
|
|
|
loadSimulationFlows(keyword)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openSimulationDialog() {
|
|
|
|
|
if (!form.value.oa_custom_model_id) {
|
|
|
|
|
ElMessage.warning('请先选择主表对应OA模型')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
simulationDialogVisible.value = true
|
|
|
|
|
simulationResult.value = null
|
|
|
|
|
simulationContract.value = {}
|
|
|
|
|
simulationPayPlans.value = []
|
|
|
|
|
simulationMappingRows.value = []
|
|
|
|
|
await loadSimulationFlows()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runSimulation() {
|
|
|
|
|
if (!simulationFlowId.value) {
|
|
|
|
|
ElMessage.warning('请选择合同流程')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
simulationLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const res = await contractAPI.simulateFromFlow(simulationFlowId.value, form.value)
|
|
|
|
|
const data = res?.data || res || {}
|
|
|
|
|
const prepared = data.prepared_contract || {}
|
|
|
|
|
simulationResult.value = prepared
|
|
|
|
|
simulationContract.value = prepared.contract || {}
|
|
|
|
|
simulationPayPlans.value = Array.isArray(prepared.pay_plans) ? prepared.pay_plans : []
|
|
|
|
|
simulationMappingRows.value = buildSimulationMappingRows(simulationContract.value)
|
|
|
|
|
ElMessage.success('模拟生成完成,未写入任何合同数据')
|
|
|
|
|
} catch (e) {
|
|
|
|
|
ElMessage.error(e?.message || '模拟失败')
|
|
|
|
|
} finally {
|
|
|
|
|
simulationLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadData() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await contractSettingsAPI.get()
|
|
|
|
|
const data = res?.data || res
|
|
|
|
|
if (data) {
|
|
|
|
|
form.value = {
|
|
|
|
|
oa_custom_model_id: data.oa_custom_model_id || '',
|
|
|
|
|
oa_custom_model_id_for_payplan: data.oa_custom_model_id_for_payplan || '',
|
|
|
|
|
oa_custom_model_id: normalizeNullableId(data.oa_custom_model_id),
|
|
|
|
|
oa_custom_model_id_for_payplan: normalizeNullableId(data.oa_custom_model_id_for_payplan),
|
|
|
|
|
contract_field_mapping: data.contract_field_mapping?.length ? data.contract_field_mapping : getEmptyForm().contract_field_mapping,
|
|
|
|
|
payplan_field_mapping: data.payplan_field_mapping?.length ? data.payplan_field_mapping : getEmptyForm().payplan_field_mapping
|
|
|
|
|
}
|
|
|
|
|
@ -176,13 +513,13 @@ async function loadData() {
|
|
|
|
|
async function loadOptions() {
|
|
|
|
|
try {
|
|
|
|
|
const modelsRes = await contractSettingsAPI.getOaModels()
|
|
|
|
|
oaModels.value = modelsRes?.data || modelsRes || []
|
|
|
|
|
oaModels.value = normalizeModelOptions(modelsRes?.data || modelsRes || [])
|
|
|
|
|
} catch (e) {
|
|
|
|
|
oaModels.value = []
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const subRes = await contractSettingsAPI.getOaSubModels()
|
|
|
|
|
oaSubModels.value = subRes?.data || subRes || []
|
|
|
|
|
oaSubModels.value = normalizeModelOptions(subRes?.data || subRes || [])
|
|
|
|
|
} catch (e) {
|
|
|
|
|
oaSubModels.value = []
|
|
|
|
|
}
|
|
|
|
|
@ -285,5 +622,29 @@ watch(() => [form.value.oa_custom_model_id, form.value.oa_custom_model_id_for_pa
|
|
|
|
|
.w-200 {
|
|
|
|
|
width: 200px;
|
|
|
|
|
}
|
|
|
|
|
.w-120 {
|
|
|
|
|
width: 120px;
|
|
|
|
|
}
|
|
|
|
|
.mb-16 {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
.simulation-flow-picker {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
.simulation-flow-select {
|
|
|
|
|
width: 520px;
|
|
|
|
|
}
|
|
|
|
|
.flow-option {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
}
|
|
|
|
|
.flow-option small {
|
|
|
|
|
color: #909399;
|
|
|
|
|
}
|
|
|
|
|
.simulation-contract-form {
|
|
|
|
|
max-width: 1000px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|