feat: 合同文档模板设置
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<div class="system-setting-contract-file">
|
||||
<Breadcrumb
|
||||
:items="['menu.setting', 'menu.setting.system', 'setting.contract']"
|
||||
/>
|
||||
<a-card class="full_height">
|
||||
<a-row :gutter="24">
|
||||
<a-col
|
||||
v-if="contractFileInfo && Object.keys(contractFileInfo).length"
|
||||
:span="7"
|
||||
>
|
||||
<icon-arrow-left /><span class="ml-[10px]">合同文档模板设置</span>
|
||||
<div
|
||||
class="mt-[24px] rounded-[6px] border-solid border-[#efefef] border-[1px] px-[12px] py-[20px]"
|
||||
>
|
||||
<h4 class="text-[16px] font-bold">{{ contractFileInfo?.name }}</h4>
|
||||
<p class="text-[14px] mt-[12px]"
|
||||
><span class="mr-[5px] text-[#101010]">适用范围</span
|
||||
>立思广州分公司</p
|
||||
>
|
||||
<p class="text-[14px] mt-[4px]"
|
||||
><span class="mr-[5px] text-[#101010]">适用项目</span
|
||||
>星河商务中心</p
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center mt-[24px]">
|
||||
<a-input-search class="w-[256px] h-[30px]" placeholder="搜索名称" />
|
||||
<div
|
||||
class="flex items-center border-[1px] border-solid border-[#165DFF] px-[4px] py-[6px] ml-[10px]"
|
||||
>
|
||||
<icon-google />
|
||||
<p class="ml-[5px] text-[#165DFF]">AI助手</p>
|
||||
</div>
|
||||
</div>
|
||||
<a-tabs default-active-key="1" class="mt-[10px]">
|
||||
<a-tab-pane key="1" title="租赁条款"> 租赁条款 </a-tab-pane>
|
||||
<a-tab-pane key="2" title="物业条款"> w物业条款 </a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-col>
|
||||
<a-col :span="17">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<icon-upload />
|
||||
</template>
|
||||
上传文档
|
||||
</a-button>
|
||||
<a-button class="ml-[10px]">
|
||||
<template #icon>
|
||||
<icon-cloud-download />
|
||||
</template>
|
||||
下载文档
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<p>2025-09-10 00:25:30更新</p>
|
||||
<a-button type="primary" class="ml-[10px]">
|
||||
<template #icon>
|
||||
<icon-storage />
|
||||
</template>
|
||||
保存
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- OnlyOffice 文档编辑器 -->
|
||||
<div class="onlyoffice-wrapper">
|
||||
<!-- <OnlyOfficeEditor v-if="contractFileInfo?.url" :document-url="contractFileInfo.url"
|
||||
:document-title="contractFileInfo.name || '合同文档'"
|
||||
:document-type="getDocumentType(contractFileInfo.url)" :height="'600px'" :editable="true"
|
||||
:mode="'edit'" @ready="onEditorReady" @error="onEditorError" @save="onDocumentSave"
|
||||
@close="onEditorClose" />
|
||||
<div v-else class="no-document">
|
||||
<a-empty description="暂无文档内容" />
|
||||
</div> -->
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { getWordSettingFromId } from '@/api/setting';
|
||||
import { CreateFileSettingParam } from '@/types/setting';
|
||||
|
||||
const contractFileInfo = ref<CreateFileSettingParam | null>(null);
|
||||
const route = useRoute();
|
||||
|
||||
// 获取文档数据
|
||||
const getWordSettingFromIdData = async () => {
|
||||
const id: any = route?.query?.id;
|
||||
const { code, data } = await getWordSettingFromId(id);
|
||||
if (code === 200 && data) {
|
||||
const { contractWordSetting } = data;
|
||||
contractFileInfo.value = contractWordSetting || null;
|
||||
contractFileInfo.value.url =
|
||||
'http://oss.kola.work/documents%2F20250910%2F%E5%90%88%E5%90%8C%E6%A8%A1%E7%89%88.docx?Expires=1757479342&OSSAccessKeyId=LTAI5tDDNxXnZq7QcpZEf6hR&Signature=ydBzbF2yCyRWZ0G52s%2FPr%2BFIUzo%3D';
|
||||
}
|
||||
};
|
||||
|
||||
// 根据文件 URL 获取文档类型
|
||||
const getDocumentType = (url: string): 'word' | 'excel' | 'powerpoint' => {
|
||||
if (!url) return 'word';
|
||||
|
||||
const extension = url.toLowerCase().split('.').pop();
|
||||
switch (extension) {
|
||||
case 'xlsx':
|
||||
case 'xls':
|
||||
return 'excel';
|
||||
case 'pptx':
|
||||
case 'ppt':
|
||||
return 'powerpoint';
|
||||
case 'docx':
|
||||
case 'doc':
|
||||
default:
|
||||
return 'word';
|
||||
}
|
||||
};
|
||||
|
||||
// OnlyOffice 编辑器事件处理
|
||||
const onEditorReady = () => {
|
||||
console.log('OnlyOffice 编辑器已准备就绪');
|
||||
Message.success('文档编辑器加载完成');
|
||||
};
|
||||
|
||||
const onEditorError = (error: string) => {
|
||||
console.error('OnlyOffice 编辑器错误:', error);
|
||||
Message.error(`编辑器错误: ${error}`);
|
||||
};
|
||||
|
||||
const onDocumentSave = (data: any) => {
|
||||
console.log('文档保存事件:', data);
|
||||
Message.success('文档已保存');
|
||||
// 这里可以添加保存后的处理逻辑,比如更新服务器数据
|
||||
};
|
||||
|
||||
const onEditorClose = () => {
|
||||
console.log('编辑器关闭');
|
||||
Message.info('编辑器已关闭');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getWordSettingFromIdData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.onlyoffice-wrapper {
|
||||
margin-top: 16px;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.no-document {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.system-setting-contract-file {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.full_height {
|
||||
height: calc(100vh - 120px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.arco-row) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.arco-col) {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -61,7 +61,6 @@
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useDataTable from '@/composables/useDataTable';
|
||||
import {
|
||||
getWordSettingFromId,
|
||||
delWordSetting,
|
||||
getWordSettingList,
|
||||
disableWordSetting,
|
||||
@@ -72,7 +71,7 @@
|
||||
import { camelToSnake } from '@/utils/caseConvert';
|
||||
// import contractNumberFormDrawer from './drawer/contract-number-form-drawer.vue';
|
||||
import contractFileForm from './modal/contract-file-form.vue';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
const {
|
||||
data: renderData,
|
||||
columns: projectColumn,
|
||||
@@ -91,7 +90,7 @@
|
||||
initialPageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const contractWordList = ref<ContractWordItem[]>([]);
|
||||
const departmentListData = ref<DepartmentItem[]>([]);
|
||||
const showModal = ref<boolean>(false);
|
||||
@@ -200,26 +199,16 @@
|
||||
// 处理错误
|
||||
}
|
||||
};
|
||||
const getWordSettingFromIdData = async (id: number) => {
|
||||
const { code, data } = await getWordSettingFromId(id);
|
||||
if (code === 200 && data) {
|
||||
const { contractCodeSetting } = data;
|
||||
currentNumberInfo.value = contractCodeSetting || null;
|
||||
currentNumberInfo.value.arrange = camelToSnake(
|
||||
currentNumberInfo.value.arrange
|
||||
);
|
||||
}
|
||||
};
|
||||
const handleAddContractNumber = () => {
|
||||
// currentNumberInfo.value = null;
|
||||
isEdit.value = false;
|
||||
showModal.value = true;
|
||||
};
|
||||
const handleEditContractNumber = async (id: number) => {
|
||||
await getWordSettingFromIdData(id);
|
||||
isEdit.value = true;
|
||||
// currentNumberInfo.value = info
|
||||
showModal.value = true;
|
||||
router.push({
|
||||
name: 'system-setting-contract-file-edit',
|
||||
query: { id },
|
||||
});
|
||||
};
|
||||
// 初始化数据
|
||||
onMounted(() => {
|
||||
|
||||
@@ -0,0 +1,709 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="
|
||||
currentContractTemplateInfo &&
|
||||
Object.keys(currentContractTemplateInfo).length
|
||||
"
|
||||
class="system-setting-contract-file pr-[20px]"
|
||||
>
|
||||
<Breadcrumb
|
||||
:items="['menu.setting', 'menu.setting.system', 'setting.contract']"
|
||||
/>
|
||||
<div class="flex items-center justify-between cursor-pointer mb-[20px]">
|
||||
<div @click="router.go(-1)">
|
||||
<icon-arrow-left /> <span class="ml-[10px]">合同文档模板设置</span>
|
||||
</div>
|
||||
<a-button type="primary" @click="handleSubmit">保存</a-button>
|
||||
</div>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="7">
|
||||
<a-card class="full_height rounded-[5px]">
|
||||
<div class="pb-[10px] border-b-[1px] border-b-solid border-[#f0f0f0]">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-[#000]">{{ currentContractTemplateInfo.name }}</p>
|
||||
<p class="cursor-pointer" @click="handleBaseInfoEdit"
|
||||
><icon-edit
|
||||
/></p>
|
||||
</div>
|
||||
<a-tag class="mt-[5px]" color="green">{{
|
||||
currentContractTemplateInfo.contractType
|
||||
}}</a-tag>
|
||||
<p class="text-[#000] mt-[5px]">
|
||||
<span class="text-[#999999] mr-[5px]">适用范围</span>
|
||||
{{ currentContractTemplateInfo.departmentName }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="py-[10px]">
|
||||
<h3 class="text-[16px] text-[#505968]">基本配置</h3>
|
||||
<p
|
||||
:class="[
|
||||
'cursor-pointer mt-[8px] px-[10px] h-[48px] leading-[48px] text-left text-[14px]',
|
||||
'rounded-[2px] border-[1px] border-solid border-[#f8f8f8]',
|
||||
{ isActive: currentId === 0 },
|
||||
]"
|
||||
@click="handleChoose(0, '基本信息')"
|
||||
>
|
||||
基本信息</p
|
||||
>
|
||||
<div class="flex items-center justify-between mt-[8px]">
|
||||
<h3 class="text-[16px] text-[#505968]">条款配置</h3>
|
||||
<p class="cursor-pointer">
|
||||
<a-popover trigger="click" position="br">
|
||||
<icon-plus />
|
||||
<template #content>
|
||||
<ul>
|
||||
<li
|
||||
v-for="item in allTermsList"
|
||||
:key="item.id"
|
||||
class="cursor-pointer px-[10px] py-[5px] border-b-[1px] border-b-solid border-[#f0f0f0]"
|
||||
@click="handleAddTerms(item.id)"
|
||||
>
|
||||
{{ item.name }}</li
|
||||
>
|
||||
</ul>
|
||||
</template>
|
||||
</a-popover>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-for="item in currentContractTemplateInfo.terms"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'cursor-pointer mt-[8px] px-[10px] h-[48px] leading-[48px] text-left text-[14px]',
|
||||
'rounded-[2px] border-[1px] border-solid border-[#f8f8f8]',
|
||||
{ isActive: currentId === item.id },
|
||||
]"
|
||||
@click="handleChoose(item.id, item.name)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
<contractTemplateForm
|
||||
v-model:visible="showModal"
|
||||
:is-edit="true"
|
||||
:department-list-data="departmentListData"
|
||||
:record="currentContractTemplateInfo"
|
||||
@submitted="getTemplateSettingFromIdData"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="17">
|
||||
<a-card class="full_height rounded-[5px]">
|
||||
<div class="flex items-center justify-between mb-[10px]">
|
||||
<p class="flex items-center">{{ title }}</p>
|
||||
<a-button
|
||||
v-if="currentId > 0"
|
||||
type="text"
|
||||
status="danger"
|
||||
@click="handleDeleteTerm"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
<template #default>删除条款</template>
|
||||
</a-button>
|
||||
</div>
|
||||
<!-- 基本信息内容 -->
|
||||
<div v-if="currentId === 0" class="mt-[5px]">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="baseInfoForm"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<!-- 合同编号模板 -->
|
||||
<a-form-item
|
||||
label="合同编号模板"
|
||||
field="contractCodeSettingId"
|
||||
required
|
||||
>
|
||||
<a-select
|
||||
v-model="baseInfoForm.contractCodeSettingId"
|
||||
placeholder="请选择合同编号模板"
|
||||
@change="chooseCodeSettingId"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in codeSettingList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<div
|
||||
v-if="currentContractTemplateInfo.contractCode"
|
||||
class="p-[10px] bg-[#eaf3fe]"
|
||||
>
|
||||
合同编号预览:{{
|
||||
currentContractTemplateInfo.contractCode
|
||||
}}</div
|
||||
>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<!-- 合同类型 -->
|
||||
<a-form-item label="合同类型" field="type" required>
|
||||
<ul class="options">
|
||||
<li
|
||||
v-for="item in contractTaxMap"
|
||||
:key="item.type"
|
||||
:class="[
|
||||
'subItem',
|
||||
{ isActive: item.type === baseInfoForm.type },
|
||||
]"
|
||||
@click="chooseTaxType(item.type)"
|
||||
>
|
||||
{{ item.text }}</li
|
||||
>
|
||||
</ul>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<p class="my-[20px] font-bold text-[15px] text-[#101010]"
|
||||
>其他信息(选填)</p
|
||||
>
|
||||
<a-form-item label="合同标签" field="tags">
|
||||
<a-select
|
||||
v-model="baseInfoForm.tags"
|
||||
multiple
|
||||
placeholder="请选择合同标签"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in contractTagsList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" field="remark">
|
||||
<a-textarea
|
||||
v-model="baseInfoForm.remark"
|
||||
placeholder="输入合同备注"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
<template v-else>
|
||||
<a-tag
|
||||
v-for="item of termTabList"
|
||||
:key="item.key"
|
||||
:style="{ color: item.isChoose === 1 ? '#165DFF' : '' }"
|
||||
:color="item.isChoose === 1 ? '#eaf3fe' : ''"
|
||||
:class="[
|
||||
'mr-[10px] mb-[10px] cursor-pointer text-[#1D2129]',
|
||||
{ 'text-[#165DFF]': item.isChoose == 1 },
|
||||
]"
|
||||
@click="handleToggleTerm(item)"
|
||||
>
|
||||
{{ item.title }}
|
||||
<icon-close v-if="item.isChoose === 1" />
|
||||
</a-tag>
|
||||
<a-tag class="cursor-pointer mb-[10px]" @click="handleAddTerm">
|
||||
<icon-plus />
|
||||
优惠条款
|
||||
</a-tag>
|
||||
<div class="mt-[20px]">
|
||||
<bond
|
||||
v-if="isBondShown"
|
||||
ref="bondRef"
|
||||
:title="'保证金条款(租金保证金)'"
|
||||
/>
|
||||
<incremental
|
||||
v-if="isIncrementalShown"
|
||||
ref="incrementalRef"
|
||||
:title="'递增条款'"
|
||||
/>
|
||||
<discount
|
||||
v-for="(d, di) in discountChosen"
|
||||
:key="d.key"
|
||||
:ref="(el) => setDiscountRef(el, di)"
|
||||
:title="d.title"
|
||||
/>
|
||||
<lateFee
|
||||
v-if="isLateFeeShown"
|
||||
ref="lateFeeRef"
|
||||
:title="'滞纳金条款'"
|
||||
/>
|
||||
<rentSetting
|
||||
ref="rentSettingRef"
|
||||
:term-setting-info="termSettingInfo"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, reactive, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import {
|
||||
getTemplateSettingFromId,
|
||||
getTermsSettingFromId,
|
||||
getDepartmentList,
|
||||
getAllCodeSettingList,
|
||||
getContractTagsList,
|
||||
updateTemplateSetting,
|
||||
getAllTermsSettingList,
|
||||
} from '@/api/setting';
|
||||
import contractTemplateForm from './modal/contract-template-form.vue';
|
||||
import { DepartmentItem } from '@/types/setting';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import bond from './terms/bond.vue';
|
||||
import incremental from './terms/incremental.vue';
|
||||
import discount from './terms/discount.vue';
|
||||
import lateFee from './terms/late-fee.vue';
|
||||
import rentSetting from './terms/rent-setting.vue';
|
||||
const currentContractTemplateInfo = ref<any>(null);
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const showModal = ref<boolean>(false);
|
||||
const title = ref<string>('基本信息');
|
||||
const currentId = ref<number>(0);
|
||||
const departmentListData = ref<DepartmentItem[]>([]);
|
||||
const contractTaxMap = [
|
||||
// 1是含税2不含税
|
||||
{ type: 1, text: '含税合同' },
|
||||
{ type: 2, text: '不含税合同' },
|
||||
];
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const defaultForm: any = {
|
||||
id: null,
|
||||
contractCodeSettingId: null,
|
||||
saveType: 2, // 保存类别:1 名称与适用范围数据,2 基础信息数据,3 条款数据)
|
||||
tags: [],
|
||||
remark: '',
|
||||
type: 1,
|
||||
};
|
||||
const codeSettingList = ref<any>([]);
|
||||
const baseInfoForm = reactive<any>({ ...defaultForm });
|
||||
const allTermsList = ref<any>([]);
|
||||
// refs for terms components
|
||||
const rentSettingRef = ref();
|
||||
const bondRef = ref();
|
||||
const incrementalRef = ref();
|
||||
const lateFeeRef = ref();
|
||||
const discountRefs = ref<any[]>([]);
|
||||
// 校验规则
|
||||
const rules = reactive({
|
||||
contractCodeSettingId: [
|
||||
{ required: true, message: '合同编号模板不能为空' },
|
||||
],
|
||||
type: [{ required: true, message: '请选择合同类型' }],
|
||||
});
|
||||
const termSettingInfo = ref<any>({});
|
||||
const termTabList = ref([
|
||||
{
|
||||
key: 'bond',
|
||||
title: '保证金条款',
|
||||
isChoose: 1,
|
||||
type: 'bond',
|
||||
},
|
||||
{
|
||||
key: 'incremental',
|
||||
title: '递增条款',
|
||||
isChoose: 1,
|
||||
type: 'incremental',
|
||||
},
|
||||
{
|
||||
key: 'discount-0',
|
||||
title: '单价折扣',
|
||||
isChoose: 1,
|
||||
type: 'unitDiscount',
|
||||
},
|
||||
{
|
||||
key: 'discount',
|
||||
title: '优惠条款',
|
||||
isChoose: 1,
|
||||
type: 'discount',
|
||||
},
|
||||
{
|
||||
key: 'lateFee',
|
||||
title: '滞纳金条款',
|
||||
isChoose: 1,
|
||||
type: 'lateFee',
|
||||
},
|
||||
]);
|
||||
const contractTagsList = ref<any>([]);
|
||||
const getTemplateSettingFromIdData = async () => {
|
||||
const id: any = route?.query?.id;
|
||||
const { code, data } = await getTemplateSettingFromId(id);
|
||||
if (code === 200 && data) {
|
||||
const { contractTemplateSetting } = data;
|
||||
currentContractTemplateInfo.value = contractTemplateSetting || null;
|
||||
Object.keys(baseInfoForm).forEach((key) => {
|
||||
// const value = baseInfoForm[key];
|
||||
if (key === 'tags') {
|
||||
baseInfoForm.tags = contractTemplateSetting.tags
|
||||
.map((i) => i.id)
|
||||
.filter(Boolean);
|
||||
} else {
|
||||
baseInfoForm[key] = contractTemplateSetting[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
// 部门列表
|
||||
const getDepartmentListData = async () => {
|
||||
try {
|
||||
const { code, data } = await getDepartmentList();
|
||||
if (code === 200) {
|
||||
departmentListData.value = data.departmentList;
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('获取部门列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 由 termTabList 计算可见的优惠条款列表
|
||||
const discountChosen = computed<any[]>(() =>
|
||||
termTabList.value.filter(
|
||||
(i: any) => i.type === 'discount' && i.isChoose === 1
|
||||
)
|
||||
);
|
||||
// 组件显隐由接口开关 + 选择状态共同决定
|
||||
let toNum = (v: any) => Number(v || 0);
|
||||
|
||||
const isBondShown = computed(
|
||||
() =>
|
||||
toNum(termSettingInfo.value?.isBond) === 1 &&
|
||||
termTabList.value.some((i: any) => i.type === 'bond' && i.isChoose === 1)
|
||||
);
|
||||
const isIncrementalShown = computed(
|
||||
() =>
|
||||
toNum(termSettingInfo.value?.isIncremental) === 1 &&
|
||||
termTabList.value.some(
|
||||
(i: any) => i.type === 'incremental' && i.isChoose === 1
|
||||
)
|
||||
);
|
||||
const isLateFeeShown = computed(
|
||||
() =>
|
||||
toNum(termSettingInfo.value?.isLateFee) === 1 &&
|
||||
termTabList.value.some(
|
||||
(i: any) => i.type === 'lateFee' && i.isChoose === 1
|
||||
)
|
||||
);
|
||||
async function getTermsSettingFromIdData(id) {
|
||||
const { code, data } = await getTermsSettingFromId(id);
|
||||
if (code === 200) {
|
||||
termSettingInfo.value = data?.contractTermsSetting ?? {};
|
||||
// 根据后端开关更新标签显示状态(例如:isBond === 2 时,保证金标签置为未选中)
|
||||
const next = (termTabList.value || []).map((t) => ({ ...t }));
|
||||
const bondIdx = next.findIndex((t) => t.type === 'bond');
|
||||
if (bondIdx > -1) {
|
||||
next[bondIdx].isChoose =
|
||||
toNum(termSettingInfo.value?.isBond) === 1 ? 1 : 2;
|
||||
}
|
||||
// 保持其他标签不变;如需扩展递增、滞纳金可在此追加
|
||||
termTabList.value = next;
|
||||
}
|
||||
}
|
||||
const handleChoose = (id: number, str: string) => {
|
||||
if (id === currentId.value) return;
|
||||
currentId.value = id;
|
||||
title.value = str;
|
||||
getTermsSettingFromIdData(id);
|
||||
};
|
||||
const handleBaseInfoEdit = () => {
|
||||
showModal.value = true;
|
||||
};
|
||||
const getAllCodeSettingListData = async () => {
|
||||
const { code, data } = await getAllCodeSettingList();
|
||||
if (code === 200) {
|
||||
codeSettingList.value = data?.contractCodeSetting || [];
|
||||
}
|
||||
};
|
||||
const chooseCodeSettingId = (id) => {
|
||||
const info = codeSettingList.value.find((i) => i.id === id);
|
||||
if (info) {
|
||||
currentContractTemplateInfo.value.contractCode = info?.code;
|
||||
}
|
||||
};
|
||||
const chooseTaxType = (type) => {
|
||||
baseInfoForm.type = type;
|
||||
};
|
||||
const getContractTagsListData = async () => {
|
||||
const { code, data } = await getContractTagsList();
|
||||
if (code === 200 && data) {
|
||||
contractTagsList.value = data?.contractTagsSettingList;
|
||||
}
|
||||
};
|
||||
const submitBaseInfo = async () => {
|
||||
await formRef.value?.validate();
|
||||
baseInfoForm.id = Number(route?.query?.id);
|
||||
const { code, message } = await updateTemplateSetting(baseInfoForm);
|
||||
if (code === 200) {
|
||||
Message.success(message);
|
||||
}
|
||||
};
|
||||
const handleSubmit = async () => {
|
||||
if (currentId.value === 0) {
|
||||
await submitBaseInfo();
|
||||
} else {
|
||||
// 先统一执行可见子表单校验;任一失败则中断
|
||||
let hasValidationError = false;
|
||||
if ((rentSettingRef as any)?.value?.validate) {
|
||||
try {
|
||||
await (rentSettingRef as any).value.validate();
|
||||
} catch (e) {
|
||||
hasValidationError = true;
|
||||
}
|
||||
}
|
||||
if (isBondShown.value && (bondRef as any)?.value?.validate) {
|
||||
try {
|
||||
await (bondRef as any).value.validate();
|
||||
} catch (e) {
|
||||
hasValidationError = true;
|
||||
}
|
||||
}
|
||||
if (
|
||||
isIncrementalShown.value &&
|
||||
(incrementalRef as any)?.value?.validate
|
||||
) {
|
||||
try {
|
||||
await (incrementalRef as any).value.validate();
|
||||
} catch (e) {
|
||||
hasValidationError = true;
|
||||
}
|
||||
}
|
||||
if (isLateFeeShown.value && (lateFeeRef as any)?.value?.validate) {
|
||||
try {
|
||||
await (lateFeeRef as any).value.validate();
|
||||
} catch (e) {
|
||||
hasValidationError = true;
|
||||
}
|
||||
}
|
||||
const validationPromises = (discountRefs.value || [])
|
||||
.filter((r) => r?.validate)
|
||||
.map((r) =>
|
||||
r.validate().catch(() => {
|
||||
hasValidationError = true;
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(validationPromises);
|
||||
|
||||
if (hasValidationError) return;
|
||||
|
||||
// 校验通过后再收集数据
|
||||
const rentSettingData = (rentSettingRef as any)?.value?.getData?.() || {};
|
||||
const bondData = (bondRef as any)?.value?.getData?.() || {};
|
||||
const incrementalData = (incrementalRef as any)?.value?.getData?.() || {};
|
||||
const discountData = (discountRefs.value || [])
|
||||
.map((r: any) => r?.getData?.() || {})
|
||||
.filter((x: any) => x && Object.keys(x).length);
|
||||
const lateFeeData = (lateFeeRef as any)?.value?.getData?.() || {};
|
||||
|
||||
// 非空校验(仅校验当前显示/选中的条款)
|
||||
const isEmptyValue = (v: any) =>
|
||||
v === undefined ||
|
||||
v === null ||
|
||||
(typeof v === 'string' && v.trim() === '');
|
||||
const validateObject = (obj: any, label: string): string[] => {
|
||||
if (!obj || typeof obj !== 'object') return [`${label}不能为空`];
|
||||
if (Object.keys(obj).length === 0) return [`${label}不能为空`];
|
||||
const errs: string[] = [];
|
||||
Object.keys(obj).forEach((k) => {
|
||||
const val = obj[k];
|
||||
// 数值 0 视为有效
|
||||
if (typeof val === 'number') return;
|
||||
if (Array.isArray(val)) {
|
||||
if (val.length === 0) errs.push(`${label}存在未填写项`);
|
||||
} else if (isEmptyValue(val)) {
|
||||
errs.push(`${label}存在未填写项`);
|
||||
}
|
||||
});
|
||||
return errs;
|
||||
};
|
||||
const errors: string[] = [];
|
||||
// 租金/租期条款固定显示,必校验
|
||||
errors.push(...validateObject(rentSettingData, '租期条款'));
|
||||
// 保证金
|
||||
if (isBondShown.value)
|
||||
errors.push(...validateObject(bondData, '保证金条款'));
|
||||
// 递增
|
||||
if (isIncrementalShown.value)
|
||||
errors.push(...validateObject(incrementalData, '递增条款'));
|
||||
// 优惠(多个)
|
||||
if (discountChosen.value.length > 0) {
|
||||
if (discountData.length === 0) {
|
||||
errors.push('优惠条款存在未填写项');
|
||||
} else {
|
||||
discountData.forEach((d: any, idx: number) => {
|
||||
errors.push(...validateObject(d, `优惠条款${idx + 1}`));
|
||||
});
|
||||
}
|
||||
}
|
||||
// 滞纳金
|
||||
if (isLateFeeShown.value)
|
||||
errors.push(...validateObject(lateFeeData, '滞纳金条款'));
|
||||
|
||||
if (errors.length > 0) {
|
||||
Message.error(errors[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTerm =
|
||||
(currentContractTemplateInfo.value?.terms || []).find(
|
||||
(t: any) => t.id === currentId.value
|
||||
) || {};
|
||||
|
||||
const payload: any = {
|
||||
saveType: 3,
|
||||
id: Number(route?.query?.id),
|
||||
name: currentContractTemplateInfo.value?.name,
|
||||
departmentId: currentContractTemplateInfo.value?.departmentId,
|
||||
type: baseInfoForm.type,
|
||||
contractCodeSettingId: baseInfoForm.contractCodeSettingId,
|
||||
tags: baseInfoForm.tags,
|
||||
remark: baseInfoForm.remark,
|
||||
term: {
|
||||
id: currentTerm?.id,
|
||||
type: 1,
|
||||
contractTermsSettingId: currentTerm?.id,
|
||||
leaseTerm: rentSettingData,
|
||||
bondTerm: bondData,
|
||||
incrementalTerm: incrementalData,
|
||||
discountTerm: discountData,
|
||||
lateFeeTerm: lateFeeData,
|
||||
remarkTerm: { remark: '' },
|
||||
},
|
||||
};
|
||||
|
||||
updateTemplateSetting(payload).then(({ code, message }) => {
|
||||
if (code === 200) {
|
||||
Message.success(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
const getAllTermsSettingListData = async () => {
|
||||
const { code, data } = await getAllTermsSettingList();
|
||||
if (code === 200) {
|
||||
allTermsList.value = data?.contractTermsSetting || [];
|
||||
}
|
||||
};
|
||||
function handleAddTerms(id: number) {
|
||||
const allList = allTermsList.value || [];
|
||||
const found = allList.find((t: any) => t.id === id);
|
||||
if (!found) return;
|
||||
const list = currentContractTemplateInfo.value?.terms || [];
|
||||
const exists = list.some((t: any) => t.id === id);
|
||||
if (!exists) {
|
||||
// 仅插入必要字段,保持与现有结构一致
|
||||
list.push({ id: found.id, name: found.name });
|
||||
currentContractTemplateInfo.value.terms = [...list];
|
||||
}
|
||||
// 切换到新添加/已存在的条款并拉取其配置
|
||||
currentId.value = id;
|
||||
title.value = found.name;
|
||||
getTermsSettingFromIdData(id);
|
||||
}
|
||||
|
||||
function handleDeleteTerm() {
|
||||
const id = currentId.value;
|
||||
if (!id) return;
|
||||
const terms = currentContractTemplateInfo.value?.terms || [];
|
||||
const nextTerms = terms.filter((t: any) => t.id !== id);
|
||||
currentContractTemplateInfo.value.terms = nextTerms;
|
||||
|
||||
// 选择下一个可编辑的目标:优先剩余的第一项,否则回到基本信息
|
||||
if (nextTerms.length > 0) {
|
||||
currentId.value = nextTerms[0].id;
|
||||
title.value = nextTerms[0].name;
|
||||
getTermsSettingFromIdData(nextTerms[0].id);
|
||||
} else {
|
||||
currentId.value = 0;
|
||||
title.value = '基本信息';
|
||||
}
|
||||
}
|
||||
function handleToggleTerm(info: any) {
|
||||
if (info?.type === 'discount') {
|
||||
const discounts = termTabList.value.filter(
|
||||
(i: any) => i.type === 'discount'
|
||||
);
|
||||
if (discounts.length > 1) {
|
||||
// 删除重复的优惠条款,仅保留第一条
|
||||
const firstKey = discounts[0].key;
|
||||
termTabList.value = termTabList.value.filter(
|
||||
(i: any) => i.type !== 'discount' || i.key === firstKey
|
||||
);
|
||||
// 强制保留项为选中
|
||||
const keep = termTabList.value.find((i: any) => i.key === firstKey);
|
||||
if (keep) keep.isChoose = 1;
|
||||
} else {
|
||||
// 只有一条时正常切换选中状态
|
||||
info.isChoose = info.isChoose === 1 ? 2 : 1;
|
||||
}
|
||||
} else {
|
||||
info.isChoose = info.isChoose === 1 ? 2 : 1;
|
||||
}
|
||||
}
|
||||
function handleAddTerm() {
|
||||
const idx = termTabList.value.filter(
|
||||
(i: any) => i.type === 'discount'
|
||||
).length;
|
||||
const key = `discount-${Date.now()}-${idx}`;
|
||||
termTabList.value.push({
|
||||
key,
|
||||
title: `优惠条款${idx + 1}`,
|
||||
isChoose: 1,
|
||||
type: 'discount',
|
||||
});
|
||||
}
|
||||
|
||||
const setDiscountRef = (el: any, index: number) => {
|
||||
if (el) discountRefs.value[index] = el;
|
||||
};
|
||||
onMounted(() => {
|
||||
getTemplateSettingFromIdData();
|
||||
getDepartmentListData();
|
||||
getAllCodeSettingListData();
|
||||
getContractTagsListData();
|
||||
getAllTermsSettingListData();
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.isActive {
|
||||
background-color: #eaf3fe;
|
||||
border-color: #73a5f1;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f2f3f5;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 3px;
|
||||
|
||||
.subItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.isActive {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
:deep(.arco-popover-popup-content) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.arco-tag .arco-tag-custom-color) {
|
||||
color: #165dff !important;
|
||||
}
|
||||
</style>
|
||||
@@ -48,20 +48,19 @@
|
||||
</DataTable>
|
||||
<contractTemplateForm
|
||||
v-model:visible="showModal"
|
||||
:is-edit="false"
|
||||
:department-list-data="departmentListData"
|
||||
:is-edit="isEdit"
|
||||
@submitted="getTemplateSettingListData"
|
||||
/>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { TableColumnData } from '@arco-design/web-vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useDataTable from '@/composables/useDataTable';
|
||||
import {
|
||||
getTemplateSettingFromId,
|
||||
delTemplateSetting,
|
||||
getTemplateSettingList,
|
||||
getDepartmentList,
|
||||
@@ -69,10 +68,8 @@
|
||||
} from '@/api/setting';
|
||||
import { ContractTemplateSettingItem, DepartmentItem } from '@/types/setting';
|
||||
import { UserStatus } from '@/types/comm';
|
||||
import { camelToSnake } from '@/utils/caseConvert';
|
||||
// import contractNumberFormDrawer from './drawer/contract-number-form-drawer.vue';
|
||||
import contractTemplateForm from './modal/contract-template-form.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const {
|
||||
data: renderData,
|
||||
columns: projectColumn,
|
||||
@@ -96,7 +93,7 @@
|
||||
const departmentListData = ref<DepartmentItem[]>([]);
|
||||
const showModal = ref<boolean>(false);
|
||||
const isEdit = ref<boolean>(false);
|
||||
const currentNumberInfo = ref<any>(null);
|
||||
const currentContractTemplateInfo = ref<any>(null);
|
||||
// 列配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
@@ -201,26 +198,17 @@
|
||||
// 处理错误
|
||||
}
|
||||
};
|
||||
const getTemplateSettingFromIdData = async (id: number) => {
|
||||
const { code, data } = await getTemplateSettingFromId(id);
|
||||
if (code === 200 && data) {
|
||||
const { contractCodeSetting } = data;
|
||||
currentNumberInfo.value = contractCodeSetting || null;
|
||||
currentNumberInfo.value.arrange = camelToSnake(
|
||||
currentNumberInfo.value.arrange
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddContractNumber = () => {
|
||||
// currentNumberInfo.value = null;
|
||||
currentContractTemplateInfo.value = null;
|
||||
isEdit.value = false;
|
||||
showModal.value = true;
|
||||
};
|
||||
const handleEditContractNumber = async (id: number) => {
|
||||
await getTemplateSettingFromIdData(id);
|
||||
isEdit.value = true;
|
||||
// currentNumberInfo.value = info
|
||||
showModal.value = true;
|
||||
router.push({
|
||||
name: 'system-setting-contract-template-edit',
|
||||
query: { id },
|
||||
});
|
||||
};
|
||||
// 初始化数据
|
||||
onMounted(() => {
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:width="650"
|
||||
:visible="visible"
|
||||
title="租赁条款租期设置"
|
||||
unmount-on-close
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<p class="title">计费相关</p>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="付款周期" field="payCycle" required>
|
||||
<a-input-number
|
||||
v-model="form.payCycle"
|
||||
append="月1付"
|
||||
:min="1"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="税率" field="taxRate" required>
|
||||
<a-input-number
|
||||
v-model="form.taxRate"
|
||||
append="%"
|
||||
:min="0"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="计费模式" field="chargeType" required>
|
||||
<a-select v-model="form.chargeType">
|
||||
<a-option :value="1">按月计算租金</a-option>
|
||||
<a-option :value="2">按实际天数计算租金</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="合同单价默认单位" field="priceUnit" required>
|
||||
<a-select v-model="form.priceUnit">
|
||||
<a-option
|
||||
v-for="item in priceUnits"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.text }}</a-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item v-if="form.chargeType === 1">
|
||||
<div class="bg-[#eaf3fe] p-[10px] w-full">
|
||||
<icon-exclamation-circle-fill style="color: #2d5cf6" />
|
||||
按月计算租金公式=合同单价 x 合同面积
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="每天单价算法(适合不足月租期计费)"
|
||||
field="dayPriceAlgorithm"
|
||||
required
|
||||
>
|
||||
<a-select v-model="form.dayPriceAlgorithm">
|
||||
<a-option :value="1">按当前年天数换算</a-option>
|
||||
<a-option :value="2">按当前月天数换算</a-option>
|
||||
<a-option :value="3">按自定义每天单价</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="每天单价" field="dayPrice" required>
|
||||
<a-input-number v-model="form.dayPrice" :min="0.1">
|
||||
<template #append>
|
||||
<a-select v-model="form.dayPriceUnit" style="width: 100px">
|
||||
<a-option
|
||||
v-for="item in priceUnits"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.text }}</a-option
|
||||
>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col v-if="form.dayPrice && form.dayPriceUnit" :span="24">
|
||||
<a-form-item>
|
||||
<div class="bg-[#eaf3fe] p-[10px] w-full">
|
||||
<icon-exclamation-circle-fill style="color: #2d5cf6" />
|
||||
每天单价算法公式 = {{ dayPriceCalc }} x 合同面积
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="计算精度" field="calculationAccuracy" required>
|
||||
<a-select v-model="form.calculationAccuracy">
|
||||
<a-option :value="1"
|
||||
>每步计算进行精确计算,直至计算结果。</a-option
|
||||
>
|
||||
<a-option :value="2">每步计算保留2位小数,直至计算结果</a-option>
|
||||
<a-option :value="2">每步计算保留1位小数,直至计算结果</a-option>
|
||||
<a-option :value="2">每步计算不保留小数,直至计算结果</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="每期账单小数点" field="billAccuracy" required>
|
||||
<a-select v-model="form.billAccuracy">
|
||||
<a-option :value="1">2位小数</a-option>
|
||||
<a-option :value="2">1位小数</a-option>
|
||||
<a-option :value="2">不保留</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<p class="title">租期相关</p>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="租期划分模式" field="partitionMode" required>
|
||||
<a-select v-model="form.partitionMode">
|
||||
<a-option :value="1"
|
||||
>按自然月划分租期(“周期开始月”月首日到“后期结束月”月末日)</a-option
|
||||
>
|
||||
<a-option :value="2">按起止日划分租期</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<div class="bg-[#eaf3fe] p-[10px] w-full">
|
||||
<icon-exclamation-circle-fill style="color: #2d5cf6" />
|
||||
按自然月划分租期,如2025-10-01 ~ 2025-10-31
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="首月不足月租期处理"
|
||||
field="firstLessMonthType"
|
||||
required
|
||||
>
|
||||
<a-select v-model="form.firstLessMonthType">
|
||||
<a-option :value="1">独立为一个租期</a-option>
|
||||
<a-option :value="2">作为完整月来划分租期</a-option>
|
||||
<a-option :value="3">与下一租期合并为一个租期</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<div class="bg-[#eaf3fe] p-[10px] w-full">
|
||||
<icon-exclamation-circle-fill style="color: #2d5cf6" />
|
||||
与下一租期合并为一个租期,如:2月1付,2025-07-16 ~
|
||||
2025-09-30为一期账单
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="免租期、装修期租期处理"
|
||||
field="otherLeaseProcessing"
|
||||
required
|
||||
>
|
||||
<a-select v-model="form.otherLeaseProcessing">
|
||||
<a-option :value="1">免租期计入租期,装修期计入租期</a-option>
|
||||
<a-option :value="2">免租期计入租期,装修期不计入租期</a-option>
|
||||
<a-option :value="3">免租期不计入租期,装修期计入租期</a-option>
|
||||
<a-option :value="4">免租期不计入租期,装修期不计入租期</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<div class="bg-[#eaf3fe] p-[10px] w-full flex items-center">
|
||||
<p> <icon-exclamation-circle-fill style="color: #2d5cf6" /></p>
|
||||
<div class="ml-[2px]">
|
||||
<p
|
||||
>计入租期:租期从免租/装修期起始时间开始算,虽免租但算在合同期内</p
|
||||
>
|
||||
<p
|
||||
>不计入租期:租期从免租/装修期结束时间开始算,为租期之外赠送的时间</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
const priceUnits = [
|
||||
{ value: 1, text: '元/㎡*月' },
|
||||
{ value: 2, text: '元/㎡*天' },
|
||||
{ value: 3, text: '元/天' },
|
||||
{ value: 4, text: '元/月' },
|
||||
{ value: 5, text: '元/年' },
|
||||
];
|
||||
const priceUnitMap = new Map([
|
||||
[1, '元/㎡*月'],
|
||||
[2, '元/㎡*天'],
|
||||
[3, '元/天'],
|
||||
[4, '元/月'],
|
||||
[5, '元/年'],
|
||||
]);
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
record?: any | null;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
(e: 'submitted', val: any): void;
|
||||
}>();
|
||||
const isLoading = ref<boolean>(false);
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const defaultForm: any = {
|
||||
payCycle: null, // 付款周期
|
||||
taxRate: null, // 税率
|
||||
chargeType: 1, //计费模式
|
||||
priceUnit: 1, //合同单价默认单位
|
||||
dayPriceAlgorithm: 1, //每天单价算法
|
||||
dayPrice: null, //每天单价
|
||||
dayPriceUnit: 1, //每天单价单位
|
||||
calculationAccuracy: 1, //计算精度
|
||||
billAccuracy: 1, //账单小数点
|
||||
partitionMode: 1, //租期划分模式
|
||||
firstLessMonthType: 1, //首月不足月租金处理
|
||||
otherLeaseProcessing: 1, //免租期、装修期租期处理
|
||||
};
|
||||
|
||||
const form = reactive<any>({ ...defaultForm });
|
||||
// 校验规则
|
||||
const rules = reactive({
|
||||
payCycle: [{ required: true, message: '请输入付款周期' }],
|
||||
taxRate: [{ required: true, message: '请输入税率' }],
|
||||
chargeType: [{ required: true, message: '请选择计费模式' }],
|
||||
priceUnit: [{ required: true, message: '请选择合同单价默认单位' }],
|
||||
dayPriceAlgorithm: [{ required: true, message: '请选择每天单价算法' }],
|
||||
dayPrice: [{ required: true, message: '请输入每天单价' }],
|
||||
});
|
||||
|
||||
const handleOk = async () => {
|
||||
if (isLoading.value) return;
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const valid = await formRef.value?.validate();
|
||||
if (valid) return;
|
||||
emit('update:visible', false);
|
||||
emit('submitted', form);
|
||||
} catch {
|
||||
console.warn('❌ 校验未通过');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
const dayPriceCalc = computed(() => {
|
||||
const unit = priceUnitMap.get(form.dayPriceUnit);
|
||||
return `${form.dayPrice}${unit}`;
|
||||
});
|
||||
|
||||
// 打开抽屉时,用 record 合并默认值填充表单;编辑场景回填
|
||||
function applyRecordToForm(record: any) {
|
||||
const merged = { ...defaultForm, ...(record || {}) };
|
||||
Object.keys(defaultForm).forEach((key) => {
|
||||
// 仅覆盖已定义字段,保持响应式
|
||||
(form as any)[key] = (merged as any)[key];
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
applyRecordToForm(props.record);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.record,
|
||||
(val) => {
|
||||
if (props.visible) {
|
||||
applyRecordToForm(val);
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(label.arco-form-item-label) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑创建合同模板' : '新增创建合同模板'"
|
||||
title="新增创建合同模板"
|
||||
unmount-on-close
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
@@ -37,7 +37,7 @@
|
||||
import { createTemplateSetting, updateTemplateSetting } from '@/api/setting';
|
||||
import { DepartmentItem, CreateTemplateSettingParam } from '@/types/setting';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
interface TreeNode {
|
||||
id: string | number;
|
||||
name: string;
|
||||
@@ -46,7 +46,7 @@
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
isEdit: boolean;
|
||||
record?: DepartmentItem | null;
|
||||
record?: any;
|
||||
departmentListData: TreeNode[]; // 修复类型
|
||||
}>();
|
||||
|
||||
@@ -54,13 +54,13 @@
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
(e: 'submitted'): void;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const form = reactive<CreateTemplateSettingParam>({
|
||||
const form = reactive<any>({
|
||||
name: '',
|
||||
departmentId: null,
|
||||
// id: undefined,
|
||||
id: undefined,
|
||||
});
|
||||
|
||||
// 校验规则
|
||||
@@ -68,19 +68,18 @@
|
||||
name: [{ required: true, message: '请输入模板名称' }],
|
||||
departmentId: [{ required: true, message: '请选择适用范围' }],
|
||||
};
|
||||
|
||||
// Watch 弹窗控制 & 初始化
|
||||
// 表单事件
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
if (props.record) {
|
||||
// form.id = props.record?.id;
|
||||
// form.name = props.record?.name;
|
||||
// form.parent_id = props.record?.parentId ?? '';
|
||||
// form.user_id = props.record?.UserId ?? '';
|
||||
// form.remark = props.record?.remark ?? '';
|
||||
// Object.assign(form, props.record);
|
||||
Object.assign(form, {
|
||||
...props.record,
|
||||
});
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
@@ -92,21 +91,29 @@
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 表单事件
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate();
|
||||
const { code, message } = props.isEdit
|
||||
if (!form.departmentId || !form.name) return;
|
||||
if (props.isEdit) {
|
||||
// saveType 保存类别:1 名称与适用范围数据,2 基础信息数据,3 条款数据)
|
||||
form.saveType = 1;
|
||||
}
|
||||
const { code, message, data } = props.isEdit
|
||||
? await updateTemplateSetting(form)
|
||||
: await createTemplateSetting(form);
|
||||
if (code === 200) {
|
||||
Message.success(message);
|
||||
if (props.isEdit) {
|
||||
emit('submitted');
|
||||
} else {
|
||||
const info = data as any;
|
||||
const id = info.contractTemplateSettingId;
|
||||
router.push({
|
||||
name: 'system-setting-contract-template-edit',
|
||||
query: { id },
|
||||
});
|
||||
}
|
||||
handleCancel();
|
||||
// 通知父组件更新部门列表数据
|
||||
emit('submitted');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="border-solid border-[1px] border-[#f0f0f0] px-[10px] py-[15px]">
|
||||
<h4 class="text-[14px] text-[#505968] font-extrabold">{{ title }}</h4>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="mt-[15px]"
|
||||
:model="bondForm"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="保证金金额" field="num" required>
|
||||
<a-input-number
|
||||
v-model="bondForm.num"
|
||||
:min="1"
|
||||
placeholder="请输入数字"
|
||||
/>
|
||||
<ul class="options ml-[10px]">
|
||||
<li
|
||||
v-for="item in bondUnits"
|
||||
:key="item.type"
|
||||
:class="[
|
||||
'subItem',
|
||||
{ isActive: item.type === bondForm.priceType },
|
||||
]"
|
||||
@click="bondForm.priceType = item.type"
|
||||
>
|
||||
{{ item.text }}</li
|
||||
>
|
||||
</ul>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="付款日期" field="payType" required>
|
||||
<ul class="options">
|
||||
<li
|
||||
v-for="item in payDateTypes"
|
||||
:key="item.type"
|
||||
:class="[
|
||||
'subItem',
|
||||
{ isActive: item.type === bondForm.payType },
|
||||
]"
|
||||
@click="choosePayDateType(item.type)"
|
||||
>
|
||||
{{ item.text }}</li
|
||||
>
|
||||
</ul>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
defineProps<{ title?: string }>();
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const defaultForm: any = {
|
||||
num: null,
|
||||
payType: 1,
|
||||
priceType: 1,
|
||||
};
|
||||
const bondForm = reactive<any>({ ...defaultForm });
|
||||
// 校验规则
|
||||
const rules = reactive({
|
||||
num: [{ required: true, message: '请输入保证金金额' }],
|
||||
payType: [{ required: true, message: '请选择保证金单位' }],
|
||||
priceType: [{ required: true, message: '请选择付款日期' }],
|
||||
});
|
||||
const payDateTypes = [
|
||||
{ type: 1, text: '首期账单日期' },
|
||||
{ type: 2, text: '签约日期' },
|
||||
];
|
||||
const bondUnits = [
|
||||
{ type: 1, text: '个月' },
|
||||
{ type: 2, text: '元' },
|
||||
];
|
||||
const choosePayDateType = (type) => {
|
||||
bondForm.payType = type;
|
||||
};
|
||||
|
||||
// 暴露保存数据
|
||||
function getData() {
|
||||
return { ...bondForm };
|
||||
}
|
||||
defineExpose({ getData });
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f2f3f5;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 3px;
|
||||
|
||||
.subItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.isActive {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div
|
||||
class="border-solid border-[1px] border-[#f0f0f0] px-[10px] py-[15px] mt-[20px]"
|
||||
>
|
||||
<h4 class="text-[14px] text-[#505968] font-extrabold">优惠条款</h4>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="mt-[15px]"
|
||||
:model="discountForm"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="7">
|
||||
<a-form-item label="优惠类型" field="type" required>
|
||||
<a-select v-model="discountForm.type" placeholder="请选择">
|
||||
<a-option :value="1">单价折扣</a-option>
|
||||
<a-option :value="2">单价减免</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item label="金额折扣" field="discount" required>
|
||||
<a-input
|
||||
v-model="discountForm.discount"
|
||||
append="折"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="11">
|
||||
<a-form-item label="作用范围" required>
|
||||
<a-input-group>
|
||||
<a-form-item no-style field="startNum">
|
||||
<a-input
|
||||
v-model="discountForm.startNum"
|
||||
prepend="第"
|
||||
append="期开始"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item no-style field="endNum">
|
||||
<a-input
|
||||
v-model="discountForm.endNum"
|
||||
prepend="持续"
|
||||
append="期"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const defaultForm: any = {
|
||||
type: 1,
|
||||
discount: null,
|
||||
startNum: null,
|
||||
endNum: null,
|
||||
};
|
||||
const discountForm = reactive<any>({ ...defaultForm });
|
||||
// 校验规则
|
||||
const rules = reactive({
|
||||
type: [{ required: true, message: '优惠类型不能为空' }],
|
||||
discount: [{ required: true, message: '金额折扣不能为空' }],
|
||||
startNum: [{ required: true, message: '开始期数不能为空' }],
|
||||
endNum: [{ required: true, message: '持续期数不能为空' }],
|
||||
});
|
||||
|
||||
// 向父组件暴露数据与校验方法
|
||||
function getData() {
|
||||
const isEmptyValue = (v: any) =>
|
||||
v === undefined ||
|
||||
v === null ||
|
||||
(typeof v === 'string' && v.trim() === '');
|
||||
const { type, discount, startNum, endNum } = discountForm;
|
||||
// 任一必填未有效填写则返回空对象,避免父层误判有数据
|
||||
if (
|
||||
isEmptyValue(type) ||
|
||||
isEmptyValue(discount) ||
|
||||
isEmptyValue(startNum) ||
|
||||
isEmptyValue(endNum)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
type: Number(type),
|
||||
discount: Number(discount),
|
||||
startNum: Number(startNum),
|
||||
endNum: Number(endNum),
|
||||
};
|
||||
}
|
||||
async function validate() {
|
||||
return formRef.value?.validate?.();
|
||||
}
|
||||
defineExpose({ getData, validate });
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f2f3f5;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 3px;
|
||||
|
||||
.subItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.isActive {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div
|
||||
class="border-solid border-[1px] border-[#f0f0f0] px-[10px] py-[15px] mt-[20px]"
|
||||
>
|
||||
<h4 class="text-[14px] text-[#505968] font-extrabold">递增条款</h4>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="mt-[15px]"
|
||||
:model="increamForm"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="递增间隔" field="num" required>
|
||||
<a-input-number
|
||||
v-model="increamForm.num"
|
||||
:min="1"
|
||||
prepend="每隔"
|
||||
append="年"
|
||||
placeholder="请输入数字"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="单价递增" field="priceIncrease" required>
|
||||
<a-input v-model="increamForm.priceIncrease" placeholder="请选择">
|
||||
<template #append>
|
||||
<a-select
|
||||
v-model="increamForm.priceIncreaseType"
|
||||
style="width: 80px"
|
||||
>
|
||||
<a-option :value="1">%</a-option>
|
||||
<a-option :value="2">元</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="保证金递增" field="bondIncrease" required>
|
||||
<a-input v-model="increamForm.bondIncrease" placeholder="请选择">
|
||||
<template #append>
|
||||
<a-select
|
||||
v-model="increamForm.bondIncreaseType"
|
||||
style="width: 80px"
|
||||
>
|
||||
<a-option :value="1">%</a-option>
|
||||
<a-option :value="2">元</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const defaultForm: any = {
|
||||
num: null,
|
||||
priceIncrease: null,
|
||||
priceIncreaseType: 1,
|
||||
bondIncrease: null,
|
||||
bondIncreaseType: 1,
|
||||
};
|
||||
const increamForm = reactive<any>({ ...defaultForm });
|
||||
// 校验规则
|
||||
const rules = reactive({
|
||||
num: [{ required: true, message: '递增间隔不能为空' }],
|
||||
priceIncrease: [{ required: true, message: '单价递增不能为空' }],
|
||||
bondIncrease: [{ required: true, message: '保证金递增不能为空' }],
|
||||
});
|
||||
|
||||
// 向父组件暴露数据与校验方法
|
||||
function getData() {
|
||||
const isEmptyValue = (v: any) =>
|
||||
v === undefined ||
|
||||
v === null ||
|
||||
(typeof v === 'string' && v.trim() === '');
|
||||
const {
|
||||
num,
|
||||
priceIncrease,
|
||||
priceIncreaseType,
|
||||
bondIncrease,
|
||||
bondIncreaseType,
|
||||
} = increamForm;
|
||||
if (
|
||||
isEmptyValue(num) ||
|
||||
isEmptyValue(priceIncrease) ||
|
||||
isEmptyValue(bondIncrease)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
num: Number(num),
|
||||
priceIncrease: Number(priceIncrease),
|
||||
priceIncreaseType: Number(priceIncreaseType),
|
||||
bondIncrease: Number(bondIncrease),
|
||||
bondIncreaseType: Number(bondIncreaseType),
|
||||
};
|
||||
}
|
||||
async function validate() {
|
||||
return formRef.value?.validate?.();
|
||||
}
|
||||
defineExpose({ getData, validate });
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f2f3f5;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 3px;
|
||||
|
||||
.subItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.isActive {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div
|
||||
class="border-solid border-[1px] border-[#f0f0f0] px-[10px] py-[15px] mt-[20px]"
|
||||
>
|
||||
<h4 class="text-[14px] text-[#505968] font-extrabold">滞纳金条款</h4>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="mt-[15px]"
|
||||
:model="lateFeeForm"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="7">
|
||||
<a-form-item label="起算天数" field="days" required>
|
||||
<a-input-number
|
||||
v-model="lateFeeForm.days"
|
||||
:min="1"
|
||||
prepend="账单逾期"
|
||||
append="天"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item label="收取方式" field="chargeType" required>
|
||||
<a-select v-model="lateFeeForm.chargeType" placeholder="请选择">
|
||||
<a-option :value="1">仅计算不生成账单</a-option>
|
||||
<a-option :value="2">达到上限生成账单</a-option>
|
||||
<a-option :value="3">生成账单每天调</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="11">
|
||||
<a-form-item label="滞纳金" field="ratio" required>
|
||||
<a-input-group>
|
||||
<a-input v-model="lateFeeForm.ratio" placeholder="请输入">
|
||||
<template #append>
|
||||
<a-select
|
||||
v-model="lateFeeForm.ratioType"
|
||||
style="width: 100px"
|
||||
>
|
||||
<a-option :value="1">%/天</a-option>
|
||||
<a-option :value="2">元/天</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input>
|
||||
<a-input v-model="lateFeeForm.ceiling" placeholder="请输入">
|
||||
<template #append>
|
||||
<a-select
|
||||
v-model="lateFeeForm.ceilingType"
|
||||
style="width: 100px"
|
||||
>
|
||||
<a-option :value="1">%上限</a-option>
|
||||
<a-option :value="2">元上限</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const defaultForm: any = {
|
||||
days: null,
|
||||
chargeType: 1,
|
||||
ratio: null,
|
||||
ratioType: 1,
|
||||
ceiling: null,
|
||||
ceilingType: 1,
|
||||
};
|
||||
const lateFeeForm = reactive<any>({ ...defaultForm });
|
||||
// 校验规则
|
||||
const rules = reactive({
|
||||
days: [{ required: true, message: '起算天数不能为空' }],
|
||||
chargeType: [{ required: true, message: '收取方式不能为空' }],
|
||||
ratio: [{ required: true, message: '滞纳金不能为空' }],
|
||||
ratioType: [{ required: true, message: '滞纳金单位不能为空' }],
|
||||
ceiling: [{ required: true, message: '上限不能为空' }],
|
||||
ceilingType: [{ required: true, message: '上限单位不能为空' }],
|
||||
});
|
||||
|
||||
// 向父组件暴露数据与校验方法
|
||||
function getData() {
|
||||
const isEmptyValue = (v: any) =>
|
||||
v === undefined ||
|
||||
v === null ||
|
||||
(typeof v === 'string' && v.trim() === '');
|
||||
const { days, chargeType, ratio, ratioType, ceiling, ceilingType } =
|
||||
lateFeeForm;
|
||||
if (
|
||||
isEmptyValue(days) ||
|
||||
isEmptyValue(chargeType) ||
|
||||
isEmptyValue(ratio) ||
|
||||
isEmptyValue(ratioType) ||
|
||||
isEmptyValue(ceiling) ||
|
||||
isEmptyValue(ceilingType)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
days: Number(days),
|
||||
chargeType: Number(chargeType),
|
||||
ratio: Number(ratio),
|
||||
ratioType: Number(ratioType),
|
||||
ceiling: Number(ceiling),
|
||||
ceilingType: Number(ceilingType),
|
||||
};
|
||||
}
|
||||
async function validate() {
|
||||
return formRef.value?.validate?.();
|
||||
}
|
||||
defineExpose({ getData, validate });
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f2f3f5;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 3px;
|
||||
|
||||
.subItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.isActive {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,312 @@
|
||||
<template>
|
||||
<div
|
||||
class="border-solid border-[1px] border-[#f0f0f0] px-[10px] py-[15px] mt-[20px]"
|
||||
>
|
||||
<h4 class="text-[14px] text-[#505968] font-extrabold">租期条款(租金)</h4>
|
||||
<div class="bg-[#edfeec] px-[10px] py-[5px]">
|
||||
<ul v-if="hasRentSetting" class="rent-list">
|
||||
<li v-for="(text, idx) in displayItems" :key="idx" class="rent-item">{{
|
||||
text
|
||||
}}</li>
|
||||
<li class="rent-action" @click="handleShowDrawer">
|
||||
<icon-settings :size="18" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="mt-[15px]"
|
||||
:model="rentSettingInfo"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="首期账单缴费日期" field="firstPayDate" required>
|
||||
<a-date-picker
|
||||
v-model="rentSettingInfo.firstPayDate"
|
||||
:disabled-date="(current) => dayjs(current).isBefore(dayjs())"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="账单缴费日期" field="payDay" required>
|
||||
<a-input-number v-model="rentSettingInfo.payDay">
|
||||
<template #prepend>
|
||||
<a-select v-model="rentSettingInfo.payDateType">
|
||||
<a-option :value="1">租期开始前</a-option>
|
||||
<a-option :value="2">租期开始后</a-option>
|
||||
<a-option :value="3">租期开始上月</a-option>
|
||||
<a-option :value="4">租期开始本月</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
<template #append>
|
||||
<a-select v-model="rentSettingInfo.payDayType">
|
||||
<a-option :value="1">日付款</a-option>
|
||||
<a-option :value="2">自然日付款</a-option>
|
||||
<a-option :value="3">工作日付款</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="默认收款账户"
|
||||
field="financeTurnoverAccountSettingId"
|
||||
required
|
||||
>
|
||||
<a-select v-model="rentSettingInfo.financeTurnoverAccountSettingId">
|
||||
<a-option
|
||||
v-for="item in turnoverAccountList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.bankAccuntName }}</a-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<rentSettingDrawer
|
||||
v-model:visible="isShowDrawer"
|
||||
:record="rentSettingInfo"
|
||||
@submitted="getRendSetting"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch, computed, onMounted } from 'vue';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
import rentSettingDrawer from '../drawer/rent-setting-drawer.vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { getUseTurnoverAccountList } from '@/api/setting';
|
||||
const isShowDrawer = ref<boolean>(false);
|
||||
// 与抽屉保持一致的默认表单,用于初始回显与编辑回填
|
||||
const defaultForm: any = {
|
||||
payCycle: null, // 付款周期 (输入类,默认不显示)
|
||||
taxRate: null, // 税率 (输入类,默认不显示)
|
||||
chargeType: 1, // 计费模式
|
||||
priceUnit: 1, // 合同单价默认单位
|
||||
dayPriceAlgorithm: 1, // 每天单价算法
|
||||
dayPrice: null, // 每天单价 (输入类,默认不显示)
|
||||
dayPriceUnit: 1, // 每天单价单位
|
||||
calculationAccuracy: 1, // 计算精度
|
||||
billAccuracy: 1, // 账单小数点
|
||||
partitionMode: 1, // 租期划分模式
|
||||
firstLessMonthType: 1, // 首月不足月租金处理
|
||||
otherLeaseProcessing: 1, // 免租期、装修期租期处理
|
||||
firstPayDate: '',
|
||||
payDay: '',
|
||||
payDateType: 1,
|
||||
payDayType: 1,
|
||||
};
|
||||
const rentSettingInfo = ref<any>({ ...defaultForm });
|
||||
const formRef = ref<FormInstance>();
|
||||
const rules = reactive({
|
||||
firstPayDate: [{ required: true, message: '请选择首期账单缴费日期' }],
|
||||
payDay: [{ required: true, message: '请输入账单缴费日期' }],
|
||||
financeTurnoverAccountSettingId: [
|
||||
{ required: true, message: '请选择默认收款账户' },
|
||||
],
|
||||
});
|
||||
|
||||
const turnoverAccountList = ref<any>([]);
|
||||
const props = defineProps<{
|
||||
termSettingInfo?: any | null;
|
||||
}>();
|
||||
// 文案映射与计算属性
|
||||
const chargeTypeMap = new Map([
|
||||
[1, '按月计算租金'],
|
||||
[2, '按实际天数计算租金'],
|
||||
]);
|
||||
const priceUnitMap = new Map([
|
||||
[1, '元/㎡*月'],
|
||||
[2, '元/㎡*天'],
|
||||
[3, '元/天'],
|
||||
[4, '元/月'],
|
||||
[5, '元/年'],
|
||||
]);
|
||||
const dayPriceAlgorithmMap = new Map([
|
||||
[1, '按当前年天数换算'],
|
||||
[2, '按当前月天数换算'],
|
||||
[3, '按自定义每天单价'],
|
||||
]);
|
||||
const calculationAccuracyMap = new Map([
|
||||
[1, '每步计算进行精确计算,直至计算结果。'],
|
||||
[2, '每步计算保留2位小数,直至计算结果'],
|
||||
[3, '每步计算保留1位小数,直至计算结果'],
|
||||
[4, '每步计算不保留小数,直至计算结果'],
|
||||
]);
|
||||
const billAccuracyMap = new Map([
|
||||
[1, '2位小数'],
|
||||
[2, '1位小数'],
|
||||
[3, '不保留'],
|
||||
]);
|
||||
const partitionModeMap = new Map([
|
||||
[1, '按自然月划分租期(“周期开始月”月首日到“后期结束月”月末日)'],
|
||||
[2, '按起止日划分租期'],
|
||||
]);
|
||||
const firstLessMonthTypeMap = new Map([
|
||||
[1, '首月不足月租期:独立为一个租期'],
|
||||
[2, '首月不足月租期:作为完整月来划分租期'],
|
||||
[3, '首月不足月租期:与下一租期合并为一个租期'],
|
||||
]);
|
||||
const otherLeaseProcessingMap = new Map([
|
||||
[1, '免租期计入租期,装修期计入租期'],
|
||||
[2, '免租期计入租期,装修期不计入租期'],
|
||||
[3, '免租期不计入租期,装修期计入租期'],
|
||||
[4, '免租期不计入租期,装修期不计入租期'],
|
||||
]);
|
||||
|
||||
const hasRentSetting = computed(
|
||||
() => Object.keys(rentSettingInfo.value || {}).length > 0
|
||||
);
|
||||
// 汇总所有字段的回显文案
|
||||
const displayItems = computed(() => {
|
||||
const list: string[] = [];
|
||||
const v = rentSettingInfo.value || {};
|
||||
// 计费相关
|
||||
if (v.payCycle !== undefined && v.payCycle !== null)
|
||||
list.push(`${v.payCycle}月1付`);
|
||||
if (v.taxRate !== undefined && v.taxRate !== null && v.taxRate !== '')
|
||||
list.push(`税率${v.taxRate}%`);
|
||||
if (v.chargeType !== undefined)
|
||||
list.push(chargeTypeMap.get(v.chargeType) || '');
|
||||
if (v.priceUnit !== undefined)
|
||||
list.push(`合同默认单价${priceUnitMap.get(v.priceUnit) || ''}`);
|
||||
if (v.dayPriceAlgorithm !== undefined)
|
||||
list.push(
|
||||
`每天单价算法:${dayPriceAlgorithmMap.get(v.dayPriceAlgorithm) || ''}`
|
||||
);
|
||||
if (v.dayPrice !== undefined && v.dayPrice !== null && v.dayPrice !== '') {
|
||||
const unit = priceUnitMap.get(v.dayPriceUnit);
|
||||
list.push(`每天单价${v.dayPrice}${unit ? unit : ''}`);
|
||||
}
|
||||
if (v.calculationAccuracy !== undefined)
|
||||
list.push(
|
||||
`计算精度:${calculationAccuracyMap.get(v.calculationAccuracy) || ''}`
|
||||
);
|
||||
if (v.billAccuracy !== undefined)
|
||||
list.push(`每期账单小数点:${billAccuracyMap.get(v.billAccuracy) || ''}`);
|
||||
// 租期相关
|
||||
if (v.partitionMode !== undefined)
|
||||
list.push(`租期划分模式:${partitionModeMap.get(v.partitionMode) || ''}`);
|
||||
if (v.firstLessMonthType !== undefined)
|
||||
list.push(firstLessMonthTypeMap.get(v.firstLessMonthType) || '');
|
||||
if (v.otherLeaseProcessing !== undefined)
|
||||
list.push(
|
||||
`免租/装修期处理:${
|
||||
otherLeaseProcessingMap.get(v.otherLeaseProcessing) || ''
|
||||
}`
|
||||
);
|
||||
// 过滤空串
|
||||
return list.filter(Boolean);
|
||||
});
|
||||
function handleShowDrawer() {
|
||||
isShowDrawer.value = true;
|
||||
}
|
||||
// 暴露方法给父组件用于保存/校验
|
||||
function getData() {
|
||||
return { ...rentSettingInfo.value };
|
||||
}
|
||||
async function validate() {
|
||||
const res = await formRef.value?.validate();
|
||||
if (res) throw new Error('租金设置校验未通过');
|
||||
return true;
|
||||
}
|
||||
function getRendSetting(value) {
|
||||
rentSettingInfo.value = { ...rentSettingInfo.value, ...(value || {}) };
|
||||
}
|
||||
async function getUseTurnoverAccountListData(info) {
|
||||
const params = {
|
||||
fee_attribute: 1,
|
||||
fee_type: info?.type || 1,
|
||||
is_bond: info?.isBond || 1,
|
||||
};
|
||||
const { code, data } = await getUseTurnoverAccountList(params);
|
||||
if (code === 200) {
|
||||
turnoverAccountList.value = data?.list || [];
|
||||
}
|
||||
}
|
||||
// 避免重复触发:使用 key 去重与并发锁
|
||||
const lastTermKey = ref<string>('');
|
||||
const isFetching = ref<boolean>(false);
|
||||
function buildTermKey(info: any) {
|
||||
if (!info) return '';
|
||||
return `${info?.id || ''}-${info?.type || ''}-${info?.isBond || ''}`;
|
||||
}
|
||||
async function triggerTurnoverList(info: any) {
|
||||
const key = buildTermKey(info);
|
||||
if (!key || key === lastTermKey.value || isFetching.value) return;
|
||||
lastTermKey.value = key;
|
||||
isFetching.value = true;
|
||||
try {
|
||||
await getUseTurnoverAccountListData(info);
|
||||
} finally {
|
||||
isFetching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
triggerTurnoverList(props.termSettingInfo);
|
||||
});
|
||||
watch(
|
||||
() => props.termSettingInfo,
|
||||
(newVal) => {
|
||||
triggerTurnoverList(newVal);
|
||||
},
|
||||
{ deep: false }
|
||||
);
|
||||
|
||||
defineExpose({ getData, validate });
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f2f3f5;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 3px;
|
||||
|
||||
.subItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.isActive {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rent-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
|
||||
.rent-item {
|
||||
line-height: 22px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(232, 255, 234, 1);
|
||||
color: rgba(0, 180, 42, 1);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(0, 180, 42, 1);
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.rent-action {
|
||||
margin-left: auto;
|
||||
order: 9999;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<a-card title="合同管理">
|
||||
<a-spin style="display: block" :loading="loading">
|
||||
<div
|
||||
v-if="contractTagsList && contractTagsList.length"
|
||||
class="project_label_wrap"
|
||||
>
|
||||
<div class="flex_center_between">
|
||||
<div class="title_wrap">
|
||||
<h4>合同类型(标签类)</h4>
|
||||
<a-checkbox
|
||||
:model-value="
|
||||
checkedLabelId.length === contractTagsList.length &&
|
||||
contractTagsList.length > 0
|
||||
"
|
||||
@change="onCheckAllLabelChange"
|
||||
>全选</a-checkbox
|
||||
>
|
||||
</div>
|
||||
<a-button type="text" @click="handleAddLabel"
|
||||
><icon-plus />新增</a-button
|
||||
>
|
||||
<labelFormModal
|
||||
v-model:visible="showLabelModal"
|
||||
:create-api="createContractTags"
|
||||
:update-api="updateContractTags"
|
||||
:del-api="delContractTags"
|
||||
:title="isEdit ? '编辑合同类型字典' : '新增合同类型字典'"
|
||||
:record="currentLabelFormInfo"
|
||||
:is-edit="isEdit"
|
||||
@submitted="getAllContractTagListData"
|
||||
/>
|
||||
</div>
|
||||
<ul class="ul_wrap">
|
||||
<li
|
||||
v-for="item in contractTagsList"
|
||||
:key="item.id"
|
||||
style="margin: 5px 30px 0 0"
|
||||
>
|
||||
<a-checkbox
|
||||
:model-value="checkedLabelId.includes(item.id)"
|
||||
@change="(val) => onProjectTypeCheckChange(item, val)"
|
||||
>
|
||||
{{ item.name }}
|
||||
<span
|
||||
class="circle"
|
||||
:style="{
|
||||
borderColor: item.color,
|
||||
backgroundColor: item.color,
|
||||
}"
|
||||
></span>
|
||||
<icon-edit
|
||||
v-if="item.type === 2"
|
||||
class="edit_icon"
|
||||
@click.stop.prevent="handleEditLabel(item, $event)"
|
||||
/>
|
||||
</a-checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import {
|
||||
getAllContractTagsList,
|
||||
checkContractTags,
|
||||
checkAllContractTags,
|
||||
createContractTags,
|
||||
updateContractTags,
|
||||
delContractTags,
|
||||
} from '@/api/setting';
|
||||
import { ProjectTypeChecked } from '@/types/comm';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import labelFormModal from './label-form-modal.vue';
|
||||
|
||||
const contractTagsList = ref<any>([]);
|
||||
const checkedLabelId = ref<number[]>([]);
|
||||
const { loading, setLoading } = useLoading(true);
|
||||
const showLabelModal = ref<boolean>(false);
|
||||
const isEdit = ref<boolean>(false);
|
||||
const currentLabelFormInfo = ref(null);
|
||||
|
||||
const checkAllProjectTypeData = async (checked: boolean) => {
|
||||
try {
|
||||
const params = { is_check: (checked ? 1 : 0) as ProjectTypeChecked };
|
||||
const { code, message } = await checkAllContractTags(params);
|
||||
if (code === 200) {
|
||||
Message.success(message);
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error(error);
|
||||
}
|
||||
};
|
||||
const checkProjectTypeData = async (params: {
|
||||
id: number;
|
||||
is_check: ProjectTypeChecked;
|
||||
}) => {
|
||||
try {
|
||||
const { code, message } = await checkContractTags(params);
|
||||
if (code === 200) {
|
||||
Message.success(message);
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error(error);
|
||||
}
|
||||
};
|
||||
const onCheckAllLabelChange = (checked: boolean) => {
|
||||
if (checked) {
|
||||
// 全选
|
||||
checkedLabelId.value = contractTagsList.value.map((item) => item.id);
|
||||
} else {
|
||||
// 取消全选
|
||||
checkedLabelId.value = [];
|
||||
}
|
||||
checkAllProjectTypeData(checked);
|
||||
};
|
||||
const onProjectTypeCheckChange = (item, checked) => {
|
||||
if (checked) {
|
||||
if (!checkedLabelId.value.includes(item.id)) {
|
||||
checkedLabelId.value.push(item.id);
|
||||
}
|
||||
} else {
|
||||
checkedLabelId.value = checkedLabelId.value.filter(
|
||||
(id) => id !== item.id
|
||||
);
|
||||
}
|
||||
checkProjectTypeData({ id: item.id, is_check: checked ? 1 : 0 });
|
||||
};
|
||||
const getAllContractTagListData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { code, data } = await getAllContractTagsList();
|
||||
if (code === 200 && data) {
|
||||
contractTagsList.value = data?.contractTagsSettingList;
|
||||
if (contractTagsList.value?.length === 0) return;
|
||||
checkedLabelId.value = contractTagsList.value
|
||||
.filter((i) => i.isCheck === 1)
|
||||
.map((i) => i.id);
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const handleAddLabel = () => {
|
||||
showLabelModal.value = true;
|
||||
isEdit.value = false;
|
||||
currentLabelFormInfo.value = null;
|
||||
};
|
||||
const handleEditLabel = (item, event) => {
|
||||
if (event) event.preventDefault();
|
||||
showLabelModal.value = true;
|
||||
isEdit.value = true;
|
||||
currentLabelFormInfo.value = item;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await getAllContractTagListData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.project_label_wrap {
|
||||
.title_wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.circle {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 50%;
|
||||
margin-left: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.edit_icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.ul_wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -27,6 +27,7 @@
|
||||
<a-col :span="18">
|
||||
<projectCard v-if="currentSettingId === 2" />
|
||||
<roomCard v-if="currentSettingId === 3" />
|
||||
<contractCard v-if="currentSettingId === 4" />
|
||||
<tenantCard v-if="currentSettingId === 5" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -42,6 +43,7 @@
|
||||
import { dataDictionaryTabs } from './contrant';
|
||||
import projectCard from './components/project-card.vue';
|
||||
import roomCard from './components/room-card.vue';
|
||||
import contractCard from './components/contract-card.vue';
|
||||
import tenantCard from './components/tenant-card.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
Reference in New Issue
Block a user