feat: 项目管理列表搜索、新增/编辑
This commit is contained in:
@@ -65,6 +65,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||
'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
|
||||
'no-param-reassign': 0,
|
||||
'prefer-regex-literals': 0,
|
||||
'import/no-extraneous-dependencies': 0,
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import {
|
||||
CreateProjectParam,
|
||||
ProjectDetailRes,
|
||||
} from '@/types/business';
|
||||
import { PaginationParams } from '@/types/comm';
|
||||
import { ProjectPaginationParams } from '@/types/comm';
|
||||
import { HttpResponse } from './interceptor';
|
||||
// import type { RouteRecordNormalized } from 'vue-router';
|
||||
// import { UserState } from '@/store/modules/user/types';
|
||||
@@ -22,7 +22,7 @@ export function getAllProjectList(): Promise<AllProjectListRes> {
|
||||
}
|
||||
// 项目列表(分页)
|
||||
export function getProjectList(
|
||||
params: PaginationParams
|
||||
params: ProjectPaginationParams
|
||||
): Promise<ProjectListRes> {
|
||||
return axios.post('project/getProjectList', params);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import axios from 'axios';
|
||||
import {
|
||||
CommonProvinceRes,
|
||||
CommonCityRes,
|
||||
CommonDistrictRes,
|
||||
AllProjectIdNameListRes,
|
||||
AllBuildingIdNameListRes,
|
||||
AllFloorIdNameListRes,
|
||||
AllInvoiceTypeIdNameListRes,
|
||||
AllFilingTypeIdNameListRes,
|
||||
UploadParam,
|
||||
BankListRes,
|
||||
} from '@/types/comm';
|
||||
import { HttpResponse } from './interceptor';
|
||||
|
||||
export interface ProjectRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
// 省份列表
|
||||
export function getProvinceList(): Promise<CommonProvinceRes> {
|
||||
return axios.get(`common/getProvinceList`);
|
||||
}
|
||||
// 城市列表
|
||||
export function getCityListByProvinceId(id: number): Promise<CommonCityRes> {
|
||||
return axios.get(`common/getCityList?province_id=${id}`);
|
||||
}
|
||||
// 区域列表
|
||||
export function getDistrictListByCityId(
|
||||
id: number
|
||||
): Promise<CommonDistrictRes> {
|
||||
return axios.get(`common/getDistrictList?city_id=${id}`);
|
||||
}
|
||||
|
||||
// 全部项目(id,名称)列表
|
||||
export function getAllProjectIdNameList(): Promise<AllProjectIdNameListRes> {
|
||||
return axios.get(`common/getAllProjectList`);
|
||||
}
|
||||
// 全部楼栋(id,名称)列表(根据项目id查询)
|
||||
export function getAllBuildingIdNameList(
|
||||
id: number
|
||||
): Promise<AllBuildingIdNameListRes> {
|
||||
return axios.get(`common/getAllBuildingList?project_id=${id}`);
|
||||
}
|
||||
// 全部楼层(id,名称)列表(根据楼栋id查询)
|
||||
export function getAllFloorIdNameList(
|
||||
id: number
|
||||
): Promise<AllFloorIdNameListRes> {
|
||||
return axios.get(`common/getAllFloorList?building_id=${id}`);
|
||||
}
|
||||
// 发票类型列表
|
||||
export function getInvoiceTypeList(): Promise<AllInvoiceTypeIdNameListRes> {
|
||||
return axios.get(`common/getInvoiceTypeList`);
|
||||
}
|
||||
// 租客归档类型列表
|
||||
export function getFilingTypeList(): Promise<AllFilingTypeIdNameListRes> {
|
||||
return axios.get(`common/getFilingTypeList`);
|
||||
}
|
||||
// 上传文档
|
||||
export function ossUpload(
|
||||
params: UploadParam | FormData
|
||||
): Promise<HttpResponse> {
|
||||
return axios.post('common/ossUpload', params);
|
||||
}
|
||||
// 下载文档
|
||||
export function ossDownload(key: string): Promise<HttpResponse> {
|
||||
return axios.get(`common/ossDownload?key=${key}`);
|
||||
}
|
||||
// 银行列表
|
||||
export function getBanksList(keyword: string): Promise<BankListRes> {
|
||||
return axios.post('common/getBanksList', { keyword });
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
# ImageUpload 图片上传组件
|
||||
|
||||
一个可复用的图片上传组件,支持自定义上传类型、文件大小限制等功能。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 支持图片格式:JPG、PNG、GIF、WEBP
|
||||
- 可自定义文件大小限制
|
||||
- 支持上传进度显示
|
||||
- 支持图片预览
|
||||
- 支持多文件上传
|
||||
- 上传成功后自动回传URL
|
||||
|
||||
## Props
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| modelValue | UploadFile[] | [] | 文件列表,支持v-model |
|
||||
| uploadType | UploadType | - | 上传类型:1合同模版,2图片 |
|
||||
| maxSize | number | 10 * 1024 * 1024 | 最大文件大小(字节) |
|
||||
| maxCount | number | - | 最大上传数量 |
|
||||
|
||||
## Events
|
||||
|
||||
| 事件名 | 参数 | 说明 |
|
||||
|--------|------|------|
|
||||
| update:modelValue | (value: UploadFile[]) | 文件列表更新时触发 |
|
||||
| upload-success | (file: UploadFile, response: UploadResponse) | 上传成功时触发 |
|
||||
| upload-error | (error: Error) | 上传失败时触发 |
|
||||
|
||||
## 类型定义
|
||||
|
||||
```typescript
|
||||
interface UploadFile {
|
||||
uid: string;
|
||||
name: string;
|
||||
url?: string;
|
||||
status: 'done' | 'uploading' | 'error';
|
||||
percent?: number;
|
||||
}
|
||||
|
||||
interface UploadResponse {
|
||||
filename: string;
|
||||
size: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
type UploadType = 1 | 2; // 1合同模版,2图片
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<ImageUpload
|
||||
v-model="images"
|
||||
:upload-type="2"
|
||||
@upload-success="handleUploadSuccess"
|
||||
@upload-error="handleUploadError"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import ImageUpload from '@/components/common/ImageUpload.vue';
|
||||
|
||||
const images = ref([]);
|
||||
|
||||
const handleUploadSuccess = (file, response) => {
|
||||
console.log('上传成功:', response.url);
|
||||
};
|
||||
|
||||
const handleUploadError = (error) => {
|
||||
console.error('上传失败:', error.message);
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### 自定义文件大小限制
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<ImageUpload
|
||||
v-model="images"
|
||||
:upload-type="2"
|
||||
:max-size="5 * 1024 * 1024"
|
||||
@upload-success="handleUploadSuccess"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 在表单中使用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<a-form-item label="项目图片" field="images">
|
||||
<ImageUpload
|
||||
v-model="form.images"
|
||||
:upload-type="2"
|
||||
:max-size="10 * 1024 * 1024"
|
||||
@upload-success="handleUploadSuccess"
|
||||
@upload-error="handleUploadError"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 组件会自动处理文件验证,包括文件大小和格式检查
|
||||
2. 上传成功后,文件对象会自动更新URL和状态
|
||||
3. 支持v-model双向绑定,父组件可以直接操作文件列表
|
||||
4. 上传类型必须从父组件传入,用于区分不同的上传场景
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<a-upload
|
||||
:list-type="listType"
|
||||
:accept="'image/*'"
|
||||
:custom-request="customRequest"
|
||||
:file-list="fileList"
|
||||
:multiple="maxCount > 1"
|
||||
:limit="maxCount"
|
||||
image-preview
|
||||
@change="handleChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { ossUpload } from '@/api/common';
|
||||
import type { UploadType } from '@/types/comm';
|
||||
|
||||
interface UploadFile {
|
||||
uid: string;
|
||||
name: string;
|
||||
url?: string;
|
||||
status: 'done' | 'uploading' | 'error';
|
||||
percent?: number;
|
||||
}
|
||||
|
||||
interface UploadResponse {
|
||||
filename: string;
|
||||
size: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string[]; // 只暴露图片url数组
|
||||
uploadType: UploadType;
|
||||
maxSize?: number; // 默认10MB
|
||||
maxCount?: number; // 最大上传数量,默认1(单图)
|
||||
listType?: 'text' | 'picture' | 'picture-card'; // 上传样式
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string[]): void;
|
||||
(e: 'upload-success', url: string): void;
|
||||
(e: 'upload-error', error: Error): void;
|
||||
}>();
|
||||
|
||||
const fileList = ref<UploadFile[]>([]);
|
||||
|
||||
// 默认最大上传数:1(单图)
|
||||
const maxCount = props.maxCount ?? 1;
|
||||
|
||||
// listType 默认值
|
||||
const listType = props.listType ?? 'picture-card';
|
||||
|
||||
// 将外部 url[] 映射为 UploadFile[]
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
fileList.value = (newVal || []).map((url, index) => ({
|
||||
uid: `${index}`,
|
||||
name: `图片-${index + 1}`,
|
||||
url,
|
||||
status: 'done',
|
||||
}));
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 自定义上传请求
|
||||
const customRequest = async (option: any) => {
|
||||
const { onProgress, onError, onSuccess, fileItem } = option;
|
||||
|
||||
try {
|
||||
const file = fileItem.file;
|
||||
const maxSize = props.maxSize || 10 * 1024 * 1024;
|
||||
const allowedTypes = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
];
|
||||
|
||||
// 文件大小校验
|
||||
if (file.size > maxSize) {
|
||||
onError(
|
||||
new Error(`文件大小不能超过${Math.round(maxSize / 1024 / 1024)}MB`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 文件类型校验
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
onError(new Error('只支持 JPG、PNG、GIF、WEBP 格式的图片'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 模拟上传进度
|
||||
let progress = 0;
|
||||
const progressInterval = setInterval(() => {
|
||||
progress += Math.random() * 30;
|
||||
if (progress >= 90) {
|
||||
clearInterval(progressInterval);
|
||||
progress = 90;
|
||||
}
|
||||
onProgress({ percent: 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 response = data as UploadResponse;
|
||||
|
||||
fileItem.status = 'done';
|
||||
fileItem.url = response.url;
|
||||
onProgress({ percent: 100 });
|
||||
onSuccess(response);
|
||||
|
||||
// 新的图片 url
|
||||
let updatedUrls = [...props.modelValue];
|
||||
if (maxCount === 1) {
|
||||
updatedUrls = [response.url];
|
||||
} else {
|
||||
updatedUrls.push(response.url);
|
||||
// 截取至 maxCount
|
||||
updatedUrls = updatedUrls.slice(0, maxCount);
|
||||
}
|
||||
|
||||
emit('update:modelValue', updatedUrls);
|
||||
emit('upload-success', response.url);
|
||||
} else {
|
||||
const error = new Error(message || '上传失败');
|
||||
onError(error);
|
||||
emit('upload-error', error);
|
||||
}
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error('上传失败');
|
||||
onError(err);
|
||||
emit('upload-error', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 同步 fileList(删除时触发)
|
||||
const handleChange = (newFileList: UploadFile[]) => {
|
||||
const validUrls = newFileList
|
||||
.filter((f) => f.status === 'done' && f.url)
|
||||
.map((f) => f.url!);
|
||||
|
||||
emit('update:modelValue', validUrls);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-upload-list-picture-card) {
|
||||
.arco-upload-list-item {
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import Chart from './chart/index.vue';
|
||||
import Breadcrumb from './breadcrumb/index.vue';
|
||||
import DataTable from './common/DataTable.vue';
|
||||
import ImageUpload from './common/ImageUpload.vue';
|
||||
|
||||
// Manually introduce ECharts modules to reduce packing size
|
||||
|
||||
@@ -33,5 +34,6 @@ export default {
|
||||
Vue.component('Chart', Chart);
|
||||
Vue.component('Breadcrumb', Breadcrumb);
|
||||
Vue.component('DataTable', DataTable);
|
||||
Vue.component('ImageUpload', ImageUpload);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,10 +9,11 @@ import {
|
||||
} from './comm';
|
||||
|
||||
export interface BuildingItem {
|
||||
customId?: string;
|
||||
id?: string; // 楼栋id
|
||||
name: string; // 楼栋名称
|
||||
up_floor?: number; // 地上层数
|
||||
down_floor?: number; // 地下层数
|
||||
up_floor?: number | null; // 地上层数
|
||||
down_floor?: number | null; // 地下层数
|
||||
}
|
||||
export interface AllProjectsItem {
|
||||
id: number; // 项目id
|
||||
@@ -57,9 +58,9 @@ export interface CreateProjectParam {
|
||||
id?: number;
|
||||
type: ProjectType; // 项目类型:1写字楼,2商业综合体,3园区
|
||||
name: string; // 楼栋名称
|
||||
province_id: number; // 省份id
|
||||
city_id: number; // 城市id
|
||||
district_id: number; // 区域id
|
||||
province_id: number | null; // 省份id
|
||||
city_id: number | null; // 城市id
|
||||
district_id: number | null; // 区域id
|
||||
address: string; // 详细地址
|
||||
images: string[]; // 图片数组
|
||||
buildings: BuildingItem[]; // 楼栋数据
|
||||
@@ -93,4 +94,6 @@ export interface ProjectInfo extends BaseEntity {
|
||||
buildings: BuildingInfo[]; // 楼栋数组
|
||||
}
|
||||
|
||||
export type ProjectDetailRes = ProjectInfo;
|
||||
export type ProjectDetailRes = HttpResponse<{
|
||||
project: ProjectInfo;
|
||||
}>;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { HttpResponse } from '@/api/interceptor';
|
||||
// ==================== 通用类型定义 ====================
|
||||
export interface BaseEntity {
|
||||
id: number;
|
||||
@@ -30,3 +31,66 @@ 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图片
|
||||
|
||||
// ==================== 公共接口类型定义 ====================
|
||||
|
||||
export interface CommonProvinceItem {
|
||||
ID?: number;
|
||||
id?: number;
|
||||
name?: string; // 省份
|
||||
Name?: string; // 省份
|
||||
code: string; // 编码
|
||||
}
|
||||
export interface CommonCityItem extends CommonProvinceItem {
|
||||
ProvinceId: number; // 所属省份id
|
||||
}
|
||||
export interface CommonDistrictItem extends CommonProvinceItem {
|
||||
CityId: number; // 所属城市id
|
||||
}
|
||||
// 省份
|
||||
export type CommonProvinceRes = HttpResponse<{
|
||||
provinceList?: CommonProvinceItem[];
|
||||
}>;
|
||||
// 城市
|
||||
export type CommonCityRes = HttpResponse<{
|
||||
cityList?: CommonCityItem[];
|
||||
}>;
|
||||
// 区域
|
||||
export type CommonDistrictRes = HttpResponse<{
|
||||
districtList?: CommonDistrictItem[];
|
||||
}>;
|
||||
// 全部项目(id、名称的列表)
|
||||
export interface AllIdNameItem {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
export interface ProjectPaginationParams extends PaginationParams {
|
||||
type?: ProjectType[] | null;
|
||||
province_ids?: number[] | null;
|
||||
rent_rate_min?: number | null;
|
||||
rent_rate_max?: number | null;
|
||||
}
|
||||
export type AllProjectIdNameListRes = HttpResponse<{
|
||||
projectsList?: AllIdNameItem[];
|
||||
}>;
|
||||
export type AllBuildingIdNameListRes = HttpResponse<{
|
||||
buildingList?: AllIdNameItem[];
|
||||
}>;
|
||||
export type AllFloorIdNameListRes = HttpResponse<{
|
||||
floorList?: AllIdNameItem[];
|
||||
}>;
|
||||
export type AllInvoiceTypeIdNameListRes = HttpResponse<{
|
||||
invoiceTypeList?: AllIdNameItem[];
|
||||
}>;
|
||||
export type AllFilingTypeIdNameListRes = HttpResponse<{
|
||||
filingTypeList?: AllIdNameItem[];
|
||||
}>;
|
||||
|
||||
export interface UploadParam {
|
||||
file: any;
|
||||
type: UploadType;
|
||||
}
|
||||
export type BankListRes = HttpResponse<{
|
||||
banksList?: AllIdNameItem[];
|
||||
}>;
|
||||
|
||||
@@ -34,5 +34,15 @@ export function debounce<T extends (...args: any[]) => any>(
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
export const generateRandomId = (length = 8) => {
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
const charsLength = chars.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * charsLength));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export default null;
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<!-- 项目名称 -->
|
||||
<a-form-item label="条款名称" field="name" required>
|
||||
<a-form-item label="项目名称" field="name" required>
|
||||
<a-input
|
||||
v-model="form.name"
|
||||
:placeholder="$t('please.enter', { placeholder: '项目名称' })"
|
||||
@@ -26,7 +26,7 @@
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<!-- 项目类型 -->
|
||||
<a-form-item label="项目类型" field="name" required>
|
||||
<a-form-item label="项目类型" field="type" required>
|
||||
<a-select v-model="form.type" placeholder="请选择项目类型">
|
||||
<a-option
|
||||
v-for="item in projectTypeList"
|
||||
@@ -40,16 +40,131 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
<!-- 项目地址 -->
|
||||
<a-form-item label="项目地址" field="type" required>
|
||||
<!-- <a-space>
|
||||
<a-select v-model="form.finance_fee_id" :style="{ width: '200px' }" placeholder="请选择省份...">
|
||||
<a-option v-for="item in financeFeeList" :key="item.id" :value="item.id">
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
<a-select v-model="form.finance_fee_periodicity" :style="{ width: '200px' }"
|
||||
:options="financeFeeFeriodicityList" placeholder="请选择..." />
|
||||
</a-space> -->
|
||||
<a-form-item label="项目地址" field="address" required>
|
||||
<a-row :gutter="12" wrap :style="{ width: '100%' }">
|
||||
<a-col :span="12">
|
||||
<a-select
|
||||
v-model="form.province_id"
|
||||
:style="{ width: '100%' }"
|
||||
placeholder="请选择省份..."
|
||||
@change="handleChangeProvince"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in provinceList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-row :gutter="12">
|
||||
<a-col :span="12">
|
||||
<a-select
|
||||
v-model="form.city_id"
|
||||
:disabled="cityList.length === 0"
|
||||
:style="{ width: '100%' }"
|
||||
placeholder="请选择城市..."
|
||||
@change="handleChangeCity"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in cityList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-select
|
||||
v-model="form.district_id"
|
||||
:disabled="districtList.length === 0"
|
||||
:style="{ width: '100%' }"
|
||||
placeholder="请选择区域..."
|
||||
>
|
||||
<a-option
|
||||
v-for="item in districtList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
<a-col :span="24" style="margin-top: 10px">
|
||||
<a-input
|
||||
v-model="form.address"
|
||||
:disabled="!form.district_id"
|
||||
placeholder="请输入详细地址..."
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item label="项目图片" field="images">
|
||||
<ImageUpload
|
||||
v-model="form.images"
|
||||
:upload-type="2"
|
||||
:max-count="5"
|
||||
:list-type="'picture-card'"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
<div class="flex_center_between">
|
||||
<p class="left">楼栋管理</p>
|
||||
<div class="right">
|
||||
<a-button type="outline" @click="handleAddBuilding">
|
||||
<icon-plus /> 新增楼栋
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<draggable
|
||||
v-model="form.buildings"
|
||||
:animation="200"
|
||||
:item-key="props.isEdit ? 'id' : 'customId'"
|
||||
@end="handleDragEnd"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<a-row align="center" :gutter="12" style="margin-top: 10px">
|
||||
<a-col :span="2">
|
||||
<icon-unordered-list :size="24" />
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-input
|
||||
v-model="element.name"
|
||||
placeholder="输入楼栋号(如A座)"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number
|
||||
v-model="element.up_floor"
|
||||
prepend="地面"
|
||||
append="层"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number
|
||||
v-model="element.down_floor"
|
||||
prepend="地下"
|
||||
append="层"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="2">
|
||||
<icon-delete
|
||||
:size="16"
|
||||
style="cursor: pointer"
|
||||
@click="handleDelBuilding(element.customId)"
|
||||
/>
|
||||
</a-col>
|
||||
<a-divider />
|
||||
</a-row>
|
||||
</template>
|
||||
</draggable>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
@@ -58,10 +173,25 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, reactive, onMounted } from 'vue';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
// import { Message } from '@arco-design/web-vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { projectTypeList } from '@/utils/constant';
|
||||
// import { CreateTermSettingParam, FeeManagement } from '@/types/setting';
|
||||
// import { FeeManagementType } from '@/types/comm';
|
||||
import {
|
||||
CommonProvinceItem,
|
||||
CommonCityItem,
|
||||
CommonDistrictItem,
|
||||
ProjectType,
|
||||
} from '@/types/comm';
|
||||
import {
|
||||
getProvinceList,
|
||||
getCityListByProvinceId,
|
||||
getDistrictListByCityId,
|
||||
} from '@/api/common';
|
||||
import { createProject, updateProject } from '@/api/business';
|
||||
import { CreateProjectParam } from '@/types/business';
|
||||
import ImageUpload from '@/components/common/ImageUpload.vue';
|
||||
import draggable from 'vuedraggable';
|
||||
import { generateRandomId } from '@/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
isEdit: boolean;
|
||||
@@ -71,34 +201,128 @@
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
(e: 'submitted'): void;
|
||||
}>();
|
||||
|
||||
const createEmptyBuilding = () => {
|
||||
return {
|
||||
customId: generateRandomId(),
|
||||
name: '',
|
||||
up_floor: null,
|
||||
down_floor: null,
|
||||
};
|
||||
};
|
||||
const isLoading = ref<boolean>(false);
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const defaultForm: any = {
|
||||
name: '', // 名称
|
||||
type: 1, // 条款类型:1租赁类,2物业类,3特殊类,4其他类
|
||||
is_fee: 1, // 是否勾选费项:1是,2否
|
||||
finance_fee_id: null, // 财务费项id(除保证金外
|
||||
finance_fee_periodicity: 1, // 周期性:1周期性付款,2一次性付款
|
||||
is_bond: 2, // 是否勾选保证金:1是,2否
|
||||
finance_bond_fee_id: null, // 财务保证金费项id
|
||||
is_incremental: 2, // 是否勾选递增项:1是,2否
|
||||
is_discount: 2, // 是否勾选优惠项:1是,2否
|
||||
is_late_fee: 2, // 是否勾选滞纳金:1是,2否
|
||||
|
||||
const defaultForm: CreateProjectParam = {
|
||||
type: 1 as ProjectType, // 项目类型:1写字楼,2商业综合体,3园区
|
||||
name: '', // 楼栋名称
|
||||
province_id: null, // 省份id
|
||||
city_id: null, // 城市id
|
||||
district_id: null, // 区域id
|
||||
address: '', // 详细地址
|
||||
images: [], // 图片数组
|
||||
buildings: [createEmptyBuilding()], // 楼栋数据
|
||||
};
|
||||
const form = reactive<any>({ ...defaultForm });
|
||||
const form = reactive<CreateProjectParam>({ ...defaultForm });
|
||||
// 校验规则
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: '请输入条款名称' }],
|
||||
type: [{ required: true, message: '请选择条款类别' }],
|
||||
name: [{ required: true, message: '请输入项目名称' }],
|
||||
type: [{ required: true, message: '请选择项目类型' }],
|
||||
address: [
|
||||
{ required: true, message: '详细地址不能为空' },
|
||||
{
|
||||
'validator': (_, cb) => {
|
||||
if (!form.province_id) {
|
||||
cb('请选择项目地址所在省份');
|
||||
} else if (!form.city_id) {
|
||||
cb('请选择项目地址所在城市');
|
||||
} else if (!form.district_id) {
|
||||
cb('请选择项目地址所在区域');
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
},
|
||||
'validate-trigger': 'blur',
|
||||
},
|
||||
],
|
||||
});
|
||||
const provinceList = ref<CommonProvinceItem[]>([]);
|
||||
const cityList = ref<CommonCityItem[]>([]);
|
||||
const districtList = ref<CommonDistrictItem[]>([]);
|
||||
|
||||
const getProvinceListData = async () => {
|
||||
const { code, data } = await getProvinceList();
|
||||
if (code === 200 && data) {
|
||||
provinceList.value = Array.isArray(data?.provinceList)
|
||||
? data?.provinceList
|
||||
: [];
|
||||
}
|
||||
};
|
||||
const getCityListByProvinceIdData = async (provinceId: number) => {
|
||||
const { code, data } = await getCityListByProvinceId(provinceId);
|
||||
if (code === 200 && data) {
|
||||
cityList.value = Array.isArray(data?.cityList) ? data?.cityList : [];
|
||||
}
|
||||
};
|
||||
const getDistrictListByCityIdData = async (cityId: number) => {
|
||||
const { code, data } = await getDistrictListByCityId(cityId);
|
||||
if (code === 200 && data) {
|
||||
districtList.value = Array.isArray(data?.districtList)
|
||||
? data?.districtList
|
||||
: [];
|
||||
}
|
||||
};
|
||||
const handleChangeProvince = async (id) => {
|
||||
form.city_id = null;
|
||||
await getCityListByProvinceIdData(id);
|
||||
};
|
||||
const handleChangeCity = async (id) => {
|
||||
form.district_id = null;
|
||||
await getDistrictListByCityIdData(id);
|
||||
};
|
||||
|
||||
const handleAddBuilding = () => {
|
||||
form.buildings.push(createEmptyBuilding());
|
||||
};
|
||||
const handleDelBuilding = (id: string) => {
|
||||
if (form.buildings.length === 1) return;
|
||||
const index = form.buildings.findIndex((b) => b.customId === id);
|
||||
if (index !== -1) {
|
||||
form.buildings.splice(index, 1);
|
||||
}
|
||||
};
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
async (val) => {
|
||||
if (val) {
|
||||
console.log(props.record, 'props.record');
|
||||
|
||||
if (props.isEdit && props.record) {
|
||||
Object.assign(form, props.record);
|
||||
const record = props.record;
|
||||
if (record?.buildings?.length) {
|
||||
record.buildings.forEach((element) => {
|
||||
element.up_floor = element.upFloor;
|
||||
element.down_floor = element.downFloor;
|
||||
});
|
||||
}
|
||||
if (record?.imageUrls?.length) {
|
||||
record.images = record?.imageUrls;
|
||||
}
|
||||
form.name = record.name;
|
||||
Object.keys(form).forEach((key) => {
|
||||
form[key] = record[key];
|
||||
});
|
||||
form.province_id = record.provinceId;
|
||||
form.city_id = record.cityId;
|
||||
form.district_id = record.districtId;
|
||||
form.id = record.id;
|
||||
// Object.assign(form, record);
|
||||
if (form.province_id) {
|
||||
await getCityListByProvinceIdData(form.province_id);
|
||||
}
|
||||
if (form.city_id) {
|
||||
await getDistrictListByCityIdData(form.city_id);
|
||||
}
|
||||
} else {
|
||||
Object.assign(form, defaultForm);
|
||||
}
|
||||
@@ -107,12 +331,45 @@
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const handleOk = async () => {};
|
||||
const handleOk = async () => {
|
||||
if (isLoading.value) return;
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const valid = await formRef.value.validate();
|
||||
if (valid) return;
|
||||
form.buildings = form.buildings.map((item) => {
|
||||
return {
|
||||
name: item.name,
|
||||
up_floor: item.up_floor,
|
||||
down_floor: item.down_floor,
|
||||
id: item?.id,
|
||||
};
|
||||
});
|
||||
const params = {
|
||||
...form,
|
||||
};
|
||||
const res = props?.isEdit
|
||||
? await updateProject(params)
|
||||
: await createProject(params);
|
||||
if (res?.code === 200) {
|
||||
Message.success(res?.message);
|
||||
emit('submitted');
|
||||
emit('update:visible', false);
|
||||
}
|
||||
} catch (err) {
|
||||
// 捕获异常,暂不处理
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
onMounted(() => {});
|
||||
const handleDragEnd = () => {};
|
||||
onMounted(() => {
|
||||
getProvinceListData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
<div class="flex_center_between">
|
||||
<a-space>
|
||||
<a-input-search
|
||||
v-model="queryParams.keyword"
|
||||
:style="{ width: '320px' }"
|
||||
:placeholder="t('please.enter', { placeholder: t('content') })"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<a-button>
|
||||
<template #icon>
|
||||
@@ -23,7 +25,7 @@
|
||||
</template>
|
||||
{{ t('table.export') }}
|
||||
</a-button>
|
||||
<a-button type="primary">
|
||||
<a-button type="primary" @click="handleAddProject">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
@@ -44,23 +46,37 @@
|
||||
@page-change="onPageChange"
|
||||
@page-size-change="onPageSizeChange"
|
||||
>
|
||||
<!-- <template #type="{ record }">
|
||||
<a-tag checkable :color="getTypeColor(record.type)">{{
|
||||
getTypeText(record.type)
|
||||
}}</a-tag>
|
||||
<template #type="{ record }">
|
||||
<a-tag checkable :color="getProjectTypeColor(record.type)">
|
||||
{{ record.typeName }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #rate="{ record }">
|
||||
<a-progress :percent="record.rate" color="green" :style="{ width: '100px' }" />
|
||||
</template> -->
|
||||
<template #operate>
|
||||
<a-progress
|
||||
:percent="Number(record.rentRate)"
|
||||
color="green"
|
||||
:style="{ width: '100px' }"
|
||||
/>
|
||||
</template>
|
||||
<template #operate="{ record }">
|
||||
<a-space>
|
||||
<a-button>查看</a-button>
|
||||
<a-button type="primary">编辑</a-button>
|
||||
<a-button
|
||||
:loading="isLoadingEdit"
|
||||
type="primary"
|
||||
@click="handleEditProject(record.id)"
|
||||
>编辑</a-button
|
||||
>
|
||||
<a-button>更多</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</DataTable>
|
||||
</a-card>
|
||||
<projectFormDrawer
|
||||
v-model:visible="showDrawer"
|
||||
:is-edit="isEdit"
|
||||
:record="currentProjectInfo"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -70,14 +86,18 @@
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { TableColumnData } from '@arco-design/web-vue';
|
||||
import useDataTable from '@/composables/useDataTable';
|
||||
import { ProjectItem } from '@/types/business';
|
||||
import { getProjectList } from '@/api/business';
|
||||
import { ProjectItem, ProjectInfo } from '@/types/business';
|
||||
import { getProjectList, getProjectDetail } from '@/api/business';
|
||||
import { ProjectPaginationParams, ProjectType } from '@/types/comm';
|
||||
import projectFormDrawer from './drawer/project-form-drawer.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { loading } = useLoading(false);
|
||||
|
||||
const showDrawer = ref<boolean>(false);
|
||||
const isLoadingEdit = ref<boolean>(false);
|
||||
const isEdit = ref<boolean>(false);
|
||||
const currentProjectInfo = ref<ProjectInfo | null>(null);
|
||||
const projectList = ref<ProjectItem[]>([]);
|
||||
|
||||
const {
|
||||
data: renderData,
|
||||
columns: projectColumn,
|
||||
@@ -100,28 +120,46 @@
|
||||
const columns: TableColumnData[] = [
|
||||
{ title: '项目信息', dataIndex: 'name', key: 'name' },
|
||||
{ title: '项目类型', dataIndex: 'type', key: 'type', slotName: 'type' },
|
||||
{ title: '管理面积', dataIndex: 'size', key: 'size' },
|
||||
{ title: '可租面积', dataIndex: 'kezu', key: 'kezu' },
|
||||
{ title: '出租率', dataIndex: 'rate', key: 'rate', slotName: 'rate' },
|
||||
{ title: '房源数量', dataIndex: 'count', key: 'count' },
|
||||
{ title: '创建时间', dataIndex: 'create_time', key: 'create_time' },
|
||||
{ title: '管理面积', dataIndex: 'RentAreaStr', key: 'RentAreaStr' },
|
||||
{ title: '可租面积', dataIndex: 'RoomAreaStr', key: 'RoomAreaStr' },
|
||||
{
|
||||
title: '出租率',
|
||||
dataIndex: 'RentRateStr',
|
||||
key: 'RentRateStr',
|
||||
slotName: 'rate',
|
||||
},
|
||||
{ title: '房源数量', dataIndex: 'roomNum', key: 'roomNum' },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt' },
|
||||
{ title: '操作', key: 'operate', slotName: 'operate', width: 200 },
|
||||
];
|
||||
|
||||
const queryParams = ref<ProjectPaginationParams>({
|
||||
page: current.value,
|
||||
page_size: pageSize.value,
|
||||
keyword: '',
|
||||
type: null,
|
||||
province_ids: null,
|
||||
rent_rate_min: null,
|
||||
rent_rate_max: null,
|
||||
});
|
||||
const getProjectListData = async () => {
|
||||
const params = {
|
||||
page: current.value,
|
||||
page_size: pageSize.value,
|
||||
};
|
||||
setLoading(true);
|
||||
const { code, data } = await getProjectList(params);
|
||||
const { code, data } = await getProjectList(queryParams.value);
|
||||
if (code === 200 && data) {
|
||||
const { projectsList, page } = data;
|
||||
projectList.value = Array.isArray(projectsList) ? projectsList : [];
|
||||
setData(projectList.value);
|
||||
setTotal(page?.total ? page.total : 0);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const getProjectDetailData = async (id: number) => {
|
||||
isLoadingEdit.value = true;
|
||||
const { code, data } = await getProjectDetail(id);
|
||||
if (code === 200 && data) {
|
||||
currentProjectInfo.value = data.project;
|
||||
}
|
||||
isLoadingEdit.value = false;
|
||||
};
|
||||
|
||||
// 分页事件处理
|
||||
const onPageChange = (page: number) => {
|
||||
@@ -132,6 +170,23 @@
|
||||
setPageSize(size);
|
||||
pageSize.value = size;
|
||||
};
|
||||
const handleAddProject = () => {
|
||||
isEdit.value = false;
|
||||
currentProjectInfo.value = null;
|
||||
showDrawer.value = true;
|
||||
};
|
||||
const handleEditProject = async (id: number) => {
|
||||
isEdit.value = true;
|
||||
await getProjectDetailData(id);
|
||||
showDrawer.value = true;
|
||||
};
|
||||
const handleSearch = () => {
|
||||
getProjectListData();
|
||||
};
|
||||
const getProjectTypeColor = (type: ProjectType) => {
|
||||
const typeMap = { 1: 'green', 2: 'red', 3: 'orange' };
|
||||
return typeMap[type];
|
||||
};
|
||||
onMounted(() => {
|
||||
setColumns(columns);
|
||||
getProjectListData();
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
// import { CreateCodeSettingParam } from '@/types/setting'
|
||||
import draggable from 'vuedraggable';
|
||||
|
||||
const isLoading = ref<boolean>(false);
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
isEdit: boolean;
|
||||
@@ -240,7 +241,9 @@
|
||||
form.arrange = JSON.parse(JSON.stringify(arrangeList.value));
|
||||
};
|
||||
const handleOk = async () => {
|
||||
if (isLoading.value) return;
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const valid = await formRef.value.validate();
|
||||
if (valid) return;
|
||||
// 同步编号排序
|
||||
@@ -265,6 +268,8 @@
|
||||
}
|
||||
} catch (err) {
|
||||
// 捕获异常,暂不处理
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
(e: 'submitted'): void;
|
||||
}>();
|
||||
|
||||
const isLoading = ref<boolean>(false);
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const defaultForm: CreateTermSettingParam = {
|
||||
@@ -182,7 +182,9 @@
|
||||
};
|
||||
|
||||
const handleOk = async () => {
|
||||
if (isLoading.value) return;
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const valid = await formRef.value.validate();
|
||||
if (valid) return;
|
||||
const { is_bond, is_fee, finance_bond_fee_id, finance_fee_id } = form;
|
||||
@@ -208,6 +210,8 @@
|
||||
}
|
||||
} catch (err) {
|
||||
// 捕获异常,暂不处理
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
import { CreateFeeParams } from '@/types/setting';
|
||||
import { feeTypeList } from '@/utils/constant';
|
||||
|
||||
const isLoading = ref<boolean>(false);
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
isEdit: boolean;
|
||||
@@ -132,7 +133,9 @@
|
||||
{ immediate: true }
|
||||
);
|
||||
const handleOk = async () => {
|
||||
if (isLoading.value) return;
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const valid = await formRef.value.validate();
|
||||
if (valid) return;
|
||||
const res = props?.isEdit ? await updateFee(form) : await createFee(form);
|
||||
@@ -143,6 +146,8 @@
|
||||
}
|
||||
} catch (err) {
|
||||
// 捕获异常,暂不处理
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
import { updateUserRole } from '@/api/setting';
|
||||
import { UpdateUserRoleParam, RoleItem } from '@/types/setting';
|
||||
|
||||
const isLoading = ref<boolean>(false);
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
roleId: number | null;
|
||||
@@ -76,7 +77,9 @@
|
||||
{ immediate: true }
|
||||
);
|
||||
const handleOk = async () => {
|
||||
if (isLoading.value) return;
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const valid = await formRef.value.validate();
|
||||
if (valid) return;
|
||||
const params = { ...form };
|
||||
@@ -88,6 +91,8 @@
|
||||
}
|
||||
} catch (error) {
|
||||
// 捕获异常,暂不处理
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
import { updateRole, createRole } from '@/api/setting';
|
||||
import { RoleFormParam, RoleItem } from '@/types/setting';
|
||||
|
||||
const isLoading = ref<boolean>(false);
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
isEdit: boolean;
|
||||
@@ -92,7 +93,9 @@
|
||||
{ immediate: true }
|
||||
);
|
||||
const handleOk = async () => {
|
||||
if (isLoading.value) return;
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const valid = await formRef.value.validate();
|
||||
if (valid) return;
|
||||
const params = { ...form };
|
||||
@@ -106,6 +109,8 @@
|
||||
}
|
||||
} catch (err) {
|
||||
// 捕获异常,暂不处理
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
import { createUser, updateUser } from '@/api/setting';
|
||||
import { DepartmentItem, RoleItem, FormUserItem } from '@/types/setting';
|
||||
|
||||
const isLoading = ref<boolean>(false);
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
isEdit: boolean;
|
||||
@@ -242,7 +243,9 @@
|
||||
{ immediate: true }
|
||||
);
|
||||
const handleOk = async () => {
|
||||
if (isLoading.value) return;
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const valid = await formRef.value.validate();
|
||||
if (valid) return;
|
||||
const params = { ...form };
|
||||
@@ -255,6 +258,8 @@
|
||||
emit('update:visible', false);
|
||||
} catch (err) {
|
||||
// 处理错误
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user