|
|
|
|
@ -0,0 +1,470 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="contract-sign-field">
|
|
|
|
|
<!-- 提交用隐藏字段 -->
|
|
|
|
|
<input :name="fieldName" type="hidden" :value="normalizedValue" />
|
|
|
|
|
|
|
|
|
|
<!-- 当前展示文本(有值时展示) -->
|
|
|
|
|
<span v-if="hasValue && displayText" class="contract-sign-label" :title="displayText">
|
|
|
|
|
{{ displayText }}
|
|
|
|
|
</span>
|
|
|
|
|
<span
|
|
|
|
|
v-else-if="hasValue"
|
|
|
|
|
class="contract-sign-label contract-sign-label--muted"
|
|
|
|
|
:title="normalizedValue"
|
|
|
|
|
>
|
|
|
|
|
ID: {{ normalizedValue }}
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
<el-button v-if="!hasValue" type="primary" size="small" @click="openDialog">合同签订</el-button>
|
|
|
|
|
<el-button v-if="hasValue" type="primary" size="small" @click="openDialog">重新签订</el-button>
|
|
|
|
|
<el-button v-if="hasValue" size="small" @click="clearValue">清除</el-button>
|
|
|
|
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
title="合同签订"
|
|
|
|
|
:visible.sync="dialogVisible"
|
|
|
|
|
width="60%"
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
append-to-body
|
|
|
|
|
@open="onDialogOpen"
|
|
|
|
|
@close="onDialogClose"
|
|
|
|
|
>
|
|
|
|
|
<el-form
|
|
|
|
|
:model="form"
|
|
|
|
|
:rules="rules"
|
|
|
|
|
ref="formRef"
|
|
|
|
|
label-width="120px"
|
|
|
|
|
v-loading="loading"
|
|
|
|
|
>
|
|
|
|
|
<el-form-item label="合同编号" prop="contract_no">
|
|
|
|
|
<el-input v-model="form.contract_no" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="标题">
|
|
|
|
|
<el-input v-model="form.title" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="甲方">
|
|
|
|
|
<el-input v-model="form.party_a" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="乙方">
|
|
|
|
|
<el-input v-model="form.party_b" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="总额">
|
|
|
|
|
<el-input-number v-model="form.amount_total" :min="0" :step="1000" style="width: 100%" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="金额类型">
|
|
|
|
|
<el-select v-model="form.amount_type" placeholder="请选择金额类型" style="width: 100%">
|
|
|
|
|
<el-option label="闭口合同(金额确定)" value="fixed" />
|
|
|
|
|
<el-option label="框架协议/开口合同(金额不确定)" value="open" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="状态">
|
|
|
|
|
<el-select v-model="form.status" style="width: 100%">
|
|
|
|
|
<el-option label="草稿" value="draft" />
|
|
|
|
|
<el-option label="生效" value="approved" />
|
|
|
|
|
<el-option label="终止" value="terminated" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="签订日期">
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="form.sign_date"
|
|
|
|
|
type="date"
|
|
|
|
|
value-format="yyyy-MM-dd"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="开始日期">
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="form.start_date"
|
|
|
|
|
type="date"
|
|
|
|
|
value-format="yyyy-MM-dd"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="结束日期">
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="form.end_date"
|
|
|
|
|
type="date"
|
|
|
|
|
value-format="yyyy-MM-dd"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
<el-divider content-position="left">付款计划</el-divider>
|
|
|
|
|
<div class="payplan-toolbar">
|
|
|
|
|
<el-button type="primary" size="small" @click="addPayPlan">新增计划</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<el-table :data="form.pay_plans" border size="small" class="mb-16">
|
|
|
|
|
<el-table-column prop="phase_no" label="期次" width="100">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-input-number v-model="row.phase_no" :min="1" size="small" />
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="due_date" label="计划付款日" width="150">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="row.due_date"
|
|
|
|
|
type="date"
|
|
|
|
|
value-format="yyyy-MM-dd"
|
|
|
|
|
size="small"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="amount_plan" label="计划金额" width="140">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-input-number
|
|
|
|
|
v-model="row.amount_plan"
|
|
|
|
|
:min="0"
|
|
|
|
|
:step="1000"
|
|
|
|
|
size="small"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="操作" width="100">
|
|
|
|
|
<template #default="{ $index }">
|
|
|
|
|
<el-button type="danger" link size="small" @click="removePayPlan($index)">移除</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
|
|
|
<el-button type="primary" :loading="saving" @click="handleSubmit">保存</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
import request from "@/utils/request";
|
|
|
|
|
import { Message } from 'element-ui';
|
|
|
|
|
import {
|
|
|
|
|
getFlowDetailForContract,
|
|
|
|
|
getContractSettings,
|
|
|
|
|
getContractByFlowId,
|
|
|
|
|
createContract,
|
|
|
|
|
} from "@/api/flow";
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
name: "ContractSignField",
|
|
|
|
|
props: {
|
|
|
|
|
fieldName: { type: String, required: true },
|
|
|
|
|
value: { type: [String, Number], default: "" },
|
|
|
|
|
display: { type: String, default: "" },
|
|
|
|
|
flowId: { type: [String, Number], default: "" },
|
|
|
|
|
},
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
dialogVisible: false,
|
|
|
|
|
loading: false,
|
|
|
|
|
saving: false,
|
|
|
|
|
formRef: null,
|
|
|
|
|
form: {
|
|
|
|
|
contract_no: "",
|
|
|
|
|
title: "",
|
|
|
|
|
party_a: "",
|
|
|
|
|
party_b: "",
|
|
|
|
|
amount_total: 0,
|
|
|
|
|
amount_type: "fixed",
|
|
|
|
|
status: "draft",
|
|
|
|
|
sign_date: "",
|
|
|
|
|
start_date: "",
|
|
|
|
|
end_date: "",
|
|
|
|
|
pay_plans: [],
|
|
|
|
|
},
|
|
|
|
|
rules: {
|
|
|
|
|
contract_no: [{ required: true, message: "请填写合同编号", trigger: "blur" }],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
normalizedValue() {
|
|
|
|
|
return this.value === null || this.value === undefined ? "" : String(this.value);
|
|
|
|
|
},
|
|
|
|
|
hasValue() {
|
|
|
|
|
return this.normalizedValue !== "";
|
|
|
|
|
},
|
|
|
|
|
displayText() {
|
|
|
|
|
return this.hasValue ? this.display || "" : "";
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
openDialog() {
|
|
|
|
|
this.dialogVisible = true;
|
|
|
|
|
},
|
|
|
|
|
async onDialogOpen() {
|
|
|
|
|
this.resetForm();
|
|
|
|
|
if (this.flowId) {
|
|
|
|
|
// 先检查是否存在相同flow_id的合同,如果存在则加载合同数据
|
|
|
|
|
const contractExists = await this.loadExistingContract();
|
|
|
|
|
if (!contractExists) {
|
|
|
|
|
// 如果不存在合同,则从流程数据预填
|
|
|
|
|
this.loadFlowDataAndPrefill();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onDialogClose() {
|
|
|
|
|
// 关闭时不做处理
|
|
|
|
|
},
|
|
|
|
|
resetForm() {
|
|
|
|
|
this.form = {
|
|
|
|
|
contract_no: "",
|
|
|
|
|
title: "",
|
|
|
|
|
party_a: "",
|
|
|
|
|
party_b: "",
|
|
|
|
|
amount_total: 0,
|
|
|
|
|
amount_type: "fixed",
|
|
|
|
|
status: "draft",
|
|
|
|
|
sign_date: "",
|
|
|
|
|
start_date: "",
|
|
|
|
|
end_date: "",
|
|
|
|
|
pay_plans: [],
|
|
|
|
|
};
|
|
|
|
|
if (this.$refs.formRef) {
|
|
|
|
|
this.$refs.formRef.clearValidate();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
async loadExistingContract() {
|
|
|
|
|
if (!this.flowId) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.loading = true;
|
|
|
|
|
try {
|
|
|
|
|
const res = await getContractByFlowId(this.flowId);
|
|
|
|
|
const contractData = res?.data || res;
|
|
|
|
|
|
|
|
|
|
if (contractData) {
|
|
|
|
|
// 填充合同数据
|
|
|
|
|
this.form.contract_no = contractData.contract_no || "";
|
|
|
|
|
this.form.title = contractData.title || "";
|
|
|
|
|
this.form.party_a = contractData.party_a || "";
|
|
|
|
|
this.form.party_b = contractData.party_b || "";
|
|
|
|
|
this.form.amount_total = contractData.amount_total || 0;
|
|
|
|
|
this.form.amount_type = contractData.amount_type || "fixed";
|
|
|
|
|
this.form.status = contractData.status || "draft";
|
|
|
|
|
this.form.sign_date = contractData.sign_date || "";
|
|
|
|
|
this.form.start_date = contractData.start_date || "";
|
|
|
|
|
this.form.end_date = contractData.end_date || "";
|
|
|
|
|
|
|
|
|
|
// 填充付款计划
|
|
|
|
|
if (contractData.pay_plans && Array.isArray(contractData.pay_plans)) {
|
|
|
|
|
this.form.pay_plans = contractData.pay_plans.map((plan) => ({
|
|
|
|
|
phase_no: plan.phase_no || 1,
|
|
|
|
|
due_date: plan.due_date || "",
|
|
|
|
|
amount_plan: plan.amount_plan || 0,
|
|
|
|
|
}));
|
|
|
|
|
} else {
|
|
|
|
|
this.form.pay_plans = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("加载已有合同失败:", e);
|
|
|
|
|
Message.error("加载已有合同失败");
|
|
|
|
|
return false;
|
|
|
|
|
} finally {
|
|
|
|
|
this.loading = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
async loadFlowDataAndPrefill() {
|
|
|
|
|
if (!this.flowId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.loading = true;
|
|
|
|
|
try {
|
|
|
|
|
// 并行请求flow详情和合同设置
|
|
|
|
|
const [flowDetailRes, settingsRes] = await Promise.all([
|
|
|
|
|
getFlowDetailForContract(this.flowId),
|
|
|
|
|
getContractSettings(this.flowId),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const flowDetail = flowDetailRes?.data || flowDetailRes;
|
|
|
|
|
const settings = settingsRes?.data || settingsRes;
|
|
|
|
|
|
|
|
|
|
if (!flowDetail?.flow?.data) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const flowData = flowDetail.flow.data;
|
|
|
|
|
const contractMapping = (settings?.contract_field_mapping || []);
|
|
|
|
|
const payplanMapping = (settings?.payplan_field_mapping || []);
|
|
|
|
|
|
|
|
|
|
// 根据映射填充合同字段
|
|
|
|
|
if (contractMapping && contractMapping.length > 0) {
|
|
|
|
|
contractMapping.forEach((map) => {
|
|
|
|
|
const budgetField = map.budget_field;
|
|
|
|
|
const oaField = map.oa_field;
|
|
|
|
|
if (budgetField && oaField && flowData[oaField] !== undefined) {
|
|
|
|
|
if (this.form.hasOwnProperty(budgetField)) {
|
|
|
|
|
this.form[budgetField] = flowData[oaField];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 填充付款计划
|
|
|
|
|
if (settings.oa_custom_model_id_for_payplan && flowDetail.customModel) {
|
|
|
|
|
// 尝试从子表提取
|
|
|
|
|
const payplanField = flowDetail.customModel.fields.find(
|
|
|
|
|
(f) => f.type === "relation" && f.sub_custom_model_id === settings.oa_custom_model_id_for_payplan
|
|
|
|
|
);
|
|
|
|
|
if (payplanField && flowData[payplanField.name]) {
|
|
|
|
|
let payplanRows = flowData[payplanField.name];
|
|
|
|
|
if (typeof payplanRows === "string") {
|
|
|
|
|
try {
|
|
|
|
|
payplanRows = JSON.parse(payplanRows);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
payplanRows = [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(payplanRows) && payplanRows.length > 0) {
|
|
|
|
|
this.form.pay_plans = this.mapPayPlans(payplanRows, payplanMapping);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (flowData.pay_plans && Array.isArray(flowData.pay_plans)) {
|
|
|
|
|
// 从主表的pay_plans字段提取
|
|
|
|
|
this.form.pay_plans = this.mapPayPlans(flowData.pay_plans, payplanMapping);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("加载流程数据失败:", e);
|
|
|
|
|
Message.error("加载流程数据失败");
|
|
|
|
|
} finally {
|
|
|
|
|
this.loading = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
mapPayPlans(rows, mapping) {
|
|
|
|
|
if (!Array.isArray(rows) || !Array.isArray(mapping) || mapping.length === 0) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return rows.map((row, index) => {
|
|
|
|
|
const item = {};
|
|
|
|
|
mapping.forEach((map) => {
|
|
|
|
|
const budgetField = map.budget_field;
|
|
|
|
|
const oaField = map.oa_field;
|
|
|
|
|
if (budgetField && oaField && row[oaField] !== undefined) {
|
|
|
|
|
item[budgetField] = row[oaField];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (!item.phase_no) {
|
|
|
|
|
item.phase_no = index + 1;
|
|
|
|
|
}
|
|
|
|
|
if (item.amount_plan === undefined) {
|
|
|
|
|
item.amount_plan = 0;
|
|
|
|
|
}
|
|
|
|
|
return item;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
addPayPlan() {
|
|
|
|
|
const maxPhaseNo =
|
|
|
|
|
this.form.pay_plans.length > 0
|
|
|
|
|
? Math.max(...this.form.pay_plans.map((p) => p.phase_no || 0))
|
|
|
|
|
: 0;
|
|
|
|
|
this.form.pay_plans.push({
|
|
|
|
|
phase_no: maxPhaseNo + 1,
|
|
|
|
|
due_date: "",
|
|
|
|
|
amount_plan: 0,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
removePayPlan(index) {
|
|
|
|
|
this.form.pay_plans.splice(index, 1);
|
|
|
|
|
},
|
|
|
|
|
async handleSubmit() {
|
|
|
|
|
if (!this.$refs.formRef) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.$refs.formRef.validate(async (valid) => {
|
|
|
|
|
if (!valid) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.saving = true;
|
|
|
|
|
try {
|
|
|
|
|
const payload = {
|
|
|
|
|
flow_id: this.flowId ? parseInt(this.flowId) : null,
|
|
|
|
|
contract: {
|
|
|
|
|
contract_no: this.form.contract_no,
|
|
|
|
|
title: this.form.title,
|
|
|
|
|
party_a: this.form.party_a,
|
|
|
|
|
party_b: this.form.party_b,
|
|
|
|
|
amount_total: this.form.amount_total,
|
|
|
|
|
amount_type: this.form.amount_type,
|
|
|
|
|
status: this.form.status,
|
|
|
|
|
sign_date: this.form.sign_date || null,
|
|
|
|
|
start_date: this.form.start_date || null,
|
|
|
|
|
end_date: this.form.end_date || null,
|
|
|
|
|
},
|
|
|
|
|
pay_plans: this.form.pay_plans.map((p) => ({
|
|
|
|
|
phase_no: p.phase_no,
|
|
|
|
|
due_date: p.due_date || null,
|
|
|
|
|
amount_plan: p.amount_plan || 0,
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const res = await createContract(payload);
|
|
|
|
|
const data = res?.data || res;
|
|
|
|
|
|
|
|
|
|
if (data?.id) {
|
|
|
|
|
const displayText = data.contract_no + (data.title ? ` / ${data.title}` : "");
|
|
|
|
|
this.$emit("input", String(data.id));
|
|
|
|
|
this.$emit("update:display", displayText);
|
|
|
|
|
this.dialogVisible = false;
|
|
|
|
|
Message.success("合同创建成功");
|
|
|
|
|
} else {
|
|
|
|
|
Message.error("创建合同失败");
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("创建合同失败:", e);
|
|
|
|
|
const msg = e?.response?.data?.msg || e?.message || "创建合同失败";
|
|
|
|
|
Message.error(msg);
|
|
|
|
|
} finally {
|
|
|
|
|
this.saving = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
clearValue() {
|
|
|
|
|
this.$emit("input", "");
|
|
|
|
|
this.$emit("update:display", "");
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.contract-sign-field {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.contract-sign-label {
|
|
|
|
|
color: #606266;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 160px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.contract-sign-label--muted {
|
|
|
|
|
color: #909399;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.payplan-toolbar {
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mb-16 {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|