|
|
<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-dialog
|
|
|
title="合同签订"
|
|
|
:visible.sync="dialogVisible"
|
|
|
width="75%"
|
|
|
:close-on-click-modal="false"
|
|
|
append-to-body
|
|
|
@open="onDialogOpen"
|
|
|
@close="onDialogClose"
|
|
|
>
|
|
|
<el-form
|
|
|
:model="form"
|
|
|
:rules="rules"
|
|
|
ref="formRef"
|
|
|
label-width="140px"
|
|
|
v-loading="loading"
|
|
|
>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="合同编号" prop="contract_no_without_prefix">
|
|
|
<el-input v-model="form.contract_no_without_prefix">
|
|
|
<template slot="prepend">CZHT</template>
|
|
|
</el-input>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="合同名称" prop="title">
|
|
|
<el-input v-model="form.title" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-form-item label="合同主要内容" prop="main_content">
|
|
|
<el-input v-model="form.main_content" type="textarea" :rows="3" />
|
|
|
</el-form-item>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="甲方" prop="party_a">
|
|
|
<el-input v-model="form.party_a" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="乙方" prop="party_b">
|
|
|
<el-input v-model="form.party_b" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="合同金额(元)" prop="amount_total" required>
|
|
|
<el-input-number
|
|
|
v-model="form.amount_total"
|
|
|
:min="0"
|
|
|
:precision="2"
|
|
|
style="width: 100%"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="项目预算金额(元)" prop="budget_amount">
|
|
|
<el-input-number v-model="form.budget_amount" :min="0" :precision="2" style="width: 100%" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="金额类型" prop="amount_type">
|
|
|
<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-col>
|
|
|
<el-col :span="12" v-if="form.amount_type === 'open'">
|
|
|
<el-form-item label="金额说明" prop="amount_description">
|
|
|
<el-input v-model="form.amount_description" type="textarea" :rows="2" placeholder="请说明开口合同的金额情况" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="合同类型" prop="contract_type_id">
|
|
|
<el-select v-model="form.contract_type_id" placeholder="请选择合同类型" filterable clearable style="width: 100%" @change="handleContractTypeChange">
|
|
|
<el-option
|
|
|
v-for="type in contractTypeOptions"
|
|
|
:key="type.id"
|
|
|
:label="type.name"
|
|
|
:value="type.id"
|
|
|
/>
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="签订日期" prop="sign_date">
|
|
|
<el-date-picker
|
|
|
v-model="form.sign_date"
|
|
|
type="date"
|
|
|
value-format="yyyy-MM-dd"
|
|
|
style="width: 100%"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="申请日期" prop="apply_date">
|
|
|
<el-date-picker
|
|
|
v-model="form.apply_date"
|
|
|
type="date"
|
|
|
value-format="yyyy-MM-dd"
|
|
|
style="width: 100%"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="合同履行期" prop="perform_period">
|
|
|
<el-input v-model="form.perform_period" placeholder="例如:2025-01-01 至 2025-12-31" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="付款方式" prop="pay_method">
|
|
|
<el-input v-model="form.pay_method" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="预算年度" prop="fund_source_year_id">
|
|
|
<el-select v-model="form.fund_source_year_id" placeholder="请选择预算年度" filterable clearable style="width: 100%" @change="handleFundSourceYearChange">
|
|
|
<el-option
|
|
|
v-for="year in budgetYearOptions"
|
|
|
:key="year.value"
|
|
|
:label="year.label"
|
|
|
:value="year.value"
|
|
|
/>
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="项目经费来源" prop="fund_source_budget_data_id">
|
|
|
<el-select v-model="form.fund_source_budget_data_id" placeholder="请选择项目经费来源" filterable clearable style="width: 100%">
|
|
|
<el-option-group
|
|
|
v-for="group in fundSourceOptions"
|
|
|
:key="group.label"
|
|
|
:label="group.label"
|
|
|
>
|
|
|
<el-option
|
|
|
v-for="option in group.options"
|
|
|
:key="option.value"
|
|
|
:label="option.label"
|
|
|
:value="option.value"
|
|
|
/>
|
|
|
</el-option-group>
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="是否为政府采购" prop="is_government_purchase">
|
|
|
<el-switch v-model="form.is_government_purchase" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="招标代理机构">
|
|
|
<el-input v-model="form.tender_agent" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="采购方式" prop="purchase_method_id">
|
|
|
<el-select v-model="form.purchase_method_id" placeholder="请选择采购方式" filterable clearable style="width: 100%">
|
|
|
<el-option
|
|
|
v-for="method in purchaseMethodOptions"
|
|
|
:key="method.id"
|
|
|
:label="method.name"
|
|
|
:value="method.id"
|
|
|
/>
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="合同履行情况">
|
|
|
<el-input v-model="form.perform_status" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="是否验收" prop="is_accepted">
|
|
|
<el-switch v-model="form.is_accepted" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-form-item label="采购类别" prop="purchase_category_id">
|
|
|
<el-select v-model="form.purchase_category_id" placeholder="请选择采购类别" filterable clearable style="width: 100%">
|
|
|
<el-option
|
|
|
v-for="category in purchaseCategoryOptions"
|
|
|
:key="category.id"
|
|
|
:label="category.name"
|
|
|
:value="category.id"
|
|
|
/>
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="科室" prop="owner_department_ids_array">
|
|
|
<el-select v-model="form.owner_department_ids_array" multiple placeholder="请选择科室" filterable clearable style="width: 100%">
|
|
|
<el-option v-for="dept in departmentList" :key="dept.id" :label="dept.name" :value="dept.id.toString()" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="合同签订/变更经办人" prop="handler_admin_ids_array">
|
|
|
<el-select v-model="form.handler_admin_ids_array" multiple placeholder="请选择经办人" filterable clearable style="width: 100%">
|
|
|
<el-option v-for="user in userList" :key="user.id" :label="user.name" :value="user.id.toString()" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="申请科室经办人" prop="apply_handler_id_array">
|
|
|
<el-select v-model="form.apply_handler_id_array" placeholder="请选择" filterable multiple clearable style="width: 100%">
|
|
|
<el-option v-for="user in userList" :key="user.id" :label="user.name" :value="user.id.toString()" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="采购科室经办人" prop="purchase_handler_id">
|
|
|
<el-select v-model="form.purchase_handler_id" placeholder="请选择" filterable clearable style="width: 100%">
|
|
|
<el-option v-for="user in userList" :key="user.id" :label="user.name" :value="user.id" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-form-item label="备注">
|
|
|
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
|
|
</el-form-item>
|
|
|
<el-form-item label="附件">
|
|
|
<el-upload
|
|
|
:action="uploadAction"
|
|
|
:headers="uploadHeaders"
|
|
|
:before-upload="beforeUpload"
|
|
|
:on-success="handleUploadSuccess"
|
|
|
:on-error="handleUploadError"
|
|
|
:on-remove="handleUploadRemove"
|
|
|
:file-list="attachmentFileList"
|
|
|
:limit="1"
|
|
|
>
|
|
|
<el-button type="primary">上传文件</el-button>
|
|
|
<template #tip>
|
|
|
<div class="el-upload__tip">只能上传 JPG/PNG/PDF/DOC/DOCX/XLS/XLSX 格式的文件,且不超过 10MB</div>
|
|
|
</template>
|
|
|
</el-upload>
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-divider content-position="left">付款计划</el-divider>
|
|
|
<el-form-item prop="pay_plans" style="margin-bottom: 0;">
|
|
|
<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 payplan-table" style="width: 100%">
|
|
|
<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-item>
|
|
|
</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 { getToken } from '@/utils/auth';
|
|
|
import {
|
|
|
getFlowDetailForContract,
|
|
|
getContractSettings,
|
|
|
getContractByFlowId,
|
|
|
createContract,
|
|
|
getBudgetYearOptions,
|
|
|
} from "@/api/flow";
|
|
|
import { userListNoAuth, departmentListNoAuth } from "@/api/common";
|
|
|
|
|
|
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,
|
|
|
userList: [],
|
|
|
departmentList: [],
|
|
|
attachmentFileList: [],
|
|
|
budgetYearOptions: [],
|
|
|
fundSourceOptions: [],
|
|
|
purchaseCategoryOptions: [],
|
|
|
contractTypeOptions: [],
|
|
|
purchaseMethodOptions: [],
|
|
|
suppressFundSourceYearWatcher: false,
|
|
|
form: {
|
|
|
contract_no: "",
|
|
|
contract_no_without_prefix: "",
|
|
|
title: "",
|
|
|
main_content: "",
|
|
|
party_a: "",
|
|
|
party_b: "",
|
|
|
amount_total: null,
|
|
|
amount_type: "fixed",
|
|
|
amount_description: "",
|
|
|
budget_amount: 0,
|
|
|
contract_type: "",
|
|
|
sign_date: "",
|
|
|
apply_date: "",
|
|
|
perform_period: "",
|
|
|
pay_method: "",
|
|
|
fund_source: "",
|
|
|
fund_source_year_id: null,
|
|
|
fund_source_budget_data_id: null,
|
|
|
is_government_purchase: false,
|
|
|
tender_agent: "",
|
|
|
purchase_method: "",
|
|
|
perform_status: "",
|
|
|
is_accepted: false,
|
|
|
purchase_category: "",
|
|
|
purchase_category_id: null,
|
|
|
handler_admin_ids: "",
|
|
|
handler_admin_ids_array: [],
|
|
|
apply_handler_id: "",
|
|
|
apply_handler_id_array: [],
|
|
|
purchase_handler_id: null,
|
|
|
owner_department_id: null,
|
|
|
owner_department_ids: "",
|
|
|
owner_department_ids_array: [],
|
|
|
remark: "",
|
|
|
attachment_id: null,
|
|
|
pay_plans: [],
|
|
|
},
|
|
|
rules: {},
|
|
|
};
|
|
|
},
|
|
|
computed: {
|
|
|
normalizedValue() {
|
|
|
return this.value === null || this.value === undefined ? "" : String(this.value);
|
|
|
},
|
|
|
hasValue() {
|
|
|
return this.normalizedValue !== "";
|
|
|
},
|
|
|
displayText() {
|
|
|
return this.hasValue ? this.display || "" : "";
|
|
|
},
|
|
|
uploadAction() {
|
|
|
// 使用环境变量中的上传API,如果没有则使用默认路径
|
|
|
const uploadAPI = process.env.VUE_APP_UPLOAD_API;
|
|
|
if (uploadAPI) {
|
|
|
return uploadAPI;
|
|
|
}
|
|
|
// 如果没有配置,使用 baseURL + /upload-file
|
|
|
const baseURL = process.env.VUE_APP_BASE_API || '';
|
|
|
// 确保路径正确:baseURL 可能是 /api 或 /api/,需要统一处理
|
|
|
const apiPath = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
|
|
|
return `${apiPath}/upload-file`;
|
|
|
},
|
|
|
uploadHeaders() {
|
|
|
const token = getToken();
|
|
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
|
},
|
|
|
},
|
|
|
watch: {
|
|
|
'form.fund_source_year_id'(newVal, oldVal) {
|
|
|
// 当预算年度变化时,自动加载项目经费来源选项
|
|
|
if (newVal) {
|
|
|
if (!this.suppressFundSourceYearWatcher) {
|
|
|
this.fetchFundSourceOptions(newVal);
|
|
|
}
|
|
|
} else {
|
|
|
this.fundSourceOptions = [];
|
|
|
if (!this.suppressFundSourceYearWatcher) {
|
|
|
this.form.fund_source_budget_data_id = null;
|
|
|
}
|
|
|
}
|
|
|
// 如果年度变化了,清空之前选择的经费来源
|
|
|
if (newVal !== oldVal && !this.suppressFundSourceYearWatcher) {
|
|
|
this.form.fund_source_budget_data_id = null;
|
|
|
}
|
|
|
},
|
|
|
// 监听多选下拉变化,触发校验
|
|
|
'form.owner_department_ids_array'() {
|
|
|
if (this.$refs.formRef) {
|
|
|
this.$refs.formRef.validateField('owner_department_ids_array', () => {});
|
|
|
}
|
|
|
},
|
|
|
'form.handler_admin_ids_array'() {
|
|
|
if (this.$refs.formRef) {
|
|
|
this.$refs.formRef.validateField('handler_admin_ids_array', () => {});
|
|
|
}
|
|
|
},
|
|
|
'form.apply_handler_id_array'() {
|
|
|
if (this.$refs.formRef) {
|
|
|
this.$refs.formRef.validateField('apply_handler_id_array', () => {});
|
|
|
}
|
|
|
},
|
|
|
'form.pay_plans'() {
|
|
|
if (this.$refs.formRef) {
|
|
|
this.$refs.formRef.validateField('pay_plans', () => {});
|
|
|
}
|
|
|
},
|
|
|
// 监听金额类型变化,重新校验金额说明
|
|
|
'form.amount_type'() {
|
|
|
if (this.$refs.formRef) {
|
|
|
this.$refs.formRef.validateField('amount_description', () => {});
|
|
|
}
|
|
|
},
|
|
|
// 监听合同类型变化,动态调整预算年度和项目经费来源的必填校验
|
|
|
'form.contract_type_id'() {
|
|
|
// 当合同类型改变时,重新验证相关字段
|
|
|
if (this.$refs.formRef) {
|
|
|
this.$nextTick(() => {
|
|
|
this.$refs.formRef.validateField('fund_source_year_id');
|
|
|
this.$refs.formRef.validateField('fund_source_budget_data_id');
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
},
|
|
|
async mounted() {
|
|
|
// 组件初始化时,如果value为空但有flowId,尝试从流程数据恢复合同
|
|
|
if (!this.hasValue && this.flowId) {
|
|
|
try {
|
|
|
const res = await getContractByFlowId(this.flowId);
|
|
|
const contractData = res?.data || res;
|
|
|
|
|
|
if (contractData && contractData.id) {
|
|
|
// 找到已创建的合同,自动更新value和display
|
|
|
const displayText = contractData.contract_no + (contractData.title ? ` / ${contractData.title}` : "");
|
|
|
this.$emit("input", String(contractData.id));
|
|
|
this.$emit("update:display", displayText);
|
|
|
}
|
|
|
} catch (e) {
|
|
|
// 静默失败,不影响组件正常使用
|
|
|
console.warn("从流程数据恢复合同时出错:", e);
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
openDialog() {
|
|
|
this.dialogVisible = true;
|
|
|
},
|
|
|
// 判断是否为编辑模式(已有合同ID)
|
|
|
isEditMode() {
|
|
|
return this.hasValue && this.normalizedValue && parseInt(this.normalizedValue);
|
|
|
},
|
|
|
// 初始化校验规则
|
|
|
initRules() {
|
|
|
// 新建时:只校验到付款方式(包含付款方式)
|
|
|
// 注意:新建时不校验合同编号(不显示红色星号);编辑时仍保持必填校验
|
|
|
const baseRulesCommon = {
|
|
|
title: [{ required: true, message: "请填写合同名称", trigger: "blur" }],
|
|
|
main_content: [{ required: true, message: "请填写合同主要内容", trigger: "blur" }],
|
|
|
party_a: [{ required: true, message: "请填写甲方", trigger: "blur" }],
|
|
|
party_b: [{ required: true, message: "请填写乙方", trigger: "blur" }],
|
|
|
amount_total: [{ required: true, message: "请填写合同金额", trigger: ["blur", "change"] }],
|
|
|
amount_type: [{ required: true, message: "请选择金额类型", trigger: "change" }],
|
|
|
amount_description: [
|
|
|
{
|
|
|
validator: (rule, value, callback) => {
|
|
|
if (this.form.amount_type === 'open' && !value) {
|
|
|
callback(new Error('开口合同时,金额说明不能为空'));
|
|
|
} else {
|
|
|
callback();
|
|
|
}
|
|
|
},
|
|
|
trigger: 'blur'
|
|
|
}
|
|
|
],
|
|
|
budget_amount: [
|
|
|
{ required: true, message: "请填写项目预算金额", trigger: "blur" },
|
|
|
{
|
|
|
validator: (rule, value, callback) => {
|
|
|
if (value === null || value === undefined || value === '') {
|
|
|
callback(new Error('请填写项目预算金额'));
|
|
|
} else if (value === 0) {
|
|
|
callback(new Error('项目预算金额不能为0'));
|
|
|
} else {
|
|
|
callback();
|
|
|
}
|
|
|
},
|
|
|
trigger: 'blur'
|
|
|
}
|
|
|
],
|
|
|
contract_type_id: [{ required: true, message: "请选择合同类型", trigger: "change" }],
|
|
|
sign_date: [{ required: true, message: "请选择签订日期", trigger: "change" }],
|
|
|
apply_date: [{ required: true, message: "请选择申请日期", trigger: "change" }],
|
|
|
perform_period: [{ required: true, message: "请填写合同履行期", trigger: "blur" }],
|
|
|
pay_method: [{ required: true, message: "请填写付款方式", trigger: "blur" }],
|
|
|
};
|
|
|
|
|
|
// 新建时只校验到付款方式(包含付款方式)
|
|
|
if (!this.isEditMode()) {
|
|
|
this.rules = {
|
|
|
...baseRulesCommon,
|
|
|
};
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 编辑时:合同编号必填
|
|
|
const baseRules = {
|
|
|
contract_no_without_prefix: [{ required: true, message: "请填写合同编号", trigger: "blur" }],
|
|
|
...baseRulesCommon,
|
|
|
};
|
|
|
|
|
|
// 编辑时全量校验(与合同编辑页面对齐)
|
|
|
this.rules = {
|
|
|
...baseRules,
|
|
|
fund_source_year_id: [
|
|
|
{
|
|
|
validator: (rule, value, callback) => {
|
|
|
// 默认必填,只有当合同类型明确是"收入类"时才撤销必填校验
|
|
|
const contractTypeName = this.getContractTypeName();
|
|
|
if (contractTypeName === '收入类') {
|
|
|
// 收入类不需要必填
|
|
|
callback();
|
|
|
} else {
|
|
|
// 其他情况(包括未选择合同类型)都需要必填
|
|
|
if (!value) {
|
|
|
callback(new Error('请选择预算年度'));
|
|
|
} else {
|
|
|
callback();
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
trigger: 'change'
|
|
|
}
|
|
|
],
|
|
|
fund_source_budget_data_id: [
|
|
|
{
|
|
|
validator: (rule, value, callback) => {
|
|
|
// 默认必填,只有当合同类型明确是"收入类"时才撤销必填校验
|
|
|
const contractTypeName = this.getContractTypeName();
|
|
|
if (contractTypeName === '收入类') {
|
|
|
// 收入类不需要必填
|
|
|
callback();
|
|
|
} else {
|
|
|
// 其他情况(包括未选择合同类型)都需要必填
|
|
|
if (!value) {
|
|
|
callback(new Error('请选择项目经费来源'));
|
|
|
} else {
|
|
|
callback();
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
trigger: 'change'
|
|
|
}
|
|
|
],
|
|
|
is_government_purchase: [{ required: true, message: "请选择是否为政府采购", trigger: "change" }],
|
|
|
purchase_category_id: [{ required: true, message: "请选择采购类别", trigger: "change" }],
|
|
|
purchase_method_id: [{ required: true, message: "请选择采购方式", trigger: "change" }],
|
|
|
is_accepted: [{ required: true, message: "请选择是否验收", trigger: "change" }],
|
|
|
handler_admin_ids_array: [
|
|
|
{
|
|
|
required: true,
|
|
|
validator: (rule, value, callback) => {
|
|
|
const ids = Array.isArray(value) ? value : [];
|
|
|
if (ids.length === 0) {
|
|
|
callback(new Error('请选择合同签订/变更经办人'));
|
|
|
} else {
|
|
|
callback();
|
|
|
}
|
|
|
},
|
|
|
trigger: ['change', 'blur']
|
|
|
}
|
|
|
],
|
|
|
apply_handler_id_array: [
|
|
|
{
|
|
|
required: true,
|
|
|
validator: (rule, value, callback) => {
|
|
|
const ids = Array.isArray(value) ? value : [];
|
|
|
if (ids.length === 0) {
|
|
|
callback(new Error('请选择申请科室经办人'));
|
|
|
} else {
|
|
|
callback();
|
|
|
}
|
|
|
},
|
|
|
trigger: ['change', 'blur']
|
|
|
}
|
|
|
],
|
|
|
owner_department_ids_array: [
|
|
|
{
|
|
|
required: true,
|
|
|
validator: (rule, value, callback) => {
|
|
|
const ids = Array.isArray(value) ? value : [];
|
|
|
if (ids.length === 0) {
|
|
|
callback(new Error('请选择科室'));
|
|
|
} else {
|
|
|
callback();
|
|
|
}
|
|
|
},
|
|
|
trigger: ['change', 'blur']
|
|
|
}
|
|
|
],
|
|
|
purchase_handler_id: [{ required: true, message: "请选择采购科室经办人", trigger: "change" }],
|
|
|
pay_plans: [
|
|
|
{
|
|
|
validator: (rule, value, callback) => {
|
|
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
|
|
callback(new Error('至少需要一条付款计划'));
|
|
|
return;
|
|
|
}
|
|
|
const errors = [];
|
|
|
value.forEach((plan, index) => {
|
|
|
const planErrors = [];
|
|
|
if (!plan.phase_no || plan.phase_no <= 0) {
|
|
|
planErrors.push('期次');
|
|
|
}
|
|
|
if (!plan.due_date) {
|
|
|
planErrors.push('到期日期');
|
|
|
}
|
|
|
if (!plan.amount_plan || plan.amount_plan <= 0) {
|
|
|
planErrors.push('金额');
|
|
|
}
|
|
|
if (planErrors.length > 0) {
|
|
|
errors.push(`第${index + 1}条付款计划的${planErrors.join('、')}不能为空`);
|
|
|
}
|
|
|
});
|
|
|
if (errors.length > 0) {
|
|
|
callback(new Error(errors[0]));
|
|
|
} else {
|
|
|
callback();
|
|
|
}
|
|
|
},
|
|
|
trigger: ['change', 'blur']
|
|
|
}
|
|
|
]
|
|
|
};
|
|
|
},
|
|
|
// 获取合同类型名称
|
|
|
getContractTypeName() {
|
|
|
if (!this.form.contract_type_id) {
|
|
|
return null;
|
|
|
}
|
|
|
const contractType = this.contractTypeOptions.find(type => type.id === this.form.contract_type_id);
|
|
|
return contractType ? contractType.name : null;
|
|
|
},
|
|
|
handleFundSourceYearChange() {
|
|
|
this.form.fund_source_budget_data_id = null;
|
|
|
if (this.form.fund_source_year_id) {
|
|
|
this.fetchFundSourceOptions(this.form.fund_source_year_id);
|
|
|
}
|
|
|
},
|
|
|
// 处理合同类型变化
|
|
|
handleContractTypeChange() {
|
|
|
// 当合同类型改变时,重新验证预算年度和项目经费来源
|
|
|
if (this.$refs.formRef) {
|
|
|
this.$nextTick(() => {
|
|
|
this.$refs.formRef.validateField('fund_source_year_id');
|
|
|
this.$refs.formRef.validateField('fund_source_budget_data_id');
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
async fetchBudgetYearOptions() {
|
|
|
try {
|
|
|
const response = await getBudgetYearOptions(false);
|
|
|
const arr = Array.isArray(response) ? response : (response?.data || []);
|
|
|
this.budgetYearOptions = arr.map((y) => ({
|
|
|
value: y.value,
|
|
|
label: y.label || (y.year ? `${y.year}年` : `${y.value}年`),
|
|
|
status: y.status, // 保留状态信息
|
|
|
}));
|
|
|
// 默认选择 ACTIVE 状态或最新的年度(与合同编辑页面保持一致)
|
|
|
// 注意:只在新建时默认选择,编辑时应该保持原有选择
|
|
|
if (this.budgetYearOptions.length > 0 && !this.form.fund_source_year_id && !this.isEditMode()) {
|
|
|
const activeYear = this.budgetYearOptions.find(y => y.status === 'ACTIVE');
|
|
|
if (activeYear) {
|
|
|
this.form.fund_source_year_id = activeYear.value;
|
|
|
await this.fetchFundSourceOptions(activeYear.value);
|
|
|
} else {
|
|
|
this.form.fund_source_year_id = this.budgetYearOptions[0].value;
|
|
|
await this.fetchFundSourceOptions(this.budgetYearOptions[0].value);
|
|
|
}
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error('获取预算年度列表失败', e);
|
|
|
Message.error('获取预算年度列表失败:' + (e.message || '未知错误'));
|
|
|
this.budgetYearOptions = [];
|
|
|
}
|
|
|
},
|
|
|
async fetchFundSourceOptions(yearId) {
|
|
|
if (!yearId) {
|
|
|
this.fundSourceOptions = [];
|
|
|
return;
|
|
|
}
|
|
|
try {
|
|
|
// 注意:响应拦截器已经返回了 res.data,所以 response 直接就是数据数组
|
|
|
const response = await request.get('/api/budget/contracts/fund-source-options', { params: { year_id: yearId } });
|
|
|
// 检查是否是数组格式(正常的响应)
|
|
|
if (Array.isArray(response)) {
|
|
|
this.fundSourceOptions = response;
|
|
|
} else if (response && response.code === 0 && Array.isArray(response.data)) {
|
|
|
// 兼容处理:如果响应拦截器没有处理,fallback 到原始格式
|
|
|
this.fundSourceOptions = response.data;
|
|
|
} else {
|
|
|
this.fundSourceOptions = [];
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error('获取项目经费来源列表失败', e);
|
|
|
Message.error('获取项目经费来源列表失败:' + (e.message || '未知错误'));
|
|
|
this.fundSourceOptions = [];
|
|
|
}
|
|
|
},
|
|
|
async fetchBudgetDataDetail(budgetDataId) {
|
|
|
if (!budgetDataId) {
|
|
|
return null;
|
|
|
}
|
|
|
try {
|
|
|
const response = await request.get(`/api/budget/budget-data/${budgetDataId}`);
|
|
|
return response?.data || response || null;
|
|
|
} catch (e) {
|
|
|
console.error('获取项目经费来源详情失败', e);
|
|
|
return null;
|
|
|
}
|
|
|
},
|
|
|
async restoreFundSourceByBudgetDataId(budgetDataId) {
|
|
|
if (!budgetDataId) {
|
|
|
return;
|
|
|
}
|
|
|
const budgetData = await this.fetchBudgetDataDetail(budgetDataId);
|
|
|
const yearId = budgetData?.year_id || budgetData?.budget_year?.id || budgetData?.budgetYear?.id;
|
|
|
if (yearId) {
|
|
|
this.suppressFundSourceYearWatcher = true;
|
|
|
this.form.fund_source_year_id = yearId;
|
|
|
await this.fetchFundSourceOptions(yearId);
|
|
|
this.form.fund_source_budget_data_id = budgetDataId;
|
|
|
this.$nextTick(() => {
|
|
|
this.suppressFundSourceYearWatcher = false;
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
async fetchPurchaseCategoryOptions() {
|
|
|
try {
|
|
|
// 注意:响应拦截器已经返回了 res.data,所以 response 直接就是数据
|
|
|
const response = await request.get('/api/budget/purchase-categories');
|
|
|
// 与合同编辑页面保持一致:只获取启用的采购类别
|
|
|
if (Array.isArray(response)) {
|
|
|
this.purchaseCategoryOptions = response;
|
|
|
} else if (response && response.code === 0 && Array.isArray(response.data)) {
|
|
|
// 兼容处理:如果响应拦截器没有处理,fallback 到原始格式
|
|
|
this.purchaseCategoryOptions = response.data;
|
|
|
} else {
|
|
|
this.purchaseCategoryOptions = [];
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error('获取采购类别列表失败', e);
|
|
|
Message.error('获取采购类别列表失败:' + (e.message || '未知错误'));
|
|
|
this.purchaseCategoryOptions = [];
|
|
|
}
|
|
|
},
|
|
|
async fetchContractTypeOptions() {
|
|
|
try {
|
|
|
// 注意:响应拦截器已经返回了 res.data,所以 response 直接就是数据
|
|
|
// 与合同编辑页面保持一致:只获取启用的合同类型(使用 /api/budget/contract-types 而不是 /all)
|
|
|
const response = await request.get('/api/budget/contract-types');
|
|
|
if (Array.isArray(response)) {
|
|
|
this.contractTypeOptions = response;
|
|
|
} else if (response && response.code === 0 && Array.isArray(response.data)) {
|
|
|
// 兼容处理:如果响应拦截器没有处理,fallback 到原始格式
|
|
|
this.contractTypeOptions = response.data;
|
|
|
} else {
|
|
|
this.contractTypeOptions = [];
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error('获取合同类型列表失败', e);
|
|
|
Message.error('获取合同类型列表失败:' + (e.message || '未知错误'));
|
|
|
this.contractTypeOptions = [];
|
|
|
}
|
|
|
},
|
|
|
async fetchPurchaseMethodOptions() {
|
|
|
try {
|
|
|
// 注意:响应拦截器已经返回了 res.data,所以 response 直接就是数据
|
|
|
// 与合同编辑页面保持一致:只获取启用的采购方式(使用 /api/budget/purchase-methods 而不是 /all)
|
|
|
const response = await request.get('/api/budget/purchase-methods');
|
|
|
if (Array.isArray(response)) {
|
|
|
this.purchaseMethodOptions = response;
|
|
|
} else if (response && response.code === 0 && Array.isArray(response.data)) {
|
|
|
// 兼容处理:如果响应拦截器没有处理,fallback 到原始格式
|
|
|
this.purchaseMethodOptions = response.data;
|
|
|
} else {
|
|
|
this.purchaseMethodOptions = [];
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error('获取采购方式列表失败', e);
|
|
|
Message.error('获取采购方式列表失败:' + (e.message || '未知错误'));
|
|
|
this.purchaseMethodOptions = [];
|
|
|
}
|
|
|
},
|
|
|
beforeUpload(file) {
|
|
|
const isValidType = ['image/jpeg', 'image/png', 'image/jpg', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'].includes(file.type);
|
|
|
const isLt10M = file.size / 1024 / 1024 < 10;
|
|
|
|
|
|
if (!isValidType) {
|
|
|
Message.error('只能上传 JPG/PNG/PDF/DOC/DOCX/XLS/XLSX 格式的文件!');
|
|
|
return false;
|
|
|
}
|
|
|
if (!isLt10M) {
|
|
|
Message.error('文件大小不能超过 10MB!');
|
|
|
return false;
|
|
|
}
|
|
|
return true;
|
|
|
},
|
|
|
handleUploadSuccess(response) {
|
|
|
if (response.code === 0) {
|
|
|
const fileData = response.data;
|
|
|
this.form.attachment_id = fileData.id;
|
|
|
this.attachmentFileList = [{
|
|
|
name: fileData.original_name || fileData.name || '文件',
|
|
|
url: fileData.url || (fileData.folder && fileData.name ? `${fileData.folder}/${fileData.name}` : ''),
|
|
|
uid: fileData.id,
|
|
|
id: fileData.id
|
|
|
}];
|
|
|
Message.success('上传成功');
|
|
|
} else {
|
|
|
Message.error('上传失败:' + (response.msg || '未知错误'));
|
|
|
}
|
|
|
},
|
|
|
handleUploadError(error) {
|
|
|
console.error('上传错误:', error);
|
|
|
Message.error('上传失败,请重试');
|
|
|
},
|
|
|
handleUploadRemove() {
|
|
|
this.form.attachment_id = null;
|
|
|
this.attachmentFileList = [];
|
|
|
Message.success('已移除文件');
|
|
|
},
|
|
|
async loadUserList() {
|
|
|
try {
|
|
|
// 使用 common.js 中的函数,确保路径正确
|
|
|
// 注意:响应拦截器返回的是 res.data,所以这里 response 已经是 data 部分
|
|
|
const response = await userListNoAuth({ rows: 1000 });
|
|
|
// simpleUser 返回分页数据,格式: { data: [...], current_page: 1, ... }
|
|
|
if (response && response.data && Array.isArray(response.data)) {
|
|
|
this.userList = response.data;
|
|
|
} else if (Array.isArray(response)) {
|
|
|
// 兼容直接返回数组的情况
|
|
|
this.userList = response;
|
|
|
} else {
|
|
|
this.userList = [];
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error('获取用户列表失败', e);
|
|
|
Message.error('获取用户列表失败:' + (e.message || '未知错误'));
|
|
|
this.userList = [];
|
|
|
}
|
|
|
},
|
|
|
async loadDepartmentList() {
|
|
|
try {
|
|
|
// 使用 common.js 中的函数,确保路径正确
|
|
|
// 注意:响应拦截器返回的是 res.data,所以这里 response 已经是 data 部分(数组)
|
|
|
const response = await departmentListNoAuth({ rows: 1000 });
|
|
|
// simpleDepartment 直接返回数组
|
|
|
if (Array.isArray(response)) {
|
|
|
this.departmentList = response;
|
|
|
} else {
|
|
|
this.departmentList = [];
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error('获取部门列表失败', e);
|
|
|
Message.error('获取部门列表失败:' + (e.message || '未知错误'));
|
|
|
this.departmentList = [];
|
|
|
}
|
|
|
},
|
|
|
async onDialogOpen() {
|
|
|
this.resetForm();
|
|
|
await this.loadUserList();
|
|
|
await this.loadDepartmentList();
|
|
|
await this.fetchBudgetYearOptions();
|
|
|
await this.fetchPurchaseCategoryOptions();
|
|
|
await this.fetchContractTypeOptions();
|
|
|
await this.fetchPurchaseMethodOptions();
|
|
|
// 初始化校验规则
|
|
|
this.initRules();
|
|
|
// 清除表单校验状态,避免新建时触发校验
|
|
|
this.$nextTick(() => {
|
|
|
if (this.$refs.formRef) {
|
|
|
this.$refs.formRef.clearValidate();
|
|
|
}
|
|
|
});
|
|
|
// 优先尝试加载已有合同(通过合同ID或flowId)
|
|
|
const contractExists = await this.loadExistingContract();
|
|
|
// 如果找不到已有合同,且提供了flowId,则从流程数据预填
|
|
|
if (!contractExists && this.flowId) {
|
|
|
await this.loadFlowDataAndPrefill();
|
|
|
} else if (contractExists && this.flowId && this.isEditMode()) {
|
|
|
await this.loadFlowDataAndPrefill({
|
|
|
onlyEmpty: true,
|
|
|
fields: this.getResignSupplementFields(),
|
|
|
});
|
|
|
}
|
|
|
// 重新初始化校验规则(因为可能已经加载了合同数据,需要区分新建/编辑)
|
|
|
this.initRules();
|
|
|
// 再次清除校验状态,确保填充数据后不会触发校验
|
|
|
this.$nextTick(() => {
|
|
|
if (this.$refs.formRef) {
|
|
|
// 如果是编辑模式,在数据加载完成后,需要正确设置多选字段的校验状态
|
|
|
if (this.isEditMode()) {
|
|
|
// 延迟一下,确保DOM已更新和数据已填充
|
|
|
this.$nextTick(() => {
|
|
|
if (this.$refs.formRef) {
|
|
|
// 对于多选字段,如果数组有值,清除校验错误;如果为空,触发校验显示错误
|
|
|
// 这样确保编辑模式下,已填充的字段不显示错误,未填充的字段显示错误
|
|
|
const fieldsToCheck = [
|
|
|
{ prop: 'owner_department_ids_array', arrayField: 'owner_department_ids_array' },
|
|
|
{ prop: 'handler_admin_ids_array', arrayField: 'handler_admin_ids_array' },
|
|
|
{ prop: 'apply_handler_id_array', arrayField: 'apply_handler_id_array' },
|
|
|
];
|
|
|
|
|
|
fieldsToCheck.forEach(({ prop, arrayField }) => {
|
|
|
const ids = this.form[arrayField];
|
|
|
if (ids && Array.isArray(ids) && ids.length > 0) {
|
|
|
// 有值,清除该字段的校验错误
|
|
|
this.$refs.formRef.clearValidate(prop);
|
|
|
} else {
|
|
|
// 无值,先清除之前的错误状态,然后触发校验显示错误
|
|
|
// 这样可以确保校验状态正确更新
|
|
|
this.$refs.formRef.clearValidate(prop);
|
|
|
this.$nextTick(() => {
|
|
|
if (this.$refs.formRef) {
|
|
|
this.$refs.formRef.validateField(prop, () => {});
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
} else {
|
|
|
// 新建模式,清除所有校验状态
|
|
|
this.$refs.formRef.clearValidate();
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
onDialogClose() {
|
|
|
// 关闭时不做处理
|
|
|
},
|
|
|
resetForm() {
|
|
|
this.form = {
|
|
|
contract_no: "",
|
|
|
contract_no_without_prefix: "",
|
|
|
title: "",
|
|
|
main_content: "",
|
|
|
party_a: "",
|
|
|
party_b: "",
|
|
|
amount_total: null,
|
|
|
amount_type: "fixed",
|
|
|
amount_description: "",
|
|
|
budget_amount: 0,
|
|
|
contract_type_id: null,
|
|
|
contract_type: "", // 保留旧字段,用于兼容
|
|
|
sign_date: "",
|
|
|
apply_date: "",
|
|
|
perform_period: "",
|
|
|
pay_method: "",
|
|
|
fund_source: "",
|
|
|
fund_source_year_id: null,
|
|
|
fund_source_budget_data_id: null,
|
|
|
is_government_purchase: false,
|
|
|
tender_agent: "",
|
|
|
purchase_method_id: null,
|
|
|
purchase_method: "", // 保留旧字段,用于兼容
|
|
|
perform_status: "",
|
|
|
is_accepted: false,
|
|
|
purchase_category: "",
|
|
|
purchase_category_id: null,
|
|
|
handler_admin_ids: "",
|
|
|
handler_admin_ids_array: [],
|
|
|
apply_handler_id: "",
|
|
|
apply_handler_id_array: [],
|
|
|
purchase_handler_id: null,
|
|
|
owner_department_id: null,
|
|
|
owner_department_ids: "",
|
|
|
owner_department_ids_array: [],
|
|
|
remark: "",
|
|
|
attachment_id: null,
|
|
|
pay_plans: [],
|
|
|
};
|
|
|
this.attachmentFileList = [];
|
|
|
this.fundSourceOptions = [];
|
|
|
if (this.$refs.formRef) {
|
|
|
this.$refs.formRef.clearValidate();
|
|
|
}
|
|
|
},
|
|
|
async loadExistingContract() {
|
|
|
this.loading = true;
|
|
|
try {
|
|
|
let contractData = null;
|
|
|
|
|
|
// 优先通过合同ID加载(如果字段已有值)
|
|
|
if (this.hasValue && this.normalizedValue) {
|
|
|
try {
|
|
|
const contractId = parseInt(this.normalizedValue);
|
|
|
if (contractId) {
|
|
|
// 通过合同ID获取合同详情(使用完整路径,与其他API调用保持一致)
|
|
|
const res = await request.get(`/api/budget/contracts/${contractId}`);
|
|
|
// 响应拦截器返回的是 res.data,所以这里 res 已经是合同数据
|
|
|
if (res && res.id) {
|
|
|
contractData = res;
|
|
|
}
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.warn('通过合同ID加载失败,尝试通过flowId加载:', e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果通过ID加载失败,且提供了flowId,尝试通过flowId加载
|
|
|
if (!contractData && this.flowId) {
|
|
|
const res = await getContractByFlowId(this.flowId);
|
|
|
contractData = res?.data || res;
|
|
|
}
|
|
|
|
|
|
if (contractData) {
|
|
|
// 处理合同编号:拆分CZHT前缀
|
|
|
if (contractData.contract_no) {
|
|
|
if (contractData.contract_no.startsWith('CZHT')) {
|
|
|
this.form.contract_no_without_prefix = contractData.contract_no.substring(4);
|
|
|
} else {
|
|
|
this.form.contract_no_without_prefix = contractData.contract_no;
|
|
|
}
|
|
|
}
|
|
|
this.form.contract_no = contractData.contract_no || "";
|
|
|
this.form.title = contractData.title || "";
|
|
|
this.form.main_content = contractData.main_content || "";
|
|
|
this.form.party_a = contractData.party_a || "";
|
|
|
this.form.party_b = contractData.party_b || "";
|
|
|
this.form.amount_total = contractData.amount_total !== null && contractData.amount_total !== undefined ? contractData.amount_total : null;
|
|
|
this.form.amount_type = contractData.amount_type || "fixed";
|
|
|
this.form.amount_description = contractData.amount_description || "";
|
|
|
this.form.budget_amount = contractData.budget_amount || 0;
|
|
|
// 优先使用ID字段,如果不存在则使用旧字段(兼容历史数据)
|
|
|
this.form.contract_type_id = contractData.contract_type_id || null;
|
|
|
this.form.contract_type = contractData.contract_type || ""; // 保留旧字段,用于兼容
|
|
|
this.form.sign_date = contractData.sign_date || "";
|
|
|
this.form.apply_date = contractData.apply_date || "";
|
|
|
this.form.perform_period = contractData.perform_period || "";
|
|
|
this.form.pay_method = contractData.pay_method || "";
|
|
|
this.form.fund_source = contractData.fund_source || "";
|
|
|
this.form.fund_source_year_id = contractData.fund_source_year_id || null;
|
|
|
this.form.fund_source_budget_data_id = contractData.fund_source_budget_data_id || null;
|
|
|
this.form.is_government_purchase = contractData.is_government_purchase ?? null;
|
|
|
this.form.tender_agent = contractData.tender_agent || "";
|
|
|
// 优先使用ID字段,如果不存在则使用旧字段(兼容历史数据)
|
|
|
this.form.purchase_method_id = contractData.purchase_method_id || null;
|
|
|
this.form.purchase_method = contractData.purchase_method || ""; // 保留旧字段,用于兼容
|
|
|
this.form.perform_status = contractData.perform_status || "";
|
|
|
this.form.is_accepted = contractData.is_accepted ?? null;
|
|
|
this.form.purchase_category = contractData.purchase_category || "";
|
|
|
this.form.purchase_category_id = contractData.purchase_category_id || null;
|
|
|
this.form.apply_handler_id = contractData.apply_handler_id || "";
|
|
|
this.form.purchase_handler_id = contractData.purchase_handler_id || null;
|
|
|
this.form.owner_department_id = contractData.owner_department_id || null;
|
|
|
this.form.remark = contractData.remark || "";
|
|
|
this.form.attachment_id = contractData.attachment_id || null;
|
|
|
|
|
|
// 项目经费来源优先:可从 budget_data_id 反推预算年度,预算年度只用于加载下拉选项。
|
|
|
if (this.form.fund_source_budget_data_id) {
|
|
|
await this.restoreFundSourceByBudgetDataId(this.form.fund_source_budget_data_id);
|
|
|
} else if (this.form.fund_source_year_id) {
|
|
|
await this.fetchFundSourceOptions(this.form.fund_source_year_id);
|
|
|
}
|
|
|
|
|
|
// 处理科室多选:转换为字符串数组(因为下拉组件的value是字符串)
|
|
|
if (contractData.owner_department_ids) {
|
|
|
const ids = typeof contractData.owner_department_ids === 'string'
|
|
|
? contractData.owner_department_ids.split(',').filter(id => id)
|
|
|
: (Array.isArray(contractData.owner_department_ids) ? contractData.owner_department_ids : []);
|
|
|
this.form.owner_department_ids_array = ids.map(id => String(id));
|
|
|
} else {
|
|
|
this.form.owner_department_ids_array = [];
|
|
|
}
|
|
|
|
|
|
// 处理经办人多选:转换为字符串数组(因为下拉组件的value是字符串)
|
|
|
if (contractData.handler_admin_ids) {
|
|
|
const ids = typeof contractData.handler_admin_ids === 'string'
|
|
|
? contractData.handler_admin_ids.split(',').filter(id => id)
|
|
|
: (Array.isArray(contractData.handler_admin_ids) ? contractData.handler_admin_ids : []);
|
|
|
this.form.handler_admin_ids_array = ids.map(id => String(id));
|
|
|
} else {
|
|
|
this.form.handler_admin_ids_array = [];
|
|
|
}
|
|
|
|
|
|
// 处理申请科室经办人多选:转换为字符串数组(因为下拉组件的value是字符串)
|
|
|
if (contractData.apply_handler_id) {
|
|
|
const ids = typeof contractData.apply_handler_id === 'string'
|
|
|
? contractData.apply_handler_id.split(',').filter(id => id)
|
|
|
: (Array.isArray(contractData.apply_handler_id) ? contractData.apply_handler_id : []);
|
|
|
this.form.apply_handler_id_array = ids.map(id => String(id));
|
|
|
} else {
|
|
|
this.form.apply_handler_id_array = [];
|
|
|
}
|
|
|
|
|
|
// 处理附件
|
|
|
if (contractData.attachment_id && contractData.attachment) {
|
|
|
this.attachmentFileList = [{
|
|
|
name: contractData.attachment.original_name || contractData.attachment.name || '文件',
|
|
|
url: contractData.attachment.url || (contractData.attachment.folder && contractData.attachment.name ? `${contractData.attachment.folder}/${contractData.attachment.name}` : ''),
|
|
|
uid: contractData.attachment.id,
|
|
|
id: contractData.attachment.id
|
|
|
}];
|
|
|
} else {
|
|
|
this.attachmentFileList = [];
|
|
|
}
|
|
|
|
|
|
// 填充付款计划
|
|
|
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;
|
|
|
}
|
|
|
},
|
|
|
getResignSupplementFields() {
|
|
|
return [
|
|
|
'fund_source',
|
|
|
'fund_source_year_id',
|
|
|
'fund_source_budget_data_id',
|
|
|
'is_government_purchase',
|
|
|
'tender_agent',
|
|
|
'purchase_method_id',
|
|
|
'purchase_method',
|
|
|
'perform_status',
|
|
|
'is_accepted',
|
|
|
'purchase_category',
|
|
|
'purchase_category_id',
|
|
|
'handler_admin_ids',
|
|
|
'apply_handler_id',
|
|
|
'purchase_handler_id',
|
|
|
'owner_department_id',
|
|
|
'owner_department_ids',
|
|
|
'remark',
|
|
|
'attachment_id',
|
|
|
'pay_plans',
|
|
|
];
|
|
|
},
|
|
|
isEmptyPrefillValue(value) {
|
|
|
if (value === null || value === undefined || value === '') {
|
|
|
return true;
|
|
|
}
|
|
|
if (Array.isArray(value)) {
|
|
|
return value.length === 0;
|
|
|
}
|
|
|
return false;
|
|
|
},
|
|
|
applyMappingTransform(value, map) {
|
|
|
if (!map || map.value_transform !== 'json_field') {
|
|
|
return value;
|
|
|
}
|
|
|
|
|
|
const jsonField = map.json_field || 'id';
|
|
|
let parsed = value;
|
|
|
if (typeof parsed === 'string') {
|
|
|
try {
|
|
|
parsed = JSON.parse(parsed);
|
|
|
} catch (e) {
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (Array.isArray(parsed)) {
|
|
|
parsed = parsed[0] || null;
|
|
|
}
|
|
|
if (!parsed || typeof parsed !== 'object') {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
return parsed[jsonField] ?? null;
|
|
|
},
|
|
|
getFirstJsonObject(value) {
|
|
|
let parsed = value;
|
|
|
if (typeof parsed === 'string') {
|
|
|
try {
|
|
|
parsed = JSON.parse(parsed);
|
|
|
} catch (e) {
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
if (Array.isArray(parsed)) {
|
|
|
parsed = parsed[0] || null;
|
|
|
}
|
|
|
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
|
},
|
|
|
resolveMappedValue(rawValue, map, budgetField) {
|
|
|
const transformed = this.applyMappingTransform(rawValue, map);
|
|
|
if (budgetField === 'attachment_id' && (transformed === rawValue || transformed === null || transformed === undefined)) {
|
|
|
const fileInfo = this.getFirstJsonObject(rawValue);
|
|
|
return fileInfo?.id ?? transformed;
|
|
|
}
|
|
|
return transformed;
|
|
|
},
|
|
|
async setPrefillValue(field, value, onlyEmpty = false, rawValue = null) {
|
|
|
if (!Object.prototype.hasOwnProperty.call(this.form, field) || value === undefined || value === null || value === '') {
|
|
|
return;
|
|
|
}
|
|
|
if (onlyEmpty && !this.isEmptyPrefillValue(this.form[field])) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (field === 'fund_source_budget_data_id') {
|
|
|
await this.restoreFundSourceByBudgetDataId(value);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
this.form[field] = value;
|
|
|
|
|
|
if (field === 'owner_department_ids') {
|
|
|
const ids = typeof value === 'string'
|
|
|
? value.split(',').filter(id => id)
|
|
|
: (Array.isArray(value) ? value : []);
|
|
|
this.form.owner_department_ids_array = ids.map(id => String(id));
|
|
|
if (!this.form.owner_department_id && ids.length > 0) {
|
|
|
this.form.owner_department_id = parseInt(ids[0]);
|
|
|
}
|
|
|
}
|
|
|
if (field === 'handler_admin_ids') {
|
|
|
const ids = typeof value === 'string'
|
|
|
? value.split(',').filter(id => id)
|
|
|
: (Array.isArray(value) ? value : []);
|
|
|
this.form.handler_admin_ids_array = ids.map(id => String(id));
|
|
|
}
|
|
|
if (field === 'apply_handler_id') {
|
|
|
const ids = typeof value === 'string'
|
|
|
? value.split(',').filter(id => id)
|
|
|
: (Array.isArray(value) ? value : []);
|
|
|
this.form.apply_handler_id_array = ids.map(id => String(id));
|
|
|
}
|
|
|
if (field === 'attachment_id') {
|
|
|
const fileInfo = this.getFirstJsonObject(rawValue);
|
|
|
this.attachmentFileList = fileInfo ? [{
|
|
|
name: fileInfo.original_name || fileInfo.name || '文件',
|
|
|
url: fileInfo.url || (fileInfo.folder && fileInfo.name ? `${fileInfo.folder}/${fileInfo.name}` : ''),
|
|
|
uid: fileInfo.id || value,
|
|
|
id: fileInfo.id || value
|
|
|
}] : [];
|
|
|
}
|
|
|
if (field === 'fund_source_year_id' && value) {
|
|
|
await this.fetchFundSourceOptions(value);
|
|
|
}
|
|
|
},
|
|
|
async loadFlowDataAndPrefill(options = {}) {
|
|
|
if (!this.flowId) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
this.loading = true;
|
|
|
try {
|
|
|
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 || []);
|
|
|
const onlyEmpty = !!options.onlyEmpty;
|
|
|
const allowedFields = Array.isArray(options.fields) ? options.fields : null;
|
|
|
|
|
|
if (contractMapping && contractMapping.length > 0) {
|
|
|
for (const map of contractMapping) {
|
|
|
const budgetField = map.budget_field;
|
|
|
const oaField = map.oa_field;
|
|
|
if (allowedFields && !allowedFields.includes(budgetField)) {
|
|
|
continue;
|
|
|
}
|
|
|
if (budgetField && oaField && flowData[oaField] !== undefined) {
|
|
|
await this.setPrefillValue(budgetField, this.resolveMappedValue(flowData[oaField], map, budgetField), onlyEmpty, flowData[oaField]);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const shouldFillPayPlans = !allowedFields || allowedFields.includes('pay_plans');
|
|
|
if (shouldFillPayPlans && (!onlyEmpty || this.isEmptyPrefillValue(this.form.pay_plans)) && 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 (shouldFillPayPlans && (!onlyEmpty || this.isEmptyPrefillValue(this.form.pay_plans)) && flowData.pay_plans && Array.isArray(flowData.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] = this.applyMappingTransform(row[oaField], map);
|
|
|
}
|
|
|
});
|
|
|
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 {
|
|
|
// 处理合同编号:拼接CZHT前缀
|
|
|
let contractNo = this.form.contract_no_without_prefix || '';
|
|
|
if (contractNo && !contractNo.startsWith('CZHT')) {
|
|
|
contractNo = 'CZHT' + contractNo;
|
|
|
}
|
|
|
|
|
|
// 处理科室多选:将数组转换为逗号分隔的字符串
|
|
|
const ownerDepartmentIds = Array.isArray(this.form.owner_department_ids_array)
|
|
|
? this.form.owner_department_ids_array.join(',')
|
|
|
: (this.form.owner_department_ids || '');
|
|
|
|
|
|
// 如果有科室多选,取第一个作为 owner_department_id(用于兼容)
|
|
|
let ownerDepartmentId = null;
|
|
|
if (Array.isArray(this.form.owner_department_ids_array) && this.form.owner_department_ids_array.length > 0) {
|
|
|
ownerDepartmentId = parseInt(this.form.owner_department_ids_array[0]);
|
|
|
} else if (this.form.owner_department_id) {
|
|
|
ownerDepartmentId = this.form.owner_department_id;
|
|
|
}
|
|
|
|
|
|
// 处理经办人多选:将数组转换为逗号分隔的字符串
|
|
|
const handlerAdminIds = Array.isArray(this.form.handler_admin_ids_array)
|
|
|
? this.form.handler_admin_ids_array.join(',')
|
|
|
: (this.form.handler_admin_ids || '');
|
|
|
|
|
|
// 申请科室经办人多选:将数组转换为逗号分隔的字符串
|
|
|
const applyHandlerIds = Array.isArray(this.form.apply_handler_id_array)
|
|
|
? this.form.apply_handler_id_array.filter(id => id).join(',')
|
|
|
: (this.form.apply_handler_id || '');
|
|
|
|
|
|
// 如果已有合同ID,传递合同ID用于更新
|
|
|
const contractId = this.hasValue && this.normalizedValue ? parseInt(this.normalizedValue) : null;
|
|
|
|
|
|
const payload = {
|
|
|
contract_id: contractId, // 如果提供,则更新该合同
|
|
|
flow_id: this.flowId ? parseInt(this.flowId) : null,
|
|
|
contract: {
|
|
|
contract_no: contractNo,
|
|
|
title: this.form.title,
|
|
|
main_content: this.form.main_content || "",
|
|
|
party_a: this.form.party_a || "",
|
|
|
party_b: this.form.party_b || "",
|
|
|
amount_total: this.form.amount_total || 0,
|
|
|
amount_type: this.form.amount_type,
|
|
|
amount_description: this.form.amount_description || "",
|
|
|
budget_amount: this.form.budget_amount || 0,
|
|
|
contract_type_id: this.form.contract_type_id,
|
|
|
contract_type: this.form.contract_type || "", // 保留旧字段,用于兼容
|
|
|
sign_date: this.form.sign_date || null,
|
|
|
apply_date: this.form.apply_date || null,
|
|
|
perform_period: this.form.perform_period || "",
|
|
|
pay_method: this.form.pay_method || "",
|
|
|
fund_source: this.form.fund_source || "",
|
|
|
fund_source_year_id: this.form.fund_source_year_id,
|
|
|
fund_source_budget_data_id: this.form.fund_source_budget_data_id,
|
|
|
is_government_purchase: this.form.is_government_purchase || false,
|
|
|
purchase_category_id: this.form.purchase_category_id,
|
|
|
tender_agent: this.form.tender_agent || "",
|
|
|
purchase_method_id: this.form.purchase_method_id,
|
|
|
purchase_method: this.form.purchase_method || "", // 保留旧字段,用于兼容
|
|
|
perform_status: this.form.perform_status || "",
|
|
|
is_accepted: this.form.is_accepted || false,
|
|
|
purchase_category: this.form.purchase_category || "",
|
|
|
handler_admin_ids: handlerAdminIds,
|
|
|
apply_handler_id: applyHandlerIds,
|
|
|
purchase_handler_id: this.form.purchase_handler_id,
|
|
|
owner_department_id: ownerDepartmentId,
|
|
|
owner_department_ids: ownerDepartmentIds,
|
|
|
remark: this.form.remark || "",
|
|
|
attachment_id: this.form.attachment_id,
|
|
|
},
|
|
|
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(contractId ? "合同更新成功" : "合同创建成功");
|
|
|
|
|
|
// 如果是新建合同(不是更新)且有flowId,自动保存到流程字段(轻量接口)
|
|
|
if (!contractId && this.flowId && this.fieldName) {
|
|
|
try {
|
|
|
await request.post(`/api/oa/flow/save-field/${this.flowId}`, {
|
|
|
field_name: this.fieldName,
|
|
|
field_value: String(data.id),
|
|
|
});
|
|
|
// 静默保存,不显示提示
|
|
|
} catch (e) {
|
|
|
// 保存失败不影响主流程,只记录错误
|
|
|
console.error("保存合同ID到流程字段失败:", e);
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
Message.error(contractId ? "更新合同失败" : "创建合同失败");
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error("创建合同失败:", e);
|
|
|
const msg = e?.response?.data?.msg || e?.message || "创建合同失败";
|
|
|
Message.error(msg);
|
|
|
} finally {
|
|
|
this.saving = false;
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
},
|
|
|
};
|
|
|
</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;
|
|
|
}
|
|
|
|
|
|
.payplan-table {
|
|
|
width: 100% !important;
|
|
|
}
|
|
|
|
|
|
.readonly-amount :deep(.el-input__inner) {
|
|
|
cursor: not-allowed;
|
|
|
background-color: #f5f7fa;
|
|
|
color: #909399;
|
|
|
}
|
|
|
|
|
|
.readonly-amount :deep(.el-input-number__decrease),
|
|
|
.readonly-amount :deep(.el-input-number__increase) {
|
|
|
display: none;
|
|
|
pointer-events: none;
|
|
|
}
|
|
|
</style>
|