2025-10-15 11:18:48 +08:00

526 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<a-modal
:width="800"
:open="visible"
:title="mode === 'create' ? '新增组织' : mode === 'edit' ? '编辑组织' : '组织详情'"
:confirm-loading="confirmLoading"
:after-close="onAfterClose"
:cancel-text="t('button.cancel')"
:ok-text="mode === 'view' ? t('button.close') : mode === 'edit' ? '保存' : '新增'"
@ok="handleOk"
@cancel="handleCancel"
:maskClosable="false"
>
<a-form
ref="formRef"
:model="formData"
:rules="isView ? {} : formRules"
layout="vertical"
autocomplete="off"
:disabled="isView"
>
<!-- 基础信息 -->
<a-card class="mb-4" title="基础信息">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item :label="'组织名称'" name="name" :required="true">
<a-input v-model:value="formData.name" placeholder="请输入组织全称" allow-clear />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="'组织机构代码'" name="orgCode" :required="true">
<a-input v-model:value="formData.orgCode" placeholder="统一社会信用代码或组织机构代码" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item :label="'组织等级'" name="orgLv" :required="true">
<a-select v-model:value="formData.orgLv" placeholder="请选择组织等级" allow-clear>
<a-select-option
v-for="item in dicsStore.dictOptions.Level"
:key="item.dval"
:value="item.dval"
>
{{ item.introduction }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 联系人信息 -->
<a-card class="mb-4" title="联系人信息" style="margin-top: 20px">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item :label="'负责人姓名'" name="concatName" :required="true">
<a-input v-model:value="formData.concatName" placeholder="请输入负责人姓名" allow-clear />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="'联系电话'" name="concatPhone" :required="true">
<a-input v-model:value="formData.concatPhone" placeholder="请输入手机号或固话" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item :label="'法人姓名'" name="legalName">
<a-input v-model:value="formData.legalName" placeholder="非必填" allow-clear />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="'法人联系电话'" name="legalPhone">
<a-input v-model:value="formData.legalPhone" placeholder="非必填" allow-clear />
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 资质信息 -->
<a-card class="mb-4" title="资质信息" style="margin-top: 20px">
<template #extra>
<a-tooltip title="营业执照、登记机关等用于资质审核">
<info-circle-outlined style="color: #8c8c8c" />
</a-tooltip>
</template>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item :label="'营业执照名称'" name="businessLicenseName">
<a-input v-model:value="formData.businessLicenseName" placeholder="非必填" allow-clear />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="'登记机关'" name="registrationAuthorityName">
<a-input v-model:value="formData.registrationAuthorityName" placeholder="非必填" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item :label="'登记证号'" name="registrationAuthorityNo">
<a-input v-model:value="formData.registrationAuthorityNo" placeholder="非必填" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item :label="'中标组织'" name="winOrg">
<a-input v-model:value="formData.winOrg" placeholder="若为中标单位请填写,否则可留空" allow-clear />
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 资质附件 -->
<a-card class="mb-4" title="资质附件" style="margin-top: 20px">
<!-- 营业执照 -->
<a-form-item :label="'营业执照图片'" name="businessLicenseFiles">
<a-upload
v-model:file-list="formData.businessLicenseFiles"
list-type="picture-card"
:before-upload="beforeUploadImage"
:custom-request="dummyRequest"
accept="image/*"
multiple
:disabled="isView"
>
<div v-if="formData.businessLicenseFiles.length < 5">
<plus-outlined />
<div class="ant-upload-text">上传</div>
</div>
</a-upload>
<div class="upload-tip">仅支持 JPG/PNG最多5张每张 ≤5MB</div>
</a-form-item>
<!-- 登记证 -->
<a-form-item :label="'登记证附件'" name="registrationCertificateFiles">
<a-upload
v-model:file-list="formData.registrationCertificateFiles"
list-type="picture-card"
:before-upload="beforeUploadImage"
:custom-request="dummyRequest"
accept="image/*"
multiple
:disabled="isView"
>
<div v-if="formData.registrationCertificateFiles.length < 5">
<plus-outlined />
<div class="ant-upload-text">上传</div>
</div>
</a-upload>
<div class="upload-tip">仅支持 JPG/PNG最多5张每张 ≤5MB</div>
</a-form-item>
<!-- 其他资质 -->
<a-form-item :label="'其他资质附件'" name="otherQualificationFiles">
<a-upload
v-model:file-list="formData.otherQualificationFiles"
:before-upload="beforeUploadFile"
:custom-request="dummyRequest"
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
multiple
:disabled="isView"
>
<a-button>点击上传资质文件</a-button>
</a-upload>
<div class="upload-tip">支持 PDF/Word/Excel/图片,单个 ≤10MB</div>
</a-form-item>
</a-card>
<!-- 地址信息 -->
<a-card class="mb-4" title="地址信息" style="margin-top: 20px">
<a-form-item :label="'所在地区'" name="areaCodes" :required="true">
<AreaCascader
v-model:value="formData.areaValue"
@change="onAreaChange"
:disabled="isView"
/>
</a-form-item>
<a-form-item :label="'详细地址'" name="address" :required="true">
<a-textarea
v-model:value="formData.address"
placeholder="请输入街道、门牌号等详细地址"
:rows="2"
show-count
:maxlength="256"
/>
</a-form-item>
</a-card>
<!-- 其他 -->
<a-card class="mb-4" title="其他" style="margin-top: 20px">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item :label="'状态'" name="status" :required="true">
<a-select v-model:value="formData.status" style="width: 120px">
<a-select-option value="1">启用</a-select-option>
<a-select-option value="2">禁用</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item :label="'备注'" name="remark">
<a-textarea
v-model:value="formData.remark"
placeholder="可填写特殊说明、内部备注等"
:rows="3"
show-count
:maxlength="500"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive, nextTick, computed } from 'vue';
import { message } from 'ant-design-vue';
import { InfoCircleOutlined, PlusOutlined } from '@ant-design/icons-vue';
import AreaCascader from '@/components/AreaCascader/index.vue';
import { useDicsStore } from '@/store';
import apis from '@/apis';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const visible = ref(false);
const formRef = ref();
const confirmLoading = ref(false);
const dicsStore = useDicsStore();
const mode = ref('create');
const isView = computed(() => mode.value === 'view');
const isEditing = computed(() => mode.value === 'edit');
const emit = defineEmits(['success']);
const editingId = ref(null);
const formData = reactive({
name: '',
orgCode: '',
orgLv: '',
concatName: '',
concatPhone: '',
legalName: '',
legalPhone: '',
businessLicenseName: '',
registrationAuthorityName: '',
registrationAuthorityNo: '',
winOrg: '',
areaValue: [],
address: '',
remark: '',
status: '1',
businessLicenseFiles: [],
registrationCertificateFiles: [],
otherQualificationFiles: [],
});
const formRules = {
name: [{ required: true, message: '请输入组织名称' }],
orgCode: [{ required: true, message: '请输入组织机构代码' }],
orgLv: [{ required: true, message: '请选择组织等级' }],
concatName: [{ required: true, message: '请输入负责人姓名' }],
concatPhone: [{ required: true, message: '请输入联系电话' }],
status: [{ required: true, message: '请选择状态' }],
areaCodes: [
{
required: true,
validator: () => {
if (!Array.isArray(formData.areaValue) || formData.areaValue.length === 0) {
return Promise.reject(new Error('请选择服务组织地址'));
}
return Promise.resolve();
},
},
],
address: [{ required: true, message: '请输入详细地址' }],
};
const open = async ({ record = null, mode: _mode = 'create' }) => {
visible.value = true;
mode.value = _mode;
editingId.value = null;
await nextTick();
if (record && _mode !== 'create') {
editingId.value = record.id;
await getDetail(record.id);
} else {
resetForm();
}
if (formRef.value) formRef.value.clearValidate();
};
const getDetail = async (id) => {
const res = await apis.serviceMenu.getServiceOrgDetail(id);
if (res?.success) {
const data = res.data;
const areaLabels = data.areaLabels
? (Array.isArray(data.areaLabels) ? data.areaLabels : String(data.areaLabels).split('/').filter(Boolean))
: [];
const bizFiles = (data.businessLicenseUrls || []).map((url, i) => ({
uid: `biz-${i}`,
name: `营业执照-${i + 1}.jpg`,
status: 'done',
url,
}));
const regFiles = (data.registrationCertificateUrls || []).map((url, i) => ({
uid: `reg-${i}`,
name: `登记证-${i + 1}.jpg`,
status: 'done',
url,
}));
const qualFiles = (data.qualificationFileUrls || []).map((item, i) => ({
uid: `qual-${i}`,
name: item.name || `附件-${i + 1}`,
status: 'done',
url: item.url,
}));
Object.assign(formData, {
name: data.name || '',
orgCode: data.orgCode || '',
orgLv: data.orgLv || '',
concatName: data.concatName || '',
concatPhone: data.concatPhone || '',
legalName: data.legalName || '',
legalPhone: data.legalPhone || '',
businessLicenseName: data.businessLicenseName || '',
registrationAuthorityName: data.registrationAuthorityName || '',
registrationAuthorityNo: data.registrationAuthorityNo || '',
winOrg: data.winOrg || '',
areaValue: areaLabels,
address: data.address || '',
remark: data.remark || '',
status: String(data.status) || '1',
businessLicenseFiles: bizFiles,
registrationCertificateFiles: regFiles,
otherQualificationFiles: qualFiles,
});
} else {
message.error(res?.message || '获取详情失败');
}
};
const handleOk = () => {
if (mode.value === 'view') {
handleCancel();
return;
}
handleSubmit();
};
const handleSubmit = async () => {
try {
const values = await formRef.value.validateFields();
confirmLoading.value = true;
// 上传文件逻辑(略,与原逻辑一致)
const bizUrls = [];
for (const f of formData.businessLicenseFiles) {
if (f.originFileObj) bizUrls.push(await uploadFile(f.originFileObj));
else if (f.url) bizUrls.push(f.url);
}
const regUrls = [];
for (const f of formData.registrationCertificateFiles) {
if (f.originFileObj) regUrls.push(await uploadFile(f.originFileObj));
else if (f.url) regUrls.push(f.url);
}
const qualFiles = [];
for (const f of formData.otherQualificationFiles) {
if (f.originFileObj) {
const url = await uploadFile(f.originFileObj);
qualFiles.push({ name: f.name, url });
} else if (f.url) {
qualFiles.push({ name: f.name, url: f.url });
}
}
const submitData = {
...values,
areaCodes: formData.areaValue,
businessLicenseUrls: bizUrls,
registrationCertificateUrls: regUrls,
qualificationFileUrls: qualFiles,
};
let res;
if (isEditing.value) {
res = await apis.serviceMenu.updateServiceOrg(editingId.value, submitData);
} else {
res = await apis.serviceMenu.createServiceOrg(submitData);
}
confirmLoading.value = false;
if (res?.success) {
message.success(isEditing.value ? '更新成功' : '创建成功');
handleCancel();
emit('success');
} else {
message.error(res?.message || '操作失败');
}
} catch (err) {
console.error(err);
confirmLoading.value = false;
message.error('提交失败,请重试');
}
};
const handleCancel = () => {
visible.value = false;
};
const onAfterClose = () => {
resetForm();
confirmLoading.value = false;
if (formRef.value) formRef.value.clearValidate();
};
// === 上传相关 ===
const dummyRequest = ({ onSuccess }) => {
setTimeout(() => onSuccess('ok'), 0);
};
const beforeUploadImage = (file) => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片!');
return false;
}
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
message.error('图片不能超过5MB');
return false;
}
return true;
};
const beforeUploadFile = (file) => {
const allowedTypes = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'image/jpeg',
'image/png',
'image/jpg',
];
if (!allowedTypes.includes(file.type)) {
message.error('仅支持 PDF、Word、Excel、JPG、PNG 格式!');
return false;
}
if (file.size / 1024 / 1024 >= 10) {
message.error('文件不能超过10MB');
return false;
}
return true;
};
const uploadFile = async (file) => {
const formData = new FormData();
formData.append('file', file);
const res = await apis.file.upload(formData);
if (res?.success && res.data?.url) {
return res.data.url;
} else {
throw new Error(res?.message || '上传失败');
}
};
const resetForm = () => {
Object.assign(formData, {
name: '',
orgCode: '',
orgLv: '',
concatName: '',
concatPhone: '',
legalName: '',
legalPhone: '',
businessLicenseName: '',
registrationAuthorityName: '',
registrationAuthorityNo: '',
winOrg: '',
areaValue: [],
address: '',
remark: '',
status: '1',
businessLicenseFiles: [],
registrationCertificateFiles: [],
otherQualificationFiles: [],
});
};
const onAreaChange = (selectedCodes, selectedLabels) => {
formData.areaValue = [...selectedCodes];
formRef.value?.validateFields(['areaCodes']).catch(() => {});
};
defineExpose({ open, close: handleCancel });
</script>
<style lang="less" scoped>
.upload-tip {
color: #8c8c8c;
font-size: 12px;
margin-top: 4px;
}
.mb-4 {
margin-bottom: 16px;
}
.ant-upload-text {
margin-top: 4px;
}
</style>