主要讲一下物业管理系统的一下开发难点:

CMS端

1. 批量导入功能的实现

系统中,存在批量导入住户,批量导入单位,批量导入账单等功能模块,这里以批量导入单位为例子,单位数据文件的xlsx格式。
image.png

1.1 下载模板

批量导入数据的第一步是下载指定的模板

  1. // 下载模板的方法
  2. downloadFileClick() {
  3. let url = 'https://yoov-1257906831.cos.ap-hongkong.myqcloud.com/file/batch_unit_template.xlsx'
  4. // 在本页打开窗口
  5. window.open(url, "_self")
  6. },

1.2 上传xlsx文件

vue导入xlsx文件需要安装第三方插件xlsx

  1. // 安装
  2. npm install xlsx --save
  3. // 使用
  4. import XLSX from 'xlsx'
  1. // 点击上传按钮使用el-upload,在on-change事件中调用importExcel方法
  2. <el-upload class="upload-demo" ref="upload" action="/" :show-file-list="false" :on-change="importExcel"
  3. :auto-upload="false">
  4. <el-button class="download-button" size="small" type="primary">
  5. <i class="iconfont icon-shangchuan"></i>
  6. {{$t('Button.Upload')}}
  7. </el-button>
  8. </el-upload>
  9. // 導入Excel表格數據
  10. importExcel(file) {
  11. this.allData = []
  12. this.batchFile = file.raw
  13. // let file = file.files[0] // 使用传统的input方法需要加上这一步
  14. const types = file.name.split('.')[1]
  15. const fileType = ['xlsx', 'xls'].some(item => item === types)
  16. if (!fileType) {
  17. this.$message.error('格式错误!请重新选择')
  18. return
  19. }
  20. this.excel_loading = true
  21. this.file2Xce(file).then(tabJson => {
  22. if (tabJson && tabJson.length > 0) {
  23. this.gridData = tabJson[0].sheet
  24. // console.log(this.gridData)
  25. // this.gridData = tabJson[0].sheet.slice(1)
  26. // this.gridData = this.gridData.filter(item => item.length > 0)
  27. for (var i = 0; i < this.gridData.length; i++) {
  28. this.allData.push({
  29. "index": i + 1,
  30. "building_name": this.gridData[i].大廈,
  31. "floor_name": this.gridData[i].樓層,
  32. "unit_name": this.gridData[i].單位.toString(),
  33. "area": this.gridData[i].面積,
  34. "type": this.gridData[i].單位類型,
  35. })
  36. }
  37. for (var i = 0; i < this.allData.length; i++) {
  38. // // console.log(typeof this.allData[i].type)
  39. if (isNaN(Number(this.allData[i].area))) {
  40. this.allData[i].error = true
  41. this.allData[i].errorMessage = ["面積格式有誤,僅限數字"]
  42. }
  43. }
  44. this.unitData = this.allData
  45. this.errorData = this.unitData.filter(item => item.error == true)
  46. // var testData = this.unitData.filter(item => item.error != true)
  47. if (this.errorData.length == 0) {
  48. this.postData = this.allData
  49. this.pretest()
  50. }
  51. setTimeout(() => {
  52. this.excel_loading = false
  53. }, 500)
  54. }
  55. })
  56. },
  57. file2Xce(file) {
  58. return new Promise(function(resolve, reject) {
  59. const reader = new FileReader()
  60. reader.onload = function(e) {
  61. const data = e.target.result
  62. this.wb = XLSX.read(data, {
  63. type: 'binary'
  64. })
  65. const result = []
  66. this.wb.SheetNames.forEach((sheetName) => {
  67. result.push({
  68. sheetName: sheetName,
  69. sheet: XLSX.utils.sheet_to_json(this.wb.Sheets[sheetName])
  70. })
  71. })
  72. resolve(result)
  73. }
  74. reader.readAsBinaryString(file.raw)
  75. })
  76. }

1.3 数据解析

对打开的xlsx文件进行数据解析,并且把解析的数据转成json格式,与后台进行校验,返回的错误提示渲染到表格上,用户按提示修改xlsx文件内容,然后再重新上传就可以了。
image.png
代码

  1. // 数据校验
  2. async pretest() {
  3. var data = {
  4. "data": this.allData
  5. }
  6. var preResult = []
  7. const result = await Unit.pretestUnitByBatch(this.condo_id, data)
  8. if (result.pass == false) {
  9. preResult = result.result
  10. for (var i = 0; i < preResult.length; i++) {
  11. this.allData[preResult[i].index - 1].error = true
  12. this.allData[preResult[i].index - 1].errorMessage = preResult[i].message
  13. }
  14. }
  15. this.unitData = this.allData
  16. this.errorData = this.allData.filter(item => item.error == true)
  17. },
  1. // 数据提交
  2. async submit() {
  3. var data = {
  4. "data": this.postData
  5. }
  6. const result = await Unit.doUnitByBatch(this.condo_id, data)
  7. if (result.code == 1) {
  8. // console.log('success')
  9. this.$emit('uploadSuccess')
  10. } else {
  11. // this.$message.success("提交成功")
  12. this.$message.error("請先按要求修改Excel表格數據,然後再重新上傳提交!")
  13. }
  14. },

1.4 改进

目前做的版本,导入的xlsx数据与后台验证格式之后,返回错误提示,只能在xlsx源文件中修改,改进的话,可以直接在表单中修改提交的话就更好了。
image.png

2. 批量导入功能的实现

系统中,账单、活动信息、住户信息、维修信息等需要批量导出数据,这里以批量导出住户数据为例,导出的文件格式是xlsx文件。

2.1 引入第三方js文件

首先,需要引入2个第三方的js文件(Blob.js和Export2Excel.js)
image.png
model文件夹新建exportExcel.js文件

  1. export default {
  2. // 列表选中下载 excelData导出的数据,tHeader字段名称,filterVal字段
  3. downloadExcel(excelData,tHeader,filterVal,excelName) {
  4. require.ensure([], () => {
  5. let { export_json_to_excel } = require("@/excel/Export2Excel");
  6. let data = excelData.map(v => filterVal.map(j => v[j]));
  7. // 这是表格的名称。合同列表+当前日期,dateFormatStr是一个方法,格式化了日期
  8. // let excelName='合同列表'+this.dateFormatStr(new Date)
  9. export_json_to_excel(tHeader, data, excelName); // 导出的表格名称,根据需要自己命名
  10. });
  11. }
  12. }

main.js文件中添加引用

  1. import excel from './model/exportExcel.js'
  2. //注册到vue原型上
  3. Vue.prototype.excel = excel

2.2 调用download方法下载xlsx文件

fieldTitle定义xlsx文件里面的表头,fieldName定义json数据的字段名称,fieldTitle和fieldName是对应的。

  1. // 導出列表
  2. downloadExcel() {
  3. let exportData = this.residentData
  4. exportData.map(item => {
  5. item._wallet_money = item.wallet_money == null ? '0.00' : item.wallet_money
  6. })
  7. let fieldTitle = [
  8. "住戶姓名",
  9. "聯絡電話",
  10. "電郵",
  11. "身份證件號",
  12. "錢包預存金額",
  13. ]
  14. let fieldName = [
  15. "name",
  16. "phone",
  17. "email",
  18. "id_no",
  19. "_wallet_money",
  20. ]
  21. let excelName = '住户列表' + '-' + new Date().getTime()
  22. this.excel.downloadExcel(
  23. exportData,
  24. fieldTitle,
  25. fieldName,
  26. excelName
  27. )
  28. },

3. 会所设施时段收费表

根据常规时段收费表和临时收费表来渲染当前七天的时间收费表。
image.png

3.1 收费表渲染

第一步,先获取当前七天的时间,转成数组week。
image.png

  1. initData(type, cur) {
  2. this.loading = true
  3. let date = ''
  4. if (cur) {
  5. date = new Date(cur)
  6. } else {
  7. date = new Date()
  8. }
  9. // 獲取當前七天的時間
  10. this.nowTime = new Date()
  11. this.currentDay = date.getDate() // 今日日期 几号
  12. this.currentYear = date.getFullYear() // 当前年份
  13. this.currentMonth = date.getMonth() + 1 // 当前月份
  14. this.currentWeek = date.getDay() // 1...6,0 // 星期几
  15. // console.log(this.currentWeek)
  16. if (this.currentWeek === 0) {
  17. this.currentWeek = 7
  18. }
  19. const str = this.formatDate(this.currentYear, this.currentMonth, this.currentDay) // 今日日期 年-月-日
  20. // console.log(str);
  21. this.days = []
  22. for (let i = 0; i < 7; i++) {
  23. const d = new Date(str)
  24. if (type == 'prex') {
  25. d.setDate(d.getDate() - i - 1)
  26. this.days.unshift(d) //获取未来7天内的日期
  27. } else if (type == 'next') {
  28. d.setDate(d.getDate() + i + 1)
  29. this.days.push(d) //获取未来7天内的日期
  30. } else {
  31. d.setDate(d.getDate() + i)
  32. this.days.push(d) //获取未来7天内的日期
  33. }
  34. }
  35. // console.log(this.days);
  36. this.week = this.days.map(item => {
  37. return {
  38. "dateTime": item,
  39. "date": this.formatDate(item.getFullYear(), item.getMonth() + 1, item.getDate()),
  40. "dayOfWeek": item.getDay() == 0 ? this.weeks[6] : this.weeks[item.getDay() - 1],
  41. "day": item.getDay() == 0 ? 7 : item.getDay()
  42. }
  43. })
  44. this.getFeeSchedule()
  45. this.loading = false
  46. },

第二步,通过第一步获得时段请求数据,常规时段收费的dayOfWeek把常规时段按周一到周日分组,获得当周的时段收费表。
image.png
第三步,遍历临时时段收费数组,如果不为空,则根据dayOfWeek把临时收费的时段插入对应的当周时段收费表,从而实现常规时段和临时时段合并。

  1. // 获取时段收费表
  2. async getFeeSchedule() {
  3. // this.loading = true
  4. this.regular_calendars = this.temp_calendars = []
  5. this.params.start_time = this.zeroTimestanp(this.days[0])
  6. this.params.end_time = this.zeroTimestanp(this.days[6]) + 86399000
  7. const result = await Facility.getFeeSchedule(this.facility_id, this.params)
  8. this.temp_calendars = result.temp_calendars
  9. const sortResult = result.regular_calendars.sort(this.compare('start_time_of_day'))
  10. this.regular_calendars = FormatData.formatSchedule(sortResult)
  11. console.log(this.regular_calendars);
  12. console.log(this.temp_calendars);
  13. for(var i=0; i<this.temp_calendars.length; i++) {
  14. var day = this.temp_calendars[i].day_of_week[0]
  15. var realDay = this.regular_calendars[day-1]
  16. for(var j=0; j<realDay.length; j++) {
  17. if(realDay[j].start_time_of_day == this.temp_calendars[i].start_time_of_day) {
  18. // console.log('存在临时收费表');
  19. this.regular_calendars[day-1][j] = this.temp_calendars[i]
  20. }
  21. }
  22. }
  23. // console.log(this.regular_calendars);
  24. },

3.2 真实时间有效性

后台请求返回的时间,只要在有效期,都会返回对应的时段收费比如我请求2022-02-11到2022年-02-17这个时段 的收费表,如果其中一个时段收费是08:00-10:00,循环周期是周一到周日,有效期是2022-01-01到2022-02-15,其中2022-02-15是周二,理论上2022-02-16(周三)和2022-02-17(周四)是没有数据的。实际上是渲染到这个时段的数据的。

解决方法:
在每个box渲染前判断真实的时间时间戳与请求时段返回的收费表的valid_start_time和vaild_end_time进行对比,不在范围内的就取消。

  1. // 判断显示的日期是否在有效期
  2. checkValided() {
  3. var currenTime = this.dateToTimeStamp(this.day.dateTime)
  4. for(var i=0; i<this.daySchedule.length; i++) {
  5. let startTime = this.dateToTimeStamp(this.daySchedule[i].valid_start_time)
  6. let endTime = this.dateToTimeStamp(this.daySchedule[i].valid_end_time)
  7. if(currenTime > endTime || currenTime < startTime) {
  8. this.daySchedule.splice(i,1)
  9. }
  10. }
  11. },
  12. // 时间转时间戳
  13. dateToTimeStamp(date) {
  14. return new Date(new Date(date).toLocaleDateString()).getTime()
  15. },

3.3 添加临时收费

添加临时收费目前只支持修改当前时段的收费金额,不支持修改时段。
image.pngimage.png
即把点击的时间段的参数赋值到一个新的对象,并且把这个对象的temp设置为true

  1. async addSchedule() {
  2. console.log(this.boxData)
  3. var dayOfWeek = []
  4. var list = []
  5. dayOfWeek.push(this.dayOfWeek + 1)
  6. list[0] = {
  7. "day_of_week": dayOfWeek,
  8. "start_time_of_day": this.stringToTime(this.startTime),
  9. "end_time_of_day": this.stringToTime(this.endTime),
  10. "valid_start_time": this.zeroTimestanp(this.choiceDate) + this.stringToTime(this.startTime),
  11. "valid_end_time": this.zeroTimestanp(this.choiceDate) + this.stringToTime(this.endTime),
  12. "price": parseFloat(this.price),
  13. "on_sale": true,
  14. "temp": true
  15. }
  16. var data = {
  17. "facility_id": parseInt(this.$route.params.id),
  18. "list": list
  19. }
  20. console.log(data)
  21. const result = await Facility.addFeeSchedule(data)
  22. if (result.code == 1) {
  23. this.$message.success("添加成功")
  24. this.$emit('success')
  25. } else {
  26. this.$message.error("時間段衝突,請檢查修改再提交")
  27. }
  28. },

3.4 修改临时收费

  1. async updateSchedule() {
  2. console.log('更新')
  3. var dayOfWeek = []
  4. var list = []
  5. dayOfWeek.push(this.dayOfWeek + 1)
  6. const data = {
  7. "id": this.boxData.id,
  8. "facility_id": parseInt(this.$route.params.id),
  9. "day_of_week": dayOfWeek,
  10. "start_time_of_day": this.stringToTime(this.startTime),
  11. "end_time_of_day": this.stringToTime(this.endTime),
  12. "valid_start_time": this.zeroTimestanp(this.choiceDate) + this.stringToTime(this.startTime),
  13. "valid_end_time": this.zeroTimestanp(this.choiceDate) + this.stringToTime(this.endTime),
  14. "price": parseFloat(this.price),
  15. "on_sale": true,
  16. "temp": true
  17. }
  18. console.log(data);
  19. const result = await Facility.updateFeeSchedule(data)
  20. if (result.code == 2) {
  21. this.$message.success("更新成功")
  22. this.$emit('success')
  23. } else {
  24. this.$message.error("時間段衝突,請檢查修改再提交")
  25. }
  26. },

3.5 临时下架收费

如果是临时时段收费的上架或者下架,则修改on_sale参数为true或者false即可;
如果是常规时段收费的下架相当于添加一个临时时段,temp为true,on_sale为false;
常规时段收费的上架则相当于把创建的临时时段删除即可;

  1. // 上架或者下架
  2. async changeOnSale(item, day) {
  3. // console.log(item)
  4. // 臨時時段上下架
  5. if(item.temp == true) {
  6. if(item.on_sale == true) {
  7. var data = JSON.parse(JSON.stringify(item))
  8. console.log(data)
  9. data.on_sale = !data.on_sale
  10. data.facility_id = Number(this.$route.params.id)
  11. data.valid_start_time = new Date(data.valid_start_time).getTime()
  12. data.valid_end_time = new Date(data.valid_end_time).getTime()
  13. console.log(data)
  14. const result = await Facility.updateFeeSchedule(data)
  15. console.log(result)
  16. if(result.code == 2) {
  17. this.$message.success(item.on_sale ? '下架成功' : '上架成功')
  18. this.$emit('changeOnSaleSuccess')
  19. } else {
  20. this.$message.error(item.on_sale ? '下架失敗' : '上架失敗')
  21. }
  22. } else {
  23. this.deleteSchedule(item)
  24. }
  25. }
  26. // 常規時段上下架
  27. else {
  28. // 下架
  29. console.log(item)
  30. console.log(day)
  31. var dayOfWeek = []
  32. var list=[]
  33. dayOfWeek.push(day.day)
  34. if(item.on_sale == true) {
  35. list[0] = {
  36. "day_of_week": dayOfWeek,
  37. "start_time_of_day": item.start_time_of_day,
  38. "end_time_of_day": item.end_time_of_day,
  39. "valid_start_time": this.zeroTimestanp(day.date) + item.start_time_of_day,
  40. "valid_end_time": this.zeroTimestanp(day.date) + item.end_time_of_day,
  41. "price": item.price,
  42. "on_sale": false,
  43. "temp": true
  44. }
  45. var data = {
  46. "facility_id": parseInt(this.$route.params.id),
  47. "list": list
  48. }
  49. console.log(data);
  50. const result = await Facility.addFeeSchedule(data)
  51. if(result.code == 1) {
  52. this.$message.success("下架成功")
  53. this.$emit('changeOnSaleSuccess')
  54. } else {
  55. this.$message.error("下架失敗")
  56. }
  57. } else {
  58. }
  59. }
  60. },

APP端