chore(config): 添加项目配置文件和隐私协议

- 添加 .editorconfig 文件统一代码风格
- 添加 .env.development 和 .env.example 环境变量配置
- 添加 .eslintignore 和 .eslintrc.js 代码检查配置
- 添加 .gitignore 版本控制忽略文件配置
- 添加 .prettierignore 代码格式化忽略配置
- 添加隐私协议 HTML 文件
- 添加 accesskey 编辑组件基础结构
This commit is contained in:
2026-01-03 15:37:56 +08:00
commit 50d4497639
1146 changed files with 220891 additions and 0 deletions

View File

@@ -0,0 +1,772 @@
<!-- User edit弹窗 -->
<template>
<ele-modal
width="75%"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '修改房源' : '添加房源'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<div style="background-color: #f3f3f3; padding: 8px">
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ md: { span: 7 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 17 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-card title="基本信息" :bordered="false">
<template #extra
><a-button type="link" @click="handleEditStatus">{{
editStatus ? '预览' : '编辑'
}}</a-button></template
>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="房源ID" name="houseId" v-if="isUpdate" :autoLink="false">
<span>{{ form.houseId }}</span>
</a-form-item>
<a-form-item label="项目名称" name="houseTitle">
<a-input
v-if="editStatus"
allow-clear
placeholder="请输入标题"
v-model:value="form.houseTitle"
/>
<span v-else>{{ form.houseTitle }}</span>
</a-form-item>
<a-form-item label="业主电话" name="phone">
<a-input
v-if="editStatus"
allow-clear
placeholder="请输入业主电话"
v-model:value="form.phone"
/>
<span v-else>{{ form.phone }}</span>
</a-form-item>
<a-form-item label="面积(莱)" name="extent">
<a-input
v-if="editStatus"
allow-clear
placeholder="请输入该房屋面积"
v-model:value="form.extent"
/>
<span v-else>{{ form.extent }}</span>
</a-form-item>
<a-form-item label="单价(莱)" name="rent">
<a-input
v-if="editStatus"
allow-clear
placeholder="请输入单价"
v-model:value="form.rent"
/>
<span v-else>{{ form.rent }}</span>
</a-form-item>
<a-form-item label="总价" name="monthlyRent">
<a-input
v-if="editStatus"
allow-clear
placeholder="请输入总价"
v-model:value="form.monthlyRent"
/>
<span v-else>{{ form.monthlyRent }}</span>
</a-form-item>
<!-- <a-form-item label="房号" name="roomNumber">-->
<!-- <a-input-->
<!-- v-if="editStatus"-->
<!-- allow-clear-->
<!-- placeholder="请输入房号"-->
<!-- v-model:value="form.roomNumber"-->
<!-- />-->
<!-- <span v-else>{{ form.roomNumber }}</span>-->
<!-- </a-form-item>-->
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<!-- <a-form-item label="楼层" name="floor">-->
<!-- <DictSelect-->
<!-- v-if="editStatus"-->
<!-- dict-code="floor"-->
<!-- v-model:value="form.floor"-->
<!-- placeholder="请选择楼层"-->
<!-- />-->
<!-- <span v-else>{{ form.floor }}</span>-->
<!-- </a-form-item>-->
<!-- <a-form-item label="租赁方式" name="leaseMethod">-->
<!-- <DictSelect-->
<!-- v-if="editStatus"-->
<!-- dict-code="leaseMethod"-->
<!-- v-model:value="form.leaseMethod"-->
<!-- placeholder="请选择租赁方式"-->
<!-- />-->
<!-- <span v-else>{{ form.leaseMethod }}</span>-->
<!-- </a-form-item>-->
<!-- <a-form-item label="房源朝向" name="toward">-->
<!-- <DictSelect-->
<!-- v-if="editStatus"-->
<!-- dict-code="toward"-->
<!-- v-model:value="form.toward"-->
<!-- placeholder="请选择房源朝向"-->
<!-- />-->
<!-- <span v-else>{{ form.toward }}</span>-->
<!-- </a-form-item>-->
<a-form-item label="是否可溢价" name="premium">
<DictSelect
v-if="editStatus"
dict-code="premium"
v-model:value="form.premium"
placeholder="是否可溢价"
/>
<span v-else>{{ form.premium }}</span>
</a-form-item>
<!-- <a-form-item label="租期" name="tenancy">-->
<!-- <DictSelect-->
<!-- v-if="editStatus"-->
<!-- dict-code="tenancy"-->
<!-- v-model:value="form.tenancy"-->
<!-- placeholder="租期"-->
<!-- />-->
<!-- <span v-else>{{ form.tenancy }}</span>-->
<!-- </a-form-item>-->
<a-form-item label="产权信息" name="property">
<a-input
v-if="editStatus"
allow-clear
placeholder="请输入产权信息"
v-model:value="form.property"
/>
<span v-else>{{ form.property }}</span>
</a-form-item>
<a-form-item label="房产经纪人" name="nickname">
<SelectUser
:placeholder="`请选择发布人`"
v-model:value="form.nickname"
v-if="editStatus"
@done="chooseUserId"
/>
<span v-else>{{ form.nickname }}</span>
</a-form-item>
<a-form-item label="备注">
<a-textarea
v-if="editStatus"
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
<div v-else>{{ form.comments }}</div>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<!-- <a-form-item label="所在地区" name="region">-->
<!-- <a-input-group compact v-if="editStatus">-->
<!-- <a-input-->
<!-- disabled-->
<!-- style="width: calc(100% - 32px)"-->
<!-- v-model:value="form.region"-->
<!-- placeholder="所属区域"-->
<!-- />-->
<!-- <a-tooltip title="选择位置">-->
<!-- <a-button @click="openMapPicker">-->
<!-- <template #icon><EnvironmentOutlined /></template>-->
<!-- </a-button>-->
<!-- </a-tooltip>-->
<!-- </a-input-group>-->
<!-- <span v-else>{{ form.region }}</span>-->
<!-- </a-form-item>-->
<a-form-item label="所在省份" name="province">
<a-input
v-if="editStatus"
allow-clear
placeholder="请输入所在省份"
v-model:value="form.province"
/>
<span v-else>{{ form.province }}</span>
</a-form-item>
<a-form-item label="所在城市" name="city">
<a-input
v-if="editStatus"
allow-clear
placeholder="请输入所在城市"
v-model:value="form.city"
/>
<span v-else>{{ form.city }}</span>
</a-form-item>
<a-form-item label="详细地址" name="address">
<a-input
v-if="editStatus"
allow-clear
placeholder="请输入详细地址"
v-model:value="form.address"
/>
<span v-else>{{ form.address }}</span>
</a-form-item>
<!-- <a-form-item label="经度" name="longitude">-->
<!-- <a-input-->
<!-- v-if="editStatus"-->
<!-- allow-clear-->
<!-- placeholder="请输入经度"-->
<!-- v-model:value="form.longitude"-->
<!-- />-->
<!-- <span v-else>{{ form.longitude }}</span>-->
<!-- </a-form-item>-->
<!-- <a-form-item label="纬度" name="latitude">-->
<!-- <a-input-->
<!-- v-if="editStatus"-->
<!-- allow-clear-->
<!-- placeholder="请输入纬度"-->
<!-- v-model:value="form.latitude"-->
<!-- />-->
<!-- <span v-else>{{ form.latitude }}</span>-->
<!-- </a-form-item>-->
</a-col>
</a-row>
</a-card>
<a-divider style="height: 8px" />
<a-card title="房源相册" :bordered="false">
<!-- <template #extra><a-button type="link" @click="handleEditStatus">上传</a-button></template>-->
<div class="content">
<ele-image-upload
v-model:value="files"
:limit="9"
:drag="true"
:item-style="{ width: '150px', height: '113px' }"
:upload-handler="uploadHandlerImages"
@upload="onUploadImages"
/>
<small class="ele-text-placeholder">
请上传应用截图(最多9张)建议宽度800*600像素小于20M/
</small>
</div>
</a-card>
<a-divider style="height: 8px" />
<a-card title="房源视频" :bordered="false">
<!-- <template #extra><a-button type="link" @click="handleEditStatus">上传</a-button></template>-->
<div class="content">
<UploadFile
accept="video/*"
v-model:value="form.videoUrl"
:maxCount="1"
:drag="true"
:item-style="{ width: '150px', height: '113px' }"
:showUploadList="true"
/>
<small class="ele-text-placeholder">
</small>
</div>
</a-card>
<a-divider style="height: 8px" />
<a-card title="房源标签" :bordered="false">
<DictSelectMultiple
v-if="editStatus"
dict-code="houseLabel"
v-model:value="houseLabelData"
placeholder="房源标签"
/>
<span v-else>
<a-space>
<a-tag v-for="tag in houseLabelData">{{ tag }}</a-tag>
</a-space>
</span>
</a-card>
<a-card title="办公室配套" :bordered="false">
<a-textarea
v-if="editStatus"
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.supporting"
/>
<div v-else>{{ form.supporting }}</div>
</a-card>
<a-divider style="height: 8px" />
<a-card title="详细介绍" :bordered="false">
<template #extra
><a-button type="link" @click="handleEditStatus">{{
editStatus ? '预览' : '编辑'
}}</a-button></template
>
<!-- 编辑器 -->
<tinymce-editor
v-if="editStatus"
v-model:value="content"
:init="config"
placeholder="图片直接粘贴自动上传"
@paste="onPaste"
/>
<!-- 预览 -->
<tinymce-editor
v-else
v-model:value="content"
:init="viewConfig"
/>
</a-card>
</a-form>
<!-- 地图位置选择弹窗 -->
<ele-map-picker
:need-city="true"
:dark-mode="darkMode"
v-model:visible="showMap"
:center="[108.374959, 22.767024]"
:search-type="1"
:zoom="12"
@done="onDone"
/>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref, reactive, watch, computed} from 'vue';
import { message } from 'ant-design-vue';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { FormInstance } from 'ant-design-vue/es/form';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { HouseInfo } from '@/api/house/houseInfo/model';
import {uploadFile, uploadOss} from '@/api/system/file';
import { CenterPoint } from 'ele-admin-pro/es/ele-map-picker/types';
import useFormData from '@/utils/use-form-data';
import { addHouseInfo, updateHouseInfo } from '@/api/house/houseInfo';
import { User } from '@/api/system/user/model';
import TinymceEditor from "@/components/TinymceEditor/index.vue";
// 是否是修改
const isUpdate = ref(false);
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: HouseInfo | null;
// 类型
type?: number;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 用户头像
const avatar = ref<ItemType[]>([]);
// 已上传数据
const files = ref<ItemType[]>([]);
// 已上传数据
const files2 = ref<ItemType[]>([]);
// 是否显示地图选择弹窗
const showMap = ref(false);
// 省市区
const city = ref<string[]>([]);
const { darkMode } = storeToRefs(themeStore);
const editStatus = ref<boolean>(false);
const formRef = ref<FormInstance | null>(null);
const uploadImgContent = ref<string>('');
const content = ref('');
// 表单数据
const { form, resetFields, assignFields } = useFormData<HouseInfo>({
houseId: undefined,
userId: undefined,
houseTitle: undefined,
cityByHouse: undefined,
houseType: undefined,
leaseMethod: undefined,
rent: undefined,
monthlyRent: undefined,
extent: undefined,
floor: undefined,
roomNumber: undefined,
realName: undefined,
nickname: undefined,
houseLabel: undefined,
address: undefined,
longitude: undefined,
latitude: undefined,
phone: undefined,
password: undefined,
toward: undefined,
files: undefined,
videoUrl: undefined,
content: undefined,
expirationTime: undefined,
province: undefined,
city: undefined,
region: undefined,
area: undefined,
status: undefined,
comments: '',
sortNumber: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
isEdit: undefined,
commission: undefined,
premium: undefined,
propertyFees: undefined,
tenancy: undefined,
supporting: undefined,
property: undefined
});
const monthlyRent = computed<number>(() => {
const {extent, rent} = form
if(extent && rent) {
return extent * rent
}else {
return 0
}
})
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
houseTitle: [
{
required: true,
type: 'string',
message: '请输入房源标题',
trigger: 'blur'
}
],
// phone: [
// {
// required: true,
// type: 'string',
// message: '请输入合法手机号码',
// trigger: 'blur'
// }
// ],
nickname: [
{
required: true,
type: 'string',
message: '请输入昵称',
trigger: 'blur'
}
],
roles: [
{
required: true,
message: '请选择角色',
type: 'array',
trigger: 'blur'
}
]
// securityStatus: [
// {
// required: true,
// type: 'string',
// message: '请选择安全状态',
// trigger: 'blur'
// }
// ],
// companyName: [
// {
// required: true,
// type: 'string',
// message: '请选择租赁单位',
// trigger: 'blur'
// }
// ],
// customerName: [
// {
// required: true,
// type: 'string',
// message: '请选择承租单位',
// trigger: 'blur'
// }
// ],
// projectRegion: [
// {
// required: true,
// type: 'string',
// message: '请输入房源地址',
// trigger: 'blur'
// }
// ]
});
let houseLabelData = ref<any[]>([])
const initHouseLabel = ()=> {
if(form.houseLabel) {
houseLabelData.value = JSON.parse(form.houseLabel)
}
}
const handleEditStatus = () => {
editStatus.value = !editStatus.value;
};
const chooseUserId = (data: User) => {
console.log(data.nickname,'data......nickname')
form.nickname = data.nickname;
form.userId = data.userId;
};
/* 地图选择后回调 */
const onDone = (location: CenterPoint) => {
console.log(location);
city.value = [
`${location.city?.province}`,
`${location.city?.city}`,
`${location.city?.district}`
];
form.province = `${location.city?.province}`;
form.city = `${location.city?.city}`;
form.region = `${location.city?.district}`;
form.address = `${location.address}`;
form.latitude = `${location.lat}`;
form.longitude = `${location.lng}`;
showMap.value = false;
};
/* 打开位置选择 */
const openMapPicker = () => {
showMap.value = true;
};
/* 上传事件 */
const uploadHandlerImages = (file: File) => {
const item: ItemType = {
file,
uid: (file as any).uid,
name: file.name
};
if (!file.type.startsWith('image')) {
message.error('只能选择图片');
return;
}
if (file.size / 1024 / 1024 > 20) {
message.error('大小不能超过 2MB');
return;
}
onUploadImages(item);
};
// 上传文件
const onUploadImages = (item) => {
const { file } = item;
uploadOss(file)
.then((data) => {
files.value.push({
uid: data.id,
url: data.url,
status: 'done'
});
})
.catch((e) => {
message.error(e.message);
});
};
const onChange = (e) => {
uploadImgContent.value = e.originalEvent.value.content;
}
const editorRef = ref<InstanceType<typeof TinymceEditor> | null>(null);
const config = ref({
height: 500,
images_upload_handler: (blobInfo, success, error) => {
const file = blobInfo.blob();
// 使用 axios 上传,实际开发这段建议写在 api 中再调用 api
const formData = new FormData();
formData.append('file', file, file.name);
uploadFile(<File>file)
.then((result) => {
if (result.length) {
console.log(file.size / 1024);
if (file.size / 1024 / 1024 > 2) {
error('图片大小不能超过 2MB');
}
success(result.url);
} else {
error('上传失败');
}
})
.catch((e) => {
message.error(e.message);
});
},
});
const viewConfig = ref({
toolbar: false,
menubar: false,
height: 620,
darkTheme: true,
inline: true
// quickbars_insert_toolbar: false
});
/* 粘贴图片上传服务器并插入编辑器 */
const onPaste = (e) => {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
let hasFile = false;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
let file = items[i].getAsFile();
const item: ItemType = {
file,
uid: (file as any).lastModified,
name: file.name
};
uploadFile(<File>item.file)
.then((result) => {
const addPath = `<img class="content-img" src="${result.url}">\n\r`;
content.value = content.value.replace(uploadImgContent.value,addPath)
})
.catch((e) => {
message.error(e.message);
});
hasFile = true;
}
}
if (hasFile) {
e.preventDefault();
}
}
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form,
content: content.value,
files: JSON.stringify(files.value),
houseLabel: JSON.stringify(houseLabelData.value),
monthlyRent: monthlyRent.value,
type: props.type
};
const saveOrUpdate = isUpdate.value ? updateHouseInfo : addHouseInfo;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
const reload = () => {
loading.value = true;
};
reload();
watch(
() => props.visible,
(visible) => {
if (visible) {
content.value = '';
if (props.data) {
assignFields({
...props.data
});
files.value = [];
avatar.value = [];
city.value = [
`${props.data.province}`,
`${props.data.city}`,
`${props.data.region}`
];
if (props.data.content) {
content.value = props.data.content;
}
if (props.data.files) {
const arr = JSON.parse(props.data.files);
// 检查 arr 是否为数组
if (Array.isArray(arr)) {
arr.forEach((d, i) => {
files.value.push({
uid: d.uid,
url: d.url,
status: 'done'
});
});
} else {
console.error('props.data.files 解析后不是一个数组:', arr);
}
}
reload();
isUpdate.value = true;
} else {
avatar.value = [];
files.value = []
files2.value = []
houseLabelData.value = []
editStatus.value = true;
isUpdate.value = false;
}
initHouseLabel()
} else {
files.value = []
files2.value = []
resetFields();
}
}
);
</script>
<style lang="less">
.tab-pane {
min-height: 300px;
}
.ml-10 {
margin-left: 5px;
}
.upload-text {
margin-right: 70px;
}
.upload-image {
margin-bottom: 30px;
display: flex;
justify-content: center;
text-align: center;
}
</style>