feat: 设置->合同设置->合同文档模板设置

This commit is contained in:
Zzz1zz 2025-08-26 23:13:34 +08:00
parent ddb35b10f1
commit fc07ed06d5
16 changed files with 1348 additions and 95 deletions

View file

@ -72,3 +72,7 @@ export function ossDownload(key: string): Promise<HttpResponse> {
export function getBanksList(keyword: string): Promise<BankListRes> {
return axios.post('common/getBanksList', { keyword });
}
// 获取勾选部门的项目列表
export function getDeptProjectListFromDeptId(id: number): Promise<any> {
return axios.get(`common/getDeptProjectList?departmentId=${id}`);
}

View file

@ -35,6 +35,10 @@ import {
CreateTermSettingParam,
SettingContractTermListRes,
SettingContractTermInfo,
SettingContractTemplateListRes,
CreateTemplateSettingParam,
ContractWordFormParam,
ContractWordSettingListRes,
} from '@/types/setting';
import { PaginationParams, TenantDictionaryType } from '@/types/comm';
import { HttpResponse } from './interceptor';
@ -71,7 +75,14 @@ export function departmentSetDown(id: number): Promise<HttpResponse> {
export function delDepartment(id: number): Promise<HttpResponse> {
return axios.get(`/department/delDepartment?id=${id}`);
}
// 保存部门项目数据
export function saveDataPermission(params: any): Promise<HttpResponse> {
return axios.post('/department/saveDataPermission', params);
}
// 获取部门项目数据
export function getDataPermission(id: number): Promise<HttpResponse> {
return axios.get(`/department/getDataPermission?id=${id}`);
}
// ==================== 用户相关 API ====================
export function getAllUserList(
params: PaginationParams
@ -405,3 +416,81 @@ export function getUseFeeList(
): Promise<FeeManagementListRes> {
return axios.post(`financeSetting/getUseFeeList`, params);
}
// 合同设置-合同模板管理-全部启用列表
export function getAllTemplateSettingList(): Promise<SettingContractNumbeRes> {
return axios.get(`contractSetting/getAllTemplateSettingList`);
}
// 合同设置-合同模板管理-新增
export function createTemplateSetting(
params: CreateTemplateSettingParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/createTemplateSetting`, params);
}
// 合同设置-合同模板管理-编辑
export function updateTemplateSetting(
params: CreateTemplateSettingParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/updateTemplateSetting`, params);
}
// 合同设置-合同模板管理-删除
export function delTemplateSetting(id: number): Promise<HttpResponse> {
return axios.get(`contractSetting/delTemplateSetting?id=${id}`);
}
// 合同设置-合同模板管理-单个详情
export function getTemplateSettingFromId(
id: number
): Promise<SettingContractNumbeRes> {
return axios.get(`contractSetting/getTemplateSetting?id=${id}`);
}
// 合同设置-合同模板管理-启用/禁用
export function disableTemplateSetting(
params: DisableUserParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/disableTemplateSetting`, params);
}
// 合同设置-合同模板管理-列表(带分页)
export function getTemplateSettingList(
params: PaginationParams
): Promise<SettingContractTemplateListRes> {
return axios.post(`contractSetting/getTemplateSettingList`, params);
}
// 合同设置-合同文档模板管理-全部启用列表
export function getAllWordSettingList(): Promise<ContractWordSettingListRes> {
return axios.get(`contractSetting/getAllWordSettingList`);
}
// 合同设置-合同文档模板管理-新增
export function createWordSetting(
params: ContractWordFormParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/createWordSetting`, params);
}
// 合同设置-合同文档模板管理-编辑(更新)
export function updateWordSetting(
params: ContractWordFormParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/updateWordSetting`, params);
}
// 合同设置-合同文档模板管理-删除
export function delWordSetting(id: number): Promise<HttpResponse> {
return axios.get(`contractSetting/delWordSetting?id=${id}`);
}
// 合同设置-合同文档模板管理-单个详情
export function getWordSettingFromId(
id: number
): Promise<SettingContractNumbeRes> {
return axios.get(`contractSetting/getWordSetting?id=${id}`);
}
// 合同设置-合同文档模板管理-启用/禁用
export function disableWordSetting(
params: DisableUserParam
): Promise<HttpResponse> {
return axios.post(`contractSetting/disableWordSetting`, params);
}
// 合同设置-合同文档模板管理-列表(带分页)
export function getWordSettingList(
params: PaginationParams
): Promise<ContractWordSettingListRes> {
return axios.post(`contractSetting/getWordSettingList`, params);
}

View file

@ -0,0 +1,165 @@
<template>
<a-upload
draggable
:accept="accept"
:custom-request="customRequest"
:multiple="maxCount > 1"
:limit="maxCount"
@change="handleChange"
>
<div style="padding: 20px; text-align: center">
<icon-upload style="font-size: 32px; color: #666" />
<div style="margin-top: 8px">
<p>点击或拖拽文件到此处上传</p>
<p v-if="accept">支持的格式:{{ accept }}</p>
<p>最大支持 {{ maxSizeText }}</p>
</div>
</div>
</a-upload>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { ossUpload } from '@/api/common';
import type { UploadType } from '@/types/comm';
interface UploadResponse {
filename: string;
size: number;
signedPutURL: string;
objectKey: string;
}
const props = defineProps<{
objectKeys: string[];
signedPutURL: string[];
uploadType: UploadType;
maxSize?: number; // 单位字节
maxCount?: number;
accept?: string; // 限制类型,例如 "application/pdf" 或 ".doc,.docx,.xls" 或 "application/pdf,.docx"
}>();
const emit = defineEmits<{
(e: 'update:objectKeys', value: string[]): void;
(e: 'update:signedPutURL', value: string[]): void;
(
e: 'upload-success',
payload: { objectKey: string; signedPutURL: string }
): void;
(e: 'upload-error', error: Error): void;
}>();
const maxCount = props.maxCount ?? 1;
const accept = props.accept ?? '*/*'; // 默认支持所有文件
const maxSizeText = computed(() => {
const size = props.maxSize || 10 * 1024 * 1024;
return `${Math.round(size / 1024 / 1024)}MB`;
});
// 上传逻辑
const customRequest = async (option: any) => {
const { onProgress, onError, onSuccess, fileItem } = option;
try {
const file = fileItem.file;
const maxSize = props.maxSize || 10 * 1024 * 1024;
// 校验大小
if (file.size > maxSize) {
onError(
new Error(`文件大小不能超过 ${Math.round(maxSize / 1024 / 1024)}MB`)
);
return;
}
// 校验格式
if (props.accept && props.accept !== '*/*') {
const acceptList = props.accept.split(',').map((x) => x.trim());
const isValid = acceptList.some((type) => {
if (type.includes('/')) {
// MIME 类型,例如 application/pdf
return file.type === type;
}
if (type.startsWith('.')) {
// 扩展名,例如 .pdf,.docx
return file.name.toLowerCase().endsWith(type.toLowerCase());
}
return false;
});
if (!isValid) {
onError(new Error(`文件格式不支持,只允许:${props.accept}`));
return;
}
}
// 模拟上传进度
let progress = 0;
const progressInterval = setInterval(() => {
progress += Math.random() * 30;
if (progress >= 90) {
clearInterval(progressInterval);
progress = 90;
}
// onProgress({ percent: Math.round(progress) })
onProgress(Math.round(progress));
}, 200);
// 上传到服务器
const formData = new FormData();
formData.append('file', file);
formData.append('type', props.uploadType.toString());
const { code, data, message } = await ossUpload(formData);
clearInterval(progressInterval);
if (code === 200 && data) {
const res = data as UploadResponse;
onProgress(100);
onSuccess(res);
// 更新父组件
let newKeys = [...props.objectKeys];
let newUrls = [...props.signedPutURL];
if (maxCount === 1) {
newKeys = [res.objectKey];
newUrls = [res.signedPutURL];
} else {
newKeys.push(res.objectKey);
newUrls.push(res.signedPutURL);
newKeys = newKeys.slice(0, maxCount);
newUrls = newUrls.slice(0, maxCount);
}
emit('update:objectKeys', newKeys);
emit('update:signedPutURL', newUrls);
emit('upload-success', {
objectKey: res.objectKey,
signedPutURL: res.signedPutURL,
});
} else {
const error = new Error(message || '上传失败');
onError(error);
emit('upload-error', error);
}
} catch (e) {
const err = e instanceof Error ? e : new Error('上传失败');
onError(err);
emit('upload-error', err);
}
};
// 删除文件时更新父组件
const handleChange = (newFileList: any[]) => {
const newKeys = newFileList
.filter((f) => f.status === 'done' && f.response?.objectKey)
.map((f) => f.response.objectKey);
const newUrls = newFileList
.filter((f) => f.status === 'done' && f.response?.signedPutURL)
.map((f) => f.response.signedPutURL);
emit('update:objectKeys', newKeys);
emit('update:signedPutURL', newUrls);
};
</script>

View file

@ -45,13 +45,11 @@ export default function useDepartmentList(autoFetch = true) {
}
};
const getDepartmentDetailData = async (
id: number
): Promise<DepartmentItem | null> => {
const getDepartmentDetailData = async (id: number): Promise<any> => {
try {
const { code, data } = await getDepartmentDetail(id);
if (code === 200 && data?.department) {
return data.department;
if (code === 200 && data) {
return data;
}
return null;
} catch (error) {

View file

@ -5,7 +5,6 @@ import {
BaseEntity,
DeleteStatus,
UserStatus,
ProjectType,
} from './comm';
export interface BuildingItem {
@ -73,7 +72,7 @@ export type ProjectListRes = HttpResponse<{
}>;
export interface CreateProjectParam {
id?: number | null;
type: ProjectType; // 项目类型:1写字楼,2商业综合体,3园区
type: number | null; // 项目类型
name: string; // 楼栋名称
province_id: number | null; // 省份id
city_id: number | null; // 城市id
@ -95,7 +94,7 @@ export interface BuildingInfo extends BaseEntity {
export interface ProjectInfo extends BaseEntity {
systemTenantId: 17; // 租户id
name: string; // 项目名称
type: ProjectType; // 类型:1写字楼,2商业综合体,3园区
type: number; // 类型:1写字楼,2商业综合体,3园区
provinceId: number; // 省份id
cityId: number; // 城市id
districtId: number; // 区id

View file

@ -30,7 +30,6 @@ export type cancelCodeRecycling = 0 | 1; // 作废合同编号回收:0不回
export type CommStatus = 1 | 2; // 1:是 2否
export type FeeManagementType = 1 | 2; // 费项属性:1收入费项,2支出费项
export type FeeType = 1 | 2 | 3 | 4; // 费项类型:1租赁类,2物业类,3特殊类,4其他类
export type ProjectType = 1 | 2 | 3; // 项目类型:1写字楼,2商业综合体,3园区
export type UploadType = 1 | 2; // 1合同模版,2图片
// ==================== 公共接口类型定义 ====================
@ -66,7 +65,7 @@ export interface AllIdNameItem {
name: string;
}
export interface ProjectPaginationParams extends PaginationParams {
type?: ProjectType[] | null;
type?: number[] | null;
province_ids?: number[] | null;
rent_rate_min?: number | null;
rent_rate_max?: number | null;

View file

@ -49,6 +49,8 @@ export interface DepartmentItem extends BaseEntity {
export interface DepartmentDetailData {
department: DepartmentItem;
projects?: any;
users?: any;
}
// ==================== 用户相关类型 ====================
@ -284,6 +286,65 @@ export interface FeeSelectListParams {
fee_type: FeeType; // 费项类型:1租赁类,2物业类,3特殊类,4其他类
is_bond?: CommStatus; // 是否保证金:1是,2否
}
export interface ContractTemplateSettingItem extends BaseEntity {
systemTenantId: number;
departmentId: number;
contractCodeSettingId: number;
name: string;
type: number;
remark: string;
createUserId: number;
status: string;
isDelete?: DeleteStatus; // 是否选择:1是,0否
statusStr: string;
}
export interface CreateTemplateSettingParam {
id?: number;
name: string;
departmentId: number | null;
}
export interface ContractWordItem extends BaseEntity {
systemTenantId: number; // 系统租户id
name: string; // 名称
departmentId: number; // 部门id
isAllProjects: number; // 是否所有项目:1是,2否
url: string; // 模版url
createUserId: number;
status: number;
isDelete: number; // 是否删除:0否,1是
statusStr: string;
projectName: string;
departmentName: string;
}
export interface ContractWordFormParam {
name: string; // 名称
departmentId: number; // 适用范围
url: string; // 模板url
projects: number[]; // 适用项目
id?: number;
}
export interface CreateFileSettingProjectItem {
id: number; // 项目id
name: string; // 名称
contractWordSettingId: number;
}
export interface CreateFileSettingParam {
name: string;
departmentId: number | null;
url: string | null;
projects: CreateFileSettingProjectItem[] | []; // 项目ids数组(若为空数组则为全部项目)
systemTenantId?: number;
isAllProjects?: number; // 是否所有项目:1是,2否
createUserId?: number;
status?: number;
isDelete?: number; // 是否删除:0否,1是
statusStr?: string;
departmentName?: string;
createdAt?: string;
updatedAt?: string;
id?: number | null;
}
// ==================== 响应类型别名 ====================
export type DepartmentFormRes = HttpResponse<DepartmentIdData>;
export type DepartmentList = DepartmentItem[];
@ -338,3 +399,17 @@ export type SettingContractTermListRes = HttpResponse<{
export type SettingContractTermInfo = HttpResponse<{
contractTermsSetting: TermSettingItem;
}>;
export type SettingContractTemplateListRes = HttpResponse<{
contractTemplateSetting: ContractTemplateSettingItem[];
page?: PaginationParams & PaginationResult;
}>;
export type CreateTemplateSettingRes = HttpResponse<{
contractTemplateSettingId: number;
}>;
export type contractWordSettingRes = HttpResponse<{
contractTermSettingId: number;
}>;
export type ContractWordSettingListRes = HttpResponse<{
contractWordSetting: ContractWordItem[];
page?: PaginationParams & PaginationResult;
}>;

View file

@ -2,7 +2,7 @@
<a-drawer
:width="670"
:visible="visible"
:title="isEdit ? '编辑条款设置' : '编辑条款设置'"
:title="isEdit ? '编辑条款设置' : '新增条款设置'"
unmount-on-close
@ok="handleOk"
@cancel="handleCancel"
@ -30,10 +30,10 @@
<a-select v-model="form.type" placeholder="请选择项目类型">
<a-option
v-for="item in projectTypeList"
:key="item.value"
:value="item.value"
:key="item.id"
:value="item.id"
>
{{ item.label }}
{{ item.name }}
</a-option>
</a-select>
</a-form-item>
@ -175,12 +175,10 @@
import { ref, watch, reactive, onMounted } from 'vue';
import type { FormInstance } from '@arco-design/web-vue';
import { Message } from '@arco-design/web-vue';
import { projectTypeList } from '@/utils/constant';
import {
CommonProvinceItem,
CommonCityItem,
CommonDistrictItem,
ProjectType,
} from '@/types/comm';
import {
getProvinceList,
@ -189,7 +187,9 @@
} from '@/api/common';
import { createProject, updateProject } from '@/api/business';
import { CreateProjectParam } from '@/types/business';
import { projectTypeInfo } from '@/types/setting';
import ImageUpload from '@/components/common/ImageUpload.vue';
import { getAllProjectTypeList } from '@/api/setting';
import draggable from 'vuedraggable';
import { generateRandomId } from '@/utils';
@ -210,13 +210,14 @@
down_floor: null,
};
};
const projectTypeList = ref<projectTypeInfo[]>([]);
const isLoading = ref<boolean>(false);
// 表单数据
const formRef = ref<FormInstance>();
const defaultForm: CreateProjectParam = {
id: null,
type: 1 as ProjectType, // 项目类型:1写字楼,2商业综合体,3园区
type: null,
name: '', // 楼栋名称
province_id: null, // 省份id
city_id: null, // 城市id
@ -283,7 +284,12 @@
form.district_id = null;
await getDistrictListByCityIdData(id);
};
const getAllProjectTypeListData = async () => {
const { code, data } = await getAllProjectTypeList();
if (code === 200 && data) {
projectTypeList.value = data?.projectTypesList;
}
};
const handleAddBuilding = () => {
form.buildings.push(createEmptyBuilding());
};
@ -298,8 +304,6 @@
() => props.visible,
async (val) => {
if (val) {
console.log(props.record, 'props.record');
if (props.isEdit && props.record) {
const record = props.record;
if (record?.buildings?.length) {
@ -369,9 +373,12 @@
const handleCancel = () => {
emit('update:visible', false);
};
const handleDragEnd = () => {};
const handleDragEnd = () => {
console.log('handleDragEnd');
};
onMounted(() => {
getProvinceListData();
getAllProjectTypeListData();
});
</script>

View file

@ -47,7 +47,7 @@
@page-size-change="onPageSizeChange"
>
<template #type="{ record }">
<a-tag checkable :color="getProjectTypeColor(record.type)">
<a-tag checkable :color="record.color">
{{ record.typeName }}
</a-tag>
</template>
@ -60,15 +60,8 @@
</template>
<template #operate="{ record }">
<a-space>
<a-button
:loading="isLoadingDetail"
@click="handleDetail(record.id)"
>查看</a-button
>
<a-button
:loading="isLoadingEdit"
type="primary"
@click="handleEditProject(record.id)"
<a-button @click="handleDetail(record.id)">查看</a-button>
<a-button type="primary" @click="handleEditProject(record.id)"
>编辑</a-button
>
<a-popover trigger="click" position="bottom">
@ -114,7 +107,7 @@
import useDataTable from '@/composables/useDataTable';
import { ProjectItem } from '@/types/business';
import { getProjectList, getProjectDetail } from '@/api/business';
import { ProjectPaginationParams, ProjectType } from '@/types/comm';
import { ProjectPaginationParams } from '@/types/comm';
import projectFormDrawer from './drawer/project-form-drawer.vue';
import projectInfoDrawer from './drawer/project-info-drawer.vue';
import projectListFilterDrawer from './drawer/project-list-filter-drawer.vue';
@ -185,7 +178,12 @@
setLoading(false);
};
const getProjectDetailData = async (id: number, edit: boolean) => {
if (currentProjectInfo.value && currentProjectInfo.value?.id === id) return;
if (
currentProjectInfo.value &&
currentProjectInfo.value?.id === id &&
!edit
)
return;
edit ? (isLoadingEdit.value = true) : (isLoadingDetail.value = true);
const { code, data } = await getProjectDetail(id);
if (code === 200 && data) {
@ -226,11 +224,13 @@
showDrawer.value = true;
};
const handleEditProject = async (id: number) => {
if (isLoadingEdit.value) return;
isEdit.value = true;
await getProjectDetailData(id, true);
showDrawer.value = true;
};
const handleDetail = async (id: number) => {
if (isLoadingDetail.value) return;
await getProjectDetailData(id, false);
showProjectInfoDrawer.value = true;
};
@ -240,10 +240,6 @@
const handleFilter = () => {
showListFilterDrawer.value = true;
};
const getProjectTypeColor = (type: ProjectType) => {
const typeMap = { 1: 'green', 2: 'red', 3: 'orange' };
return typeMap[type];
};
onMounted(() => {
setColumns(columns);
getProjectListData();

View file

@ -0,0 +1,230 @@
<template>
<a-card title="合同文档模板设置">
<template #extra>
<a-button type="primary" @click="handleAddContractNumber"
><icon-plus />新增</a-button
>
</template>
<DataTable
:data="renderData"
:columns="projectColumn"
:loading="loading"
:total="total"
:current="current"
:page-size="pageSize"
pagination
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
>
<template #realname="{ record }">
<span>{{ record.realname }}</span>
<icon-idcard v-if="record.isManager === 1" />
</template>
<template #status="{ record }">
<a-tag :color="record.status == 1 ? 'green' : 'red'">
{{ record.status == 1 ? $t('enable') : $t('disable') }}</a-tag
>
</template>
<template #operate="{ record }">
<a-space>
<a-button type="primary" @click="handleEditContractNumber(record.id)">
{{ $t('edit') }}</a-button
>
<a-popover trigger="click" position="bottom">
<a-button>{{ $t('more') }}</a-button>
<template #content>
<a-space direction="vertical">
<a-button @click="handleToggleStatus(record.id, record.status)">
{{ record.status == 1 ? $t('disable') : $t('enable') }}
</a-button>
<a-button status="danger" @click="handleDel(record.id)">
{{ $t('delete') }}
</a-button>
</a-space>
</template>
</a-popover>
</a-space>
</template>
</DataTable>
<contractFileForm
v-model:visible="showModal"
:department-list-data="departmentListData"
:is-edit="isEdit"
@submitted="getWordSettingListData"
/>
</a-card>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import type { TableColumnData } from '@arco-design/web-vue';
import { Message } from '@arco-design/web-vue';
import useDataTable from '@/composables/useDataTable';
import {
getWordSettingFromId,
delWordSetting,
getWordSettingList,
disableWordSetting,
getDepartmentList,
} from '@/api/setting';
import { ContractWordItem, DepartmentItem } from '@/types/setting';
import { UserStatus } from '@/types/comm';
import { camelToSnake } from '@/utils/caseConvert';
// import contractNumberFormDrawer from './drawer/contract-number-form-drawer.vue';
import contractFileForm from './modal/contract-file-form.vue';
const {
data: renderData,
columns: projectColumn,
loading,
total,
current,
pageSize,
setData,
setColumns,
setLoading,
setTotal,
setCurrent,
setPageSize,
} = useDataTable({
initialPage: 1,
initialPageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
});
const contractWordList = ref<ContractWordItem[]>([]);
const departmentListData = ref<DepartmentItem[]>([]);
const showModal = ref<boolean>(false);
const isEdit = ref<boolean>(false);
const currentNumberInfo = ref<any>(null);
// 列配置
const columns: TableColumnData[] = [
{
title: '模板名称',
dataIndex: 'name',
key: 'name',
width: 120,
ellipsis: true,
},
{
title: '适用范围',
dataIndex: 'departmentName',
key: 'departmentName',
render: ({ record }) => record.departmentName || '-',
},
{
title: '适用项目',
dataIndex: 'projectName',
key: 'projectName',
render: ({ record }) => record.projectName || '-',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
slotName: 'status',
},
{
title: '操作',
key: 'operate',
slotName: 'operate',
width: 200,
},
];
// 获取列表数据
const getWordSettingListData = async () => {
const params = {
page: current.value,
page_size: pageSize.value,
};
setLoading(true);
const { code, data } = await getWordSettingList(params);
if (code === 200 && data) {
const { contractWordSetting, page } = data;
contractWordList.value = Array.isArray(contractWordSetting)
? contractWordSetting
: [];
setData(contractWordList.value);
if (page?.total) setTotal(page?.total);
}
setLoading(false);
};
// 部门列表
const getDepartmentListData = async () => {
try {
const { code, data } = await getDepartmentList();
if (code === 200) {
departmentListData.value = data.departmentList;
}
} catch (error) {
// console.error('获取部门列表失败:', error);
}
};
// 分页事件处理
const onPageChange = (page: number) => {
setCurrent(page);
current.value = page;
getWordSettingListData();
};
const onPageSizeChange = (size: number) => {
setPageSize(size);
pageSize.value = size;
getWordSettingListData();
};
const handleToggleStatus = async (id: number, currentStatus: UserStatus) => {
try {
const status: UserStatus = currentStatus === 1 ? 2 : 1;
const param = { id, status };
const { code, message } = await disableWordSetting(param);
if (code === 200) {
Message.success(message);
getWordSettingListData();
} else {
Message.error(message);
}
} catch (error) {
// 处理错误
}
};
const handleDel = async (id: number) => {
try {
const { code, message } = await delWordSetting(id);
if (code === 200) {
Message.success(message);
getWordSettingListData();
} else {
Message.error(message);
}
} catch (error) {
// 处理错误
}
};
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;
};
// 初始化数据
onMounted(() => {
setColumns(columns);
getWordSettingListData();
getDepartmentListData();
});
</script>

View file

@ -1,3 +1,231 @@
<template>
<div>123</div>
<a-card title="创建合同模板设置">
<template #extra>
<a-button type="primary" @click="handleAddContractNumber"
><icon-plus />新增</a-button
>
</template>
<DataTable
:data="renderData"
:columns="projectColumn"
:loading="loading"
:total="total"
:current="current"
:page-size="pageSize"
pagination
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
>
<template #realname="{ record }">
<span>{{ record.realname }}</span>
<icon-idcard v-if="record.isManager === 1" />
</template>
<template #status="{ record }">
<a-tag :color="record.status == 1 ? 'green' : 'red'">
{{ record.status == 1 ? $t('enable') : $t('disable') }}</a-tag
>
</template>
<template #operate="{ record }">
<a-space>
<a-button type="primary" @click="handleEditContractNumber(record.id)">
{{ $t('edit') }}</a-button
>
<a-popover trigger="click" position="bottom">
<a-button>{{ $t('more') }}</a-button>
<template #content>
<a-space direction="vertical">
<a-button @click="handleToggleStatus(record.id, record.status)">
{{ record.status == 1 ? $t('disable') : $t('enable') }}
</a-button>
<a-button status="danger" @click="handleDel(record.id)">
{{ $t('delete') }}
</a-button>
</a-space>
</template>
</a-popover>
</a-space>
</template>
</DataTable>
<contractTemplateForm
v-model:visible="showModal"
:department-list-data="departmentListData"
:is-edit="isEdit"
@submitted="getTemplateSettingListData"
/>
</a-card>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
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,
disableTemplateSetting,
} 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 {
data: renderData,
columns: projectColumn,
loading,
total,
current,
pageSize,
setData,
setColumns,
setLoading,
setTotal,
setCurrent,
setPageSize,
} = useDataTable({
initialPage: 1,
initialPageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
});
const contractTemplateList = ref<ContractTemplateSettingItem[]>([]);
const departmentListData = ref<DepartmentItem[]>([]);
const showModal = ref<boolean>(false);
const isEdit = ref<boolean>(false);
const currentNumberInfo = ref<any>(null);
// 列配置
const columns: TableColumnData[] = [
{
title: '模板名称',
dataIndex: 'name',
key: 'name',
width: 120,
ellipsis: true,
},
{
title: '合同类型',
dataIndex: 'contractType',
key: 'contractType',
width: 250,
render: ({ record }) => record.contractType || '-',
},
{
title: '适用范围',
dataIndex: 'departmentName',
key: 'departmentName',
render: ({ record }) => record.departmentName || '-',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
slotName: 'status',
},
{
title: '操作',
key: 'operate',
slotName: 'operate',
width: 200,
},
];
// 获取列表数据
const getTemplateSettingListData = async () => {
const params = {
page: current.value,
page_size: pageSize.value,
};
setLoading(true);
const { code, data } = await getTemplateSettingList(params);
if (code === 200 && data) {
const { contractTemplateSetting, page } = data;
contractTemplateList.value = Array.isArray(contractTemplateSetting)
? contractTemplateSetting
: [];
setData(contractTemplateList.value);
if (page?.total) setTotal(page?.total);
}
setLoading(false);
};
// 部门列表
const getDepartmentListData = async () => {
try {
const { code, data } = await getDepartmentList();
if (code === 200) {
departmentListData.value = data.departmentList;
}
} catch (error) {
// console.error('获取部门列表失败:', error);
}
};
// 分页事件处理
const onPageChange = (page: number) => {
setCurrent(page);
current.value = page;
getTemplateSettingListData();
};
const onPageSizeChange = (size: number) => {
setPageSize(size);
pageSize.value = size;
getTemplateSettingListData();
};
const handleToggleStatus = async (id: number, currentStatus: UserStatus) => {
try {
const status: UserStatus = currentStatus === 1 ? 2 : 1;
const param = { id, status };
const { code, message } = await disableTemplateSetting(param);
if (code === 200) {
Message.success(message);
getTemplateSettingListData();
} else {
Message.error(message);
}
} catch (error) {
// 处理错误
}
};
const handleDel = async (id: number) => {
try {
const { code, message } = await delTemplateSetting(id);
if (code === 200) {
Message.success(message);
getTemplateSettingListData();
} else {
Message.error(message);
}
} catch (error) {
// 处理错误
}
};
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;
isEdit.value = false;
showModal.value = true;
};
const handleEditContractNumber = async (id: number) => {
await getTemplateSettingFromIdData(id);
isEdit.value = true;
// currentNumberInfo.value = info
showModal.value = true;
};
// 初始化数据
onMounted(() => {
setColumns(columns);
getTemplateSettingListData();
getDepartmentListData();
});
</script>

View file

@ -1,3 +0,0 @@
<template>
<div>123</div>
</template>

View file

@ -23,8 +23,8 @@
<a-col :span="18">
<contractNumber v-if="currentSettingId === 1" />
<contractTerms v-if="currentSettingId === 2" />
<ContractTemplate v-if="currentSettingId === 3" />
<createContractTemplate v-if="currentSettingId === 4" />
<contractFile v-if="currentSettingId === 3" />
<ContractTemplate v-if="currentSettingId === 4" />
</a-col>
</a-row>
</a-card>
@ -37,7 +37,7 @@
import contractNumber from './contract-number.vue';
import contractTerms from './contract-terms.vue';
import ContractTemplate from './contract-template.vue';
import createContractTemplate from './create-contract-template.vue';
import contractFile from './contract-file.vue';
// import { } from '@/api/setting';
// import { Message } from '@arco-design/web-vue';

View file

@ -0,0 +1,167 @@
<template>
<a-modal
:visible="visible"
:title="isEdit ? '编辑创建合同模板' : '新增创建合同模板'"
unmount-on-close
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
auto-label-width
layout="vertical"
>
<!-- 模版名称 -->
<a-form-item label="模版名称" field="name" required>
<a-input v-model="form.name" placeholder="请输入模板名称" />
</a-form-item>
<!-- 适用范围(部门) -->
<a-form-item label="适用范围" field="departmentId">
<a-tree-select
v-model="form.departmentId"
:data="props.departmentListData"
:field-names="{ key: 'id', title: 'name', children: 'children' }"
placeholder="请选择适用范围"
allow-clear
/>
</a-form-item>
<a-form-item label="适用项目" field="projects">
<a-select
v-model="form.projects"
multiple
:disabled="!form.departmentId"
placeholder="请选择适用项目"
>
<template v-if="deptProjectList.length">
<a-option
v-for="item in deptProjectList"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</a-option>
</template>
</a-select>
</a-form-item>
<a-form-item label="上传文档" field="urlArr">
<FileUpload
v-model:object-keys="form.urlArr"
v-model:signed-put-u-r-l="form.urlArr"
:max-count="1"
:upload-type="1"
accept="doc,.docx"
/>
<!-- <a-upload draggable action="/" /> -->
<!-- <FileUpload tip="仅支持.doc / .docx格式,最大文件尺寸10MB" v-model:object-keys="form.url"
v-model:signed-put-u-r-l="form.url" list-type="picture-card" /> -->
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import type { FormInstance } from '@arco-design/web-vue';
import { createWordSetting, updateWordSetting } from '@/api/setting';
import { DepartmentItem } from '@/types/setting';
import { getDeptProjectListFromDeptId } from '@/api/common';
import { Message } from '@arco-design/web-vue';
import FileUpload from '@/components/common/FileUpload.vue';
interface TreeNode {
id: string | number;
name: string;
children?: TreeNode[];
}
const props = defineProps<{
visible: boolean;
isEdit: boolean;
record?: DepartmentItem | null;
departmentListData: TreeNode[]; // 修复类型
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'submitted'): void;
}>();
// 表单数据
const formRef = ref<FormInstance>();
const form = reactive<any>({
name: '',
departmentId: null,
projects: [],
url: '',
urlArr: [],
id: null,
});
const deptProjectList = ref<any>([]);
// 校验规则
const rules = {
name: [{ required: true, message: '请输入模板名称' }],
departmentId: [{ required: true, message: '请选择适用范围' }],
projects: [{ required: true, message: '请选择适用项目' }],
urlArr: [{ required: true, message: '请上传合同文档' }],
};
const getDeptProjectListFromDeptIdData = async (id) => {
const { code, data } = await getDeptProjectListFromDeptId(id);
if (code === 200) {
deptProjectList.value = data?.deptProjectList || [];
}
};
// 表单事件
const handleCancel = () => {
emit('update:visible', false);
};
// Watch 弹窗控制 & 初始化
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);
} else {
Object.assign(form, {
id: undefined,
name: '',
departmentId: null,
});
}
}
},
{ immediate: true }
);
watch(
() => form.departmentId,
(id) => {
if (id) {
deptProjectList.value = [];
getDeptProjectListFromDeptIdData(id);
}
}
);
const handleSubmit = async () => {
await formRef.value?.validate();
const params = JSON.parse(JSON.stringify(form));
params.url = form.urlArr[0];
delete params.urlArr;
const { code, message } = props.isEdit
? await updateWordSetting(params)
: await createWordSetting(params);
if (code === 200) {
Message.success(message);
handleCancel();
// 通知父组件更新部门列表数据
emit('submitted');
}
};
</script>

View file

@ -0,0 +1,112 @@
<template>
<a-modal
:visible="visible"
:title="isEdit ? '编辑创建合同模板' : '新增创建合同模板'"
unmount-on-close
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
auto-label-width
layout="vertical"
>
<!-- 模版名称 -->
<a-form-item label="模版名称" field="name" required>
<a-input v-model="form.name" placeholder="请输入模板名称" />
</a-form-item>
<!-- 适用范围(部门) -->
<a-form-item label="适用范围" field="user_id">
<a-tree-select
v-model="form.departmentId"
:data="props.departmentListData"
:field-names="{ key: 'id', title: 'name', children: 'children' }"
placeholder="请选择适用范围"
allow-clear
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import type { FormInstance } from '@arco-design/web-vue';
import { createTemplateSetting, updateTemplateSetting } from '@/api/setting';
import { DepartmentItem, CreateTemplateSettingParam } from '@/types/setting';
import { Message } from '@arco-design/web-vue';
interface TreeNode {
id: string | number;
name: string;
children?: TreeNode[];
}
const props = defineProps<{
visible: boolean;
isEdit: boolean;
record?: DepartmentItem | null;
departmentListData: TreeNode[]; // 修复类型
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'submitted'): void;
}>();
// 表单数据
const formRef = ref<FormInstance>();
const form = reactive<CreateTemplateSettingParam>({
name: '',
departmentId: null,
// id: undefined,
});
// 校验规则
const rules = {
name: [{ required: true, message: '请输入模板名称' }],
departmentId: [{ required: true, message: '请选择适用范围' }],
};
// Watch 弹窗控制 & 初始化
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);
} else {
Object.assign(form, {
id: undefined,
name: '',
departmentId: null,
});
}
}
},
{ immediate: true }
);
// 表单事件
const handleCancel = () => {
emit('update:visible', false);
};
const handleSubmit = async () => {
await formRef.value?.validate();
const { code, message } = props.isEdit
? await updateTemplateSetting(form)
: await createTemplateSetting(form);
if (code === 200) {
Message.success(message);
handleCancel();
// 通知父组件更新部门列表数据
emit('submitted');
}
};
</script>

View file

@ -63,13 +63,8 @@
</template>
</a-tree>
</a-col>
<a-col :span="12">
<a-descriptions
v-if="departmentDetailInfo"
:column="1"
layout="horizontal"
bordered
>
<a-col v-if="departmentDetailInfo" :span="12">
<a-descriptions :column="1" layout="horizontal" bordered>
<a-descriptions-item :label="t('department-name')">
{{ departmentDetailInfo.name }}</a-descriptions-item
>
@ -83,6 +78,39 @@
{{ departmentDetailInfo.remark }}</a-descriptions-item
>
</a-descriptions>
<a-card title="选择的项目" style="margin-top: 15px">
<template #extra>
<a-button type="primary" @click="handleSaveProject"
>保存</a-button
>
</template>
<ul>
<li
v-for="item in allProjetList"
:key="item.id"
style="margin-top: 10px"
>
<a-checkbox
:model-value="projectIdCheck.includes(item.id)"
@change="(val) => onCheckChangeProject(val, item)"
>{{ item.name }}</a-checkbox
>
<ul v-if="item?.buildings?.length" class="flex">
<li
v-for="subItem in item.buildings"
:key="subItem.id"
:class="[
'building_item',
{ choose: projectBuildingIdCheck.includes(subItem.id) },
]"
@click="handleChooseBuildingId(item, subItem.id)"
>
{{ subItem.name }}
</li>
</ul>
</li>
</ul>
</a-card>
</a-col>
</a-row>
<departmentFormModal
@ -106,8 +134,10 @@
departmentSetUp,
departmentSetDown,
delDepartment,
saveDataPermission,
} from '@/api/setting';
import { DepartmentItem, UserList } from '@/types/setting';
import { getAllProjectList } from '@/api/business';
import { debounce } from '@/utils';
import { Message } from '@arco-design/web-vue';
import departmentFormModal from './components/department-form-modal.vue';
@ -121,7 +151,8 @@
const expandedKeys = ref<(string | number)[]>([]);
const { t } = useI18n();
const projectIdCheck = ref<any>([]); // 已勾选的项目
const projectBuildingIdCheck = ref<any>([]); // 已勾选的项目楼栋
watch(
allDepartmentKeys,
(val) => {
@ -133,7 +164,7 @@
const showModal = ref<boolean>(false);
const isEdit = ref<boolean>(false);
const departmentDetailInfo = ref<DepartmentItem | null>(null);
const allProjetList = ref<any>(null);
interface TreeNode {
id: string | number;
isShowEditIcon?: boolean;
@ -150,22 +181,137 @@
});
};
/**
* 通用切换器
* @param type "project" | "building"
* @param project 当前项目对象 { id, buildings }
* @param targetId 被点击的 id(项目 id 或楼栋 id)
*/
const toggleCheck = (
type: 'project' | 'building',
project: any,
targetId: number
) => {
const projectId = project.id;
const buildingIds =
project.buildings?.map((b: any) => b.id).filter(Boolean) || [];
if (type === 'project') {
if (projectIdCheck.value.includes(projectId)) {
// 取消项目 → 去掉该项目和所有楼栋
projectIdCheck.value = projectIdCheck.value.filter(
(id) => id !== projectId
);
projectBuildingIdCheck.value = projectBuildingIdCheck.value.filter(
(id) => !buildingIds.includes(id)
);
} else {
// 勾选项目 → 加上项目和所有楼栋
projectIdCheck.value.push(projectId);
projectBuildingIdCheck.value.push(
...buildingIds.filter(
(id) => !projectBuildingIdCheck.value.includes(id)
)
);
}
}
if (type === 'building') {
if (projectBuildingIdCheck.value.includes(targetId)) {
// 取消楼栋
projectBuildingIdCheck.value = projectBuildingIdCheck.value.filter(
(id) => id !== targetId
);
// 如果该项目没有任何楼栋被选中 → 去掉项目
const stillChecked = buildingIds.some((id) =>
projectBuildingIdCheck.value.includes(id)
);
if (!stillChecked) {
projectIdCheck.value = projectIdCheck.value.filter(
(id) => id !== projectId
);
}
} else {
// 勾选楼栋
projectBuildingIdCheck.value.push(targetId);
// 确保项目也勾选
if (!projectIdCheck.value.includes(projectId)) {
projectIdCheck.value.push(projectId);
}
}
}
};
// 勾选/取消项目
const onCheckChangeProject = (_: any, project: any) => {
toggleCheck('project', project, project.id);
};
// 勾选/取消楼栋
const handleChooseBuildingId = (project: any, buildingId: number) => {
toggleCheck('building', project, buildingId);
};
// 生成保存所需的 projects
const getSaveProjects = () => {
const result: { project_id: number; building_id: number }[] = [];
// 遍历所有勾选的项目
projectIdCheck.value.forEach((pid: number) => {
const project = allProjetList.value?.find((p: any) => p.id === pid);
if (!project) return;
const buildingIds =
project.buildings?.map((b: any) => b.id).filter(Boolean) || [];
// 过滤出该项目下被选中的楼栋
const selectedBuildings = buildingIds.filter((bid) =>
projectBuildingIdCheck.value.includes(bid)
);
// 没有楼栋时,不生成数据(保证数据干净)
selectedBuildings.forEach((bid) => {
result.push({
project_id: project.id,
building_id: bid,
});
});
});
return result;
};
const currentNode = ref<TreeNode | null>(null);
const onSelect = debounce(
async (_: (string | number)[], event: { node: TreeNode }) => {
const { node } = event;
if (node.id === currentNode.value?.id) return;
currentNode.value = node;
// 递归重置所有节点的 isShowEditIcon
resetEditIcon(departmentListData.value);
// 只设置当前节点
if (currentNode.value.level !== 1) {
node.isShowEditIcon = true;
if (node.id !== currentNode.value?.id) {
currentNode.value = node;
// 递归重置所有节点的 isShowEditIcon
resetEditIcon(departmentListData.value);
// 只设置当前节点
if (currentNode.value.level !== 1) {
node.isShowEditIcon = true;
}
const data = await getDepartmentDetailData(
Number(currentNode.value.id)
);
departmentDetailInfo.value = data.department;
if (Array.isArray(data?.projects)) {
// 楼栋勾选
projectBuildingIdCheck.value = data.projects.map(
(item) => item.building_id
);
// 项目勾选(去重)
projectIdCheck.value = [
...new Set(data.projects.map((item) => item.project_id)),
];
} else {
projectIdCheck.value = [];
projectBuildingIdCheck.value = [];
}
}
departmentDetailInfo.value = await getDepartmentDetailData(
Number(currentNode.value.id)
);
},
300
);
@ -174,32 +320,36 @@
const allUserList = ref<UserList>([]);
const userKeyword = ref<string>('');
const getAllUserListData = async () => {
try {
const { code, data } = await getAllUserList({
keyword: userKeyword.value,
});
if (code === 200) {
const { user } = data;
allUserList.value = Array.isArray(user) ? user : [];
}
} catch (error) {
// 处理错误
async function getAllUserListData() {
const { code, data } = await getAllUserList({
keyword: userKeyword.value,
});
if (code === 200) {
const { user } = data;
allUserList.value = Array.isArray(user) ? user : [];
}
};
}
const handleAdd = () => {
// 所有项目信息
async function getAllProjectListData() {
const { code, data } = await getAllProjectList();
if (code === 200) {
allProjetList.value = data?.projectsList || [];
}
}
function handleAdd() {
isEdit.value = false;
departmentDetailInfo.value = null;
showModal.value = true;
};
}
const handleEdit = () => {
function handleEdit() {
showModal.value = true;
isEdit.value = true;
};
}
const handleDelete = async (item: { id: string | number }) => {
async function handleDelete(item: { id: string | number }) {
const { id } = item;
try {
const { code, message } = await delDepartment(Number(id));
@ -210,9 +360,9 @@
} catch (error) {
// 处理错误
}
};
}
const handleMoveUp = async (item: { id: string | number }) => {
async function handleMoveUp(item: { id: string | number }) {
const { id } = item;
try {
const { code, message } = await departmentSetUp(Number(id));
@ -225,25 +375,34 @@
} catch (error) {
// 处理错误
}
};
}
const handleMoveDown = async (item: { id: string | number }) => {
async function handleMoveDown(item: { id: string | number }) {
const { id } = item;
try {
const { code, message } = await departmentSetDown(Number(id));
if (code === 200) {
Message.success(message);
getDepartmentListData();
} else {
Message.info(message);
}
} catch (error) {
// 处理错误
const { code, message } = await departmentSetDown(Number(id));
if (code === 200) {
Message.success(message);
getDepartmentListData();
} else {
Message.info(message);
}
}
const handleSaveProject = async () => {
if (!currentNode?.value?.id) return Message.warning('请先选择部门');
const saveProjects = getSaveProjects();
if (saveProjects.length === 0) return Message.warning('请先选择项目');
const params = {
id: currentNode.value?.id,
projects: saveProjects,
};
const { code, message } = await saveDataPermission(params);
if (code === 200) Message.success(message);
return code;
};
onMounted(() => {
getAllUserListData();
getAllProjectListData();
});
</script>
@ -253,4 +412,32 @@
height: 100%;
padding: 15px 0;
}
.flex {
display: flex;
align-items: center;
margin-top: 10px;
.building_item {
background-color: #efefef;
color: rgba(154, 154, 154, 1);
width: 100px;
height: 28px;
line-height: 28px;
border: 1px solid #efefef;
margin-right: 16px;
text-align: center;
cursor: pointer;
&:last-child {
margin-right: 0;
}
&.choose {
background-color: #eaf3fe;
color: rgba(22, 93, 255, 1);
border-color: rgba(22, 93, 255, 1);
}
}
}
</style>