Commit 88f005b57b8d4411eb1c45fbe5b60192264bd187

Authored by wuxw
1 parent 15981f1e

业主页面开发完成

src/api/owner/ownerApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +import { getCommunityId } from '@/api/community/communityApi'
  3 +
  4 +// 查询业主列表
  5 +export function queryOwners(params) {
  6 + return new Promise((resolve, reject) => {
  7 + params.communityId = getCommunityId()
  8 + request({
  9 + url: '/owner.queryOwners',
  10 + method: 'get',
  11 + params
  12 + }).then(response => {
  13 + const res = response.data
  14 + if (res.code == 0) {
  15 + resolve(res)
  16 + } else {
  17 + reject(new Error(res.msg || '查询业主列表失败'))
  18 + }
  19 + }).catch(error => {
  20 + reject(error)
  21 + })
  22 + })
  23 +}
  24 +
  25 +// 保存业主
  26 +export function saveOwner(data) {
  27 + return new Promise((resolve, reject) => {
  28 + data.communityId = getCommunityId()
  29 + request({
  30 + url: '/owner.saveOwner',
  31 + method: 'post',
  32 + data
  33 + }).then(response => {
  34 + const res = response.data
  35 + if (res.code == 0) {
  36 + resolve(res)
  37 + } else {
  38 + reject(new Error(res.msg || '保存业主失败'))
  39 + }
  40 + }).catch(error => {
  41 + reject(error)
  42 + })
  43 + })
  44 +}
  45 +
  46 +// 编辑业主
  47 +export function editOwner(data) {
  48 + return new Promise((resolve, reject) => {
  49 + data.communityId = getCommunityId()
  50 + request({
  51 + url: '/owner.editOwner',
  52 + method: 'post',
  53 + data
  54 + }).then(response => {
  55 + const res = response.data
  56 + if (res.code == 0) {
  57 + resolve(res)
  58 + } else {
  59 + reject(new Error(res.msg || '编辑业主失败'))
  60 + }
  61 + }).catch(error => {
  62 + reject(error)
  63 + })
  64 + })
  65 +}
  66 +
  67 +// 删除业主
  68 +export function deleteOwner(data) {
  69 + return new Promise((resolve, reject) => {
  70 + data.communityId = getCommunityId()
  71 + request({
  72 + url: '/owner.deleteOwner',
  73 + method: 'post',
  74 + data
  75 + }).then(response => {
  76 + const res = response.data
  77 + if (res.code == 0) {
  78 + resolve(res)
  79 + } else {
  80 + reject(new Error(res.msg || '删除业主失败'))
  81 + }
  82 + }).catch(error => {
  83 + reject(error)
  84 + })
  85 + })
  86 +}
  87 +
  88 +// 上传图片
  89 +export function uploadImage(data) {
  90 + return new Promise((resolve, reject) => {
  91 + const formData = new FormData()
  92 + formData.append('uploadFile', data.file)
  93 + formData.append('communityId', getCommunityId())
  94 +
  95 + request({
  96 + url: 'uploadImage',
  97 + method: 'post',
  98 + data: formData,
  99 + headers: {
  100 + 'Content-Type': 'multipart/form-data'
  101 + }
  102 + }).then(response => {
  103 + const res = response.data
  104 + if (res.code == 0) {
  105 + resolve(res)
  106 + } else {
  107 + reject(new Error(res.msg || '上传图片失败'))
  108 + }
  109 + }).catch(error => {
  110 + reject(error)
  111 + })
  112 + })
  113 +}
  114 +
  115 +// 获取属性规格
  116 +export function getAttrSpec(specCd) {
  117 + return new Promise((resolve, reject) => {
  118 + request({
  119 + url: '/attrSpec.listAttrSpecs',
  120 + method: 'get',
  121 + params: { specCd, communityId: getCommunityId() }
  122 + }).then(response => {
  123 + const res = response.data
  124 + if (res.code == 0) {
  125 + resolve(res.data)
  126 + } else {
  127 + reject(new Error(res.msg || '获取属性规格失败'))
  128 + }
  129 + }).catch(error => {
  130 + reject(error)
  131 + })
  132 + })
  133 +}
  134 +
  135 +// 获取属性值
  136 +export function getAttrValue(specCd) {
  137 + return new Promise((resolve, reject) => {
  138 + request({
  139 + url: '/attrValue.listAttrValues',
  140 + method: 'get',
  141 + params: { specCd, communityId: getCommunityId() }
  142 + }).then(response => {
  143 + const res = response.data
  144 + if (res.code == 0) {
  145 + resolve(res.data)
  146 + } else {
  147 + reject(new Error(res.msg || '获取属性值失败'))
  148 + }
  149 + }).catch(error => {
  150 + reject(error)
  151 + })
  152 + })
  153 +}
0 154 \ No newline at end of file
... ...
src/components/owner/addOwner.vue 0 → 100644
  1 +<template>
  2 + <el-dialog :title="$t('listOwner.buttons.add')" :visible.sync="visible" width="800px" @close="resetForm">
  3 + <el-form ref="form" :model="form" :rules="rules" label-width="120px">
  4 + <el-row :gutter="20">
  5 + <el-col :span="12">
  6 + <el-form-item :label="$t('listOwner.columns.type')" prop="personType">
  7 + <el-select v-model="form.personType" style="width:100%">
  8 + <el-option v-for="(type, index) in personTypes" :key="index" :label="type.label" :value="type.value" />
  9 + </el-select>
  10 + </el-form-item>
  11 +
  12 + <el-form-item :label="$t('listOwner.columns.role')" prop="personRole">
  13 + <el-select v-model="form.personRole" style="width:100%">
  14 + <el-option v-for="role in personRoles" :key="role.value" :label="role.label" :value="role.value" />
  15 + </el-select>
  16 + </el-form-item>
  17 +
  18 + <el-form-item :label="$t('listOwner.columns.name')" prop="name">
  19 + <el-input v-model="form.name" :placeholder="$t('listOwner.placeholders.name')" />
  20 + </el-form-item>
  21 +
  22 + <el-form-item v-if="form.personType === 'C'" :label="$t('listOwner.buttons.member')">
  23 + <el-input v-model="form.concactPerson" />
  24 + </el-form-item>
  25 +
  26 + <el-form-item :label="$t('listOwner.columns.phone')" prop="link">
  27 + <el-input v-model="form.link" :placeholder="$t('listOwner.placeholders.phone')" />
  28 + </el-form-item>
  29 +
  30 + <el-form-item v-if="form.personType === 'P'" :label="$t('listOwner.columns.gender')" prop="sex">
  31 + <el-select v-model="form.sex" style="width:100%">
  32 + <el-option v-for="gender in genders" :key="gender.value" :label="gender.label" :value="gender.value" />
  33 + </el-select>
  34 + </el-form-item>
  35 + </el-col>
  36 +
  37 + <el-col :span="12" class="text-center">
  38 + <el-image style="width: 200px; height: 150px; border: 1px dashed #ccc;"
  39 + :src="form.ownerPhotoUrl || '/img/noPhoto.jpg'" fit="cover" />
  40 + <el-upload class="mt-10" :show-file-list="false" :before-upload="beforeUpload" :http-request="uploadImage">
  41 + <el-button size="small" type="primary">
  42 + {{ $t('common.upload') }}
  43 + </el-button>
  44 + </el-upload>
  45 + </el-col>
  46 + </el-row>
  47 +
  48 + <el-row :gutter="20">
  49 + <el-col :span="12">
  50 + <el-form-item :label="$t('listOwner.columns.backupPhone')">
  51 + <el-input v-model="form.concactLink" />
  52 + </el-form-item>
  53 + </el-col>
  54 +
  55 + <el-col :span="12">
  56 + <el-form-item :label="$t('listOwner.columns.address')">
  57 + <el-input v-model="form.address" />
  58 + </el-form-item>
  59 + </el-col>
  60 + </el-row>
  61 +
  62 + <el-row :gutter="20">
  63 + <el-col :span="12">
  64 + <el-form-item v-if="form.personType === 'C'" :label="$t('listOwner.columns.idCard')">
  65 + <el-input v-model="form.idCard" />
  66 + </el-form-item>
  67 +
  68 + <el-form-item v-else :label="$t('listOwner.columns.idCard')">
  69 + <el-input v-model="form.idCard" />
  70 + </el-form-item>
  71 + </el-col>
  72 +
  73 + <el-col :span="12">
  74 + <el-form-item :label="$t('common.remark')">
  75 + <el-input v-model="form.remark" />
  76 + </el-form-item>
  77 + </el-col>
  78 + </el-row>
  79 +
  80 + <div v-for="(item, index) in attrs" :key="index">
  81 + <el-row :gutter="20" v-if="index % 2 === 0">
  82 + <el-col :span="12">
  83 + <el-form-item :label="item.specName">
  84 + <el-input v-if="item.specType === '2233'" v-model="item.value" :placeholder="item.specHoldplace" />
  85 + <el-select v-else-if="item.specType === '3344'" v-model="item.value" style="width:100%">
  86 + <el-option v-for="val in item.values" :key="val.value" :label="val.valueName" :value="val.value" />
  87 + </el-select>
  88 + </el-form-item>
  89 + </el-col>
  90 +
  91 + <el-col :span="12" v-if="index < attrs.length - 1">
  92 + <el-form-item :label="attrs[index + 1].specName">
  93 + <el-input v-if="attrs[index + 1].specType === '2233'" v-model="attrs[index + 1].value"
  94 + :placeholder="attrs[index + 1].specHoldplace" />
  95 + <el-select v-else-if="attrs[index + 1].specType === '3344'" v-model="attrs[index + 1].value"
  96 + style="width:100%">
  97 + <el-option v-for="val in attrs[index + 1].values" :key="val.value" :label="val.valueName"
  98 + :value="val.value" />
  99 + </el-select>
  100 + </el-form-item>
  101 + </el-col>
  102 + </el-row>
  103 + </div>
  104 + </el-form>
  105 +
  106 + <div slot="footer" class="dialog-footer">
  107 + <el-button @click="visible = false">
  108 + {{ $t('listOwner.buttons.cancel') }}
  109 + </el-button>
  110 + <el-button type="primary" @click="saveOwner">
  111 + {{ $t('listOwner.buttons.save') }}
  112 + </el-button>
  113 + </div>
  114 + </el-dialog>
  115 +</template>
  116 +
  117 +<script>
  118 +import { saveOwner, getAttrValue, uploadImage } from '@/api/owner/ownerApi'
  119 +import { getAttrSpecList } from '@/api/dev/attrSpecApi'
  120 +
  121 +export default {
  122 + name: 'AddOwner',
  123 + data() {
  124 + return {
  125 + visible: false,
  126 + form: {
  127 + name: '',
  128 + link: '',
  129 + address: '',
  130 + sex: '0',
  131 + remark: '',
  132 + ownerPhoto: '',
  133 + ownerPhotoUrl: '',
  134 + idCard: '',
  135 + personType: 'P',
  136 + personRole: '1',
  137 + concactPerson: '',
  138 + concactLink: ''
  139 + },
  140 + attrs: [],
  141 + personTypes: [
  142 + { value: 'P', label: this.$t('listOwner.personType.personal') },
  143 + { value: 'C', label: this.$t('listOwner.personType.company') }
  144 + ],
  145 + personRoles: [
  146 + { value: '1', label: this.$t('listOwner.role.owner') },
  147 + { value: '2', label: this.$t('listOwner.role.tenant') }
  148 + ],
  149 + genders: [
  150 + { value: '0', label: this.$t('listOwner.gender.male') },
  151 + { value: '1', label: this.$t('listOwner.gender.female') }
  152 + ],
  153 + rules: {
  154 + name: [
  155 + { required: true, message: this.$t('listOwner.rules.nameRequired'), trigger: 'blur' },
  156 + { min: 2, max: 64, message: this.$t('listOwner.rules.nameLength'), trigger: 'blur' }
  157 + ],
  158 + personType: [
  159 + { required: true, message: this.$t('listOwner.rules.typeRequired'), trigger: 'change' }
  160 + ],
  161 + personRole: [
  162 + { required: true, message: this.$t('listOwner.rules.roleRequired'), trigger: 'change' }
  163 + ],
  164 + link: [
  165 + { required: true, message: this.$t('listOwner.rules.phoneRequired'), trigger: 'blur' },
  166 + { pattern: /^1[3-9]\d{9}$/, message: this.$t('listOwner.rules.phoneFormat'), trigger: 'blur' }
  167 + ],
  168 + sex: [
  169 + { required: true, message: this.$t('listOwner.rules.genderRequired'), trigger: 'change' }
  170 + ]
  171 + }
  172 + }
  173 + },
  174 + methods: {
  175 + open() {
  176 + this.visible = true
  177 + this.loadAttributes()
  178 + },
  179 +
  180 + async loadAttributes() {
  181 + try {
  182 + const data = await getAttrSpecList({ page: 1, row: 100, tableName: 'building_owner_attr' })
  183 + this.attrs = data.filter(item => item.specShow === 'Y')
  184 +
  185 + for (const attr of this.attrs) {
  186 + if (attr.specType === '3344') {
  187 + attr.values = await getAttrValue(attr.specCd)
  188 + attr.value = ''
  189 + }
  190 + }
  191 + } catch (error) {
  192 + console.error('Failed to load attributes:', error)
  193 + }
  194 + },
  195 +
  196 + beforeUpload(file) {
  197 + const isJPG = file.type === 'image/jpeg'
  198 + const isLt2M = file.size / 1024 / 1024 < 2
  199 +
  200 + if (!isJPG) {
  201 + this.$message.error(this.$t('listOwner.upload.jpgOnly'))
  202 + }
  203 + if (!isLt2M) {
  204 + this.$message.error(this.$t('listOwner.upload.sizeLimit'))
  205 + }
  206 + return isJPG && isLt2M
  207 + },
  208 +
  209 + async uploadImage({ file }) {
  210 + try {
  211 + const res = await uploadImage({ file })
  212 + this.form.ownerPhoto = res.data.fileId
  213 + this.form.ownerPhotoUrl = res.data.url
  214 + } catch (error) {
  215 + this.$message.error(this.$t('listOwner.upload.failed'))
  216 + }
  217 + },
  218 +
  219 + async saveOwner() {
  220 + this.$refs.form.validate(async valid => {
  221 + if (!valid) return
  222 +
  223 + try {
  224 + const params = {
  225 + ...this.form,
  226 + attrs: this.attrs.map(attr => ({
  227 + specCd: attr.specCd,
  228 + value: attr.value
  229 + }))
  230 + }
  231 +
  232 + await saveOwner(params)
  233 + this.$message.success(this.$t('listOwner.saveSuccess'))
  234 + this.visible = false
  235 + this.$emit('success')
  236 + } catch (error) {
  237 + this.$message.error(this.$t('listOwner.saveFailed'))
  238 + }
  239 + })
  240 + },
  241 +
  242 + resetForm() {
  243 + this.$refs.form.resetFields()
  244 + this.form = {
  245 + name: '',
  246 + link: '',
  247 + address: '',
  248 + sex: '0',
  249 + remark: '',
  250 + ownerPhoto: '',
  251 + ownerPhotoUrl: '',
  252 + idCard: '',
  253 + personType: 'P',
  254 + personRole: '1',
  255 + concactPerson: '',
  256 + concactLink: ''
  257 + }
  258 + this.attrs.forEach(attr => {
  259 + attr.value = ''
  260 + })
  261 + }
  262 + }
  263 +}
  264 +</script>
  265 +
  266 +<style scoped>
  267 +.text-center {
  268 + text-align: center;
  269 +}
  270 +
  271 +.mt-10 {
  272 + margin-top: 10px;
  273 +}
  274 +</style>
0 275 \ No newline at end of file
... ...
src/components/owner/deleteOwner.vue 0 → 100644
  1 +<template>
  2 + <el-dialog :title="$t('listOwner.buttons.delete')" :visible.sync="visible" width="500px">
  3 + <p>{{ $t('listOwner.confirmDelete') }}</p>
  4 +
  5 + <div slot="footer" class="dialog-footer">
  6 + <el-button @click="visible = false">
  7 + {{ $t('listOwner.buttons.cancel') }}
  8 + </el-button>
  9 + <el-button type="primary" @click="confirmDelete">
  10 + {{ $t('common.confirm') }}
  11 + </el-button>
  12 + </div>
  13 + </el-dialog>
  14 +</template>
  15 +
  16 +<script>
  17 +import { deleteOwner } from '@/api/owner/ownerApi'
  18 +
  19 +export default {
  20 + name: 'DeleteOwner',
  21 + data() {
  22 + return {
  23 + visible: false,
  24 + owner: {}
  25 + }
  26 + },
  27 + methods: {
  28 + open(owner) {
  29 + this.owner = { ...owner }
  30 + this.visible = true
  31 + },
  32 +
  33 + async confirmDelete() {
  34 + try {
  35 + await deleteOwner({
  36 + ownerId: this.owner.ownerId
  37 + })
  38 + this.$message.success(this.$t('listOwner.deleteSuccess'))
  39 + this.visible = false
  40 + this.$emit('success')
  41 + } catch (error) {
  42 + this.$message.error(this.$t('listOwner.deleteFailed'))
  43 + }
  44 + }
  45 + }
  46 +}
  47 +</script>
0 48 \ No newline at end of file
... ...
src/components/owner/editOwner.vue 0 → 100644
  1 +<template>
  2 + <el-dialog :title="$t('listOwner.buttons.edit')" :visible.sync="visible" width="800px" @close="resetForm">
  3 + <el-form ref="form" :model="form" :rules="rules" label-width="120px">
  4 + <el-row :gutter="20">
  5 + <el-col :span="12">
  6 + <el-form-item :label="$t('listOwner.columns.type')" prop="personType">
  7 + <el-select v-model="form.personType" style="width:100%">
  8 + <el-option v-for="(type,index) in personTypes" :key="index" :label="type.label" :value="type.value" />
  9 + </el-select>
  10 + </el-form-item>
  11 +
  12 + <el-form-item :label="$t('listOwner.columns.role')" prop="personRole">
  13 + <el-select v-model="form.personRole" style="width:100%">
  14 + <el-option v-for="role in personRoles" :key="role.value" :label="role.label" :value="role.value" />
  15 + </el-select>
  16 + </el-form-item>
  17 +
  18 + <el-form-item :label="$t('listOwner.columns.name')" prop="name">
  19 + <el-input v-model="form.name" :placeholder="$t('listOwner.placeholders.name')" />
  20 + </el-form-item>
  21 +
  22 + <el-form-item v-if="form.personType === 'C'" :label="$t('listOwner.buttons.member')">
  23 + <el-input v-model="form.concactPerson" />
  24 + </el-form-item>
  25 +
  26 + <el-form-item :label="$t('listOwner.columns.phone')" prop="link">
  27 + <el-input v-model="form.link" :placeholder="$t('listOwner.placeholders.phone')" />
  28 + </el-form-item>
  29 +
  30 + <el-form-item v-if="form.personType === 'P'" :label="$t('listOwner.columns.gender')" prop="sex">
  31 + <el-select v-model="form.sex" style="width:100%">
  32 + <el-option v-for="gender in genders" :key="gender.value" :label="gender.label" :value="gender.value" />
  33 + </el-select>
  34 + </el-form-item>
  35 + </el-col>
  36 +
  37 + <el-col :span="12" class="text-center">
  38 + <el-image style="width: 200px; height: 150px; border: 1px dashed #ccc;"
  39 + :src="form.ownerPhotoUrl || '/img/noPhoto.jpg'" fit="cover" />
  40 + <el-upload class="mt-10" :show-file-list="false" :before-upload="beforeUpload" :http-request="uploadImage">
  41 + <el-button size="small" type="primary">
  42 + {{ $t('common.upload') }}
  43 + </el-button>
  44 + </el-upload>
  45 + </el-col>
  46 + </el-row>
  47 +
  48 + <el-row :gutter="20">
  49 + <el-col :span="12">
  50 + <el-form-item :label="$t('listOwner.columns.backupPhone')">
  51 + <el-input v-model="form.concactLink" />
  52 + </el-form-item>
  53 + </el-col>
  54 +
  55 + <el-col :span="12">
  56 + <el-form-item :label="$t('listOwner.columns.address')">
  57 + <el-input v-model="form.address" />
  58 + </el-form-item>
  59 + </el-col>
  60 + </el-row>
  61 +
  62 + <el-row :gutter="20">
  63 + <el-col :span="12">
  64 + <el-form-item v-if="form.personType === 'C'" :label="$t('listOwner.columns.idCard')">
  65 + <el-input v-model="form.idCard" />
  66 + </el-form-item>
  67 +
  68 + <el-form-item v-else :label="$t('listOwner.columns.idCard')">
  69 + <el-input v-model="form.idCard" />
  70 + </el-form-item>
  71 + </el-col>
  72 +
  73 + <el-col :span="12">
  74 + <el-form-item :label="$t('common.remark')">
  75 + <el-input v-model="form.remark" />
  76 + </el-form-item>
  77 + </el-col>
  78 + </el-row>
  79 +
  80 + <div v-for="(item, index) in attrs" :key="index">
  81 + <el-row :gutter="20" v-if="index % 2 === 0">
  82 + <el-col :span="12">
  83 + <el-form-item :label="item.specName">
  84 + <el-input v-if="item.specType === '2233'" v-model="item.value" :placeholder="item.specHoldplace" />
  85 + <el-select v-else-if="item.specType === '3344'" v-model="item.value" style="width:100%">
  86 + <el-option v-for="val in item.values" :key="val.value" :label="val.valueName" :value="val.value" />
  87 + </el-select>
  88 + </el-form-item>
  89 + </el-col>
  90 +
  91 + <el-col :span="12" v-if="index < attrs.length - 1">
  92 + <el-form-item :label="attrs[index + 1].specName">
  93 + <el-input v-if="attrs[index + 1].specType === '2233'" v-model="attrs[index + 1].value"
  94 + :placeholder="attrs[index + 1].specHoldplace" />
  95 + <el-select v-else-if="attrs[index + 1].specType === '3344'" v-model="attrs[index + 1].value"
  96 + style="width:100%">
  97 + <el-option v-for="val in attrs[index + 1].values" :key="val.value" :label="val.valueName"
  98 + :value="val.value" />
  99 + </el-select>
  100 + </el-form-item>
  101 + </el-col>
  102 + </el-row>
  103 + </div>
  104 + </el-form>
  105 +
  106 + <div slot="footer" class="dialog-footer">
  107 + <el-button @click="visible = false">
  108 + {{ $t('listOwner.buttons.cancel') }}
  109 + </el-button>
  110 + <el-button type="primary" @click="updateOwner">
  111 + {{ $t('listOwner.buttons.save') }}
  112 + </el-button>
  113 + </div>
  114 + </el-dialog>
  115 +</template>
  116 +
  117 +<script>
  118 +import { editOwner, getAttrValue, uploadImage } from '@/api/owner/ownerApi'
  119 +import { getAttrSpecList } from '@/api/dev/attrSpecApi'
  120 +
  121 +export default {
  122 + name: 'EditOwner',
  123 + data() {
  124 + return {
  125 + visible: false,
  126 + form: {
  127 + ownerId: '',
  128 + name: '',
  129 + link: '',
  130 + address: '',
  131 + sex: '',
  132 + remark: '',
  133 + ownerPhoto: '',
  134 + ownerPhotoUrl: '',
  135 + idCard: '',
  136 + personType: 'P',
  137 + personRole: '1',
  138 + concactPerson: '',
  139 + concactLink: ''
  140 + },
  141 + attrs: [],
  142 + personTypes: [
  143 + { value: 'P', label: this.$t('listOwner.personType.personal') },
  144 + { value: 'C', label: this.$t('listOwner.personType.company') }
  145 + ],
  146 + personRoles: [
  147 + { value: '1', label: this.$t('listOwner.role.owner') },
  148 + { value: '2', label: this.$t('listOwner.role.tenant') }
  149 + ],
  150 + genders: [
  151 + { value: '0', label: this.$t('listOwner.gender.male') },
  152 + { value: '1', label: this.$t('listOwner.gender.female') }
  153 + ],
  154 + rules: {
  155 + name: [
  156 + { required: true, message: this.$t('listOwner.rules.nameRequired'), trigger: 'blur' },
  157 + { min: 2, max: 64, message: this.$t('listOwner.rules.nameLength'), trigger: 'blur' }
  158 + ],
  159 + personType: [
  160 + { required: true, message: this.$t('listOwner.rules.typeRequired'), trigger: 'change' }
  161 + ],
  162 + personRole: [
  163 + { required: true, message: this.$t('listOwner.rules.roleRequired'), trigger: 'change' }
  164 + ],
  165 + link: [
  166 + { required: true, message: this.$t('listOwner.rules.phoneRequired'), trigger: 'blur' },
  167 + { pattern: /^1[3-9]\d{9}$/, message: this.$t('listOwner.rules.phoneFormat'), trigger: 'blur' }
  168 + ],
  169 + sex: [
  170 + { required: true, message: this.$t('listOwner.rules.genderRequired'), trigger: 'change' }
  171 + ]
  172 + }
  173 + }
  174 + },
  175 + methods: {
  176 + open(owner) {
  177 + this.form = { ...owner }
  178 + this.visible = true
  179 + this.loadAttributes()
  180 + },
  181 +
  182 + async loadAttributes() {
  183 + try {
  184 + const data = await getAttrSpecList({ page: 1, row: 100, tableName: 'building_owner_attr'})
  185 + this.attrs = data.filter(item => item.specShow === 'Y')
  186 +
  187 + for (const attr of this.attrs) {
  188 + if (attr.specType === '3344') {
  189 + attr.values = await getAttrValue(attr.specCd)
  190 + }
  191 +
  192 + // 设置已有属性值
  193 + if (this.form.ownerAttrDtos) {
  194 + const attrDto = this.form.ownerAttrDtos.find(dto => dto.specCd === attr.specCd)
  195 + attr.value = attrDto ? attrDto.value : ''
  196 + }
  197 + }
  198 + } catch (error) {
  199 + console.error('Failed to load attributes:', error)
  200 + }
  201 + },
  202 +
  203 + beforeUpload(file) {
  204 + const isJPG = file.type === 'image/jpeg'
  205 + const isLt2M = file.size / 1024 / 1024 < 2
  206 +
  207 + if (!isJPG) {
  208 + this.$message.error(this.$t('listOwner.upload.jpgOnly'))
  209 + }
  210 + if (!isLt2M) {
  211 + this.$message.error(this.$t('listOwner.upload.sizeLimit'))
  212 + }
  213 + return isJPG && isLt2M
  214 + },
  215 +
  216 + async uploadImage({ file }) {
  217 + try {
  218 + const res = await uploadImage({ file })
  219 + this.form.ownerPhoto = res.data.fileId
  220 + this.form.ownerPhotoUrl = res.data.url
  221 + } catch (error) {
  222 + this.$message.error(this.$t('listOwner.upload.failed'))
  223 + }
  224 + },
  225 +
  226 + async updateOwner() {
  227 + this.$refs.form.validate(async valid => {
  228 + if (!valid) return
  229 +
  230 + try {
  231 + const params = {
  232 + ...this.form,
  233 + attrs: this.attrs.map(attr => ({
  234 + specCd: attr.specCd,
  235 + value: attr.value
  236 + }))
  237 + }
  238 +
  239 + await editOwner(params)
  240 + this.$message.success(this.$t('listOwner.updateSuccess'))
  241 + this.visible = false
  242 + this.$emit('success')
  243 + } catch (error) {
  244 + this.$message.error(this.$t('listOwner.updateFailed'))
  245 + }
  246 + })
  247 + },
  248 +
  249 + resetForm() {
  250 + this.$refs.form.resetFields()
  251 + this.form = {
  252 + ownerId: '',
  253 + name: '',
  254 + link: '',
  255 + address: '',
  256 + sex: '',
  257 + remark: '',
  258 + ownerPhoto: '',
  259 + ownerPhotoUrl: '',
  260 + idCard: '',
  261 + personType: 'P',
  262 + personRole: '1',
  263 + concactPerson: '',
  264 + concactLink: ''
  265 + }
  266 + this.attrs = []
  267 + }
  268 + }
  269 +}
  270 +</script>
  271 +
  272 +<style scoped>
  273 +.text-center {
  274 + text-align: center;
  275 +}
  276 +
  277 +.mt-10 {
  278 + margin-top: 10px;
  279 +}
  280 +</style>
0 281 \ No newline at end of file
... ...
src/components/room/searchRoom.vue 0 → 100644
  1 +<template>
  2 + <el-dialog :title="$t('searchRoom.title')" :visible.sync="dialogVisible" width="70%" @close="handleClose">
  3 + <div class="ibox">
  4 + <el-row v-if="showSearchCondition">
  5 + <el-col :span="8">
  6 + <el-input :placeholder="$t('searchRoom.floorNumPlaceholder')" :readonly="floorNumInputReadonly"
  7 + v-model.trim="currentFloorNum" clearable />
  8 + </el-col>
  9 + <el-col :span="8" :offset="1">
  10 + <el-input :placeholder="$t('searchRoom.roomNumPlaceholder')" v-model.trim="currentRoomNum" clearable />
  11 + </el-col>
  12 + <el-col :span="6" :offset="1">
  13 + <el-button type="primary" @click="searchRooms">
  14 + <i class="el-icon-search"></i>
  15 + {{ $t('searchRoom.search') }}
  16 + </el-button>
  17 + <el-button type="primary" @click="resetRooms">
  18 + <i class="el-icon-refresh"></i>
  19 + {{ $t('searchRoom.reset') }}
  20 + </el-button>
  21 + </el-col>
  22 + </el-row>
  23 +
  24 + <el-table :data="rooms" style="width: 100%; margin-top: 15px" border stripe>
  25 + <el-table-column prop="roomId" :label="$t('searchRoom.roomId')" align="center" />
  26 + <el-table-column prop="floorNum" :label="$t('searchRoom.floorNum')" align="center">
  27 + <template #default="{ row }">
  28 + {{ row.floorNum }}{{ $t('searchRoom.building') }}
  29 + </template>
  30 + </el-table-column>
  31 + <el-table-column prop="unitNum" :label="$t('searchRoom.unitNum')" align="center">
  32 + <template #default="{ row }">
  33 + {{ row.unitNum }}{{ $t('searchRoom.unit') }}
  34 + </template>
  35 + </el-table-column>
  36 + <el-table-column prop="roomNum" :label="$t('searchRoom.roomNum')" align="center">
  37 + <template #default="{ row }">
  38 + {{ row.roomNum }}{{ $t('searchRoom.room') }}
  39 + </template>
  40 + </el-table-column>
  41 + <el-table-column prop="layer" :label="$t('searchRoom.layer')" align="center">
  42 + <template #default="{ row }">
  43 + {{ row.layer }}{{ $t('searchRoom.floor') }}
  44 + </template>
  45 + </el-table-column>
  46 + <el-table-column :label="$t('searchRoom.operation')" align="center" width="120">
  47 + <template #default="{ row }">
  48 + <el-button type="primary" size="mini" @click="chooseRoom(row)">
  49 + {{ $t('searchRoom.choose') }}
  50 + </el-button>
  51 + </template>
  52 + </el-table-column>
  53 + </el-table>
  54 +
  55 + <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
  56 + :current-page="pagination.currentPage" :page-sizes="[10, 15, 20, 30]" :page-size="pagination.pageSize"
  57 + layout="total, sizes, prev, pager, next, jumper" :total="pagination.total" style="margin-top: 15px" />
  58 + </div>
  59 + </el-dialog>
  60 +</template>
  61 +
  62 +<script>
  63 +import { queryRooms, queryRoomsWithSell, queryRoomsWithOutSell } from '@/api/room/roomApi'
  64 +import { getCommunityId } from '@/api/community/communityApi'
  65 +
  66 +export default {
  67 + name: 'SearchRoom',
  68 + props: {
  69 + roomFlag: {
  70 + type: String,
  71 + default: ''
  72 + },
  73 + showSearchCondition: {
  74 + type: String,
  75 + default: 'true'
  76 + }
  77 + },
  78 + data() {
  79 + return {
  80 + dialogVisible: false,
  81 + rooms: [],
  82 + currentRoomNum: '',
  83 + currentFloorNum: '',
  84 + floorNumInputReadonly: false,
  85 + pagination: {
  86 + currentPage: 1,
  87 + pageSize: 10,
  88 + total: 0
  89 + }
  90 + }
  91 + },
  92 + computed: {
  93 + isShowSearchCondition() {
  94 + return this.showSearchCondition === 'true'
  95 + }
  96 + },
  97 + methods: {
  98 + open() {
  99 + this.dialogVisible = true
  100 + this.refreshSearchRoomData()
  101 + this.loadAllRoomInfo()
  102 + },
  103 + handleClose() {
  104 + this.dialogVisible = false
  105 + },
  106 + async loadAllRoomInfo() {
  107 + try {
  108 + const params = {
  109 + page: this.pagination.currentPage,
  110 + row: this.pagination.pageSize,
  111 + communityId: getCommunityId(),
  112 + roomNum: this.currentRoomNum,
  113 + floorNum: this.currentFloorNum,
  114 + roomFlag: this.roomFlag
  115 + }
  116 +
  117 + let response
  118 + if (this.roomFlag === '1') {
  119 + response = await queryRoomsWithSell(params)
  120 + } else if (this.roomFlag === '2') {
  121 + response = await queryRoomsWithOutSell(params)
  122 + } else {
  123 + response = await queryRooms(params)
  124 + }
  125 +
  126 + this.rooms = response.rooms || []
  127 + this.pagination.total = response.records || 0
  128 + } catch (error) {
  129 + console.error('Failed to load room info:', error)
  130 + this.$message.error(this.$t('searchRoom.loadFailed'))
  131 + }
  132 + },
  133 + chooseRoom(room) {
  134 + this.$emit('chooseRoom', room)
  135 + this.$emit('load-data', { roomId: room.roomId })
  136 + this.dialogVisible = false
  137 + },
  138 + searchRooms() {
  139 + this.pagination.currentPage = 1
  140 + this.loadAllRoomInfo()
  141 + },
  142 + resetRooms() {
  143 + this.currentFloorNum = ''
  144 + this.currentRoomNum = ''
  145 + this.pagination.currentPage = 1
  146 + this.loadAllRoomInfo()
  147 + },
  148 + refreshSearchRoomData() {
  149 + this.currentRoomNum = ''
  150 + },
  151 + handleSizeChange(val) {
  152 + this.pagination.pageSize = val
  153 + this.loadAllRoomInfo()
  154 + },
  155 + handleCurrentChange(val) {
  156 + this.pagination.currentPage = val
  157 + this.loadAllRoomInfo()
  158 + },
  159 + listenerFloorInfo(floorInfo) {
  160 + this.currentFloorNum = floorInfo.floorNum
  161 + this.floorNumInputReadonly = true
  162 + this.searchRooms()
  163 + },
  164 + showOwnerRooms(rooms) {
  165 + this.dialogVisible = true
  166 + this.rooms = rooms
  167 + }
  168 + },
  169 + mounted() {
  170 + this.$on('listener-floor-info', this.listenerFloorInfo)
  171 + this.$on('show-owner-rooms', this.showOwnerRooms)
  172 + },
  173 + beforeDestroy() {
  174 + this.$off('listener-floor-info', this.listenerFloorInfo)
  175 + this.$off('show-owner-rooms', this.showOwnerRooms)
  176 + }
  177 +}
  178 +</script>
  179 +
  180 +<style scoped>
  181 +.el-input {
  182 + width: 100%;
  183 +}
  184 +</style>
0 185 \ No newline at end of file
... ...
src/i18n/commonLang.js
... ... @@ -40,6 +40,7 @@ export const messages = {
40 40 enabled: 'Enabled',
41 41 disabled: 'Disabled',
42 42 import: 'Import',
  43 + remark: 'Remark',
43 44 }
44 45 },
45 46 zh: {
... ... @@ -83,6 +84,7 @@ export const messages = {
83 84 enabled: '启用',
84 85 disabled: '禁用',
85 86 import: '导入',
  87 + remark: '备注',
86 88 }
87 89 }
88 90 }
89 91 \ No newline at end of file
... ...
src/i18n/index.js
... ... @@ -127,6 +127,7 @@ import { messages as addRoomViewMessages } from &#39;../views/room/addRoomViewLang&#39;
127 127 import { messages as roomBindOwnerMessages } from '../views/owner/roomBindOwnerLang'
128 128 import { messages as deleteOwnerRoomMessages } from '../views/owner/deleteOwnerRoomLang'
129 129 import { messages as shopsMessages } from '../views/room/shopsLang'
  130 +import { messages as listOwnerMessages } from '../views/owner/listOwnerLang'
130 131  
131 132 Vue.use(VueI18n)
132 133  
... ... @@ -258,6 +259,7 @@ const messages = {
258 259 ...roomBindOwnerMessages.en,
259 260 ...deleteOwnerRoomMessages.en,
260 261 ...shopsMessages.en,
  262 + ...listOwnerMessages.en,
261 263 },
262 264 zh: {
263 265 ...loginMessages.zh,
... ... @@ -385,6 +387,7 @@ const messages = {
385 387 ...roomBindOwnerMessages.zh,
386 388 ...deleteOwnerRoomMessages.zh,
387 389 ...shopsMessages.zh,
  390 + ...listOwnerMessages.zh,
388 391 }
389 392 }
390 393  
... ...
src/router/index.js
... ... @@ -621,6 +621,17 @@ const routes = [
621 621 name: '/pages/property/shops',
622 622 component: () => import('@/views/room/shopsList.vue')
623 623 },
  624 + {
  625 + path: '/pages/property/listOwner',
  626 + name: '/pages/property/listOwner',
  627 + component: () => import('@/views/owner/listOwner.vue')
  628 + },
  629 + {
  630 + path: '/views/owner/ownerBindRoom',
  631 + name: '/views/owner/ownerBindRoom',
  632 + component: () => import('@/views/owner/ownerBindRoom.vue')
  633 + },
  634 +
624 635 // 其他子路由可以在这里添加
625 636 ]
626 637 },
... ...
src/views/owner/listOwner.vue 0 → 100644
  1 +<template>
  2 + <div class="list-owner-container">
  3 + <el-row :gutter="20">
  4 + <el-col :span="3">
  5 + <el-card class="role-card">
  6 + <ul class="role-list">
  7 + <li v-for="(item, index) in personRoles" :key="index" class="role-item"
  8 + :class="{ 'role-item-active': conditions.personRole === item.statusCd }" @click="switchPersonRole(item)">
  9 + {{ item.name }}
  10 + </li>
  11 + </ul>
  12 + </el-card>
  13 + </el-col>
  14 +
  15 + <el-col :span="21">
  16 + <el-card class="search-card">
  17 + <div slot="header" class="text-left">
  18 + <span>{{ $t('listOwner.searchTitle') }}</span>
  19 + </div>
  20 + <el-form :model="conditions" label-width="0">
  21 + <el-row :gutter="20">
  22 + <el-col :span="4">
  23 + <el-form-item>
  24 + <el-select v-model="conditions.personType" :placeholder="$t('listOwner.personTypeName')" style="width:100%">
  25 + <el-option v-for="(type,index) in personTypes" :key="index" :label="type.label" :value="type.value" />
  26 + </el-select>
  27 + </el-form-item>
  28 + </el-col>
  29 +
  30 + <el-col :span="4">
  31 + <el-form-item>
  32 + <el-input v-model="conditions.name" :placeholder="$t('listOwner.placeholders.name')" />
  33 + </el-form-item>
  34 + </el-col>
  35 +
  36 + <el-col :span="4">
  37 + <el-form-item>
  38 + <el-input v-model="conditions.roomName" :placeholder="$t('listOwner.placeholders.room')" />
  39 + </el-form-item>
  40 + </el-col>
  41 +
  42 + <el-col :span="4">
  43 + <el-form-item>
  44 + <el-input v-model="conditions.link" :placeholder="$t('listOwner.placeholders.phone')" type="number" />
  45 + </el-form-item>
  46 + </el-col>
  47 +
  48 + <el-col :span="4">
  49 + <el-form-item>
  50 + <el-input v-model="conditions.idCard" :placeholder="$t('listOwner.placeholders.idCard')" />
  51 + </el-form-item>
  52 + </el-col>
  53 +
  54 + <el-col :span="4">
  55 + <el-form-item>
  56 + <el-button type="primary" @click="queryOwnerMethod">
  57 + {{ $t('listOwner.buttons.search') }}
  58 + </el-button>
  59 + <el-button @click="resetOwnerMethod">
  60 + {{ $t('listOwner.buttons.reset') }}
  61 + </el-button>
  62 + </el-form-item>
  63 + </el-col>
  64 + </el-row>
  65 + </el-form>
  66 + </el-card>
  67 +
  68 + <el-card class="owner-card">
  69 + <div slot="header" class="clearfix flex justify-between">
  70 + <span>{{ $t('listOwner.ownerInfo') }}</span>
  71 + <el-button type="primary" size="small" class="float-right" @click="openAddOwnerModal">
  72 + {{ $t('listOwner.buttons.add') }}
  73 + </el-button>
  74 + </div>
  75 +
  76 + <el-table :data="owners" border v-loading="loading" style="width:100%">
  77 + <el-table-column :label="$t('listOwner.columns.face')" width="80" align="center">
  78 + <template slot-scope="scope">
  79 + <el-image style="width:60px;height:60px;border-radius:5px;cursor:pointer;"
  80 + :src="scope.row.faceUrl || '/img/noPhoto.jpg'" :preview-src-list="[scope.row.faceUrl]" fit="cover" />
  81 + </template>
  82 + </el-table-column>
  83 +
  84 + <el-table-column :label="$t('listOwner.columns.name')" align="center">
  85 + <template slot-scope="scope">
  86 + <div>{{ scope.row.name }}</div>
  87 + <div v-if="scope.row.personType === 'C'">{{ scope.row.concactPerson }}</div>
  88 + </template>
  89 + </el-table-column>
  90 +
  91 + <el-table-column prop="personTypeName" :label="$t('listOwner.columns.type')" align="center" />
  92 +
  93 + <el-table-column prop="personRoleName" :label="$t('listOwner.columns.role')" align="center" />
  94 +
  95 + <el-table-column :label="$t('listOwner.columns.gender')" align="center">
  96 + <template slot-scope="scope">
  97 + {{ scope.row.sex === 0 ? $t('listOwner.gender.male') : $t('listOwner.gender.female') }}
  98 + </template>
  99 + </el-table-column>
  100 +
  101 + <el-table-column prop="idCard" :label="$t('listOwner.columns.idCard')" align="center">
  102 + <template slot-scope="scope">
  103 + {{ scope.row.idCard || '-' }}
  104 + </template>
  105 + </el-table-column>
  106 +
  107 + <el-table-column prop="link" :label="$t('listOwner.columns.phone')" align="center" />
  108 +
  109 + <el-table-column prop="concactLink" :label="$t('listOwner.columns.backupPhone')" align="center">
  110 + <template slot-scope="scope">
  111 + {{ scope.row.concactLink || '-' }}
  112 + </template>
  113 + </el-table-column>
  114 +
  115 + <el-table-column prop="address" :label="$t('listOwner.columns.address')" align="center">
  116 + <template slot-scope="scope">
  117 + {{ scope.row.address || '-' }}
  118 + </template>
  119 + </el-table-column>
  120 +
  121 + <el-table-column :label="$t('listOwner.columns.rooms')" align="center">
  122 + <template slot-scope="scope">
  123 + <el-link type="primary" @click="viewOwnerRooms(scope.row)">
  124 + {{ scope.row.roomCount || 0 }}
  125 + </el-link>
  126 + </template>
  127 + </el-table-column>
  128 +
  129 + <el-table-column :label="$t('listOwner.columns.members')" align="center">
  130 + <template slot-scope="scope">
  131 + <el-link v-if="scope.row.ownerTypeCd === '1001'" type="primary" @click="viewOwnerMembers(scope.row)">
  132 + {{ scope.row.memberCount || 0 }}
  133 + </el-link>
  134 + <span v-else>-</span>
  135 + </template>
  136 + </el-table-column>
  137 +
  138 + <el-table-column :label="$t('listOwner.columns.cars')" align="center">
  139 + <template slot-scope="scope">
  140 + <el-link type="primary" @click="viewOwnerCars(scope.row)">
  141 + {{ scope.row.carCount || 0 }}
  142 + </el-link>
  143 + </template>
  144 + </el-table-column>
  145 +
  146 + <el-table-column :label="$t('listOwner.columns.debts')" align="center">
  147 + <template slot-scope="scope">
  148 + <el-link type="primary" @click="viewOweFees(scope.row)">
  149 + {{ scope.row.oweFee || '0.00' }}
  150 + </el-link>
  151 + </template>
  152 + </el-table-column>
  153 +
  154 + <el-table-column v-for="(item, index) in listColumns" :key="index" :label="item" align="center">
  155 + <template slot-scope="scope">
  156 + {{ scope.row.listValues[index] || '-' }}
  157 + </template>
  158 + </el-table-column>
  159 +
  160 + <el-table-column :label="$t('listOwner.columns.operation')" width="300" align="center" fixed="right">
  161 + <template slot-scope="scope">
  162 + <el-dropdown>
  163 + <el-button type="primary" size="mini">
  164 + {{ $t('listOwner.buttons.edit') }}
  165 + </el-button>
  166 + <el-dropdown-menu slot="dropdown">
  167 + <el-dropdown-item @click.native="openEditOwnerModal(scope.row)">
  168 + {{ $t('listOwner.actions.editOwner') }}
  169 + </el-dropdown-item>
  170 + <el-dropdown-item @click.native="openDeleteOwnerModal(scope.row)">
  171 + {{ $t('listOwner.actions.deleteOwner') }}
  172 + </el-dropdown-item>
  173 + <el-dropdown-item @click.native="openAddOwnerRoom(scope.row)">
  174 + {{ $t('listOwner.actions.addRoom') }}
  175 + </el-dropdown-item>
  176 + <el-dropdown-item @click.native="openDeleteOwnerRoom(scope.row)">
  177 + {{ $t('listOwner.buttons.unbindRoom') }}
  178 + </el-dropdown-item>
  179 + </el-dropdown-menu>
  180 + </el-dropdown>
  181 +
  182 + <el-button v-if="scope.row.ownerTypeCd === '1001'" type="info" size="mini"
  183 + @click="openOwnerDetailMember(scope.row)">
  184 + {{ $t('listOwner.buttons.member') }}
  185 + </el-button>
  186 +
  187 + <el-button type="info" size="mini" @click="openOwnerDetail(scope.row)">
  188 + {{ $t('listOwner.buttons.detail') }}
  189 + </el-button>
  190 + </template>
  191 + </el-table-column>
  192 + </el-table>
  193 +
  194 + <el-pagination :current-page.sync="currentPage" :page-sizes="[10, 20, 30, 50]" :page-size="pageSize"
  195 + :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
  196 + @current-change="handlePageChange" />
  197 + </el-card>
  198 + </el-col>
  199 + </el-row>
  200 +
  201 + <add-owner ref="addOwner" @success="handleOwnerSuccess" />
  202 + <edit-owner ref="editOwner" @success="handleOwnerSuccess" />
  203 + <delete-owner ref="deleteOwner" @success="handleOwnerSuccess" />
  204 + <owner-rooms ref="ownerRooms" />
  205 + <owner-members ref="ownerMembers" />
  206 + <owner-cars ref="ownerCars" />
  207 + <owner-owe-fees ref="ownerOweFees" />
  208 +
  209 + </div>
  210 +</template>
  211 +
  212 +<script>
  213 +import { queryOwners } from '@/api/owner/ownerApi'
  214 +import AddOwner from '@/components/owner/addOwner'
  215 +import EditOwner from '@/components/owner/editOwner'
  216 +import DeleteOwner from '@/components/owner/deleteOwner'
  217 +import { getAttrSpecList } from '@/api/dev/attrSpecApi'
  218 +import OwnerRooms from '@/components/owner/ownerRooms'
  219 +import OwnerMembers from '@/components/owner/ownerMembers'
  220 +import OwnerCars from '@/components/owner/ownerCars'
  221 +import OwnerOweFees from '@/components/owner/ownerOweFees'
  222 +
  223 +
  224 +export default {
  225 + name: 'ListOwner',
  226 + components: {
  227 + AddOwner,
  228 + EditOwner,
  229 + DeleteOwner,
  230 + OwnerRooms,
  231 + OwnerMembers,
  232 + OwnerCars,
  233 + OwnerOweFees
  234 + },
  235 + data() {
  236 + return {
  237 + loading: false,
  238 + owners: [],
  239 + personRoles: [],
  240 + personTypes: [
  241 + { value: '', label: this.$t('common.select') },
  242 + { value: 'P', label: this.$t('listOwner.personType.personal') },
  243 + { value: 'C', label: this.$t('listOwner.personType.company') }
  244 + ],
  245 + conditions: {
  246 + personRole: '',
  247 + name: '',
  248 + roomName: '',
  249 + link: '',
  250 + idCard: '',
  251 + personType: ''
  252 + },
  253 + listColumns: [],
  254 + currentPage: 1,
  255 + pageSize: 10,
  256 + total: 0
  257 + }
  258 + },
  259 + created() {
  260 + this.getPersonRoles()
  261 + this.getColumns()
  262 + this.listOwnerData()
  263 + },
  264 + methods: {
  265 + async getPersonRoles() {
  266 + this.personRoles = [
  267 + { statusCd: '', name: this.$t('listOwner.role.all') },
  268 + { statusCd: '1', name: this.$t('listOwner.role.owner') },
  269 + { statusCd: '2', name: this.$t('listOwner.role.tenant') }
  270 + ]
  271 + },
  272 +
  273 + async getColumns() {
  274 + try {
  275 + const data = await getAttrSpecList({
  276 + page: 1,
  277 + row: 100,
  278 + tableName:'building_owner_attr'})
  279 + this.listColumns = data
  280 + .filter(item => item.specShow === 'Y')
  281 + .map(item => item.specName)
  282 + } catch (error) {
  283 + console.error('Failed to get columns:', error)
  284 + }
  285 + },
  286 +
  287 + async listOwnerData() {
  288 + this.loading = true
  289 + try {
  290 + const params = {
  291 + ...this.conditions,
  292 + page: this.currentPage,
  293 + row: this.pageSize
  294 + }
  295 + const { data, records } = await queryOwners(params)
  296 + this.owners = data
  297 + this.total = records
  298 + this.dealOwnerAttr(data)
  299 + } catch (error) {
  300 + console.error('Failed to get owner list:', error)
  301 + this.$message.error(this.$t('listOwner.fetchError'))
  302 + } finally {
  303 + this.loading = false
  304 + }
  305 + },
  306 +
  307 + dealOwnerAttr(owners) {
  308 + owners.forEach(owner => {
  309 + owner.listValues = []
  310 + if (!owner.ownerAttrDtos || owner.ownerAttrDtos.length === 0) return
  311 +
  312 + this.listColumns.forEach(column => {
  313 + const attr = owner.ownerAttrDtos.find(dto => dto.specName === column)
  314 + owner.listValues.push(attr ? attr.valueName : '')
  315 + })
  316 + })
  317 + },
  318 +
  319 + switchPersonRole(item) {
  320 + this.conditions.personRole = item.statusCd
  321 + this.listOwnerData()
  322 + },
  323 +
  324 + queryOwnerMethod() {
  325 + this.currentPage = 1
  326 + this.listOwnerData()
  327 + },
  328 +
  329 + resetOwnerMethod() {
  330 + this.conditions = {
  331 + personRole: '',
  332 + name: '',
  333 + roomName: '',
  334 + link: '',
  335 + idCard: '',
  336 + personType: ''
  337 + }
  338 + this.listOwnerData()
  339 + },
  340 +
  341 + handleSizeChange(size) {
  342 + this.pageSize = size
  343 + this.listOwnerData()
  344 + },
  345 +
  346 + handlePageChange(page) {
  347 + this.currentPage = page
  348 + this.listOwnerData()
  349 + },
  350 +
  351 + openAddOwnerModal() {
  352 + this.$refs.addOwner.open()
  353 + },
  354 +
  355 + openEditOwnerModal(owner) {
  356 + this.$refs.editOwner.open(owner)
  357 + },
  358 +
  359 + openDeleteOwnerModal(owner) {
  360 + this.$refs.deleteOwner.open(owner)
  361 + },
  362 +
  363 + handleOwnerSuccess() {
  364 + this.listOwnerData()
  365 + },
  366 +
  367 + viewOwnerRooms(owner) {
  368 + console.log('View owner rooms:', owner)
  369 + // 实际项目中跳转到房屋页面
  370 + this.$refs.ownerRooms.open(owner)
  371 +
  372 + },
  373 +
  374 + viewOwnerMembers(owner) {
  375 + console.log('View owner members:', owner)
  376 + // 实际项目中跳转到成员页面
  377 + this.$refs.ownerMembers.open(owner.ownerId)
  378 +
  379 + },
  380 +
  381 + viewOwnerCars(owner) {
  382 + console.log('View owner cars:', owner)
  383 + // 实际项目中跳转到车辆页面
  384 + this.$refs.ownerCars.open(owner.ownerId)
  385 +
  386 + },
  387 +
  388 + viewOweFees(owner) {
  389 + console.log('View owe fees:', owner)
  390 + // 实际项目中跳转到欠费页面
  391 + this.$refs.ownerOweFees.open(owner.ownerId)
  392 +
  393 + },
  394 +
  395 + openAddOwnerRoom(owner) {
  396 + console.log('Add owner room:', owner)
  397 + // 实际项目中跳转到绑定房屋页面
  398 + this.$router.push(`/views/owner/ownerBindRoom?ownerId=${owner.ownerId}`)
  399 +
  400 + },
  401 +
  402 + openDeleteOwnerRoom(owner) {
  403 + console.log('Delete owner room:', owner)
  404 + // 实际项目中跳转到解绑房屋页面
  405 + this.$router.push(`/views/owner/deleteOwnerRoom?ownerId=${owner.ownerId}`)
  406 +
  407 + },
  408 +
  409 + openOwnerDetail(owner) {
  410 + console.log('Owner detail:', owner)
  411 + // 实际项目中跳转到详情页面
  412 + },
  413 +
  414 + openOwnerDetailMember(owner) {
  415 + console.log('Owner detail member:', owner)
  416 + // 实际项目中跳转到成员详情页面
  417 +
  418 + }
  419 + }
  420 +}
  421 +</script>
  422 +
  423 +<style scoped>
  424 +.list-owner-container {
  425 + padding: 20px;
  426 +}
  427 +
  428 +.role-card {
  429 + height: 100%;
  430 +}
  431 +
  432 +.role-list {
  433 + list-style: none;
  434 + padding: 0;
  435 + margin: 0;
  436 +}
  437 +
  438 +.role-item {
  439 + padding: 10px;
  440 + text-align: center;
  441 + cursor: pointer;
  442 + border-radius: 4px;
  443 + margin-bottom: 5px;
  444 + transition: all 0.3s;
  445 +}
  446 +
  447 +.role-item:hover {
  448 + background-color: #f5f7fa;
  449 +}
  450 +
  451 +.role-item-active {
  452 + background-color: #409eff;
  453 + color: white;
  454 +}
  455 +
  456 +.search-card,
  457 +.owner-card {
  458 + margin-bottom: 20px;
  459 +}
  460 +
  461 +.float-right {
  462 + float: right;
  463 +}
  464 +
  465 +.el-dropdown {
  466 + margin-right: 10px;
  467 +}
  468 +</style>
0 469 \ No newline at end of file
... ...
src/views/owner/listOwnerLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + listOwner: {
  4 + title: 'Owner Management',
  5 + searchTitle: 'Search Conditions',
  6 + ownerInfo: 'Owner Information',
  7 + selectRoom: 'Select Room',
  8 + role: {
  9 + all: 'All',
  10 + owner: 'Owner',
  11 + tenant: 'Tenant'
  12 + },
  13 + personType: {
  14 + personal: 'Personal',
  15 + company: 'Company'
  16 + },
  17 + personTypeName:'Person Type',
  18 + gender: {
  19 + male: 'Male',
  20 + female: 'Female'
  21 + },
  22 + columns: {
  23 + face: 'Face',
  24 + name: 'Name',
  25 + type: 'Type',
  26 + role: 'Role',
  27 + gender: 'Gender',
  28 + idCard: 'ID Card',
  29 + phone: 'Phone',
  30 + backupPhone: 'Backup Phone',
  31 + address: 'Address',
  32 + rooms: 'Rooms',
  33 + members: 'Members',
  34 + cars: 'Cars',
  35 + debts: 'Debts',
  36 + operation: 'Operation'
  37 + },
  38 + placeholders: {
  39 + name: 'Please enter name',
  40 + room: 'Building-Unit-Room',
  41 + phone: 'Please enter phone',
  42 + idCard: 'Please enter ID card'
  43 + },
  44 + buttons: {
  45 + search: 'Search',
  46 + reset: 'Reset',
  47 + add: 'Add',
  48 + save: 'Save',
  49 + cancel: 'Cancel',
  50 + edit: 'Edit',
  51 + delete: 'Delete',
  52 + bindRoom: 'Bind Room',
  53 + unbindRoom: 'Unbind Room',
  54 + member: 'Member',
  55 + detail: 'Detail'
  56 + },
  57 + actions: {
  58 + editOwner: 'Edit Owner',
  59 + deleteOwner: 'Delete Owner',
  60 + addRoom: 'Add Room'
  61 + },
  62 + confirmDelete: 'Are you sure to delete?'
  63 + }
  64 + },
  65 + zh: {
  66 + listOwner: {
  67 + title: '业主管理',
  68 + searchTitle: '查询条件',
  69 + ownerInfo: '业主信息',
  70 + selectRoom: '选择房屋',
  71 + role: {
  72 + all: '全部',
  73 + owner: '业主',
  74 + tenant: '租客'
  75 + },
  76 + personType: {
  77 + personal: '个人',
  78 + company: '公司'
  79 + },
  80 + personTypeName:'人员类型',
  81 + gender: {
  82 + male: '男',
  83 + female: '女'
  84 + },
  85 + columns: {
  86 + face: '人脸',
  87 + name: '客户名称',
  88 + type: '人员类型',
  89 + role: '人员角色',
  90 + gender: '性别',
  91 + idCard: '证件号',
  92 + phone: '联系手机',
  93 + backupPhone: '备用手机',
  94 + address: '地址',
  95 + rooms: '房屋数',
  96 + members: '业主成员',
  97 + cars: '车辆数',
  98 + debts: '欠费',
  99 + operation: '操作'
  100 + },
  101 + placeholders: {
  102 + name: '请输入客户名称',
  103 + room: '楼栋-单元-房屋',
  104 + phone: '请输入联系方式',
  105 + idCard: '请输入业主身份证号'
  106 + },
  107 + buttons: {
  108 + search: '查询',
  109 + reset: '重置',
  110 + add: '添加',
  111 + save: '保存',
  112 + cancel: '取消',
  113 + edit: '修改',
  114 + delete: '删除',
  115 + bindRoom: '入住房屋',
  116 + unbindRoom: '房屋解绑',
  117 + member: '成员',
  118 + detail: '详情'
  119 + },
  120 + actions: {
  121 + editOwner: '修改业主',
  122 + deleteOwner: '删除业主',
  123 + addRoom: '入住房屋'
  124 + },
  125 + confirmDelete: '确认是否删除!'
  126 + }
  127 + }
  128 +}
0 129 \ No newline at end of file
... ...
src/views/owner/ownerBindRoom.vue 0 → 100644
  1 +<template>
  2 + <div class="room-bind-owner-container">
  3 + <el-card class="box-card">
  4 + <div slot="header" class="flex justify-between">
  5 + <div>{{ $t('roomBindOwner.title') }}</div>
  6 + <div class="header-tools">
  7 + <el-button type="primary" size="small" icon="el-icon-close" @click="_goBack">
  8 + {{ $t('common.back') }}
  9 + </el-button>
  10 + </div>
  11 + </div>
  12 +
  13 + <el-row>
  14 + <el-col :span="24">
  15 + <el-form label-width="120px">
  16 + <el-form-item :label="$t('roomBindOwner.owner')">
  17 + <el-input v-model.trim="roomBindOwnerInfo.ownerName"
  18 + :placeholder="$t('roomBindOwner.ownerPlaceholder')" disabled />
  19 + </el-form-item>
  20 + <el-form-item :label="$t('roomBindOwner.room')">
  21 + <el-col :span="18">
  22 + <el-input v-model.trim="roomBindOwnerInfo.roomName"
  23 + :placeholder="$t('roomBindOwner.roomPlaceholder')" disabled />
  24 + </el-col>
  25 + <el-col :span="4" :offset="1">
  26 + <el-button type="primary" size="small" icon="el-icon-plus" @click="_openChooseRoom">
  27 + {{ $t('listOwner.selectRoom') }}
  28 + </el-button>
  29 + </el-col>
  30 + </el-form-item>
  31 +
  32 +
  33 +
  34 + <el-form-item :label="$t('roomBindOwner.startTime')">
  35 + <el-date-picker v-model="roomBindOwnerInfo.startTime" type="date"
  36 + :placeholder="$t('roomBindOwner.startTimePlaceholder')" value-format="yyyy-MM-dd"
  37 + class="addStartTime" />
  38 + </el-form-item>
  39 +
  40 + <el-form-item :label="$t('roomBindOwner.endTime')">
  41 + <el-date-picker v-model="roomBindOwnerInfo.endTime" type="date"
  42 + :placeholder="$t('roomBindOwner.endTimePlaceholder')" value-format="yyyy-MM-dd"
  43 + class="addEndTime" />
  44 + </el-form-item>
  45 + </el-form>
  46 + </el-col>
  47 + </el-row>
  48 + </el-card>
  49 +
  50 + <el-row>
  51 + <el-col :span="24" class="text-right" style="margin-top: 20px;">
  52 + <el-button type="primary" icon="el-icon-check" @click="saveRoomBindOwnerInfo">
  53 + {{ $t('common.submit') }}
  54 + </el-button>
  55 + </el-col>
  56 + </el-row>
  57 +
  58 + <search-room ref="searchRoomRef" :roomFlag="2" @chooseRoom="handleChooseRoom" />
  59 + </div>
  60 +</template>
  61 +
  62 +<script>
  63 +import { sellRoom } from '@/api/owner/roomBindOwnerApi'
  64 +import { getCommunityId } from '@/api/community/communityApi'
  65 +import searchRoom from '@/components/room/searchRoom'
  66 +import { getDateYYYYMMDD } from '@/utils/dateUtil'
  67 +import { queryOwners } from '@/api/owner/ownerApi'
  68 +
  69 +export default {
  70 + name: 'RoomBindOwnerList',
  71 + components: {
  72 + searchRoom
  73 + },
  74 + data() {
  75 + return {
  76 + roomBindOwnerInfo: {
  77 + roomId: '',
  78 + roomName: '',
  79 + ownerId: '',
  80 + ownerName: '',
  81 + state: '2001',
  82 + startTime: '',
  83 + endTime: '2099-01-01',
  84 + },
  85 + communityId: ''
  86 + }
  87 + },
  88 + created() {
  89 + this.communityId = getCommunityId()
  90 + this.roomBindOwnerInfo.startTime = getDateYYYYMMDD()
  91 + this.initData()
  92 + },
  93 + methods: {
  94 + initData() {
  95 + const ownerId = this.$route.query.ownerId
  96 + if (ownerId) {
  97 + this.roomBindOwnerInfo.ownerId = ownerId
  98 + this.listOwner(ownerId)
  99 + }
  100 + },
  101 + async listOwner(ownerId) {
  102 + try {
  103 + const params = {
  104 + page: 1,
  105 + row: 1,
  106 + memberId: ownerId,
  107 + communityId: this.communityId
  108 + }
  109 + const { data } = await queryOwners(params)
  110 + this.roomBindOwnerInfo.ownerName = data[0].name
  111 + } catch (error) {
  112 + this.$message.error(this.$t('roomBindOwner.fetchRoomError'))
  113 + }
  114 + },
  115 + handleChooseRoom(room) {
  116 + this.roomBindOwnerInfo.roomName = room.roomName
  117 + this.roomBindOwnerInfo.roomId = room.roomId
  118 + },
  119 + _openChooseRoom() {
  120 + this.$refs.searchRoomRef.open()
  121 + },
  122 + _goBack() {
  123 + this.$router.go(-1)
  124 + },
  125 + roomBindOwnerValidate() {
  126 + const rules = {
  127 + ownerId: { required: true, message: this.$t('roomBindOwner.ownerRequired') },
  128 + roomId: { required: true, message: this.$t('roomBindOwner.roomRequired') },
  129 + startTime: { required: true, message: this.$t('roomBindOwner.startTimeRequired') },
  130 + endTime: { required: true, message: this.$t('roomBindOwner.endTimeRequired') }
  131 + }
  132 +
  133 + const errors = []
  134 + Object.keys(rules).forEach(key => {
  135 + if (!this.roomBindOwnerInfo[key]) {
  136 + errors.push(rules[key].message)
  137 + }
  138 + })
  139 +
  140 + if (errors.length > 0) {
  141 + this.$message.error(errors[0])
  142 + return false
  143 + }
  144 +
  145 + const start = new Date(this.roomBindOwnerInfo.startTime)
  146 + const end = new Date(this.roomBindOwnerInfo.endTime)
  147 + if (start >= end) {
  148 + this.$message.error(this.$t('roomBindOwner.timeError'))
  149 + return false
  150 + }
  151 +
  152 + return true
  153 + },
  154 + async saveRoomBindOwnerInfo() {
  155 + if (!this.roomBindOwnerValidate()) return
  156 +
  157 + try {
  158 + const data = {
  159 + ...this.roomBindOwnerInfo,
  160 + communityId: this.communityId
  161 + }
  162 +
  163 + const res = await sellRoom(data)
  164 + if (res.code === 0) {
  165 + this.$message.success(this.$t('common.submitSuccess'))
  166 + this._goBack()
  167 + } else {
  168 + this.$message.error(res.msg || this.$t('common.submitFailed'))
  169 + }
  170 + } catch (error) {
  171 + this.$message.error(this.$t('common.requestError'))
  172 + }
  173 + }
  174 + }
  175 +}
  176 +</script>
  177 +
  178 +<style scoped>
  179 +.room-bind-owner-container {
  180 + padding: 20px;
  181 +}
  182 +
  183 +.header-tools {}
  184 +
  185 +.text-right {
  186 + text-align: right;
  187 +}
  188 +
  189 +.addStartTime,
  190 +.addEndTime {
  191 + width: 100%;
  192 +}
  193 +</style>
0 194 \ No newline at end of file
... ...
src/views/owner/roomBindOwnerList.vue
1 1 <template>
2 2 <div class="room-bind-owner-container">
3 3 <el-card class="box-card">
4   - <div slot="header" class="clearfix">
5   - <h5>{{ $t('roomBindOwner.title') }}</h5>
  4 + <div slot="header" class="flex justify-between" >
  5 + <div>{{ $t('roomBindOwner.title') }}</div>
6 6 <div class="header-tools">
7 7 <el-button type="primary" size="small" icon="el-icon-close" @click="_goBack">
8 8 {{ $t('common.back') }}
... ... @@ -179,9 +179,7 @@ export default {
179 179 }
180 180  
181 181 .header-tools {
182   - position: absolute;
183   - top: 20px;
184   - right: 20px;
  182 +
185 183 }
186 184  
187 185 .text-right {
... ...
src/views/room/roomLang.js
... ... @@ -359,6 +359,25 @@ export const messages = {
359 359 successMessage: 'Unit deleted successfully',
360 360 errorMessage: 'Failed to delete unit'
361 361 },
  362 + searchRoom: {
  363 + title: 'Select Room',
  364 + floorNumPlaceholder: 'Enter building number',
  365 + roomNumPlaceholder: 'Enter room number',
  366 + search: 'Search',
  367 + reset: 'Reset',
  368 + roomId: 'Room ID',
  369 + floorNum: 'Building Number',
  370 + unitNum: 'Unit Number',
  371 + roomNum: 'Room Number',
  372 + layer: 'Floor',
  373 + operation: 'Operation',
  374 + choose: 'Choose',
  375 + building: 'Building',
  376 + unit: 'Unit',
  377 + room: 'Room',
  378 + floor: 'Floor',
  379 + loadFailed: 'Failed to load room information'
  380 + }
362 381 },
363 382 zh: {
364 383 roomList: {
... ... @@ -720,5 +739,24 @@ export const messages = {
720 739 successMessage: '单元删除成功',
721 740 errorMessage: '删除单元失败'
722 741 },
  742 + searchRoom: {
  743 + title: '选择房屋',
  744 + floorNumPlaceholder: '输入小区楼编号',
  745 + roomNumPlaceholder: '输入房屋编号',
  746 + search: '查询',
  747 + reset: '重置',
  748 + roomId: '房屋ID',
  749 + floorNum: '楼栋编号',
  750 + unitNum: '单元编号',
  751 + roomNum: '房屋编号',
  752 + layer: '楼层',
  753 + operation: '操作',
  754 + choose: '选择',
  755 + building: '号楼',
  756 + unit: '单元',
  757 + room: '室',
  758 + floor: '层',
  759 + loadFailed: '加载房屋信息失败'
  760 + }
723 761 }
724 762 }
725 763 \ No newline at end of file
... ...