同步6/16

This commit is contained in:
Zy
2026-06-01 18:12:58 +08:00
parent 38274ab612
commit 50e7b14fd6
183 changed files with 8956 additions and 4803 deletions

View File

@@ -28,7 +28,10 @@ VITE_APP_SNAILJOB_ADMIN = 'http://192.168.1.215/snail-job'
VITE_APP_PORT = 5173
# 登录成功跳转地址
VITE_LOGIN_BACK_URL = '/equipment/WaterDevice'
# VITE_LOGIN_BACK_URL = '/equipment/WaterDevice'
VITE_LOGIN_BACK_URL = '/index'
# 工作台正式内容:false 假内容true
VITE_LOGIN_BACK_URL_WORKBENCH_TRUE= false
# 是否展示工作台
VITE_APP_SHOW_WORK_PLATFORM = true
@@ -61,5 +64,5 @@ VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
VITE_APP_WEBSOCKET = false
# sse 开关
VITE_APP_SSE = false
VITE_APP_SSE = true

View File

@@ -19,9 +19,12 @@ VITE_APP_BASE_API = 'https://wuyeapi.bugtc.com'
# VITE_APP_BASE_API = 'http://192.168.0.222:8080'
# 登录成功跳转地址
VITE_LOGIN_BACK_URL = '/equipment/WaterDevice'
# VITE_LOGIN_BACK_URL = '/equipment/WaterDevice'
VITE_LOGIN_BACK_URL = '/index'
# 工作台正式内容:false 假内容true
VITE_LOGIN_BACK_URL_WORKBENCH_TRUE= true
# 是否展示工作台
VITE_APP_SHOW_WORK_PLATFORM = false
VITE_APP_SHOW_WORK_PLATFORM = true
# 地图类型 天地图 or 高德地图 天地图 WGS84 高德地图 GCJ02, 高德地图 需要在高德地图官网申请key
@@ -54,4 +57,4 @@ VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
VITE_APP_WEBSOCKET = false
# sse 开关
VITE_APP_SSE = false
VITE_APP_SSE = true

View File

@@ -31,6 +31,7 @@
"animate.css": "4.1.1",
"await-to-js": "3.0.0",
"axios": "1.13.6",
"big.js": "^7.0.1",
"crypto-js": "4.2.0",
"echarts": "6.0.0",
"element-china-area-data": "^6.1.0",
@@ -51,11 +52,13 @@
"vue-json-pretty": "2.6.0",
"vue-router": "5.0.3",
"vue-types": "6.0.0",
"vue3-print-nb": "^0.1.4",
"vuedraggable": "^4.1.0",
"vxe-table": "4.18.1"
},
"devDependencies": {
"@iconify/json": "^2.2.448",
"@types/big.js": "^7.0.0",
"@types/crypto-js": "4.2.2",
"@types/file-saver": "^2.0.7",
"@types/js-cookie": "3.0.6",

View File

@@ -8,7 +8,6 @@
import { useSettingsStore } from '@/store/modules/settings';
import { handleThemeStyle } from '@/utils/theme';
import { useAppStore } from '@/store/modules/app';
import { useDictStore } from './store/modules/dict';
const appStore = useAppStore();
onMounted(() => {

View File

@@ -1,38 +1,26 @@
export interface ActivityVO {
/**
* 活动管理表id
*/
id: string | number;
/**
* 标题
*/
title: string;
/**
* 作者
*/
author: string;
/**
* 内容
*/
content: string;
/**
* 发布状态 0未发布 1已发布
*/
status: string;
/**
* 浏览量
*/
pageViews: number;
/**
* 创建者姓名
*/
createName: string;
'id': string;
'villageId': string;
'title': string;
'author': string;
'content': string;
'status': string;
'pageViews': number;
'createName': string;
'coverImage': string;
'startTime': string;
'endTime': string;
'applyDeadline': string;
'signinStartTime': string;
'signinEndTime': string;
'address': string;
'quota': number;
'appliedCount': number;
'organizer': string;
'contactName': string;
'contactPhone': string;
'pointsReward': number;
'requirements': string;
}
export interface ActivityForm extends BaseEntity {

View File

@@ -48,6 +48,8 @@ export interface ComplaintVO {
* 处理描述
*/
handleContent: string;
problemImageUrl: string;
}
export interface ComplaintForm extends BaseEntity {

View File

@@ -1,6 +1,7 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { HandleLogVO, HandleLogForm, HandleLogQuery } from '@/api/system/HandleLog/type';
import { UploadFiles, UploadUserFile } from 'element-plus';
/**
* 查询办事记录列表
@@ -61,3 +62,10 @@ export const delHandleLog = (id: string | number | Array<string | number>) => {
method: 'delete'
});
};
export const uploadAttachments = (files: FormData) => {
return request({
url: '/handleLog/uploadAttachments',
method: 'post',
data: files
});
};

View File

@@ -28,6 +28,28 @@ export interface HandleLogVO {
* 办事地点
*/
handlePosition: string;
handleLogFileList: FileVO[];
}
export interface FileVO {
/**
* 文件id
*/
id: string | number;
/**
* 文件名
*/
fileName: string;
/**
* 文件路径
*/
filePath: string;
'handleLogId': string;
'ossId': string;
}
export interface HandleLogForm extends BaseEntity {

View File

@@ -8,7 +8,7 @@ import { PlanVO, PlanForm, PlanQuery } from '@/api/system/InspectionPlan/type';
* @returns {*}
*/
export const listPlan = (query?: PlanQuery): AxiosPromise<PlanVO[]> => {
export const listPlan = (query: PlanQuery | {} = {}): AxiosPromise<PlanVO[]> => {
return request({
url: '/inspection/plan/list',
method: 'get',

View File

@@ -32,7 +32,7 @@ export interface PlanVO {
/**
* 巡检设置0-必须拍照1-可以跳检
*/
inspectSetting: string;
inspectSetting: number;
/**
* 计划路线,多个用逗号分隔
@@ -106,7 +106,7 @@ export interface PlanForm extends BaseEntity {
/**
* 巡检设置0-必须拍照1-可以跳检
*/
inspectSetting?: string;
inspectSetting?: number;
/**
* 计划路线,多个用逗号分隔

View File

@@ -8,7 +8,7 @@ import { PointVO, PointForm, PointQuery } from '@/api/system/InspectionPoint/typ
* @returns {*}
*/
export const listPoint = (query?: PointQuery): AxiosPromise<PointVO[]> => {
export const listPoint = (query?: PointQuery | {}): AxiosPromise<PointVO[]> => {
return request({
url: '/inspection/point/list',
method: 'get',

View File

@@ -20,10 +20,13 @@ export const listRecord = (query?: RecordQuery): AxiosPromise<RecordVO[]> => {
* 查询巡检记录主详细
* @param id
*/
export const getRecord = (id: string | number): AxiosPromise<RecordVO> => {
export const getRecord = (id: string, date?: string): AxiosPromise<RecordVO> => {
return request({
url: '/inspection/record/' + id,
method: 'get'
method: 'get',
params: {
date
}
});
};

View File

@@ -3,7 +3,7 @@ export interface RecordVO {
actualStartTime?: string;
createByName?: string;
createTime?: string;
details?: [];
details?: DetailsVo[];
id?: string;
inspectorId?: number;
inspectorName?: string;
@@ -12,7 +12,7 @@ export interface RecordVO {
remark?: string;
routeId?: string;
routeName?: string;
status?: number;
status?: string;
statusLabel?: null;
taskName?: string;
taskTime?: string;
@@ -82,3 +82,19 @@ export interface RecordQuery extends PageQuery {
*/
taskName?: string;
}
export interface DetailsVo {
'id': string;
'taskId': string;
'pointId': string;
'pointName': string;
'location': string;
'checkTime': string;
'checkResult': string;
'photos': string;
'sortNo': string;
'remark': string;
'imgUrl': string;
'status': string;
photoslist?: string[];
}

View File

@@ -8,7 +8,7 @@ import { RouteVO, RouteForm, RouteQuery } from './type';
* @returns {*}
*/
export const listRoute = (query?: RouteQuery): AxiosPromise<RouteVO[]> => {
export const listRoute = (query: RouteQuery | {} = {}): AxiosPromise<RouteVO[]> => {
return request({
url: '/inspection/route/list',
method: 'get',
@@ -61,3 +61,11 @@ export const delRoute = (id: string | number | Array<string | number>) => {
method: 'delete'
});
};
export const changeStatusRoute = (id: string, status: string) => {
return request({
url: '/inspection/route/changeStatus',
method: 'put',
params: { id, status }
});
};

View File

@@ -1,17 +1,40 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { NoticeVO, NoticeForm, NoticeQuery } from '@/api/system/NoticePc/type';
export interface NotificationVo {
'id': 'string';
'title': 'string';
'item': 'string';
'content': 'string';
'manageIsCheck': 'string';
'repairIsCheck': 'string';
}
/**
* 查询消息通知列表
* @param query
* @returns {*}
*/
export const getNotificationList = (query?: NoticeQuery): AxiosPromise<NoticeVO[]> => {
export const getNotificationList = (query): AxiosPromise<NotificationVo[]> => {
return request({
url: '/notification/list',
method: 'get',
params: query
});
};
/**
* 消息通知 已读
* @param query
* @returns {*}
*/
export const ReadNotificationByID = (ids: string[]) => {
return request({
url: '/notification/read',
method: 'POST',
data: {
id: ids
}
});
};

View File

@@ -0,0 +1,42 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
/**
* 获取缴费方式,种类占比
* @param query
* @returns {*}
*/
export type IncomeStatsResponse = {
'totalDue': string;
'totalPaid': string;
'totalUnpaid': string;
'paidPercentage': string;
'payWx': number;
'payZfb': number;
'payOffline': number;
'totalPay': number;
'feeTypeStats': feeTypeStats[];
'dailyStats': {
'statDate': string;
'totalDue': string;
'totalPaid': string;
'totalUnpaid': string;
}[];
};
export type feeTypeStats = {
'feeType': number;
'feeName': string;
'totalDue': string;
'totalPaid': string;
'totalUnpaid': string;
'paidPercentage': string;
};
export const income_statsApi = (datetime?: string): AxiosPromise<IncomeStatsResponse> => {
return request({
url: '/api/analysis/income/stats',
method: 'get',
params: {
startDate: datetime
}
});
};

View File

@@ -0,0 +1,94 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
/**
* 查询入住情况
* @param query
* @returns {*}
*/
export const user_movein_list = (query?: {
datetime: string;
}): AxiosPromise<{
'villageId': string;
'villageName': string;
'totalHouse': number;
'occupiedHouse': number;
'occupancyRate': string;
'occupancyCount': number;
}> => {
return request({
url: '/api/analysis/occupancy/stats',
method: 'get',
params: query
});
};
/**
* 查询居住人类型列表
* @param query
* @returns {*}
*/
export const get_movein_user_type = (query?: {
datetime: string;
}): Promise<{
'owner': string;
'ownerRate': string;
'tenant': string;
'tenantRate': string;
'relatives': string;
'relativeRate': string;
'other': string;
'otherRate': string;
'totalResident': string;
}> => {
return request({
url: '/residentType/selectTotal',
method: 'get',
params: query
});
};
/**
* 查询住户性别比例
* @param query
* @returns {*}
*/
export const get_movein_user_sex = (query?: {
datetime: string;
}): Promise<{
'man': number;
'woman': number;
'total': number;
'manRate': string;
'womanRate': string;
}> => {
return request({
url: '/gender/overview',
method: 'get',
params: query
});
};
/**
* 获取迁入迁出趋势图
* @param query
* @returns {*}
*/
export const get_movein_moveout_list = (query?: {
date: string;
}): AxiosPromise<
{
'date': string;
'moveInCount': number;
'moveOutCount': number;
'totalCount': number;
}[]
> => {
return request({
url: '/api/analysis/resident/moveInOutStats',
method: 'get',
params: query
});
};

View File

@@ -55,9 +55,12 @@ export const updateQuestionnaire = (data: QuestionnaireForm) => {
* 删除问卷调查
* @param id
*/
export const delQuestionnaire = (id: string | number | Array<string | number>) => {
export const delQuestionnaire = (ids: string | string[], vailid: boolean = true) => {
return request({
url: '/questionnaire/' + id,
method: 'delete'
url: '/questionnaire/' + ids + '?isValid=' + vailid,
method: 'delete',
headers: {
silentError: 'true'
}
});
};

View File

@@ -51,7 +51,7 @@ export interface QuestionnaireForm extends BaseEntity {
/**
* 问卷ID
*/
id?: string | number;
id?: string;
/**
* 所属小区id
@@ -158,7 +158,7 @@ export interface QuestionComponentItem {
}
export interface QuestionComponentnaireForm {
id?: string | number | null;
id?: string;
title: string;
description?: string;
content: QuestionComponentItem[];

View File

@@ -1,65 +1,32 @@
export interface RepairVO {
/**
* 报修管理表id
*/
id: string | number;
/**
* 关联房屋id
*/
houseId: string | number;
/**
* 维修项目id
*/
maintenanceItemId: string | number;
/**
* 问题描述
*/
problem: string;
/**
* 报修人id(住户表id)
*/
residentId: string | number;
/**
* 手机号码
*/
phone: number;
/**
* 预约日期
*/
orderDate: string;
/**
* 问题图片地址
*/
problemImageUrl: string;
/**
* 0 已提交 1已分配 2维修中 3维修完成
*/
problemStatus: string;
/**
* 维修描述
*/
repairDes: string;
/**
* 维修图片url
*/
repairImageUrl: string;
/**
* 维修人员id
*/
repairId: string;
/** 名称(小区名-楼栋名-单元号-房间号) */
name?: string;
'id': string;
'houseId': string;
'maintenanceItemId': number;
'maintenanceItemName': number;
'problem': string;
'residentId': string;
'residentName': string;
'phone': string;
'orderDate': string;
'problemImageUrl': string;
'problemStatus': string;
'repairDes': string;
'repairImageUrl': string;
'repairId': string;
'repairName': string;
'houseName': string;
'item': string;
'projectType': number;
'updateTime': string;
'projectTypeStr': string;
'repairIsCheck': string;
'manageIsCheck': string;
'isCheckStr': number;
'repairPhone': number;
'repairRoleName': number;
'repairStatus': string;
repairUserId: string;
'createTime': string;
}
export interface RepairForm extends BaseEntity {
@@ -86,12 +53,12 @@ export interface RepairForm extends BaseEntity {
/**
* 报修人id(住户表id)
*/
residentId?: string | number;
residentId?: string;
/**
* 手机号码
*/
phone?: number;
phone?: string;
/**
* 预约日期
@@ -163,12 +130,12 @@ export interface RepairQuery extends PageQuery {
/**
* 报修人id(住户表id)
*/
residentId?: string | number;
residentId?: string;
/**
* 手机号码
*/
phone?: number;
phone?: string;
/**
* 预约日期

View File

@@ -93,6 +93,7 @@ export type BillUserlist = {
'houseNo': string;
'residentId': string;
'buildingId': string;
'parkingId': string;
};
/**
* 查询业主列表
@@ -106,3 +107,16 @@ export const queryResidentList_holder = (query: { pageNum: number; pageSize: num
params: query
});
};
/**
* 查询车位业主列表
* @param data
* @returns {*}
*/
export const queryResidentList_Cars = (query: { pageNum: number; pageSize: number }): AxiosPromise<BillUserlist[]> => {
return request({
url: '/bill/getParkingList',
method: 'GET',
params: query
});
};

View File

@@ -13,6 +13,7 @@ export interface BillVO {
* 收费项目id
*/
chargeItemId: string;
residentIds: string[];
/**
* 收费范围 0全体业主 1部分业主
@@ -48,6 +49,8 @@ export interface BillVO {
* 创建时间
*/
cerateTime: string;
parkingIds: string[];
}
export interface BillForm extends BaseEntity {

View File

@@ -13,7 +13,7 @@ export type cashlist = {
* @param post
* @returns {*}
*/
export const cashQuery = (data: { residentName?: string }): AxiosPromise<cashlist[]> => {
export const cashQuery = (data: { residentName?: string; queryType: number }): AxiosPromise<cashlist[]> => {
return request({
url: '/cash/residentList',
method: 'post',
@@ -25,6 +25,7 @@ export type billlistType = {
'villageId': string;
'residentId': string;
'houseId': string;
'detailsId': string;
'houseName': string;
'parkingId': string;
'parkingName': string;
@@ -38,11 +39,14 @@ export type billlistType = {
'endDate': string;
'money': string;
'payStatus': string;
amount: string;
residentName: string;
};
/**
* 查询账单列表
* @param post
* @returns {*}
* @param data
*/
export const billQuery = (data: { residentId?: string; payStatus: '0' | '1' }): AxiosPromise<billlistType[]> => {
return request({
@@ -51,3 +55,37 @@ export const billQuery = (data: { residentId?: string; payStatus: '0' | '1' }):
data: data
});
};
export const billEdit = (data: { 'detailsId': string[]; 'payType': string; 'payStatus': string }): AxiosPromise<billlistType[]> => {
return request({
url: '/cash/billEdit',
method: 'post',
data: data
});
};
// 打印票据
export const billPrint = (data: { 'detailsIds': string[]; 'residentId': string }): AxiosPromise<BillPrintType> => {
return request({
url: '/cash/print',
method: 'post',
data: data
});
};
export type BillPrintType = {
'totalAccount': string;
'residentName': string;
'detailsList': {
'detailsId': string;
'chargeItemName': string;
'billName': string;
'detailsTime': string;
'useCount': string;
'account': string;
'payType': string;
'remark': string;
}[];
'printDate': string;
'HouseName': string;
'title': string;
};

View File

@@ -8,11 +8,11 @@ import { ChargeItemVO, ChargeItemForm, ChargeItemQuery } from './type';
* @returns {*}
*/
export const listChargeItem = (query?: ChargeItemQuery): AxiosPromise<ChargeItemVO[]> => {
export const listChargeItem = (query?: ChargeItemQuery | {}): AxiosPromise<ChargeItemVO[]> => {
return request({
url: '/chargeItem/list',
method: 'get',
params: query
params: query,
});
};

View File

@@ -12,6 +12,7 @@ export interface ChargeItemVO {
* 收费类型
*/
typeId: string;
itemName:string;
/**
* 计费方式 0单次收费 1周期收费 2仪表收费

View File

@@ -61,3 +61,45 @@ export const delTopupRecord = (id: string | number | Array<string | number>) =>
method: 'delete'
});
};
export type TopupRecordList = {
'id': string;
'rechargeTime': string;
'villageName': string;
'residentName': string;
'houseName': string;
'fixedPriceUnit': string;
'fixedPrice': string;
'unit': string;
'deviceCode': string;
'orderId': string;
'rechargeAmount': string;
'topUpType': string;
};
/** 查询所有设备充值记录 */
export const getTopupRecordByDate = (query: {
startTime?: string;
endTime?: string;
pageSize: number;
pageNum: number;
}): AxiosPromise<TopupRecordList[]> => {
return request({
url: '/topupRecord/water/list',
method: 'get',
params: query
});
};
/** 查询所有电表充值记录 */
export const getElectricityTopupRecordByDate = (query: {
startTime?: string;
endTime?: string;
pageSize: number;
pageNum: number;
}): AxiosPromise<TopupRecordList[]> => {
return request({
url: '/topupRecord/electricity/list',
method: 'get',
params: query
});
};

View File

@@ -8,7 +8,7 @@ import { SupplierVO, SupplierForm, SupplierQuery } from './type';
* @returns {*}
*/
export const listSupplier = (query?: SupplierQuery): AxiosPromise<SupplierVO[]> => {
export const listSupplier = (query: SupplierQuery | {} = {}): AxiosPromise<SupplierVO[]> => {
return request({
url: '/warehouse/supplier/list',
method: 'get',

View File

@@ -8,7 +8,7 @@ import { AreaVO, AreaForm, AreaQuery } from '@/api/system/carArea/type';
* @returns {*}
*/
export const listArea = (query?: AreaQuery): AxiosPromise<AreaVO[]> => {
export const listArea = (query: AreaQuery | {} = {}): AxiosPromise<AreaVO[]> => {
return request({
url: '/area/list',
method: 'get',

View File

@@ -73,3 +73,14 @@ export const uploadCar = (formdata: FormData) => {
data: formdata
});
};
/**
* 获取用户端app-用户
* */
export const getAppUser = (data) => {
return request({
url: '/car/getAppUserList',
method: 'GET',
params: data
});
};

View File

@@ -2,17 +2,17 @@ export interface CarVO {
/**
* 车辆管理表id
*/
id: string | number;
id: string;
/**
* 区域id
*/
areaId: string | number;
areaId: string;
/**
* 车位id
*/
parkingId: string | number;
parkingId: string;
/**
* 持有人id(住户表id)

View File

@@ -13,10 +13,11 @@ export function getBuildinglistAPI(): AxiosPromise<BuildingVo[]> {
});
}
/** 获取所有楼栋 */
export function getBuildinglists(): AxiosPromise<BuildingVo[]> {
export function getBuildinglists(query = {}): AxiosPromise<BuildingVo[]> {
return request({
url: '/building/list',
method: 'get'
method: 'get',
params: query
});
}
/** 获取单一楼栋 */
@@ -117,7 +118,7 @@ export interface Data {
billResidentList: BillResidentList[];
chargeItemList: string[];
house: House;
parking: Parking;
parkingList: Parking[];
residentList: ResidentList[];
storeroom: Storeroom;
}
@@ -162,6 +163,8 @@ export interface Storeroom {
shareArea: string;
storeroomId: number;
storeroomNo: string;
remark: string;
unitNoId: string;
unitNo: number;
}

View File

@@ -90,5 +90,6 @@ export interface addHouseType {
'shareArea': string;
'houseType': string;
'storeroomId': string;
'parkingId': string;
'parkingId'?: string;
'parkingIds': string[];
}

View File

@@ -81,3 +81,9 @@ export const uploadHouseImage = (formdata: FormData): AxiosPromise<uploadHouseIm
data: formdata
});
};
export const getHouseOwnUser = (houseId: string): AxiosPromise<uploadHouseImagetype> => {
return request({
url: `/houseRental/ownerName/${houseId}`,
method: 'GET'
});
};

View File

@@ -15,6 +15,13 @@ export const listHouseUseType = (query?: HouseUseTypeQuery): AxiosPromise<HouseU
params: query
});
};
export const listHouseUseType_add_house = (query?: HouseUseTypeQuery): AxiosPromise<HouseUseTypeVO[]> => {
return request({
url: '/houseUseType/getList',
method: 'get',
params: query
});
};
/**
* 查询房屋类型详细

View File

@@ -13,6 +13,7 @@ export interface ResidentVO {
* 姓名
*/
name: string;
nickName: string;
/**
* 0业主 1租户
@@ -90,9 +91,12 @@ export interface ResidentForm extends BaseEntity {
name?: string;
/**
* 0业主 1租户
* 0业主
* 1亲属
* 2租户
* 3朋友
*/
type?: string;
ownerRelationship?: string;
/**
* 0 男性 1女性
@@ -152,10 +156,12 @@ export interface ResidentQuery extends PageQuery {
date?: any;
/**
* 住户类型
* 0业主 1租户
* 0业主
* 1亲属
* 2租户
* 3朋友
*/
type?: string;
ownerRelationship?: string;
/**
* 住户状态
* 0审核中 1 已认证
@@ -165,5 +171,5 @@ export interface ResidentQuery extends PageQuery {
houseId?: number;
villageId?: string;
buildingId?: number;
unitNo?: number;
unitNoId?: number;
}

View File

@@ -68,11 +68,11 @@ export const delStoreroom = (id: string | number | Array<string | number>) => {
*/
export const GetVillagelistStoreroom = (
data: { villageid: string; unitNo: string; houseId?: string; buildingId: string } = {
data: { villageid: string; unitNoId: string; houseId?: string; buildingId: string } = {
villageid: null,
buildingId: null,
houseId: null,
unitNo: null
unitNoId: null
}
): AxiosPromise<StoreroomVO[]> => {
return request({
@@ -81,7 +81,7 @@ export const GetVillagelistStoreroom = (
data: {
villageId: data.villageid,
buildingId: data.buildingId,
unitNo: data.unitNo,
unitNoId: data.unitNoId,
houseId: data.houseId
}
});

View File

@@ -12,7 +12,8 @@ export interface StoreroomVO {
/**
* 单元号
*/
unitNo: string | number;
unitNoId: string | number;
unitName: string;
/**
* 储藏室号

View File

@@ -0,0 +1,78 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { VersionVO, VersionForm, VersionQuery } from './type';
/**
* 查询版本列表
* @param query
* @returns {*}
*/
export const listVersion = (query?: VersionQuery): AxiosPromise<VersionVO[]> => {
return request({
url: '/api/app/list',
method: 'get',
params: query
});
};
/**
* 查询版本详细
* @param id
*/
export const getVersion = (id: string | number): AxiosPromise<VersionVO> => {
return request({
url: '/system/version/' + id,
method: 'get'
});
};
/**
* 新增版本
* @param data
*/
export const addVersion = (data: VersionForm) => {
return request({
url: '/api/app/release',
method: 'post',
data: data
});
};
/**
* 修改版本
* @param data
*/
export const updateVersion = (data: VersionForm) => {
return request({
url: '/system/version',
method: 'put',
data: data
});
};
/**
* 删除版本
* @param id
*/
export const delVersion = (id: string | number | Array<string | number>) => {
return request({
url: '/system/version/' + id,
method: 'delete'
});
};
/** 获取过往版本 */
export const getPastVersion = (platform: string, channel: string) => {
return request({
url: `/api/app/getPastVersion?platform=${platform}&channel=${channel}`,
method: 'get'
});
};
/** 切换版本启停 */
export const switchVersion = (id: string, status: string) => {
return request({
url: `/api/app/switchStatus/${id}/${status}`,
method: 'get'
});
};

View File

@@ -0,0 +1,182 @@
export interface VersionVO {
/**
*
*/
id: string | number;
/**
* ios/android
*/
platform: string;
/**
* 版本号(递增)
*/
versionCode: number;
/**
* 版本名称1.2.0
*/
versionName: string;
/**
* 最低支持版本
*/
minVersionCode: number;
/**
* optional/force
* optional-可选更新,
* force-强制更新
*/
updateType: string;
/**
* 升级方式
* apk-安装包更新wgt-热更新
*/
method: string;
/**
* 下载地址
*/
downloadUrl: string;
/**
* 更新日志
*/
releaseNotes: string;
/**
* 文件大小(MB)
*/
fileSize: string;
/**
* 0-禁用 1-启用
*/
status: string;
/**
* 更新app端
*/
channel: string;
}
export interface VersionForm extends BaseEntity {
/**
*
*/
id?: string | number;
/**
* ios/android
*/
platform?: string;
/**
* 版本号(递增)
*/
versionCode?: number;
/**
* 版本名称1.2.0
*/
versionName?: string;
/**
* 最低支持版本
*/
minVersionCode?: string;
/**
* optional/force
* optional-可选更新force-强制更新
*/
updateType?: string;
method?: string;
/**
* 下载地址
*/
downloadUrl?: string;
/**
* 更新日志
*/
releaseNotes?: string;
/**
* 文件大小(MB)
*/
fileSize?: string;
/**
* 0-禁用 1-启用
*/
status?: number;
/**
* 更新app端
* butler 管家端
* resident 业主端
*/
channel?: string;
}
export interface VersionQuery extends PageQuery {
/**
* ios/android
*/
platform?: string;
/**
* 版本号(递增)
*/
versionCode?: number;
/**
* 版本名称1.2.0
*/
versionName?: string;
/**
* 最低支持版本
*/
minVersionCode?: number;
/**
* optional/force
*/
updateType?: string;
/**
* 下载地址
*/
downloadUrl?: string;
/**
* 更新日志
*/
releaseNotes?: string;
/**
* 文件大小(MB)
*/
fileSize?: string;
/**
* 0-禁用 1-启用
*/
status?: number;
/**
* 更新app端
*/
channel?: string;
/**
* 日期范围参数
*/
params?: any;
}

View File

@@ -51,8 +51,7 @@ export const workSpaceRepairTrendListApi = (
* @returns {*}
*/
export const workSpaceIncomeTrendListApi = (
villageId: string,
type: number
villageId: string
): Promise<
{
date: string;
@@ -63,11 +62,32 @@ export const workSpaceIncomeTrendListApi = (
url: '/workSpace/last7DaysIncomeTrend',
method: 'GET',
params: {
villageId: villageId,
type: type
villageId: villageId
}
});
};
/**
* 查询近一周支出趋势
* @param query
* @returns {*}
*/
export const workSpaceOutcomeTrendListApi = (
villageId: string
): Promise<
{
date: string;
expenditure: string;
}[]
> => {
return request({
url: '/workSpace/last7DaysExpenditureTrend',
method: 'GET',
params: {
villageId: villageId
}
});
};
/**
* 查询当日天气
* @param query

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1780369458112" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9273" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M271.958852 64.00158c-115.276539 0-208.874831 93.670541-208.874831 208.950693C63.084021 388.337184 156.628127 481.899353 272.016651 481.899353h144.894925c35.347715 0 63.918494-28.762239 63.918495-64.106342V272.894474c-0.01445-115.36685-93.500757-208.892893-208.871219-208.892894zM416.875452 417.793011v0.025288a0.932009 0.932009 0 0 1 0.043349 0.144497H272.016651a145.241719 145.241719 0 1 1 144.858801-145.06471v144.894925zM416.897127 542.227083H272.016651c-115.370462 0-208.896506 93.515206-208.896506 208.885668 0 115.276539 93.562168 208.892893 208.856769 208.892893 115.370462 0 208.856769-93.529656 208.856769-208.900118v-144.894925c-0.018062-35.351327-28.592454-63.983518-63.936556-63.983518z m-0.021675 208.874831a144.721528 144.721528 0 0 1-88.483079 133.504905C310.564987 892.145979 291.59607 895.884854 272.016651 895.884854c-19.564969 0-38.519436-3.738874-56.354047-11.28526A144.880476 144.880476 0 0 1 272.016651 606.167252h144.920212a0.093923 0.093923 0 0 0-0.043349 0v144.920212zM751.101914 542.227083h-144.894925A63.965456 63.965456 0 0 0 542.227083 606.206989v144.894925c0 115.370462 93.515206 208.896506 208.885668 208.896506 115.276539 0 208.892893-93.60913 208.892893-208.885669-0.003612-115.370462-93.533269-208.885668-208.90373-208.885668zM884.686293 807.448736a144.927437 144.927437 0 0 1-278.519041-56.354047V606.167252h144.920212A144.996074 144.996074 0 0 1 884.686293 807.448736zM606.206989 481.899353h144.894925c115.370462 0 208.896506-93.562168 208.896506-208.950693 0-115.276539-93.60913-208.921793-208.885669-208.921792-115.370462 0-208.885668 93.497144-208.885668 208.867606v144.894925c0 35.347715 28.632191 64.109954 63.979906 64.109954z m-0.039737-209.004879A144.949112 144.949112 0 1 1 751.101914 417.959184H606.167252a0.520191 0.520191 0 0 1 0-0.144498V272.894474z" fill="" p-id="9274"></path></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -259,3 +259,31 @@ aside {
.el-button {
border-radius: 5px;
}
.bgColor {
width: 850px;
height: 850px;
background: #fdf1ee;
filter: blur(200px);
display: block;
position: absolute;
top: 0;
right: 0;
transform: translateY(-50%) translateX(0%);
pointer-events: none;
z-index: 1;
border-radius: 50%;
@media (max-width: 1540px) {
width: 800px;
height: 400px;
}
@media (max-width: 1280px) {
width: 600px;
height: 300px;
}
@media (max-width: 1030px) {
width: 400px;
height: 200px;
}
}

View File

@@ -11,6 +11,7 @@
position: relative;
overflow-x: hidden;
background: var(--el-bg-color-page);
background: #dfedff;
}
.sidebarHide {

View File

@@ -84,7 +84,7 @@ const handleClick = (item) => {
};
// 暴露方法给父组件调用
defineExpose({ open });
defineExpose({ open, close });
</script>
<style scoped>

View File

@@ -3,7 +3,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
import { ref, onMounted, onBeforeUnmount, watch, nextTick, shallowRef } from 'vue';
import * as echarts from 'echarts';
import type { EChartsOption, ECharts } from 'echarts';
@@ -19,7 +19,6 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {
width: '100%',
height: '100%',
options: () => ({}) as EChartsOption,
theme: '',
autoResize: true,
immediate: true
@@ -27,36 +26,38 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<{
ready: [chart: ECharts];
click: [params: any];
click: [params: echarts.ECElementEvent];
rendered: [];
}>();
const chartRef = ref<HTMLDivElement>();
let chartInstance: ECharts | null = null;
// 3. 使用 shallowRef 优化性能,因为 ECharts 实例不需要深层响应式
const chartInstance = shallowRef<ECharts | null>(null);
let resizeObserver: ResizeObserver | null = null;
const initChart = () => {
if (!chartRef.value) return;
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
if (chartInstance.value) {
chartInstance.value.dispose();
chartInstance.value = null;
}
chartInstance = echarts.init(chartRef.value, props.theme);
chartInstance.setOption(props.options);
const instance = echarts.init(chartRef.value, props.theme);
chartInstance.value = instance;
chartInstance.value.setOption(props.options);
emit('ready', chartInstance);
emit('ready', instance);
emit('rendered');
chartInstance.on('click', (params: any) => {
instance.on('click', (params: any) => {
emit('click', params);
});
};
const updateChart = () => {
if (chartInstance) {
chartInstance.setOption(props.options, {
if (chartInstance.value) {
chartInstance.value.setOption(props.options, {
notMerge: false,
lazyUpdate: false
});
@@ -66,15 +67,15 @@ const updateChart = () => {
// ✅ 核心修复:支持 scale 缩放后重绘
const resizeChart = () => {
if (!chartInstance) return;
chartInstance.resize();
if (!chartInstance.value) return;
chartInstance.value.resize();
};
const getInstance = () => chartInstance;
const getInstance = () => chartInstance.value;
const exportAsImage = (type: 'png' | 'svg' | 'jpeg' = 'png') => {
return (
chartInstance?.getDataURL({
chartInstance.value?.getDataURL({
type,
pixelRatio: 2,
backgroundColor: '#fff'
@@ -86,23 +87,18 @@ watch(() => props.options, updateChart, { deep: true });
onMounted(() => {
nextTick(() => initChart());
if (props.autoResize && chartRef.value) {
resizeObserver = new ResizeObserver(resizeChart);
resizeObserver.observe(chartRef.value);
}
});
// ✅ 关键:监听 window.resizescale 缩放会触发 window.resize
onMounted(() => {
window.addEventListener('resize', resizeChart);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeChart);
resizeObserver?.disconnect();
chartInstance?.dispose();
chartInstance = null;
chartInstance.value?.dispose();
chartInstance.value = null;
});
defineExpose({

View File

@@ -0,0 +1,304 @@
<template>
<slot name="default" />
<el-dialog append-to-body v-model="dialog.dialogVisible" :title="dialog.dialogTitle" width="1050">
<el-table ref="multipleTableRef" :data="tableData" border @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" width="55" type="index" />
<el-table-column label="业主" align="center">
<template #default="scope">
<span>{{ scope.row.residentName ? scope.row.residentName : '---' }}</span>
</template>
</el-table-column>
<el-table-column label="性别" align="center">
<template #default="scope">
<dict-tag :options="sys_user_sex" :value="scope.row.gender"></dict-tag>
</template>
</el-table-column>
<el-table-column label="手机号" align="center" prop="phone" />
<el-table-column label="车位" align="center" prop="parkingName" />
</el-table>
<template #footer>
<div class="dialog-footer flex flex-items-center flex-justify-between">
<pagination
style="margin-top: 0"
layout="total, prev, pager, next, jumper"
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getlist"
/>
<div>
<el-button @click="conback">退出</el-button>
<el-button type="primary" @click="confirm"> 确定 </el-button>
</div>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { getBuildinglistAPI, getHouselistAPI } from '@/api/system/house';
import { BillUserlist, queryResidentList_Cars, queryResidentList_holder } from '@/api/system/SdbBill';
import { ref, nextTick, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_user_sex } = toRefs<any>(proxy?.useDict('sys_user_sex'));
const props = defineProps({
returnvalue: {
type: String,
default: 'parkingId'
},
titleName: {
type: String,
default: '选择业主'
},
/** 是否多选 false 单选 */
isMultiple: {
type: Boolean,
default: false
}
});
const { titleName, returnvalue } = toRefs(props);
const userid = ref([]);
// ✅ 手动设置默认值(替代 withDefaults
const emit = defineEmits<{
change: [value: string | number | (string | number)[] | BillUserlist | BillUserlist[]]; // 支持返回数组
}>();
const dialog = ref({
dialogVisible: false,
dialogTitle: titleName.value
});
const queryParams = ref({
pageNum: 1,
pageSize: 10
});
const multipleTableRef = ref();
const tableData = ref<BillUserlist[]>([]);
const total = ref(0);
const loading = ref(false);
const isFetching = ref(false);
// 存储选中项
const multipleSelection = ref<BillUserlist[]>([]);
const isMultiple = computed(() => props.isMultiple ?? false);
// 优化 handleUserSelection避免重复查找
const handleUserSelection = () => {
isFetching.value = true;
if (!multipleTableRef.value || !tableData.value.length) {
isFetching.value = false;
return;
}
const rowsToSelect = tableData.value.filter((row) => {
return multipleSelection.value.some((selected) => selected.parkingId === row.parkingId);
});
multipleTableRef.value.clearSelection();
if (rowsToSelect.length > 0) {
nextTick(() => {
rowsToSelect.forEach((row) => {
multipleTableRef.value?.toggleRowSelection(row, true);
});
isFetching.value = false;
});
} else {
isFetching.value = false;
}
};
// ====================== 方法 ======================
// 获取列表
function getlist() {
isFetching.value = true; // 加锁
loading.value = true;
queryResidentList_Cars(queryParams.value)
.then((res) => {
tableData.value = res.rows;
total.value = res.total;
handleUserSelection();
})
.catch((error) => {
console.error('获取用户列表失败:', error);
})
.finally(() => {
loading.value = false;
isFetching.value = false; // 加锁
});
}
/** 楼栋列表 */
const buildinglist = ref([]);
/** 房屋列表 */
const unitlist = ref([]);
/** 筛选房屋类型 */
function handleChangeHouse(val: string) {
getBuildinglistAPI().then((res) => {
buildinglist.value = res.data;
});
}
/** 单元列表 */
function handleChangebuilding(val: string) {
getHouselistAPI(val).then((res) => {
unitlist.value = res.data.units;
});
}
// ==============================================
// ✅ 新增:处理 selection-change多选专用
// ==============================================
function handleSelectionChange(selection: BillUserlist[]) {
if (isFetching.value) return;
if (isMultiple.value) {
const currentSelectedIds = new Set(selection.map((item) => item.parkingId));
const otherPageSelections = multipleSelection.value.filter((item) => {
const isCurrentPageItem = tableData.value.some((row) => row.parkingId === item.parkingId);
if (isCurrentPageItem) {
return currentSelectedIds.has(item.parkingId);
} else {
return true;
}
});
// 3. 合并:其他页保留的 + 当前页新选中的
const finalSelection = [...otherPageSelections, ...selection];
// 4. 去重(以防万一)
const uniqueMap = new Map();
finalSelection.forEach((item) => uniqueMap.set(item.parkingId, item));
multipleSelection.value = Array.from(uniqueMap.values());
console.log(multipleSelection.value.map((item) => item.parkingId));
} else {
// --- 单选逻辑 ---
if (selection.length > 0) {
const lastSelected = selection[selection.length - 1];
multipleSelection.value = [lastSelected];
nextTick(() => {
if (selection.length > 1) {
multipleTableRef.value?.clearSelection();
multipleTableRef.value?.toggleRowSelection(lastSelected, true);
}
});
} else {
multipleSelection.value = [];
}
}
}
// ==============================================
// 最佳单选方案:点击行选中,无报错、无循环、最稳定
// ==============================================
// 优化 handleRowClick避免重复
function handleRowClick(row: BillUserlist) {
if (!multipleTableRef.value) return;
if (isMultiple.value) {
const index = multipleSelection.value.findIndex((item) => item.parkingId === row.parkingId);
if (index > -1) {
// 取消选中
multipleSelection.value.splice(index, 1);
multipleTableRef.value?.toggleRowSelection(row, false);
} else {
// 选中
multipleSelection.value.push(row);
multipleTableRef.value?.toggleRowSelection(row, true);
}
} else {
// 【单选模式】先清空,再选中当前行
multipleSelection.value = [];
multipleTableRef.value.clearSelection();
multipleSelection.value = [row];
multipleTableRef.value.toggleRowSelection(row, true);
}
}
// 重置查询
function resetQuery() {
queryParams.value = {
pageNum: 1,
pageSize: 10
};
getlist();
}
// 退出
function conback() {
dialog.value.dialogVisible = false;
resetQuery();
}
// ==============================================
// ✅ 核心修改:根据 returnvalue 返回对应的值
// ==============================================
function confirm() {
dialog.value.dialogVisible = false;
if (isMultiple.value) {
// 多选
if (returnvalue.value === '*') {
// ✅ 返回整个对象数组
emit('change', multipleSelection.value);
} else {
// 返回指定字段数组
emit(
'change',
multipleSelection.value.map((item) => item[returnvalue.value as keyof BillUserlist])
);
}
} else {
// 单选
if (returnvalue.value === '*') {
// ✅ 返回整个对象
emit('change', multipleSelection.value[0]);
} else {
// 返回指定字段
emit('change', multipleSelection.value[0][returnvalue.value as keyof BillUserlist]);
}
}
multipleSelection.value = [];
multipleTableRef.value.clearSelection();
}
defineExpose({
open: (arr: string[] = []) => {
userid.value = arr;
if (arr && arr.length > 0) {
multipleSelection.value = arr.map(
(id) =>
({
parkingId: id
}) as BillUserlist
);
console.log(multipleSelection.value);
} else {
multipleSelection.value = [];
}
getlist();
dialog.value.dialogVisible = true;
}
});
</script>
<style scoped lang="scss">
.residentBox {
width: 385px;
height: 35px;
line-height: 35px;
border: 1px solid #ccc;
border-radius: 5px;
padding: 0 10px;
.defaultbox {
display: flex;
align-items: center;
justify-content: space-between;
}
cursor: pointer;
&:hover {
background-color: #f4f8ff;
}
}
</style>

View File

@@ -0,0 +1,251 @@
<template>
<slot name="default" />
<el-dialog append-to-body v-model="dialog.dialogVisible" :title="dialog.dialogTitle" width="1050">
<el-table v-loading="loading" ref="multipleTableRef" :data="tableData" border @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="业主" align="center">
<template #default="scope">
<span>{{ scope.row.user_name ? scope.row.user_name : '---' }}</span>
</template>
</el-table-column>
<el-table-column label="手机号" align="center" prop="user_phone" />
</el-table>
<template #footer>
<div class="dialog-footer flex flex-items-center flex-justify-between">
<pagination
style="margin-top: 0"
layout="total, prev, pager, next, jumper"
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getlist"
/>
<div>
<el-button @click="conback">退出</el-button>
<el-button type="primary" @click="confirm"> 确定 </el-button>
</div>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { BillUserlist } from '@/api/system/SdbBill';
import { ref, nextTick, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
import { getAppUser } from '@/api/system/cars';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const props = defineProps({
returnvalue: {
type: String,
default: '*'
},
titleName: {
type: String,
default: '选择业主'
},
/** 是否多选 false 单选 */
isMultiple: {
type: Boolean,
default: false
}
});
const { titleName, returnvalue } = toRefs(props);
const userid = ref([]);
// ✅ 手动设置默认值(替代 withDefaults
const emit = defineEmits<{
change: [value: string | number | (string | number)[] | BillUserlist | BillUserlist[]]; // 支持返回数组
}>();
const dialog = ref({
dialogVisible: false,
dialogTitle: titleName.value
});
const queryParams = ref({
pageNum: 1,
pageSize: 10
});
const multipleTableRef = ref();
const tableData = ref<BillUserlist[]>([]);
const total = ref(0);
const loading = ref(false);
const isFetching = ref(false);
// 存储选中项
const multipleSelection = ref<BillUserlist[]>([]);
const isMultiple = computed(() => props.isMultiple ?? false);
// 优化 handleUserSelection避免重复查找
const handleUserSelection = () => {
isFetching.value = true;
if (!multipleTableRef.value || !tableData.value.length) {
isFetching.value = false;
return;
}
const rowsToSelect = tableData.value.filter((row) => {
return multipleSelection.value.some((selected) => selected.parkingId === row.parkingId);
});
multipleTableRef.value.clearSelection();
if (rowsToSelect.length > 0) {
nextTick(() => {
rowsToSelect.forEach((row) => {
multipleTableRef.value?.toggleRowSelection(row, true);
});
isFetching.value = false;
});
} else {
isFetching.value = false;
}
};
// ====================== 方法 ======================
// 获取列表
function getlist() {
isFetching.value = true; // 加锁
loading.value = true;
getAppUser(queryParams.value)
.then((res) => {
tableData.value = res.rows;
total.value = res.total;
handleUserSelection();
})
.catch((error) => {
console.error('获取用户列表失败:', error);
})
.finally(() => {
loading.value = false;
isFetching.value = false; // 加锁
});
}
// ==============================================
// ✅ 新增:处理 selection-change多选专用
// ==============================================
function handleSelectionChange(selection: BillUserlist[]) {
if (isFetching.value) return;
if (isMultiple.value) {
const currentSelectedIds = new Set(selection.map((item) => item.parkingId));
const otherPageSelections = multipleSelection.value.filter((item) => {
const isCurrentPageItem = tableData.value.some((row) => row.parkingId === item.parkingId);
if (isCurrentPageItem) {
return currentSelectedIds.has(item.parkingId);
} else {
return true;
}
});
// 3. 合并:其他页保留的 + 当前页新选中的
const finalSelection = [...otherPageSelections, ...selection];
// 4. 去重(以防万一)
const uniqueMap = new Map();
finalSelection.forEach((item) => uniqueMap.set(item.parkingId, item));
multipleSelection.value = Array.from(uniqueMap.values());
console.log(multipleSelection.value.map((item) => item.parkingId));
} else {
// --- 单选逻辑 ---
if (selection.length > 0) {
const lastSelected = selection[selection.length - 1];
multipleSelection.value = [lastSelected];
nextTick(() => {
if (selection.length > 1) {
multipleTableRef.value?.clearSelection();
multipleTableRef.value?.toggleRowSelection(lastSelected, true);
}
});
} else {
multipleSelection.value = [];
}
}
}
// 重置查询
function resetQuery() {
queryParams.value = {
pageNum: 1,
pageSize: 10
};
getlist();
}
// 退出
function conback() {
dialog.value.dialogVisible = false;
resetQuery();
}
// ==============================================
// ✅ 核心修改:根据 returnvalue 返回对应的值
// ==============================================
function confirm() {
dialog.value.dialogVisible = false;
if (isMultiple.value) {
// 多选
if (returnvalue.value === '*') {
// ✅ 返回整个对象数组
emit('change', multipleSelection.value);
} else {
// 返回指定字段数组
emit(
'change',
multipleSelection.value.map((item) => item[returnvalue.value as keyof BillUserlist])
);
}
} else {
// 单选
if (returnvalue.value === '*') {
// ✅ 返回整个对象
emit('change', multipleSelection.value[0]);
} else {
// 返回指定字段
emit('change', multipleSelection.value[0][returnvalue.value as keyof BillUserlist]);
}
}
multipleSelection.value = [];
multipleTableRef.value.clearSelection();
}
defineExpose({
open: (arr: string[] = []) => {
userid.value = arr;
if (arr && arr.length > 0) {
multipleSelection.value = arr.map(
(id) =>
({
parkingId: id
}) as BillUserlist
);
console.log(multipleSelection.value);
} else {
multipleSelection.value = [];
}
getlist();
dialog.value.dialogVisible = true;
}
});
</script>
<style scoped lang="scss">
.residentBox {
width: 385px;
height: 35px;
line-height: 35px;
border: 1px solid #ccc;
border-radius: 5px;
padding: 0 10px;
.defaultbox {
display: flex;
align-items: center;
justify-content: space-between;
}
cursor: pointer;
&:hover {
background-color: #f4f8ff;
}
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<slot name="default" />
<el-dialog append-to-body v-model="dialog.dialogVisible" :title="dialog.dialogTitle" width="1250">
<nav class="flex flex-items-center mb-8">
<el-dialog append-to-body v-model="dialog.dialogVisible" :title="dialog.dialogTitle" width="1400">
<nav class="flex flex-items-center mb-8 justify-between">
<!-- <div class="flex flex-items-center">
<div style="width: 70px; text-align: right; padding-right: 10px">房屋类型</div>
<div class="flex-1">
@@ -11,17 +11,18 @@
</el-select>
</div>
</div> -->
<div class="flex flex-items-center">
<div class="flex flex-items-center">
<div style="width: 50px; text-align: right; padding-right: 10px">楼栋</div>
<div class="flex-1"></div>
<el-select @change="handleChangebuilding" clearable style="width: 140px" v-model="queryParams.buildingId">
<el-select @change="handleChangebuilding" clearable style="width: 120px" v-model="queryParams.buildingId">
<el-option v-for="(item, index) in buildinglist" :key="index" :label="item.buildingName" :value="item.id"></el-option>
</el-select>
</div>
<div class="flex flex-items-center mr-20px">
<div style="width: 50px; text-align: right; padding-right: 10px">单元</div>
<div class="flex-1">
<el-select @change="handleChangeUnit" clearable style="width: 140px" v-model="queryParams.unitNoId">
<el-select @change="handleChangeUnit" clearable style="width: 120px" v-model="queryParams.unitNoId">
<el-option v-for="(item, index) in unitlist" :key="index" :label="item.unitNoName" :value="item.unitNoId"></el-option>
</el-select>
</div>
@@ -29,15 +30,22 @@
<div class="flex flex-items-center mr-20px">
<div style="width: 80px; text-align: right; padding-right: 10px">房屋类型</div>
<div class="flex-1">
<el-select clearable @change="handleHouseUseChange" style="width: 140px" v-model="queryParams.houseUseTypeId">
<el-select clearable @change="handleHouseUseChange" style="width: 120px" v-model="queryParams.houseUseTypeId">
<el-option v-for="(item, index) in houseUseTypeList" :key="index" :label="item.name" :value="item.id"></el-option>
</el-select>
</div>
</div>
<div class="flex flex-items-center mr-20px">
<div style="width: 60px; text-align: right; padding-right: 10px">房屋号</div>
<div class="flex-1">
<el-input clearable @clear="handleClear" style="width: 120px" v-model="queryParams.houseNo" placeholder="请输入房屋号"></el-input>
</div>
</div>
<div class="flex flex-items-center mr-20px">
<div style="width: 80px; text-align: right; padding-right: 10px">业主名称</div>
<div class="flex-1">
<el-input clearable @clear="handleClear" v-model="queryParams.residentName" placeholder="查找业主"></el-input>
<el-input clearable @clear="handleClear" style="width: 120px" v-model="queryParams.residentName" placeholder="请输入业主名称"></el-input>
</div>
</div>
</div>
<div>
@@ -45,14 +53,7 @@
<el-button @click="resetQuery">重置</el-button>
</div>
</nav>
<el-table
v-loading="loading"
@row-click="handleRowClick"
ref="multipleTableRef"
:data="tableData"
border
@selection-change="handleSelectionChange"
>
<el-table row-key="houseId" v-loading="loading" ref="multipleTableRef" :data="tableData" border @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" width="55" type="index" />
<el-table-column label="小区" align="center" prop="villageName" />
@@ -87,7 +88,7 @@
<script setup lang="ts">
import { getBuildinglistAPI, getBuildingOnly, getHouselistAPI } from '@/api/system/house';
import { listHouseUseType } from '@/api/system/houseUse';
import { listHouseUseType, listHouseUseType_add_house } from '@/api/system/houseUse';
import { BillHouse, listBillHouse } from '@/api/system/SdbBill';
import { ref, watch, nextTick, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
@@ -128,7 +129,7 @@ const dialog = ref({
const queryParams = ref({
pageNum: 1,
pageSize: 10,
// houseUseTypeId: null,
houseNo: '',
buildingId: null,
unitNoId: null,
houseUseTypeId: null,
@@ -147,29 +148,35 @@ const isMultiple = computed(() => props.isMultiple ?? false);
// 优化 handleUserSelection避免重复查找
const handleUserSelection = () => {
if (!tableData.value.length) return;
multipleSelection.value = [];
if (showids.value.length > 0) {
const arr = showids.value;
arr.forEach((item) => {
const target = tableData.value.find((row) => row.houseId == item);
target && multipleSelection.value.push(target);
});
nextTick(() => {
if (multipleTableRef.value) {
multipleTableRef.value.clearSelection();
multipleSelection.value.forEach((item) => {
multipleTableRef.value.toggleRowSelection(item, true);
});
isFetching.value = true;
if (!multipleTableRef.value || !tableData.value.length) {
isFetching.value = false;
return;
}
// 1. 获取当前页所有行的 houseId 集合,用于快速判断
// const currentPageIds = new Set(tableData.value.map((item) => item.houseId));
const rowsToSelect = tableData.value.filter((row) => {
return multipleSelection.value.some((selected) => selected.houseId === row.houseId);
});
multipleTableRef.value.clearSelection();
if (rowsToSelect.length > 0) {
nextTick(() => {
rowsToSelect.forEach((row) => {
multipleTableRef.value?.toggleRowSelection(row, true);
});
isFetching.value = false;
});
} else {
isFetching.value = false;
}
};
// ====================== 方法 ======================
const houseUseTypeList = ref([]);
const isFetching = ref(false);
// 获取列表
function getlist() {
isFetching.value = true; // 🔒 加锁:禁止 selection-change 更新数据
loading.value = true;
listBillHouse(queryParams.value)
.then((res) => {
@@ -182,6 +189,7 @@ function getlist() {
})
.finally(() => {
loading.value = false;
isFetching.value = false;
});
}
@@ -225,34 +233,44 @@ function handleChangeUnit() {
// ✅ 新增:处理 selection-change多选专用
// ==============================================
function handleSelectionChange(selection: BillHouse[]) {
// 🛑 如果正在获取数据/恢复状态,直接返回,不要更新 multipleSelection
if (isFetching.value) return;
if (isMultiple.value) {
multipleSelection.value = selection;
}
}
// ==============================================
// 最佳单选方案:点击行选中,无报错、无循环、最稳定
// ==============================================
// 优化 handleRowClick避免重复
function handleRowClick(row: BillHouse) {
if (!multipleTableRef.value) return;
if (isMultiple.value) {
const index = multipleSelection.value.findIndex((item) => item.houseId === row.houseId);
if (index > -1) {
// 取消选中
multipleSelection.value.splice(index, 1);
multipleTableRef.value?.toggleRowSelection(row, false);
// 1. 获取当前表格中所有选中行的 ID
const currentSelectedIds = new Set(selection.map((item) => item.houseId));
const otherPageSelections = multipleSelection.value.filter((item) => {
// 判断 item 是否属于当前页
const isCurrentPageItem = tableData.value.some((row) => row.houseId === item.houseId);
if (isCurrentPageItem) {
// 如果是当前页的数据,检查是否在当前 selection 中
return currentSelectedIds.has(item.houseId);
} else {
// 选中
multipleSelection.value.push(row);
multipleTableRef.value?.toggleRowSelection(row, true);
// 如果不是当前页的数据,保留(假设是之前页选的)
return true;
}
});
// 去重合并
const finalSelection = [...otherPageSelections, ...selection];
// 再次去重以防万一
const uniqueMap = new Map();
finalSelection.forEach((item) => uniqueMap.set(item.houseId, item));
multipleSelection.value = Array.from(uniqueMap.values());
console.log(multipleSelection.value.map((item) => item.houseId));
} else {
// ✅ 补充单选模式下selection-change 触发时,直接取最新的一项
// 这样配合 handleRowClick 的 clearSelection + toggle 就能实现完美单选
if (selection.length > 0) {
const lastSelected = selection[selection.length - 1];
multipleSelection.value = [lastSelected];
nextTick(() => {
if (selection.length > 1) {
multipleTableRef.value?.clearSelection();
multipleTableRef.value?.toggleRowSelection(lastSelected, true);
}
});
} else {
// 【单选模式】先清空,再选中当前行
multipleSelection.value = [];
multipleTableRef.value.clearSelection();
multipleSelection.value = [row];
multipleTableRef.value.toggleRowSelection(row, true);
}
}
}
// 重置查询
@@ -263,6 +281,7 @@ function resetQuery() {
houseUseTypeId: null,
buildingId: null,
unitNoId: null,
houseNo: '',
residentName: ''
};
getlist();
@@ -308,15 +327,20 @@ function confirm() {
defineExpose({
open: (showidArray: string[] = []) => {
multipleSelection.value = [];
showids.value = showidArray;
if (showidArray && showidArray.length > 0) {
multipleSelection.value = showidArray.map(
(id) =>
({
houseId: id
}) as BillHouse
);
}
getlist();
dialog.value.dialogVisible = true;
listHouseUseType({
pageNum: 1,
pageSize: 99999
}).then((res) => {
houseUseTypeList.value = res.rows;
listHouseUseType_add_house().then((res) => {
houseUseTypeList.value = res.data;
});
handleChangeHouse();
}

View File

@@ -28,8 +28,7 @@
</div>
</nav>
<!-- 关键去掉 @selection-change改用 row-click 实现单选永不报错 -->
<el-table ref="multipleTableRef" :data="tableData" border @row-click="handleRowClick" @selection-change="handleSelectionChange">
<el-table ref="multipleTableRef" row-key="id" :data="tableData" border @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" width="55" type="index" />
<el-table-column label="姓名" align="center" prop="name" />
@@ -38,6 +37,11 @@
<dict-tag :options="com_resident_type" :value="row.type" />
</template>
</el-table-column>
<el-table-column label="与业主关系" align="center" width="95" prop="ownerRelationship">
<template #default="{ row }">
<dict-tag :options="com_resident_relationship" :value="row.ownerRelationship" />
</template>
</el-table-column>
<el-table-column label="状态" align="center" width="85" prop="status">
<template #default="{ row }">
<dict-tag :options="com_certification_status" :value="row.status" />
@@ -45,7 +49,7 @@
</el-table-column>
<el-table-column label="证件号码" align="center" prop="idCard" />
<el-table-column label="手机号码" align="center" prop="phone" />
<el-table-column label="房间" align="center">
<el-table-column label="房间" show-overflow-tooltip align="center">
<template #default="{ row }">
{{ row.housePath }}
</template>
@@ -75,7 +79,7 @@
import { listResident } from '@/api/system/resident';
import { ResidentVO } from '@/api/system/resident/type';
import { getHousePathById } from '@/utils/house';
import { ref, watch, nextTick, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
import { ref, nextTick, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
import { useHouseStore } from '@/store/modules/house';
const houseStore = useHouseStore();
@@ -111,7 +115,7 @@ const props = defineProps<{
// ✅ 手动设置默认值(替代 withDefaults
const isMultiple = computed(() => props.isMultiple ?? false);
const returnvalue = computed(() => props.returnvalue ?? 'id'); // 👈 默认 userId
const returnvalue = computed(() => props.returnvalue ?? '*'); // 👈 默认 userId
const { userid } = toRefs(props);
@@ -140,50 +144,66 @@ const total = ref(0);
const loading = ref(false);
// ==============================================
// ✅ 新增:处理 selection-change(多选专用)
// ✅ 优化:处理 selection-change
// ==============================================
function handleSelectionChange(selection: tableType[]) {
if (isMultiple.value) {
multipleSelection.value = selection;
}
}
// 多选逻辑:合并当前页选中项和之前页选中项
const currentSelectedIds = new Set(selection.map((item) => item.id));
// ==============================================
// 最佳单选方案:点击行选中,无报错、无循环、最稳定
// ==============================================
function handleRowClick(row) {
if (isMultiple.value) {
// 多选:点击行切换选中状态
const isSelected = multipleSelection.value.some((item) => item.id === row.id);
if (isSelected) {
multipleSelection.value = multipleSelection.value.filter((item) => item.id !== row.id);
multipleTableRef.value?.toggleRowSelection(row, false);
} else {
multipleSelection.value.push(row);
multipleTableRef.value?.toggleRowSelection(row, true);
// 过滤掉当前页未选中的旧数据
const otherPageSelections = multipleSelection.value.filter((item) => {
const isCurrentPageItem = tableData.value.some((row) => row.id === item.id);
if (isCurrentPageItem) {
return currentSelectedIds.has(item.id);
}
return true; // 保留其他页的数据
});
// 合并当前页新选中的数据
const finalSelection = [...otherPageSelections, ...selection];
// 去重
const uniqueMap = new Map();
finalSelection.forEach((item) => uniqueMap.set(item.id, item));
multipleSelection.value = Array.from(uniqueMap.values());
} else {
// 单选:原有逻辑保持不变
// 单选逻辑:始终只保留最后一个选中的项
if (selection.length > 0) {
// 获取最后选中的那一项
const lastSelected = selection[selection.length - 1];
multipleSelection.value = [lastSelected];
// 关键:如果 selection 长度大于1比如之前选了一个现在又选一个el-table可能暂时上报两个
// 我们需要手动清除表格中其他的勾选状态,只保留最后一个
nextTick(() => {
if (selection.length > 1) {
multipleTableRef.value?.clearSelection();
multipleTableRef.value?.toggleRowSelection(row, true);
multipleSelection.value = [row];
multipleTableRef.value?.toggleRowSelection(lastSelected, true);
}
});
} else {
multipleSelection.value = [];
}
}
}
// ==============================================
// ✅ 修改:支持多选回显
// ==============================================
// ✅ 优化:回显逻辑
const handleUserSelection = () => {
if (!userid.value || !tableData.value.length) return;
if (!multipleTableRef.value) return;
// 先清空所有选择
multipleTableRef.value.clearSelection();
multipleSelection.value = [];
if (!props.userid && props.userid !== 0) return;
nextTick(() => {
multipleTableRef.value?.clearSelection();
if (isMultiple.value && Array.isArray(userid.value)) {
if (isMultiple.value && Array.isArray(props.userid)) {
// 多选回显
const selectedItems: tableType[] = [];
userid.value.forEach((id) => {
const findItem = tableData.value.find((item) => item.userId == id);
props.userid.forEach((id) => {
const findItem = tableData.value.find((item) => item.id == id || item.userId == id);
if (findItem) {
selectedItems.push(findItem);
multipleTableRef.value?.toggleRowSelection(findItem, true);
@@ -191,28 +211,30 @@ const handleUserSelection = () => {
});
multipleSelection.value = selectedItems;
} else {
// 单选回显(原有逻辑)
const findItem = tableData.value.find((item) => item.id == userid.value);
// 单选回显
// 注意props.userid 可能是单个 ID
const targetId = Array.isArray(props.userid) ? props.userid[0] : props.userid;
const findItem = tableData.value.find((item) => item.id == targetId || item.userId == targetId);
if (findItem) {
multipleSelection.value = [findItem];
multipleTableRef.value?.toggleRowSelection(findItem, true);
multipleSelection.value = [findItem];
}
}
});
};
watch(userid, handleUserSelection, { immediate: true });
watch(tableData, handleUserSelection);
// 打开选择框
function choiceResidentFun() {
// 初始化
getlist();
dialog.value.dialogVisible = true;
}
// 获取列表
function getlist() {
loading.value = true;
listResident(queryParams.value).then((res) => {
listResident(queryParams.value)
.then((res) => {
tableData.value = res.rows;
tableData.value.forEach((item) => {
if (item.houseId !== null) {
@@ -220,7 +242,10 @@ function getlist() {
item.housePath = houselist?.join('-') || '';
}
});
handleUserSelection();
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
}
@@ -268,6 +293,7 @@ function confirm() {
}
} else {
// 单选
console.log(multipleSelection.value[0], multipleSelection.value);
if (returnvalue.value === '*') {
// ✅ 返回整个对象
emit('change', multipleSelection.value[0]);
@@ -276,13 +302,21 @@ function confirm() {
emit('change', multipleSelection.value[0][returnvalue.value as keyof tableType]);
}
}
} else {
emit('change', null);
}
}
// 初始化
getlist();
defineExpose({
open: () => {
open: (arr) => {
if (arr && arr.length > 0) {
multipleSelection.value = arr.map((id) => ({
id: id
}));
} else {
multipleSelection.value = [];
}
getlist();
dialog.value.dialogVisible = true;
}
});

View File

@@ -2,12 +2,12 @@
<slot v-if="$slots.default" />
<div v-else class="residentBox" @click="choiceResidentFun">
<div class="defaultbox" v-if="multipleSelection.length === 0">
<div class="defaultbox" v-if="multipleSelection.length === 0 && props.username === null">
<span>请选择</span>
<el-icon><Search /></el-icon>
</div>
<div v-else class="overflow-auto overflow-hidden w-full text-ellipsis text-nowrap">
<span>姓名{{ multipleSelection[0]?.name }}</span>
<span>姓名{{ multipleSelection[0]?.nickName ?? props.username }}</span>
</div>
</div>
@@ -30,7 +30,7 @@
<el-button @click="resetQuery">重置</el-button>
</div>
</nav>
<el-table @row-click="handleRowClick" ref="multipleTableRef" :data="tableData" border @selection-change="handleSelectionChange">
<el-table ref="multipleTableRef" :data="tableData" border @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" width="55" type="index" />
<el-table-column label="姓名" align="center" prop="nickName" />
@@ -80,9 +80,13 @@ const props = defineProps({
type: String,
default: ''
},
username: {
type: String,
default: null
},
returnvalue: {
type: String,
default: 'id'
default: 'userId'
},
titleName: {
type: String,
@@ -121,6 +125,7 @@ const multipleTableRef = ref();
const tableData = ref<tableType[]>([]);
const total = ref(0);
const loading = ref(false);
const isFetching = ref(false); // 加锁,防止重复触发 selection-change
// 存储选中项
const multipleSelection = ref<tableType[]>([]);
@@ -129,29 +134,31 @@ const isMultiple = computed(() => props.isMultiple ?? false);
// 优化 handleUserSelection避免重复查找
const handleUserSelection = () => {
if (!userid.value || !tableData.value.length) return;
multipleSelection.value = [];
if (userid.value.trim() !== '') {
const arr = userid.value.split(',');
arr.forEach((item) => {
const target = tableData.value.find((row) => row.userId == item);
target && multipleSelection.value.push(target);
});
nextTick(() => {
if (multipleTableRef.value) {
multipleTableRef.value.clearSelection();
multipleSelection.value.forEach((item) => {
multipleTableRef.value.toggleRowSelection(item, true);
});
isFetching.value = true;
console.log('获取列表');
if (tableData.value.length <= 0 || !multipleTableRef.value) {
isFetching.value = false;
return;
}
const rowsToSelect = tableData.value.filter((row) => {
return multipleSelection.value.some((selected) => selected.userId === row.userId);
});
multipleTableRef.value.clearSelection();
if (rowsToSelect.length > 0) {
nextTick(() => {
rowsToSelect.forEach((row) => {
multipleTableRef.value?.toggleRowSelection(row, true);
});
isFetching.value = false;
});
} else {
isFetching.value = false;
}
};
// 监听
// 监听优化:只在 userid 变化时触发
// watch(userid, handleUserSelection, { immediate: true });
watch(userid, handleUserSelection, { immediate: true });
// watch(tableData, () => {
// if (userid.value) handleUserSelection(); // 仅在有 userid 时重新处理
// });
@@ -160,6 +167,17 @@ const handleUserSelection = () => {
// 打开选择框
function choiceResidentFun() {
dialog.value.dialogVisible = true;
multipleSelection.value = []; // 打开时清空之前的选择
if (typeof props.userid === 'string' && props.userid.trim() !== '') {
multipleSelection.value = [
{
userId: props.userid
} as ResidentVO
];
} else if (Array.isArray(props.userid)) {
multipleSelection.value = props.userid.map((id) => ({ userId: id })) as tableType[];
}
getlist();
}
// 获取列表
@@ -167,8 +185,9 @@ function getlist() {
loading.value = true;
getUserByRoleKey(userType.value)
.then((res) => {
tableData.value = res.data;
tableData.value = res.rows;
total.value = res.total;
handleUserSelection();
})
.catch((error) => {
console.error('获取用户列表失败:', error);
@@ -177,40 +196,45 @@ function getlist() {
loading.value = false;
});
}
// ==============================================
// ✅ 新增:处理 selection-change多选专用
// ==============================================
function handleSelectionChange(selection: tableType[]) {
if (isFetching.value) return;
if (isMultiple.value) {
multipleSelection.value = selection;
}
}
// ==============================================
// 最佳单选方案:点击行选中,无报错、无循环、最稳定
// ==============================================
// 优化 handleRowClick避免重复
function handleRowClick(row: tableType) {
if (!multipleTableRef.value) return;
if (isMultiple.value) {
const index = multipleSelection.value.findIndex((item) => item.id === row.id);
if (index > -1) {
// 取消选中
multipleSelection.value.splice(index, 1);
multipleTableRef.value?.toggleRowSelection(row, false);
const currentSelectedIds = new Set(selection.map((item) => item.id));
const otherPageSelections = multipleSelection.value.filter((item) => {
const isCurrentPageItem = tableData.value.some((row) => row.id === item.id);
if (isCurrentPageItem) {
return currentSelectedIds.has(item.id);
} else {
// 选中
multipleSelection.value.push(row);
multipleTableRef.value?.toggleRowSelection(row, true);
return true;
}
});
// 3. 合并:其他页保留的 + 当前页新选中的
const finalSelection = [...otherPageSelections, ...selection];
// 4. 去重(以防万一)
const uniqueMap = new Map();
finalSelection.forEach((item) => uniqueMap.set(item.id, item));
multipleSelection.value = Array.from(uniqueMap.values());
console.log(multipleSelection.value.map((item) => item.id));
} else {
// --- 单选逻辑 ---
if (selection.length > 0) {
const lastSelected = selection[selection.length - 1];
multipleSelection.value = [lastSelected];
nextTick(() => {
if (selection.length > 1) {
multipleTableRef.value?.clearSelection();
multipleTableRef.value?.toggleRowSelection(lastSelected, true);
}
});
} else {
// 【单选模式】先清空,再选中当前行
multipleSelection.value = [];
multipleTableRef.value.clearSelection();
multipleSelection.value = [row];
multipleTableRef.value.toggleRowSelection(row, true);
}
}
console.log('handleSelectionChange', selection);
}
// 重置查询
function resetQuery() {
@@ -250,23 +274,21 @@ function confirm() {
// 单选
if (returnvalue.value === '*') {
// ✅ 返回整个对象
emit('change', multipleSelection.value[0]);
emit('change', multipleSelection.value[multipleSelection.value.length - 1]);
} else {
// 返回指定字段
emit('change', multipleSelection.value[0][returnvalue.value as keyof tableType]);
emit('change', multipleSelection.value[multipleSelection.value.length - 1][returnvalue.value as keyof tableType]);
}
}
multipleSelection.value = [];
multipleTableRef.value.clearSelection();
}
// 初始化
getlist();
onMounted(() => {
// 初始化
});
defineExpose({
open: () => {
dialog.value.dialogVisible = true;
handleUserSelection();
getlist();
}
});
</script>

View File

@@ -1,7 +1,7 @@
<template>
<slot name="default" />
<el-dialog append-to-body v-model="dialog.dialogVisible" :title="dialog.dialogTitle" width="1050">
<el-table @row-click="handleRowClick" ref="multipleTableRef" :data="tableData" border @selection-change="handleSelectionChange">
<el-table ref="multipleTableRef" :data="tableData" border @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" width="55" type="index" />
<el-table-column label="业主" align="center">
@@ -49,7 +49,7 @@ const { sys_user_sex } = toRefs<any>(proxy?.useDict('sys_user_sex'));
const props = defineProps({
returnvalue: {
type: String,
default: 'houseId'
default: 'residentId'
},
titleName: {
type: String,
@@ -84,7 +84,7 @@ const multipleTableRef = ref();
const tableData = ref<BillUserlist[]>([]);
const total = ref(0);
const loading = ref(false);
const isFetching = ref(false);
// 存储选中项
const multipleSelection = ref<BillUserlist[]>([]);
@@ -92,29 +92,32 @@ const isMultiple = computed(() => props.isMultiple ?? false);
// 优化 handleUserSelection避免重复查找
const handleUserSelection = () => {
if (!tableData.value.length) return;
multipleSelection.value = [];
const arr = userid.value;
if (!arr.length) return;
arr.forEach((item) => {
const target = tableData.value.find((row) => row.residentId == item);
target && multipleSelection.value.push(target);
});
nextTick(() => {
if (multipleTableRef.value) {
multipleTableRef.value.clearSelection();
multipleSelection.value.forEach((item) => {
multipleTableRef.value.toggleRowSelection(item, true);
});
isFetching.value = true;
if (!multipleTableRef.value || !tableData.value.length) {
isFetching.value = false;
return;
}
const rowsToSelect = tableData.value.filter((row) => {
return multipleSelection.value.some((selected) => selected.residentId === row.residentId);
});
multipleTableRef.value.clearSelection();
if (rowsToSelect.length > 0) {
nextTick(() => {
rowsToSelect.forEach((row) => {
multipleTableRef.value?.toggleRowSelection(row, true);
});
isFetching.value = false;
});
} else {
isFetching.value = false;
}
};
// ====================== 方法 ======================
// 获取列表
function getlist() {
isFetching.value = true; // 加锁
loading.value = true;
queryResidentList_holder(queryParams.value)
.then((res) => {
@@ -127,6 +130,7 @@ function getlist() {
})
.finally(() => {
loading.value = false;
isFetching.value = false; // 加锁
});
}
@@ -152,8 +156,31 @@ function handleChangebuilding(val: string) {
// ✅ 新增:处理 selection-change多选专用
// ==============================================
function handleSelectionChange(selection: BillUserlist[]) {
if (isFetching.value) return;
if (isMultiple.value) {
multipleSelection.value = selection;
const currentSelectedIds = new Set(selection.map((item) => item.residentId));
const otherPageSelections = multipleSelection.value.filter((item) => {
const isCurrentPageItem = tableData.value.some((row) => row.residentId === item.residentId);
if (isCurrentPageItem) {
return currentSelectedIds.has(item.residentId);
} else {
return true;
}
});
// 3. 合并:其他页保留的 + 当前页新选中的
const finalSelection = [...otherPageSelections, ...selection];
// 4. 去重(以防万一)
const uniqueMap = new Map();
finalSelection.forEach((item) => uniqueMap.set(item.residentId, item));
multipleSelection.value = Array.from(uniqueMap.values());
console.log(multipleSelection.value.map((item) => item.residentId));
} else {
// --- 单选逻辑 ---
if (selection.length > 0) {
multipleSelection.value = [selection[selection.length - 1]];
} else {
multipleSelection.value = [];
}
}
}
// ==============================================
@@ -232,6 +259,17 @@ function confirm() {
defineExpose({
open: (arr: string[] = []) => {
userid.value = arr;
if (arr && arr.length > 0) {
multipleSelection.value = arr.map(
(id) =>
({
residentId: id
}) as BillUserlist
);
} else {
multipleSelection.value = [];
}
getlist();
dialog.value.dialogVisible = true;
}

View File

@@ -22,17 +22,24 @@
@paste="onPaste"
@focus="activeIndex = index"
@blur="onBlur"
@compositionend="onCompositionEnd(index)"
/>
</div>
</div>
<!-- 错误提示 Element Plus 原生完全一致 -->
<!-- 错误提示 -->
<transition name="el-fade-in-linear">
<div v-if="hasError && errorMessage" class="plate-error">
{{ errorMessage }}
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { LicensePlateValidator } from '@/utils/Reg';
import { ref, watch, nextTick } from 'vue';
import { LicensePlateValidator } from '@/utils/Reg';
// 省份简称
const PROVINCES = [
'京',
'津',
@@ -66,7 +73,12 @@ const PROVINCES = [
'宁',
'琼'
];
const VALID_CHARS = /^[A-HJ-NP-Z0-9]$/i;
// 合法基础字符:字母 (排除I,O) 和 数字
const VALID_CHAR_REGEX = /^[A-HJ-NP-Z0-9]$/i;
// 车牌特殊结尾字符
const SPECIAL_CHARS = ['警', '学', '挂', '临', '超', '港', '澳'];
const props = defineProps<{
modelValue: string;
@@ -85,22 +97,23 @@ const chars = ref<string[]>(Array(8).fill(''));
const hasError = ref(false);
const errorMessage = ref('');
// 外部值同步
// 外部值同步到内部
watch(
() => props.modelValue,
(val) => {
const currentValue = chars.value.join('').trim();
if (val !== currentValue) {
const cleanVal = (val || '').toUpperCase().trim();
const currentJoined = chars.value.join('').trim();
if (cleanVal !== currentJoined) {
const newChars = Array(8).fill('');
if (val) {
val
.toUpperCase()
.split('')
.forEach((c, i) => {
if (i < 8) newChars[i] = c;
});
if (cleanVal) {
const arr = cleanVal.split('');
for (let i = 0; i < Math.min(arr.length, 8); i++) {
newChars[i] = arr[i];
}
}
chars.value = newChars;
if (cleanVal) clearError();
}
},
{ immediate: true }
@@ -109,44 +122,77 @@ watch(
// 内部值同步到外部
watch(
chars,
() => {
const value = chars.value.join('').trim();
(newChars) => {
const value = newChars.join('').trim();
emit('update:modelValue', value);
emit('change', value);
// 输入时清除错误
if (hasError.value) {
clearError();
}
},
{ deep: true }
);
function onInput(index: number) {
const char = chars.value[index].toUpperCase();
let char = chars.value[index];
if (index === 0 && !PROVINCES.includes(char)) {
chars.value[index] = '';
return;
if (!char) return;
// 统一转大写 (汉字不受影响)
char = char.toUpperCase();
let isValid = false;
// 1. 第一位:必须是省份
if (index === 0) {
if (PROVINCES.includes(char)) {
isValid = true;
}
}
// 2. 特殊字符处理 (警/学/挂/临/超/港/澳)
else if (SPECIAL_CHARS.includes(char)) {
// 特殊字符通常出现在最后一位,或者倒数第二位(如临时车牌的"临"可能在最后,也可能在倒数第二?不,通常是最后)
// 为了用户体验,允许在任何非第一位的位置输入,但最终校验会判断位置是否正确
isValid = true;
}
// 3. 常规字符:字母或数字
else {
if (VALID_CHAR_REGEX.test(char)) {
isValid = true;
}
}
if (index > 0 && !VALID_CHARS.test(char)) {
if (!isValid) {
chars.value[index] = '';
return;
}
} else {
chars.value[index] = char;
if (char && index < 7) {
nextTick(() => inputRefs.value[index + 1]?.focus());
// 如果输入的是特殊字符,通常意味着车牌结束,不再自动跳转
// 如果输入的是常规字符,且不是最后一格,则跳转
if (!SPECIAL_CHARS.includes(char) && char && index < 7) {
nextTick(() => {
inputRefs.value[index + 1]?.focus();
});
}
}
}
function onCompositionEnd(index: number) {
onInput(index);
}
function onKeydown(e: KeyboardEvent, index: number) {
if (e.key === 'Backspace' && !chars.value[index] && index > 0) {
if (e.key === 'Backspace') {
if (!chars.value[index] && index > 0) {
e.preventDefault();
nextTick(() => {
inputRefs.value[index - 1]?.focus();
inputRefs.value[index - 1]?.select();
});
}
return;
}
if (e.key === 'ArrowLeft' && index > 0) {
e.preventDefault();
@@ -160,20 +206,37 @@ function onKeydown(e: KeyboardEvent, index: number) {
function onPaste(e: ClipboardEvent) {
e.preventDefault();
const text = e.clipboardData?.getData('text')?.trim().toUpperCase() || '';
const text = e.clipboardData?.getData('text')?.toUpperCase().trim() || '';
if (!text) return;
const newChars = Array(8).fill('');
text.split('').forEach((c, i) => {
if (i >= 8) return;
if (i === 0 && PROVINCES.includes(c)) newChars[i] = c;
if (i > 0 && VALID_CHARS.test(c)) newChars[i] = c;
const filledChars = [...chars.value];
let insertPos = activeIndex.value >= 0 ? activeIndex.value : 0;
for (let i = 0; i < text.length; i++) {
if (insertPos >= 8) break;
const char = text[i];
let canInsert = false;
if (insertPos === 0) {
if (PROVINCES.includes(char)) canInsert = true;
} else if (SPECIAL_CHARS.includes(char)) {
canInsert = true;
} else {
if (VALID_CHAR_REGEX.test(char)) canInsert = true;
}
if (canInsert) {
filledChars[insertPos] = char;
insertPos++;
}
}
chars.value = filledChars;
nextTick(() => {
const focusIdx = Math.min(insertPos, 7);
inputRefs.value[focusIdx]?.focus();
});
chars.value = newChars;
const lastIndex = Math.min(text.length - 1, 6);
nextTick(() => inputRefs.value[lastIndex + 1]?.focus());
}
function onBlur() {
@@ -181,49 +244,50 @@ function onBlur() {
const hasFocus = inputRefs.value.some((input) => document.activeElement === input);
if (!hasFocus) {
activeIndex.value = -1;
validate();
}
}, 10);
}, 200);
}
// ✅ 手动验证方法(核心)
function validate(): { valid: boolean; message?: string } {
const value = chars.value.join('').trim();
// 必填验证
if (props.required && !value) {
hasError.value = true;
errorMessage.value = '请输入车牌号';
return { valid: false, message: errorMessage.value };
setError('请输入车牌号');
return { valid: false, message: '请输入车牌号' };
}
// 格式验证
if (value && !isValidPlate(value)) {
hasError.value = true;
errorMessage.value = '请输入正确的车牌号格式';
return { valid: false, message: errorMessage.value };
if (!value) {
clearError();
return { valid: true };
}
if (!LicensePlateValidator.isValidPlate(value)) {
setError('车牌号格式不正确');
return { valid: false, message: '车牌号格式不正确' };
}
// 验证通过
clearError();
return { valid: true };
}
// 车牌格式验证(内置,不依赖外部工具)
function isValidPlate(plate: string): boolean {
return LicensePlateValidator.isValidPlate(plate);
function setError(msg: string) {
hasError.value = true;
errorMessage.value = msg;
}
// 清除错误
function clearError() {
hasError.value = false;
errorMessage.value = '';
}
// 暴露方法
defineExpose({
focus: () => nextTick(() => inputRefs.value[0]?.focus()),
clear: () => {
chars.value = Array(8).fill('');
clearError();
emit('update:modelValue', '');
emit('change', '');
},
validate,
clearError
@@ -235,6 +299,7 @@ defineExpose({
display: flex;
flex-direction: column;
gap: 4px;
width: fit-content;
}
.plate_line {
@@ -242,15 +307,17 @@ defineExpose({
align-items: center;
gap: 6px;
}
.plate-item {
width: 24px;
height: 24px;
line-height: 24px;
width: 32px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
background-color: #fff;
}
.plate-item.active {
@@ -265,6 +332,7 @@ defineExpose({
.plate-item.disabled {
background-color: #f5f7fa;
cursor: not-allowed;
color: #a8abb2;
}
.plate-item input {
@@ -272,10 +340,13 @@ defineExpose({
height: 100%;
border: none;
text-align: center;
font-size: 15px;
font-size: 16px;
font-weight: 500;
background: transparent;
outline: none;
padding: 0;
margin: 0;
color: inherit;
}
.plate-error {
@@ -283,5 +354,6 @@ defineExpose({
font-size: 12px;
line-height: 1.5;
margin-top: 2px;
min-height: 18px;
}
</style>

View File

@@ -1,5 +1,6 @@
<template>
<section class="app-main">
<div style="background-color: #f2f1f6; height: 100%">
<router-view v-slot="{ Component, route }" :key="$route.fullPath">
<transition :enter-active-class="animate" mode="out-in">
<keep-alive :include="tagsViewStore.cachedViews">
@@ -8,6 +9,7 @@
</transition>
</router-view>
<iframe-toggle />
</div>
</section>
</template>
@@ -54,13 +56,14 @@ function addIframe() {
<style lang="scss" scoped>
.app-main {
/* 50= navbar 50 */
min-height: calc(100vh - 50px);
// min-height: calc(100vh - 50px);
flex: 1;
width: 100%;
position: relative;
overflow: hidden;
overflow-y: auto;
background-color: #f2f1f6;
background-color: #dfedff;
}
.fixed-header + .app-main {

View File

@@ -13,7 +13,7 @@
<template v-if="appStore.device !== 'mobile'">
<el-tooltip :content="proxy.$t('navbar.message')" effect="dark" placement="bottom">
<div>
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
<el-popover placement="bottom-end" trigger="click" transition="el-zoom-in-top" :width="400" :persistent="false">
<template #reference>
<el-badge :value="newNotice > 0 ? newNotice : ''" :max="99">
<div class="right-menu-item hover-effect" style="display: block"><svg-icon icon-class="message" /></div>
@@ -37,7 +37,7 @@
<el-dropdown-item divided command="changecommunity">
<span>{{ proxy.$t('navbar.community') }}</span>
</el-dropdown-item>
<router-link to="/user/profile">
<router-link to="/system/user/profile">
<el-dropdown-item>{{ proxy.$t('navbar.personalCenter') }}</el-dropdown-item>
</router-link>
<el-dropdown-item divided command="logout">
@@ -67,7 +67,6 @@ const props = defineProps({
default: true
}
});
console.log(props.Showhamburger);
const appStore = useAppStore();
const userStore = useUserStore();
@@ -160,6 +159,7 @@ watch(
overflow: hidden;
position: relative;
background: #dfedff;
background: transparent;
border-bottom: 1px solid #dfedff;
color: #c0c4cc;
@@ -233,6 +233,7 @@ watch(
.avatar-wrapper {
margin-top: 5px;
// margin-left: 20px;
position: relative;
.user-avatar {

View File

@@ -50,7 +50,8 @@ const activeMenu = computed(() => {
}
return path;
});
const bgColor = '#eaf2ff';
// const bgColor = '#eaf2ff';
const bgColor = '#dfedff';
// const bgColor = computed(() => (sideTheme.value === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground));
const textColor = computed(() => (sideTheme.value === 'theme-dark' ? variables.menuColor : variables.menuLightColor));
</script>

View File

@@ -19,13 +19,14 @@
</template>
<el-empty v-else :description="'消息为空'"></el-empty>
</div>
<div v-if="newsList.length > 0" class="foot-box" @click="onGoToGiteeClick">前往gitee</div>
</div>
</template>
<script setup lang="ts" name="layoutBreadcrumbUserNews">
import { useNoticeStore } from '@/store/modules/notice';
const router = useRouter();
const noticeStore = useNoticeStore();
const { readAll } = useNoticeStore();
@@ -48,13 +49,10 @@ const getTableData = async () => {
//点击消息,写入已读
const onNewsClick = (item: any) => {
newsList.value[item].read = true;
//并且写入pinia
noticeStore.state.notices = newsList.value;
};
// 前往通知中心点击
const onGoToGiteeClick = () => {
window.open('https://gitee.com/dromara/RuoYi-Vue-Plus/tree/5.X/');
router.push({
path: '/system/notice'
});
};
onMounted(() => {
@@ -85,7 +83,8 @@ onMounted(() => {
}
}
.content-box {
height: 300px;
height: 400px;
overflow: auto;
font-size: 13px;
.content-box-item {

View File

@@ -2,7 +2,7 @@
<!-- 👇 加这一层全局缩放容器 -->
<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
<side-bar class="sidebar-container" />
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container" id="main-container">
<div>
<navbar ref="navbarRef" @set-layout="setLayout" />
</div>

View File

@@ -12,6 +12,7 @@ import router from './router';
// 自定义指令
import directive from './directive';
import print from 'vue3-print-nb';
// 注册插件
import plugins from './plugins/index'; // plugins
@@ -54,6 +55,8 @@ app.use(store);
app.use(i18n);
app.use(VXETable);
app.use(plugins);
app.use(print);
// 自定义指令
directive(app);

View File

@@ -22,7 +22,18 @@ let isDynamicRoutesAdded = false;
const isWhiteList = (path: string) => {
return whiteList.some((pattern) => isPathMatch(pattern, path));
};
// ✅ 关键修改:记录当前已添加的动态路由名称列表
let currentDynamicRouteNames: string[] = [];
// ✅ 关键修改:清除所有已记录的动态路由
const removeDynamicRoutes = () => {
currentDynamicRouteNames.forEach((name) => {
if (name && router.hasRoute(name)) {
router.removeRoute(name);
}
});
// 清空记录数组
currentDynamicRouteNames = [];
};
router.beforeEach(async (to, from) => {
NProgress.start();
@@ -46,6 +57,7 @@ router.beforeEach(async (to, from) => {
}
if (userStore.roles.length === 0) {
isRelogin.show = true;
removeDynamicRoutes();
// 获取用户信息
const [err, res] = await tos(userStore.getInfo());
@@ -60,14 +72,19 @@ router.beforeEach(async (to, from) => {
permissionStore.resetRoutes();
// 生成动态路由
const accessRoutes = await permissionStore.generateRoutes();
// 添加路由(避免重复添加)
accessRoutes.forEach((route) => {
if (route.name && router.hasRoute(route.name)) {
router.removeRoute(route.name);
}
if (!isHttp(route.path) && !router.hasRoute(route.name)) {
router.addRoute(route);
// 记录顶层路由名称
if (route.name) {
currentDynamicRouteNames.push(route.name as string);
}
}
});
// ✅ 标记:路由已添加
isDynamicRoutesAdded = true;

View File

@@ -53,6 +53,7 @@ export default {
// 确认窗体
confirm(content: any): Promise<MessageBoxData> {
return ElMessageBox.confirm(content, '系统提示', {
dangerouslyUseHTMLString: true, // 关键开启html渲染<br>生效换行
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'

View File

@@ -54,6 +54,7 @@ export const constantRoutes: RouteRecordRaw[] = [
{
path: 'communitylist',
name: 'communitylist',
// system:village:list
hidden: true,
meta: {
title: '小区列表'
@@ -102,27 +103,19 @@ export const constantRoutes: RouteRecordRaw[] = [
// }
]
}
// {
// path: '/user',
// component: Layout,
// hidden: true,
// redirect: 'noredirect',
// children: [
// {
// path: 'profile',
// component: () => import('@/views/system/user/profile/index.vue'),
// name: 'Profile',
// meta: { title: '个人中心', icon: 'user' }
// }
// ]
// }
];
if (import.meta.env.VITE_APP_SHOW_WORK_PLATFORM === 'true') {
const find = constantRoutes.find((item) => item.path === '');
if (find) {
find.children?.push({
path: '/index',
component: () => import('@/views/workbench/index2.vue'),
component: () => {
if (import.meta.env.VITE_LOGIN_BACK_URL_WORKBENCH_TRUE === 'true') {
return import('@/views/workbench/index.vue');
} else {
return import('@/views/workbench/index2.vue');
}
},
name: 'Index',
meta: { title: '工作台', icon: 'dashboard', affix: true }
});

View File

@@ -73,40 +73,14 @@ export const useHouseStore = defineStore(
// 初始化
const initHouse = async () => {
currentBuildingInfoList.value = [];
resertInit();
// 获取当前小区 所有楼栋信息
const res2 = await getBuildinglistAPI();
currentBuildingInfoList.value = res2.data;
// 得到小区 内 楼栋列表 -> 切换到第一个小区
// 得到小区 内 楼栋列表 -> 切换到第一个
if (res2.data.length > 0) {
switchBuilding(res2.data[0].id);
}
// 获取当前小区 第一个楼栋信息
// // ! 不同住宅类型
// if ((currentBuildingInfo.value && currentBuildingInfo.value.id === null) || buildingtype.value !== currentbuildingtype) {
// currentBuildingInfo.value = res2.data[0];
// // ! 相同住宅类型
// } else if (buildingtype.value === currentbuildingtype) {
// // 更新当前楼栋的 信息
// currentBuildingInfo.value = res2.data.find((item) => item.id === currentBuildingInfo.value.id);
// }
// if (currentBuildingInfo.value && currentBuildingInfo.value.id) {
// // 获取当前 第一个楼栋的所有信息
// const res3 = await getHouselistAPI(currentBuildingInfo.value.id);
// currentHouseInfoList.value = res3.data;
// floorslist.value = new Set();
// // 得到所有的有房屋的 楼层 数组
// currentHouseInfoList.value.units.forEach((item) => {
// item.floors.forEach((floor) => {
// floorslist.value.add(floor.floorNo);
// });
// });
// if (buildingtype.value !== currentbuildingtype) {
// buildingtype.value = currentbuildingtype;
// }
// }
};
// 选中的房屋
@@ -173,6 +147,9 @@ export const useHouseStore = defineStore(
// 获取当前 楼栋的所有信息
const res3 = await getHouselistAPI(currentBuildingInfo.value.id, houseName, houseUseTypeId);
currentHouseInfoList.value = res3.data;
console.log(res3.data);
floorslist.value = null;
floorslist.value = new Set();
// 得到所有的有房屋的 楼层 数组
@@ -216,6 +193,24 @@ export const useHouseStore = defineStore(
});
};
function resertInit() {
currentBuildingInfo.value = {
'id': null,
'villageId': null,
'villageName': '',
'buildingName': '',
'buildingUse': null,
'buildToward': '',
'buildTall': '',
'buildStructure': '',
'floorCount': null,
'remark': ''
};
currentBuildingInfoList.value = [];
currentHouseInfoList.value = null;
floorslist.value.clear();
}
// 更新数据
const updateHouse = () => {
initHouse();

View File

@@ -8,13 +8,15 @@ interface NoticeItem {
time: string;
}
export const useNoticeStore = defineStore('notice', () => {
export const useNoticeStore = defineStore(
'notice',
() => {
const state = reactive({
notices: [] as NoticeItem[]
});
const addNotice = (notice: NoticeItem) => {
state.notices.push(notice);
state.notices.unshift(notice);
};
const removeNotice = (notice: NoticeItem) => {
@@ -38,4 +40,10 @@ export const useNoticeStore = defineStore('notice', () => {
readAll,
clearNotice
};
});
},
{
persist: {
storage: sessionStorage
}
}
);

View File

@@ -70,16 +70,20 @@ export const usePermissionStore = defineStore('permission', () => {
};
const setRoutes = (newRoutes: RouteRecordRaw[]): void => {
addRoutes.value = [];
addRoutes.value = newRoutes;
routes.value = constantRoutes.concat(newRoutes);
};
const setDefaultRoutes = (routes: RouteRecordRaw[]): void => {
defaultRoutes.value = [];
defaultRoutes.value = constantRoutes.concat(routes);
};
const setTopbarRoutes = (routes: RouteRecordRaw[]): void => {
topbarRouters.value = [];
topbarRouters.value = routes;
};
const setSidebarRouters = (routes: RouteRecordRaw[]): void => {
sidebarRouters.value = [];
sidebarRouters.value = routes;
};
@@ -104,10 +108,6 @@ export const usePermissionStore = defineStore('permission', () => {
const defaultRoutes = filterAsyncRouter(defaultData);
const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
asyncRoutes.forEach((route) => {
router.addRoute(route);
});
setRoutes(rewriteRoutes);
setSidebarRouters(constantRoutes.concat(sidebarRoutes));
setDefaultRoutes(sidebarRoutes);

1
src/types/env.d.ts vendored
View File

@@ -4,6 +4,7 @@ declare module '*.vue' {
export default Component;
}
declare module 'vue3-print-nb';
// 环境变量
interface ImportMetaEnv {
VITE_APP_TITLE: string;

View File

@@ -67,7 +67,7 @@ export function validator(value: string | number, regType: RegType): boolean {
/**
* 车牌验证工具类
* 支持传统燃油、新能源、军警、武警、使馆、临时、港澳等全量车牌
* 支持传统燃油、新能源、军警、武警、使馆、临时、港澳、挂车等全量车牌
*/
export class LicensePlateValidator {
/**
@@ -75,49 +75,37 @@ export class LicensePlateValidator {
*/
private static readonly PROVINCE = '[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼]';
/**
* 使领馆前缀
*/
private static readonly EMBASSY_PREFIX = '[使领]';
/**
* 验证传统燃油车牌 (蓝牌/黄牌)
* 格式: 省份 + 字母 + 5位字符
* 格式: 省份 + 字母 + 5位字符 (数字或字母排除I,O)
* 示例: 京A·12345, 粤B·88888
* 总长7位
*/
static isTraditionalPlate(plate: string): boolean {
// 包含普通民用以及使领馆非新能源部分(如果使领馆归为此类,通常使领馆有单独规则,这里仅指普通民用)
// 注意:使领馆车牌通常格式特殊,见 isEmbassyPlate
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z][A-HJ-NP-Z0-9]{5}$`);
return reg.test(plate.toUpperCase());
if (!reg.test(plate.toUpperCase())) return false;
return true;
}
/**
* 验证新能源车牌 (绿牌)
* 小型: 省份 + 字母 + D/F + 4位数字/字母 或 省份 + 字母 + 5位数字/字母 + D/F
* 型: 省份 + 字母 + 5位数字/字母 + D/F (通常最后一位是D/F)
* 示例: 京AD12345, 京A12345D, 沪AF12345
* 规则:除了标识位 D/F 外,其余序号位必须为数字 (0-9)
* 型: 省份 + 字母 + D/F + 5位数字 或 省份 + 字母 + 5位数字 + D/F
* 大型: 省份 + 字母 + 5位数字 + D/F
* 总长8位
*/
static isNewEnergyPlate(plate: string): boolean {
// 小型新能源第2位或第7位是D/F
// 大型新能源最后一位是D/F (且总长8位)
// 综合正则:
// 1. [省份][A-HJ-NP-Z][D-F][0-9A-HJ-NP-Z]{4} (小型D/F在第2位)
// 2. [省份][A-HJ-NP-Z][0-9A-HJ-NP-Z]{4}[D-F] (小型/大型D/F在最后)
// 3. [省份][A-HJ-NP-Z][0-9A-HJ-NP-Z][D-F][0-9A-HJ-NP-Z]{3} (较少见但某些早期试点或特殊编排可能存在标准通常只认1和2)
// 更严谨的标准新能源正则 (GA 804-2018):
// 小型: [省份][A-HJ-NP-Z][D-F][0-9A-HJ-NP-Z]{4} | [省份][A-HJ-NP-Z][0-9A-HJ-NP-Z]{4}[D-F]
// 大型: [省份][A-HJ-NP-Z][0-9A-HJ-NP-Z]{5}[D-F] (其实包含在上面第二种情况的子集里只要保证总长8位)
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z](?:[D-F][0-9A-HJ-NP-Z]{4}|[0-9A-HJ-NP-Z]{5}[D-F])$`);
// 修正非D/F位仅限数字
// 1. [省份][A-HJ-NP-Z][D-F][0-9]{5} (D/F在序号第1位后跟5位数字)
// 2. [省份][A-HJ-NP-Z][0-9]{5}[D-F] (D/F在序号最后1位前跟5位数字)
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z](?:[D-F][0-9]{5}|[0-9]{5}[D-F])$`);
return reg.test(plate.toUpperCase());
}
/**
* 验证警车车牌
* 格式: 省份 + 字母 + 4位数字 + 警
* 示例: 京A·1234警
* 总长7位
*/
static isPolicePlate(plate: string): boolean {
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z][0-9]{4}警$`);
@@ -127,45 +115,47 @@ export class LicensePlateValidator {
/**
* 验证教练车牌
* 格式: 省份 + 字母 + 4位数字 + 学
* 示例: 京A·1234学
* 总长7位
*/
static isCoachPlate(plate: string): boolean {
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z][0-9]{4}学$`);
return reg.test(plate.toUpperCase());
}
/**
* 验证挂车车牌
* 格式: 省份 + 字母 + 4位数字 + 挂
* 总长7位
*/
static isTrailerPlate(plate: string): boolean {
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z][0-9]{4}挂$`);
return reg.test(plate.toUpperCase());
}
/**
* 验证武警车牌 (2013式)
* 格式: WJ + 省份/部门简称 + 5位字符
* 示例: WJ·京12345, WJ·12345X, WJ·B12345
* 注意WJ后紧跟的可能是省份简称或部门字母
* 总长8位 (WJ+1+5)
*/
static isArmedPolicePlate(plate: string): boolean {
// WJ + (省份简称 | 部门字母) + 5位字符
// 部门字母包括: 内边消水电林通海等,这里简化处理,允许字母或省份
const reg = /^WJ(?:[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼]|[A-Z])[0-9A-HJ-NP-Z]{5}$/;
const reg = /^WJ(?:[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼]|[A-HJ-NP-Z])[0-9A-HJ-NP-Z]{5}$/;
return reg.test(plate.toUpperCase());
}
/**
* 验证军队车牌 (2012式)
* 格式: 军种字母 + · + 5位字符 (实际输入常不带点)
* 军种: V(军委), K(空军), H(海军), B(北京军区), S(沈阳), L(兰州), J(济南), N(南京), G(广州), C(成都) 等
* 示例: V·12345, K·A1234
* 格式: 军种字母 + 字母/数字 + 5位字符
* 总长7位
*/
static isMilitaryPlate(plate: string): boolean {
// 简化版首字母为大写排除I,O后跟5位字符
// 更严谨应限制首字母为特定军种代号,但考虑到军改变化,此处做通用校验
// 2012式军牌格式XY·12345 (X为军种/大区Y为单位)
// 常见前缀: V, K, H, B, S, L, J, N, G, C, M, P, O, A, E 等
const reg = /^[VKBHSLJNGCMPEOA][A-HJ-NP-Z0-9][0-9A-HJ-NP-Z]{4}$/;
return reg.test(plate.toUpperCase());
}
/**
* 验证使馆/领馆车牌
* 格式: 使/领 + 3位数字 + 3位数字 (共6位数字或 使·123456)
* 示例: 使·123456, 领·123456
* 格式: 使/领 + 6位数字
* 总长7位
*/
static isEmbassyPlate(plate: string): boolean {
const reg = /^[使领][0-9]{6}$/;
@@ -175,7 +165,7 @@ export class LicensePlateValidator {
/**
* 验证港澳入出境车 (粤Z牌)
* 格式: 粤Z + 4位字符 + 港/澳
* 示例: 粤Z·1234港, 粤Z·1234澳
* 总长7位
*/
static isHKMacauPlate(plate: string): boolean {
const reg = /^粤Z[A-HJ-NP-Z0-9]{4}[港澳]$/;
@@ -185,7 +175,7 @@ export class LicensePlateValidator {
/**
* 验证临时车牌
* 格式: 省份 + 字母 + 5位字符 + 临 / 超
* 示例: 京A·12345临, 沪B·12345超
* 总长8位
*/
static isTemporaryPlate(plate: string): boolean {
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z][0-9A-HJ-NP-Z]{5}[临超]$`);
@@ -201,11 +191,17 @@ export class LicensePlateValidator {
const upperPlate = plate.toUpperCase().replace(/[·\s-]/g, ''); // 去除分隔符和空格
// 长度预检查:车牌只能是 7 位或 8 位
if (upperPlate.length !== 7 && upperPlate.length !== 8) {
return false;
}
return (
this.isTraditionalPlate(upperPlate) ||
this.isNewEnergyPlate(upperPlate) ||
this.isPolicePlate(upperPlate) ||
this.isCoachPlate(upperPlate) ||
this.isTrailerPlate(upperPlate) ||
this.isArmedPolicePlate(upperPlate) ||
this.isMilitaryPlate(upperPlate) ||
this.isEmbassyPlate(upperPlate) ||

View File

@@ -28,7 +28,7 @@ export function convertBuildingToTree(buildings: any[]): Tree[] {
const unitTree: Tree = {
label: unit.unitNoName,
value: unit.unitNoId,
type: 'unitNo',
type: 'unitNoId',
children: []
};

View File

@@ -1,3 +1,5 @@
import Big from 'big.js';
/**
* 数字金额转中文大写(支持整数、小数)
* @param num - 待转换的金额(支持数字或字符串格式)
@@ -80,3 +82,96 @@ export function convertToChineseCapital(num: number | string): string {
return result;
}
/**
* 金额加法
* @param a 数值
* @param b 数值
* @returns 保留2位小数字符串
*/
export function moneyAdd(a: number | string = 0, b: number | string = 0): string {
return new Big(a).plus(b).toFixed(2);
}
// 金额减法
export function sub(a, b) {
let r1, r2;
try {
r1 = a.toString().split('.')[1].length;
} catch (e) {
r1 = 0;
}
try {
r2 = b.toString().split('.')[1].length;
} catch (e) {
r2 = 0;
}
const m = Math.pow(10, Math.max(r1, r2));
return (a * m - b * m) / m;
}
// 金额乘法
export function mul(a, b) {
let m = 0;
const s1 = a.toString(),
s2 = b.toString();
try {
m += s1.split('.')[1].length;
} catch (e) {}
try {
m += s2.split('.')[1].length;
} catch (e) {}
return (Number(s1.replace('.', '')) * Number(s2.replace('.', ''))) / Math.pow(10, m);
}
// 金额除法
export function div(a, b) {
let t1 = 0,
t2 = 0;
try {
t1 = a.toString().split('.')[1].length;
} catch (e) {}
try {
t2 = b.toString().split('.')[1].length;
} catch (e) {}
const x = Number(a.toString().replace('.', ''));
const y = Number(b.toString().replace('.', ''));
return (x / y) * Math.pow(10, t2 - t1);
}
// 保留2位小数金额专用
export function toFixed2(val) {
return Number(val).toFixed(2);
}
/**
* 智能金额格式化(带单位:元/万元/亿元)
* 兼容数字、字符串、null、undefined
* @param {number|string} amount 金额
* @returns {string} 格式化后带单位的金额
*/
export function formatMoneyWithUnit(amount) {
// 空值处理
if (amount == null || amount === '') return '0.00 元';
const num = Number(amount);
if (isNaN(num)) return '0.00 元';
// 绝对值判断单位
const absNum = Math.abs(num);
if (absNum >= 100000000) {
// 亿元
return (num / 100000000).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' 亿元';
} else if (absNum >= 10000) {
// 万元
return (num / 10000).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' 万元';
} else {
// 元
return num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' 元';
}
}
// 带货币符号(¥)版本
export function formatMoneyWithSymbol(amount) {
return '¥' + formatMoneyWithUnit(amount);
}

View File

@@ -261,3 +261,110 @@ export function printDivNewWindow(elementId: string, title: string = '打印') {
}, 300);
};
}
/**
* 🔥 Vue后台专用 - 票据专用打印(固定尺寸 宽10cm 高25cm
* 新窗口打印指定DIV、还原原样式、屏蔽后台侧边栏/导航、适配Element Plus
* 纸张规格10cm(宽) × 25cm(高) 票据纸
*/
export function printTicketDiv(elementId: string, title: string = '票据打印', callback?: () => void) {
const el = document.getElementById(elementId);
if (!el) {
console.error('未找到打印元素');
return;
}
const win = window.open('', '_blank', 'width=1200,height=1000');
if (!win) return;
// ==============================================
// 【核心1】复制 全局样式 + Element Plus 样式
// ==============================================
const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"], style:not([scoped])'))
.map((item) => item.outerHTML)
.join('');
// ==============================================
// 【核心2】手动注入打印区域的 scoped 样式(关键!)
// ==============================================
const printScopeStyle = `
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
margin: 0;
padding: 0.3cm;
background: #fff;
font-size: 12px;
font-family: "Microsoft YaHei", sans-serif;
page-break-inside: avoid !important;
}
/* 隐藏后台布局 */
.el-container, .el-aside, .el-header, .el-footer {
display: none !important;
}
/* ========== 核心:强制 宽25cm 高10cm 无边距 ========== */
@page {
size: 25cm 10cm;
margin: 0;
}
/* 打印容器:固定尺寸,限制单页 */
#${elementId} {
width: 25cm !important;
height: 10cm !important;
margin: 0 auto;
overflow: hidden;
page-break-after: avoid !important;
page-break-inside: avoid !important;
}
/* ========== 表格:压缩高度 + 禁止拆分 ========== */
table {
border-collapse: collapse;
width: 100% !important;
table-layout: fixed;
margin: 0.1cm 0;
page-break-inside: avoid !important;
}
table th, table td {
border: 1px solid #333;
font-size: 10px;
height:40px;
min-width:80px;
text-align: center;
padding: 1px 2px; /* 缩小内边距,减少高度 */
line-height: 1.1;
white-space: normal;
overflow: hidden;
}
</style>
`;
// ==============================================
// 【核心3】写入新窗口
// ==============================================
win.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
<meta charset="UTF-8">
${styles}
${printScopeStyle}
</head>
<body>
${el.outerHTML} <!-- 直接用innerHTML复制完整结构 -->
</body>
</html>
`);
win.document.close();
win.onload = () => {
setTimeout(() => {
win.print();
win.close();
callback && callback();
}, 300);
};
}

View File

@@ -41,10 +41,10 @@ const whiteVillageId = [
'/waterDevice/generateBarCode', // 水表 条码
'/electricityDevice/generateBarCode', // 电表 条码
'/payRecordList/payRecordList/list',
'/warehouse/*'
// '/storeroom/getList/*'
// 以后直接在这里加新地址,支持 /xxx/*、/xxx/xxx 写法
'/warehouse',
'/notification/read', // 已读
'/cash/billList',
'/building/list'
];
/**
@@ -75,7 +75,10 @@ export function needVillageId(url: string): boolean {
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 对应国际化资源文件后缀
const villageid = localStorage.getItem('villageid');
config.headers['Content-Language'] = getLanguage();
config.headers['villageId'] = villageid;
const disableAutoVillage = config.headers?.autoVillage === false;
const isToken = config.headers?.isToken === false;
// 是否需要防止数据重复提交
@@ -87,7 +90,6 @@ service.interceptors.request.use(
config.headers['Authorization'] = 'Bearer ' + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
}
// get请求映射params参数
const villageid = localStorage.getItem('villageid');
if (config.method === 'get' && config.params) {
let url = '';
if (villageid != 'null' && needVillageId(config.url)) {
@@ -95,8 +97,8 @@ service.interceptors.request.use(
config.url +
'?' +
tansParams({
...config.params,
villageId: villageid
villageId: villageid,
...config.params
});
} else {
url = config.url + '?' + tansParams(config.params);
@@ -110,8 +112,8 @@ service.interceptors.request.use(
let obj = null;
if (villageid != 'null' && needVillageId(config.url)) {
obj = {
...config.data,
villageId: villageid
villageId: villageid,
...config.data
};
} else {
obj = config.data;
@@ -166,6 +168,7 @@ service.interceptors.request.use(
// 响应拦截器
service.interceptors.response.use(
(res: AxiosResponse) => {
const silentError = res.config.headers?.silentError === 'true';
if (import.meta.env.VITE_APP_ENCRYPT === 'true') {
// 加密后的 AES 秘钥
const keyStr = res.headers[encryptHeader];
@@ -213,7 +216,9 @@ service.interceptors.response.use(
return Promise.reject('无效的会话,或者会话已过期,请重新登录。');
} else if (code === HttpStatus.SERVER_ERROR) {
// 500
if (!silentError) {
ElMessage({ message: msg, type: 'error' });
}
return Promise.reject(res.data);
} else if (code === HttpStatus.WARN) {
// 601
@@ -249,7 +254,7 @@ export function download(url: string, params: any, fileName: string) {
return tansParams(params);
}
],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
headers: { 'Content-Type': 'application/x-www-form-urlencoded',silentError: 'true' },
responseType: 'blob'
}).then(async (resp: any) => {
const isLogin = blobValidate(resp);

View File

@@ -261,3 +261,33 @@ export function formatDate(date: Date | string = new Date(), format: string = 'Y
// 正则替换(通用、无需穷举)
return format.replace(/YYYY/g, YYYY).replace(/MM/g, MM).replace(/DD/g, DD).replace(/HH/g, HH).replace(/mm/g, mm).replace(/ss/g, ss);
}
/**
* 获取过去指定天数的那一天的日期字符串
* @param days 过去多少天0表示今天1表示昨天以此类推
* @param format 日期格式化模板,默认 'YYYY-MM-DD'
* @returns 格式化后的日期字符串
* @category 日期工具
* @example
* getPastDayDate(0) // 今天 -> 2023-10-27
* getPastDayDate(1) // 昨天 -> 2023-10-26
* getPastDayDate(7, 'YYYY年MM月DD日') // 7天前 -> 2023年10月20日
*/
export function getPastDayDate(days: number = 0, format: string = 'YYYY-MM-DD'): string {
// 参数校验
if (!Number.isInteger(days) || days < 0) {
console.warn('getPastDayDate: days 必须是大于等于 0 的整数');
return '';
}
const now = new Date();
// 重置时间为 00:00:00确保计算的是自然日
now.setHours(0, 0, 0, 0);
// 计算目标时间戳
const targetTime = now.getTime() - days * 24 * 60 * 60 * 1000;
const targetDate = new Date(targetTime);
// 复用 formatDate 进行格式化,保持项目内格式统一
return formatDate(targetDate, format);
}

View File

@@ -18,8 +18,8 @@
<el-form-item label="所在城市" prop="city">
<el-cascader :options="pcaTextArrlist" v-model="city" @change="cityChange"> </el-cascader>
</el-form-item>
<el-form-item label="详细地址" prop="buildingArea">
<el-input v-model.trim="form.address" placeholder="请输入详细地址" />
<el-form-item label="完整详细地址" prop="buildingArea">
<el-input v-model.trim="form.address" placeholder="请输入完整详细地址" />
</el-form-item>
<el-form-item label="建成时间" prop="buildingArea">
<el-date-picker v-model="builtTime" type="date" placeholder="选择建成时间" value-format="YYYY-MM-DD" @change="changeBuiltTime" />
@@ -184,7 +184,7 @@ const rules = ref({
function getCommunityinfo() {
const id = editId.value;
getVillageByIdAPI(id).then((res) => {
city.value = res.data.city.split('/');
city.value = res.data.city ? res.data.city.split('/') : [];
if (res.data.villageImageUrl) {
villageImg.value = res.data.villageImageUrl
.split(',')

View File

@@ -13,7 +13,16 @@
</div>
<div class="communityBody">
<div class="coummunity_card" v-for="(item, index) in list" :key="index">
<el-image :src="item.villageImageUrl ? item.villageImageUrl.split(',')[0] : defaultVilageinfoImage">
<el-image
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
:preview-src-list="item.villageImageUrl ? item.villageImageUrl.split(',') : [defaultVilageinfoImage]"
show-progress
:initial-index="4"
fit="cover"
:src="item.villageImageUrl ? item.villageImageUrl.split(',')[0] : defaultVilageinfoImage"
>
<template #error>
<div class="image-viewer-slot image-slot">
<el-icon><Picture /></el-icon>

View File

@@ -148,10 +148,14 @@ const { queryParams, form, rules } = toRefs(data);
/** 查询测试单列表 */
const getList = async () => {
loading.value = true;
const res = await listDemo(queryParams.value);
listDemo(queryParams.value)
.then((res) => {
demoList.value = res.rows;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
};
/** 取消按钮 */

View File

@@ -8,16 +8,122 @@
</template>
<div class="addBuildingFormBody">
<div class="formbox">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="150px">
<el-row>
<el-col :span="12">
<el-col :span="13">
<el-form-item label="活动标题" prop="title">
<el-input v-model.trim="form.title" placeholder="请输入活动标题" />
</el-form-item>
</el-col>
<el-col :span="11">
<el-form-item label="活动作者" prop="author">
<el-input v-model.trim="form.author" placeholder="请输入活动作者" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="13">
<el-form-item label="活动时间" prop="startTime">
<el-date-picker
v-model="activityTime"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
@update:model-value="
(data) => {
if (data && data.length > 0) {
form.startTime = data[0];
form.endTime = data[1];
} else {
form.startTime = '';
form.endTime = '';
}
}
"
type="datetimerange"
range-separator=""
start-placeholder="开始活动"
end-placeholder="结束活动"
/>
</el-form-item>
</el-col>
<el-col :span="11">
<el-form-item label="活动地点" prop="address">
<el-input v-model.trim="form.address" placeholder="请输入活动地点" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="11">
<el-form-item label="报名截止日期" prop="deadline">
<el-date-picker
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
v-model="deadline"
@update:model-value="
(data) => {
if (data) {
form.applyDeadline = data;
} else {
form.applyDeadline = '';
}
}
"
type="date"
placeholder="请选择报名截止日期"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="作者" prop="author">
<el-input v-model.trim="form.author" placeholder="请输入作者" />
<el-form-item label="主办方" prop="organizer">
<el-input v-model.trim="form.organizer" placeholder="请输入主办方" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="联系人" prop="contactName">
<el-input v-model.trim="form.contactName" placeholder="请输入联系人" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系方式" prop="contactName">
<el-input v-model.trim="form.contactPhone" placeholder="请输入联系方式" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="活动名额" prop="quota">
<div class="flex items-center">
<el-input-number v-model.trim.number="form.quota" :min="0" placeholder="0代表无限" />
<span class="ml-2 text-gray-500 text-sm" style="font-size: 12px">(填 <b>0</b> 代表<u>不限制名额</u></span>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="活动封面" prop="title">
<el-upload
ref="upload"
class="avatar-uploader"
:action="uploadUrl"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:on-exceed="handleExceed"
:on-error="handleError"
:limit="1"
>
<img v-if="form.coverImage" :src="form.coverImage" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="招募要求">
<el-input :row="8" type="textarea" v-model.trim="form.requirements" placeholder="请输入招募要求" />
</el-form-item>
</el-col>
</el-row>
@@ -29,6 +135,69 @@
</el-col>
</el-row>
<!-- <el-row>
<el-col>
<el-form-item label="活动签到">
<el-switch
@change="
(val) => {
if (val) {
initSignMap();
}
}
"
v-model="isSignin"
active-text="签到"
inactive-text="不签到"
:active-value="true"
:inactive-value="false"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="13">
<el-form-item label="签到时间" prop="signinStartTime">
<el-date-picker
v-model="SigninTime"
type="datetimerange"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
range-separator=""
@update:model-value="
(data) => {
if (data && data.length > 0) {
form.signinStartTime = data[0];
form.signinEndTime = data[1];
} else {
form.signinStartTime = '';
form.signinEndTime = '';
}
}
"
start-placeholder="开始签到"
end-placeholder="结束签到"
/>
</el-form-item>
</el-col>
</el-row>
<el-row v-if="isSignin">
<el-col>
<el-form-item label="签到设置">
<div>
<div>
<el-button type="primary" v-if="isDraw === 0" @click="startDrawCircle">开始设置签到范围</el-button>
<el-button type="success" v-if="isDraw === 1" @click="editCircle">编辑签到范围</el-button>
<el-button type="warning" v-if="isDraw === 2" @click="stopDrawCircle">结束设置签到范围</el-button>
<el-button type="danger" @click="clearCircle">清空签到范围</el-button>
</div>
<div v-loading="mapReady" class="SignMapbox">
<div id="SignMap" style="height: 100%; width: 100%"></div>
</div>
</div>
</el-form-item>
</el-col>
</el-row> -->
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="handleBack">返回</el-button>
@@ -44,44 +213,281 @@
import { ref } from 'vue';
import PageHeader from '@/components/Pageheader/index.vue';
import { addActivity, getActivity, updateActivity } from '@/api/system/Activity';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { com_repair_project_status } = toRefs<any>(proxy?.useDict('com_repair_project_status'));
import { genFileId, UploadFiles, UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
import AMapLoader from '@amap/amap-jsapi-loader';
const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/activity/uploadImage';
const aloaderkey = import.meta.env.VITE_GCJ02_KEY_GD;
const VITE_GCJ02_JS_CODE_GD = import.meta.env.VITE_GCJ02_JS_CODE_GD;
const route = useRoute();
const router = useRouter();
const formRef = ref();
const form = ref({
'id': null,
// 标题
'title': '',
// 作者
'author': '',
// 内容
'content': '',
'status': '0'
// 发布状态 0未发布 1己发布
'status': '0',
// 报名截止日期
'applyDeadline': '',
// 活动时间
'startTime': '',
'endTime': '',
// 活动地点
'address': '',
// 主办方
'organizer': '',
// 联系人
'contactName': '',
// 联系方式
'contactPhone': '',
// 开始签到时间
'signinStartTime': '',
// 结束签到时间
'signinEndTime': '',
// 活动名额
'quota': 0,
// 积分奖励
'pointsReward': 0,
// 活动封面
'coverImage': '',
// 招募要求
'requirements': ''
// 'SignInfo': {
// lng: null, // 经度
// lat: null, // 纬度
// radius: null // 半径 m
// }
});
const rules = {
title: [{ required: true, message: '请输入活动标题', trigger: 'blur' }],
author: [{ required: true, message: '请输入作者', trigger: 'blur' }],
applyDeadline: [{ required: true, message: '请输入报名截止日期', trigger: 'blur' }],
startTime: [{ required: true, message: '请选择开始日期', trigger: 'blur' }],
endTime: [{ required: true, message: '请选择结束日期', trigger: 'blur' }],
address: [{ required: true, message: '请输入活动地点', trigger: 'blur' }],
organizer: [{ required: true, message: '请输入主办方', trigger: 'blur' }],
contactName: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
contactPhone: [{ required: true, message: '请输入联系方式', trigger: 'blur' }],
coverImage: [{ required: true, message: '请上传活动封面', trigger: 'blur' }],
content: [{ required: true, message: '请输入内容', trigger: 'blur' }]
};
const userid = ref();
// 报名截止日期
const deadline = ref([]);
// 有效签到时间
const SigninTime = ref([]);
// 活动时间
const activityTime = ref([]);
// 上传文件
const upload = ref<UploadInstance>();
// 上传成功
const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
if (response.data && response.data.url) {
form.value.coverImage = response.data.url;
}
};
// 超出文件限制
const handleExceed: UploadProps['onExceed'] = (files) => {
upload.value!.clearFiles();
const file = files[0] as UploadRawFile;
file.uid = genFileId();
upload.value!.handleStart(file);
upload.value?.submit();
};
// 上传失败
const handleError: UploadProps['onError'] = (error: Error, uploadFile: UploadFile, uploadFiles: UploadFiles) => {
console.log(error);
};
// ! 投诉 ====================================================================================================
// ! 签到 =======================================
const isSignin = ref(false);
let AMapLi = null;
let map: any = null;
let circle: any = null;
let circleEditor: any = null;
let geocoder: any = null;
const mapReady = ref(false);
let mouseTool = null;
// 0 开始 1编辑 2结束
const isDraw = ref(0);
onMounted(() => {
if (route.query.id) {
userid.value = route.query.id;
init();
}
});
async function initSignMap() {
try {
mapReady.value = true;
window._AMapSecurityConfig = { securityJsCode: VITE_GCJ02_JS_CODE_GD };
console.log('开始执行 AMapLoader.load');
const AMap = await AMapLoader.load({
key: aloaderkey,
version: '2.0',
plugins: ['AMap.MouseTool', 'AMap.CircleEditor', 'AMap.Geolocation', 'AMap.Geocoder']
});
mapReady.value = false;
console.log('AMapLoader.load 执行完毕');
AMapLi = AMap;
map = new AMap.Map('SignMap', {
viewMode: '3D',
zoom: 11,
center: [116.397428, 39.90923]
});
// 解析本地存储的城市信息
const villageInfoStr = localStorage.getItem('villageInfo') || '{}';
let city = '';
try {
const villageInfo = JSON.parse(villageInfoStr);
city = (villageInfo?.city || '').trim();
} catch (err) {
console.warn('解析 villageInfo 失败', err);
}
// 初始化地理编码工具
if (city.trim() !== '') {
geocoder = new AMap.Geocoder({
city: city.trim(),
radius: 1000
});
getGeoCode(city.trim(), map);
}
// 鼠标绘制工具
mouseTool = new AMap.MouseTool(map);
mouseTool.on('draw', (e: any) => {
// 移除旧围栏
if (circle) {
map.remove(circle);
}
circle = e.obj;
});
} catch (error) {
console.error('高德地图初始化失败:', error);
}
}
// 获取 城市经纬度并定位中心
const getGeoCode = (address, map) => {
geocoder.getLocation(address, (status: string, result: any) => {
if (status === 'complete' && result.geocodes.length > 0) {
const geo = result.geocodes[0];
const lng = geo.location.lng;
const lat = geo.location.lat;
map.setCenter([lng, lat]);
map.setZoom(15);
} else {
console.log('未找到该地址');
}
});
};
// 开始绘制圆形
const startDrawCircle = () => {
resetCircle();
if (!mouseTool) return;
isDraw.value = 1; // 编辑
mouseTool.circle({
strokeColor: '#1890ff',
strokeWeight: 3,
fillColor: 'rgba(24, 144, 255, 0.2)',
fillOpacity: 0.4
});
};
// 停止绘制圆形
const stopDrawCircle = () => {
if (!mouseTool) return;
if (mouseTool) {
mouseTool.close(false);
}
if (circleEditor) {
circleEditor.close();
circleEditor = null;
}
isDraw.value = 0; // 开始
mouseTool.close(false);
if (circle) {
const center = circle.getCenter();
const radius = circle.getRadius();
console.log('中心点:', [center.lng, center.lat]);
console.log('半径:', radius, typeof radius);
// form.value.SignInfo.lat = center.lat;
// form.value.SignInfo.lng = center.lng;
// form.value.SignInfo.radius = center.radius;
}
};
// 编辑 圆形
const editCircle = () => {
if (!circle) return;
if (mouseTool) {
mouseTool.close(false);
}
isDraw.value = 2; // 结束
circleEditor = new AMapLi.CircleEditor(map, circle);
circleEditor.open();
};
// 删除圆形
const clearCircle = () => {
// resetCircle();
isDraw.value = 0; // 开始
ElMessage.success('全部清除');
};
// 重置
const resetCircle = () => {
// 不能绘画
if (mouseTool) {
mouseTool.close(false);
}
// 清除圆形
if (circle) {
map.remove(circle);
circle = null;
}
// 不能编辑
if (circleEditor) {
circleEditor.close();
circleEditor = null;
}
};
function init() {
getActivity(userid.value).then((res) => {
form.value = {
...form.value,
...res.data
};
console.log(form.value);
activityTime.value = [res.data.startTime, res.data.endTime];
SigninTime.value = [res.data.signinStartTime, res.data.signinEndTime];
deadline.value = [res.data.applyDeadline];
// if (res.data?.SignInfo !== null) {
// isSignin.value = true;
// }
});
}
if (route.query.id) {
userid.value = route.query.id;
init();
}
// ! 提交========================================
// 提交
const submitForm = () => {
console.log(form.value);
// if (!isSignin.value) {
// form.value.SignInfo = null;
// }
formRef.value?.validate(async (valid: boolean) => {
if (valid) {
if (userid.value) {
@@ -105,12 +511,63 @@ function handleBack() {
path: '/repair/Activity'
});
}
/**
* 销毁地图及所有工具实例(防内存泄漏、重复初始化)
*/
function destroyMapInstance() {
if (map) {
map.destroy();
map = null;
}
mouseTool = null;
geocoder = null;
circle = null;
AMapLi = null;
mapReady.value = false;
}
onUnmounted(() => {
// destroyMapInstance();
});
</script>
<style scoped lang="scss">
.SignMapbox {
height: 500px;
width: 500px;
margin-top: 10px;
border: 1px solid #ccc;
}
.ResidentMainBox {
display: flex;
flex-direction: column;
.avatar-uploader {
border: 1px solid #ccc;
border-radius: 6px;
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 158px;
height: 158px;
text-align: center;
}
.avatar-uploader .avatar {
width: 158px;
height: 158px;
display: block;
object-fit: cover;
}
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.ResidentMainBody {
margin-top: 20px;
flex: 1;

View File

@@ -145,10 +145,14 @@ const { queryParams, form, rules } = toRefs(data);
/** 查询活动管理列表 */
const getList = async () => {
loading.value = true;
const res = await listActivity(queryParams.value);
listActivity(queryParams.value)
.then((res) => {
activityList.value = res.rows;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
};
/** 发布按钮 */
const handleSend = (row: ActivityVO) => {
@@ -198,7 +202,7 @@ const handleAdd = () => {
/** 修改按钮操作 */
const handleUpdate = async (row?: ActivityVO) => {
const _id = row.id || ids.value[0];
const _id = row?.id || ids.value[0];
router.push({
path: '/repair/editActivity',
query: {

View File

@@ -64,7 +64,7 @@ function headerToken() {
}
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
const type = rawFile.type.split('/').pop();
if (['png', 'jpg'].includes(type)) {
if (['png', 'jpg', 'jpeg'].includes(type)) {
if (rawFile.size / 1024 / 1024 <= 10) {
return true;
} else {
@@ -92,6 +92,7 @@ const form = ref({
name: '', // 分类id、
url: '', // 设备名称
link: '', // 编号
status: '1',
byOrder: 1 // 区域
});
const rules = ref({
@@ -108,6 +109,7 @@ function init() {
id: res.data.id,
name: res.data.name, // 分类id、
url: res.data.url, // 设备名称
status: res.data.status ?? '0',
link: res.data.link, // 编号
byOrder: res.data.byOrder // 区域
};

View File

@@ -67,14 +67,14 @@
<!-- 0启用 1停用 -->
<el-switch
@change="(val: string) => handleSwitch(scope.row.id, val)"
inactive-value="0"
active-value="1"
inactive-value="1"
active-value="0"
v-model="scope.row.status"
></el-switch>
</template>
</el-table-column>
<el-table-column label="排序" align="center" prop="byOrder" />
<el-table-column label="添加时间" align="center" />
<el-table-column label="添加时间" align="center" prop="createTime" />
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['banner:banner:edit']">修改</el-button>
@@ -158,10 +158,14 @@ const { queryParams, form } = toRefs(data);
/** 查询Banner图管理列表 */
const getList = async () => {
loading.value = true;
const res = await listBanner(queryParams.value);
listBanner(queryParams.value)
.then((res) => {
bannerList.value = res.rows;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
};
/** 取消按钮 */

View File

@@ -10,17 +10,13 @@
<span>投诉信息</span>
</div>
</template>
<div class="w-full h-full CarinfoBox">
<div>
<div class="CarinfoBox">
<div class="carinfo_item">
<div class="label">投诉类型</div>
<div class="main">{{ form.type }}</div>
</div>
<div class="carinfo_item">
<div class="label">问题描述</div>
<div class="main">{{ form.problemDes }}</div>
</div>
<div class="carinfo_item">
<div class="label">投诉人姓名</div>
<div class="main">{{ form.complaintName }}</div>
@@ -43,6 +39,28 @@
</div>
</div>
</div>
<div class="CarinfoBox signle">
<div class="carinfo_item">
<div class="label">问题描述</div>
<div class="main">{{ form.problemDes }}</div>
</div>
</div>
<div class="CarinfoBox signle">
<div class="carinfo_item">
<div class="label">问题照片</div>
<div class="main">
<el-image
v-for="(item, index) in problemIamgeUrllist"
:key="index"
:src="item"
:preview-teleported="true"
:preview-src-list="problemIamgeUrllist"
style="width: 120px; height: 120px; margin-right: 10px"
></el-image>
</div>
</div>
</div>
</div>
</el-card>
</div>
<!-- 处理情况 -->
@@ -57,11 +75,18 @@
<el-form ref="formRef" :rules="rules" class="m-auto" :inline="false" label-width="100px" :model="form">
<el-form-item label="处理状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="(item, index) in com_complaint_status" :key="index" :value="item.value">{{ item.label }}</el-radio>
<!-- <el-radio v-for="(item, index) in com_complaint_status" :key="index" :value="item.value">{{ item.label }}</el-radio> -->
<el-radio value="1">处理完成</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="处理人员" prop="handleId">
<repairUser :userid="form.handleId" returnvalue="value" @change="handleChange"></repairUser>
<repairUser
:is-multiple="false"
:username="form.handleName"
:userid="form.handleId ?? ''"
returnvalue="*"
@change="handleChange"
></repairUser>
</el-form-item>
<el-form-item label="处理描述" prop="handleContent">
<el-input type="textarea" :rows="5" v-model="form.handleContent"></el-input>
@@ -104,10 +129,11 @@ const form = ref({
'createTime': null
});
const rules = {
status: [{ required: true, message: '选择投诉处理状态', trigger: 'blur' }]
status: [{ required: true, message: '选择投诉处理状态', trigger: 'blur' }],
handleContent: [{ required: true, message: '投诉处理内容不能为空', trigger: 'blur' }]
};
const userid = ref();
const complaintyTyplist = ref([]);
const problemIamgeUrllist = ref([]);
// ! 投诉 ====================================================================================================
async function init() {
const res2 = await listComplaintType();
@@ -116,6 +142,8 @@ async function init() {
...form.value,
...res.data
};
problemIamgeUrllist.value = res.data.problemImageUrl ? res.data.problemImageUrl.split(',') : [];
form.value.status = '1';
const find = res2.rows.find((item) => item.id === form.value.typeId);
if (find) {
form.value.type = find.name;
@@ -129,6 +157,7 @@ if (route.query.id) {
}
function handleChange(pop) {
console.log(pop);
form.value.handleId = pop.userId;
form.value.handleName = pop.nickName;
}
@@ -171,13 +200,21 @@ function handleBack() {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 20px;
margin-bottom: 20px;
.carinfo_item {
display: flex;
align-items: center;
.label {
margin-right: 20px;
min-width: 60px;
}
}
&.signle {
grid-template-columns: repeat(1, 1fr);
grid-template-rows: repeat(1, 1fr);
}
}
.formbox {

View File

@@ -21,12 +21,11 @@
</el-card>
</div>
</transition>
<el-card class="flex-1" shadow="never">
<el-table v-loading="loading" border :data="complaintList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="投诉类型" align="center" prop="typeName" />
<el-table-column label="问题描述" align="center" prop="problemDes" />
<el-table-column label="问题描述" show-overflow-tooltip align="center" prop="problemDes" />
<el-table-column label="投诉人姓名" align="center" prop="complaintName" />
<el-table-column label="手机号" align="center" prop="phone" />
<el-table-column label="投诉时间" align="center" prop="createTime" />
@@ -113,10 +112,14 @@ const getList = async () => {
loading.value = true;
const res2 = await listComplaintType();
complaintyTyplist.value = res2.rows;
const res = await listComplaint(queryParams.value);
listComplaint(queryParams.value)
.then((res) => {
complaintList.value = res.rows;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
};
/** 搜索按钮操作 */

View File

@@ -1,5 +1,5 @@
<template>
<div class="p-2">
<div class="p-2 h-full flex flex-col">
<Pageheader title="投诉类型管理"></Pageheader>
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
@@ -17,7 +17,7 @@
</div>
</transition>
<el-card shadow="never">
<el-card shadow="never" class="flex-1">
<template #header>
<el-row :gutter="10" class="mb8">
<!-- <el-col :span="1.5">
@@ -131,10 +131,14 @@ const { queryParams, form, rules } = toRefs(data);
/** 查询投诉类型管理列表 */
const getList = async () => {
loading.value = true;
const res = await listComplaintType(queryParams.value);
listComplaintType(queryParams.value)
.then((res) => {
complaintTypeList.value = res.rows;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
};
/** 搜索按钮操作 */
@@ -196,3 +200,9 @@ onMounted(() => {
getList();
});
</script>
<style lang="scss" scoped>
.el-table {
height: calc(100% - 80px);
}
</style>

View File

@@ -55,19 +55,19 @@
<el-table v-loading="loading" border :data="decorationList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="小区名称" align="center" prop="villageName" />
<el-table-column label="房屋号" align="center" prop="houseName" />
<el-table-column label="房屋号" show-overflow-tooltip align="center" prop="houseName" />
<el-table-column label="申请人姓名" align="center" prop="applyName" />
<el-table-column label="装修公司" align="center" prop="decorationCompany" />
<el-table-column label="装修内容" align="center" prop="decorationContent" />
<el-table-column label="装修押金" align="center" prop="decorationDeposit" />
<el-table-column label="验收结果" align="center" prop="inspectionResult">
<el-table-column label="装修押金" width="120" align="center" prop="decorationDeposit" />
<el-table-column label="验收结果" width="100" align="center" prop="inspectionResult">
<template #default="scope">
<dict-tag :options="com_decoration_status" :value="Number(scope.row.inspectionResult)"></dict-tag>
</template>
</el-table-column>
<el-table-column label="申请时间" align="center" prop="createTime" />
<el-table-column label="申请时间" width="170" align="center" prop="createTime" />
<el-table-column label="验收人" align="center" prop="inspectionName" />
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<el-table-column label="操作" width="170" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
link
@@ -245,7 +245,7 @@ const handleAdd = () => {
/** 修改按钮操作 */
const handleUpdate = async (row?: DecorationVO) => {
const _id = row.id || ids.value[0];
const _id = row?.id || ids.value[0];
router.push({
path: '/repair/editDecoration',
query: {

View File

@@ -53,6 +53,30 @@
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="附属材料">
<el-upload
:auto-upload="false"
class="upload-demo"
:file-list="fileList"
:action="uploadUrl"
:multiple="true"
ref="uploadRef"
name="files"
:headers="headerToken()"
:show-file-list="true"
:on-remove="handleAvatarRemove"
:on-change="handleProgress"
>
<div class="uploadBtn flex flex-items-center">
<el-icon><Upload /></el-icon>
<span>上传文件</span>
</div>
</el-upload>
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="handleBack">返回</el-button>
@@ -64,11 +88,17 @@
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import PageHeader from '@/components/Pageheader/index.vue';
import { addHandleLog, getHandleLog, updateHandleLog } from '@/api/system/HandleLog';
import { addHandleLog, getHandleLog, updateHandleLog, uploadAttachments } from '@/api/system/HandleLog';
import { UploadFiles, UploadProps, UploadUserFile } from 'element-plus';
import { useUserStore } from '@/store/modules/user';
import { ElMessage } from 'element-plus';
import { Upload } from '@element-plus/icons-vue'; // 确保引入图标
const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/handleLog/uploadAttachments';
const route = useRoute();
const router = useRouter();
const formRef = ref();
@@ -78,28 +108,137 @@ const form = ref({
'seekName': '',
'phone': '',
'handleTime': '',
handleLogFileList: [],
'handlePosition': ''
});
const rules = {
content: [{ required: true, message: '请输入办事内容', trigger: 'blur' }],
seekName: [{ required: true, message: '请选择求助人', trigger: 'blur' }],
handlePosition: [{ required: true, message: '请输入办事地点', trigger: 'blur' }],
handleTime: [{ required: true, message: '请输入办事时间', trigger: 'blur' }]
seekName: [{ required: true, message: '请选择求助人', trigger: 'blur' }]
};
const userid = ref();
// ! 富文本 =============================================================================================================
function handleContent(val: string) {
form.value.content = val.trim();
}
// ! 富文本 =============================================================================================================
interface CustomUploadFile extends UploadUserFile {
ossId?: string;
}
const fileList = ref<CustomUploadFile[]>([]);
function headerToken() {
const useStore = useUserStore();
return {
Authorization: 'Bearer ' + useStore.token
};
}
const handleAvatarRemove: UploadProps['onRemove'] = (uploadFile: UploadUserFile, uploadFiles: UploadFiles) => {
fileList.value = uploadFiles;
};
// 定义大小限制常量 (单位: MB)
const MAX_SINGLE_FILE_SIZE = 10 * 1024 * 1024; // 10MB in bytes
const MAX_TOTAL_FILE_SIZE = 20 * 1024 * 1024; // 20MB in bytes
// 添加一个锁,防止 on-change 递归调用导致重复提示
const isChecking = ref(false);
const handleProgress: UploadProps['onChange'] = (uploadFile: UploadUserFile, uploadFiles: UploadFiles) => {
// 如果正在校验中,直接返回,避免重复执行
if (isChecking.value) return;
// 加锁
isChecking.value = true;
// 创建一个新数组来存储校验通过的文件
const validFiles: CustomUploadFile[] = [];
let newFilesTotalSize = 0;
let hasError = false;
let errorMsg = '';
// 1. 遍历所有文件,筛选出符合单文件大小要求的文件,并计算新文件总大小
for (const file of uploadFiles) {
// 只校验新上传的文件 (有 raw 属性)
if (file.raw) {
// 检查单个文件大小
if (file.raw.size > MAX_SINGLE_FILE_SIZE) {
// 如果发现单个文件超标,标记错误,并不将该文件加入 validFiles
if (!hasError) {
errorMsg = `文件 ${file.name} 大小超过 10MB已自动移除`;
hasError = true;
}
continue;
}
newFilesTotalSize += file.raw.size;
}
// 将非超标文件(包括旧文件和未超标的新文件)暂时加入
console.log(file);
validFiles.push({
...file,
ossId: (file as CustomUploadFile).ossId ?? null
});
}
// 2. 如果存在单文件超标的情况,先处理这部分逻辑
if (hasError) {
ElMessage.error(errorMsg);
// 更新 fileList 为移除了超标文件的列表
fileList.value = validFiles;
return;
}
// 3. 检查新文件总大小是否超过 20MB
if (newFilesTotalSize > MAX_TOTAL_FILE_SIZE) {
ElMessage.error(`所有新上传文件的总大小不能超过 20MB当前已选新文件总大小约为 ${(newFilesTotalSize / 1024 / 1024).toFixed(2)}MB`);
// 策略:移除最后添加的那个导致超标的文件
// 注意这里需要找到最后添加的那个文件。uploadFile 参数就是当前触发的文件
// 但如果是一次性选择多个文件uploadFile 可能是其中一个。
// 更稳妥的方式是:如果总和超标,则移除所有新文件中最后加入的那个,或者提示用户手动减少。
// 这里我们尝试移除最后加入的一个新文件
// 找出所有新文件
const newFiles = validFiles.filter((f) => f.raw);
if (newFiles.length > 0) {
// 移除最后一个新文件
const lastNewFile = newFiles[newFiles.length - 1];
const filteredFiles = validFiles.filter((f) => f.uid !== lastNewFile.uid);
fileList.value = filteredFiles;
} else {
// 理论上不会走到这里,因为如果没有新文件,总大小不会增加
fileList.value = validFiles;
}
return;
}
// 4. 校验全部通过,更新 fileList
// 只有当 validFiles 与当前 fileList 不同时才赋值,避免不必要的触发
if (validFiles.length !== fileList.value.length || validFiles.some((f, i) => f.uid !== fileList.value[i]?.uid)) {
fileList.value = validFiles;
}
// 解锁,允许下一次变更触发校验
// 使用 nextTick 或 setTimeout 确保 DOM 更新后再解锁,防止极快连续操作的问题
setTimeout(() => {
isChecking.value = false;
}, 100);
};
// ! 初始 ====================================================================================================
function init() {
console.log('init');
getHandleLog(userid.value).then((res) => {
form.value = {
...form.value,
...res.data
};
fileList.value = res.data.handleLogFileList.map((item) => {
return {
name: item.fileName,
url: item.filePath,
ossId: item.ossId
};
});
});
}
@@ -109,7 +248,48 @@ if (route.query.id) {
}
// 提交
const submitForm = () => {
const submitForm = async () => {
// 筛选出需要上传的新文件
const newFiles = fileList.value.filter((item) => item.raw);
const oldFiles = fileList.value
.filter((item) => !item.raw)
.map((item) => {
return {
fileName: item.name,
filePath: item.url,
ossId: item.ossId
};
});
const files = [...oldFiles];
console.log('oldFiles', files);
if (newFiles.length > 0) {
const formData = new FormData();
newFiles.forEach((item) => {
if (item.raw) {
formData.append('files', item.raw);
}
});
try {
const res = await uploadAttachments(formData);
if (res.data && res.data.success && res.data.success.length > 0) {
res.data.success.forEach((item) => {
const obj = {
fileName: item.fileName,
filePath: item.url,
ossId: item.ossId
};
files.push(obj);
});
}
} catch (error) {
console.error('上传失败', error);
ElMessage.error('文件上传失败');
return; // 上传失败则终止提交
}
}
form.value.handleLogFileList = files;
formRef.value?.validate(async (valid: boolean) => {
if (valid) {
if (userid.value) {
@@ -146,6 +326,34 @@ function handleBack() {
.formbox {
width: 1000px;
margin: 0 auto;
.upload-demo {
width: 100%;
min-width: 200px;
&:deep(.el-upload) {
width: 100%;
}
&:deep(.el-upload-list) {
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
}
}
.uploadBtn {
height: 30px;
line-height: 20px;
border-radius: 5px;
background-color: rgba(255, 255, 255, 1);
color: rgba(16, 16, 16, 1);
padding-left: 20px;
font-size: 14px;
border: 1px solid rgba(204, 204, 204, 1);
width: 100%;
border: 1px solid #ccc;
span {
margin-left: 10px;
}
}
}
:deep(.el-date-editor.el-input, .el-date-editor.el-input__wrapper) {

View File

@@ -20,19 +20,6 @@
<el-card class="flex-1" shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<!-- <el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['repair:add:handlelog']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['repair:edit:handlelog']"
>修改</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['handleLog:handleLog:remove']"
>删除</el-button
>
</el-col> -->
<right-toolbar
:disable-delete="multiple"
:disable-edit="single"
@@ -55,12 +42,17 @@
</el-table-column>
<el-table-column label="求助人" align="center" prop="seekName" width="120" />
<el-table-column label="联系方式" align="center" prop="phone" width="120" />
<el-table-column label="办事地点" align="center" prop="handlePosition" />
<el-table-column label="办事时间" align="center" prop="handleTime" width="120">
<template #default="scope">
<span>{{ parseTime(scope.row.handleTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="办事地点" align="center" prop="handlePosition" />
<el-table-column label="附属材料" width="100" align="center" prop="handlePosition">
<template #default="scope">
<el-button type="primary" @click="openDiage(scope.row)" link>点击查看</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['handleLog:handleLog:edit']">编辑</el-button>
@@ -70,6 +62,22 @@
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<!-- 添加或修改办事记录对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<ul class="ulbox" v-if="dialog.files.length > 0">
<li @click="handleDown(item)" class="files_item" v-for="(item, index) in dialog.files" :key="index">
<div>
{{ item.fileName }}
</div>
<span class="clickdown">点击下载</span>
</li>
</ul>
<ul class="ulbox" v-else>
<li class="files_item_no">
<div>暂无数据</div>
</li>
</ul>
</el-dialog>
</el-card>
</div>
</template>
@@ -123,10 +131,14 @@ const { queryParams, form, rules } = toRefs(data);
/** 查询办事记录列表 */
const getList = async () => {
loading.value = true;
const res = await listHandleLog(queryParams.value);
listHandleLog(queryParams.value)
.then((res) => {
handleLogList.value = res.rows;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
};
/** 搜索按钮操作 */
@@ -158,7 +170,7 @@ const handleAdd = () => {
/** 修改按钮操作 */
const handleUpdate = async (row?: HandleLogVO) => {
const _id = row.id || ids.value[0];
const _id = row ? row.id : ids.value[0];
router.push({
path: '/repair/editHandleLog',
query: {
@@ -175,7 +187,21 @@ const handleDelete = async (row?: HandleLogVO) => {
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
const dialog = reactive({
visible: false,
title: '',
files: []
});
const openDiage = (row) => {
dialog.visible = true;
dialog.title = '查看附属材料';
console.log(row);
dialog.files = row.handleLogFileList ?? [];
};
const handleDown = (item) => {
proxy?.$download.oss(item.ossId);
};
onMounted(() => {
getList();
});
@@ -185,4 +211,36 @@ onMounted(() => {
.el-table {
height: calc(100% - 80px);
}
.ulbox {
max-height: 300px;
overflow-y: auto;
overflow-x: hidden;
}
.files_item_no {
text-align: center;
color: #999;
padding: 10px;
}
.files_item {
height: 40px;
line-height: 40px;
box-sizing: border-box;
padding: 0 10px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
&:hover {
background-color: #f5f5f5;
color: #409eff;
.clickdown {
color: #409eff;
}
}
.clickdown {
color: #cccc;
font-size: small;
}
}
</style>

View File

@@ -133,10 +133,14 @@ const { queryParams, form } = toRefs(data);
/** 查询房屋设施管理列表 */
const getList = async () => {
loading.value = true;
const res = await listHouseFacilities(queryParams.value);
listHouseFacilities(queryParams.value)
.then((res) => {
houseFacilitiesList.value = res.rows;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
};
/** 取消按钮 */
@@ -180,27 +184,11 @@ const handleAdd = () => {
/** 修改按钮操作 */
const handleUpdate = async (row?: HouseFacilitiesVO) => {
const _ids = row?.id || ids.value[0];
router.push({
path: '/system/addHouseFacilities',
query: {
id: row.id
}
});
};
/** 提交按钮 */
const submitForm = () => {
houseFacilitiesFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateHouseFacilities(form.value).finally(() => (buttonLoading.value = false));
} else {
await addHouseFacilities(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
id: _ids
}
});
};

View File

@@ -35,7 +35,7 @@
<el-row>
<el-col :span="11">
<el-form-item label="办理人" prop="handleName">
<el-input v-model.number="form.handleName" placeholder="请输入办理人姓名" />
<el-input v-model="form.handleName" placeholder="请输入办理人姓名" />
</el-form-item>
</el-col>
<el-col :span="2"></el-col>
@@ -105,7 +105,7 @@ const form = ref({
cardCode: '', // 月卡编号
cardName: null, // 持卡人姓名
phone: '', // 手机号
carNum: null, // 绑定车牌号
carNum: '', // 绑定车牌号
handleName: '', // 办理人姓名
cost: null, // 费用/元
payMethod: '', // 支付方式
@@ -128,6 +128,14 @@ const rules = ref({
trigger: 'blur'
}
],
handleName: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{
validator: (_, value) => validator(value, 'ChineseName'),
message: '请输入正确的中文姓名',
trigger: 'blur'
}
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{
@@ -145,7 +153,7 @@ const rules = ref({
return;
}
if (!LicensePlateValidator.isValidPlate(value)) {
callback(new Error('请输入正确的车牌号格式'));
callback();
return;
}
callback();
@@ -159,7 +167,8 @@ const rules = ref({
{
validator: (_, value) => validator(value, 'money'),
message: '请输入正确的金额,最多两位小数',
trigger: 'blur'
trigger: 'blur',
required: true
}
]
});

View File

@@ -54,19 +54,19 @@
<el-table v-loading="loading" border :data="monthCardList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="月卡编号" align="center" prop="cardCode" v-if="true" />
<el-table-column label="办理时间" align="center" prop="handleTime" width="180">
<el-table-column label="办理时间" align="center" prop="handleTime" width="150">
<template #default="scope">
<span>{{ parseTime(scope.row.handleTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="业主" align="center" prop="cardName" />
<el-table-column label="绑定车牌号" align="center" prop="carNum" />
<el-table-column label="剩余有效天数" align="center" prop="handleName">
<el-table-column label="业主" align="center" prop="cardName" width="150" />
<el-table-column label="绑定车牌号" align="center" prop="carNum" width="150" />
<el-table-column label="剩余有效天数" align="center" prop="handleName" width="120">
<template #default="scope">
{{ diffDays(scope.row.startTime, scope.row.endTime) }}
</template>
</el-table-column>
<el-table-column label="支付方式" align="center" prop="payMethod">
<el-table-column label="支付方式" align="center" prop="payMethod" width="150">
<template #default="scope">
<dict-tag :options="com_pay_method" :value="scope.row.payMethod" />
</template>
@@ -76,7 +76,7 @@
<dict-tag :options="com_pay_method" :value="scope.row.payMethod" />
</template>
</el-table-column> -->
<el-table-column label="备注" align="center" prop="remark"></el-table-column>
<el-table-column label="备注" show-overflow-tooltip align="center" prop="remark"></el-table-column>
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['monthCard:monthCard:edit']">修改</el-button>
@@ -206,10 +206,14 @@ const { queryParams, form, rules } = toRefs(data);
/** 查询月卡管理列表 */
const getList = async () => {
loading.value = true;
const res = await listMonthCard(queryParams.value);
listMonthCard(queryParams.value)
.then((res) => {
monthCardList.value = res.rows;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
};
/** 取消按钮 */

View File

@@ -143,10 +143,14 @@ const { queryParams, form, rules } = toRefs(data);
/** 查询公告信息列表 */
const getList = async () => {
loading.value = true;
const res = await listNotice(queryParams.value);
listNotice(queryParams.value)
.then((res) => {
noticeList.value = res.rows;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
};
/** 搜索按钮操作 */
@@ -177,7 +181,7 @@ const handleAdd = () => {
/** 修改按钮操作 */
const handleUpdate = async (row?: NoticeVO) => {
const _id = row.noticeId || ids.value[0];
const _id = row?.noticeId || ids.value[0];
router.push({
path: '/repair/editNoticePc',
query: {

View File

@@ -28,7 +28,7 @@
<h3 class="title">
代办 <span v-if="todonum !== 0">{{ todonum }}</span>
</h3>
<el-button link type="primary">清除未</el-button>
<el-button link type="primary">全部已</el-button>
</div>
<div class="flex" v-else-if="activeTabs === 2">
<h3 class="title">
@@ -36,6 +36,7 @@
</h3>
<el-button link type="primary">清除未读</el-button>
</div>
<ul v-if="activeTabs === 1">
<li
v-for="(item, index) in awaittoList"
@@ -49,59 +50,66 @@
<div class="info">{{ item.content }}保修标题{{ item.item }}</div>
</li>
</ul>
<ul v-else-if="activeTabs === 2">
<li
v-for="(item, index) in noticelist"
:key="index"
@click="handleDisable(item)"
@click="handleRead(item)"
:class="{
disable: item.disabled
disable: !isdisable(item)
}"
>
<div class="subtitle">报修工单通知</div>
<div class="info">您有一条新的报修工单需要处理保修标题下水道堵塞</div>
</li>
</ul>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="init" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { getNotificationList } from '@/api/system/Notification/index';
import pagination from '@/components/Pagination/index.vue';
import { getNotificationList, NotificationVo, ReadNotificationByID } from '@/api/system/Notification/index';
import { ref } from 'vue';
// 1 代办 2通知
const activeTabs = ref(1);
function handleClick(tab: number) {
activeTabs.value = tab;
}
const todonum = computed(() => {
return awaittoList.value.filter((item) => isdisable(item)).length;
});
const noticnum = computed(() => {
return noticelist.value.filter((item) => isdisable(item)).length;
});
const total = ref(0);
const queryParams = ref({
pageNum: 1,
pageSize: 10
});
function handleClick(tab: number) {
activeTabs.value = tab;
}
function isdisable(item) {
return item.repairIsCheck ? item.repairIsCheck === '1' : item.manageIsCheck === '1';
}
const noticelist = ref([]);
const awaittoList = ref([]);
const awaittoList = ref<NotificationVo[]>([]);
function init() {
getNotificationList({
pageNum: 1,
pageSize: 10
}).then((res) => {
awaittoList.value = res.data;
getNotificationList(queryParams.value).then((res) => {
awaittoList.value = res.rows;
total.value = res.total;
});
}
init();
function handleRead(row) {}
function handleDisable(row) {
row.disabled = true;
function handleRead(row: NotificationVo) {
ReadNotificationByID([row.id]).then(() => {
init();
});
}
</script>
@@ -112,6 +120,7 @@ function handleDisable(row) {
display: flex;
font-size: 0.8rem;
flex: 1;
overflow: hidden;
.lefttabs {
width: 275px;
height: 100%;

View File

@@ -166,10 +166,14 @@ const { queryParams, form, rules } = toRefs(data);
const getList = async () => {
loading.value = true;
// 停车缴费
const res = await listParkingCost(queryParams.value);
listParkingCost(queryParams.value)
.then((res) => {
parkingCostList.value = res.rows;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
};
/** 搜索按钮操作 */
@@ -200,7 +204,7 @@ const handleAdd = () => {
/** 修改按钮操作 */
const handleUpdate = async (row?: ParkingCostVO) => {
const _id = row.id || ids.value[0];
const _id = row?.id || ids.value[0];
router.push({
path: '/carArea/editParkingCost',
query: {

View File

@@ -10,39 +10,22 @@
<el-row>
<el-col :span="12">
<el-form-item label="参与范围" prop="cardName">
<div class="flex flex-items-center">
<el-radio-group v-model="form.scope">
<el-radio v-for="item in com_questionnaire_scope" :value="item.value" :label="item.label" :key="item.value"></el-radio>
</el-radio-group>
<Holder v-if="form.scope === '1'" returnvalue="id" ref="HolderRef" :isMultiple="true" :userid="checkoutUses" @change="handleSelect">
<template #default>
<el-button type="primary" link @click="OpenUseTablesfun">选择业主</el-button>
</template>
</Holder>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row v-if="form.scope === '1'">
<el-col :span="12">
<ul class="checkoutSideUsesbox">
<li v-for="(item, index) in checkoutUses" :key="index">
<div>
<span>姓名</span>
<span>{{ item.name }}</span>
</div>
<el-icon @click="delteUse(item.id)">
<Delete></Delete>
</el-icon>
</li>
<Holder returnvalue="*" ref="HolderRef" :isMultiple="true" :userid="checkoutUses" @change="handleSelect">
<template #default>
<li @click="OpenUseTablesfun" class="add justify-center">
<el-icon>
<DocumentAdd></DocumentAdd>
</el-icon>
<span>添加业主</span>
</li>
</template>
</Holder>
</ul>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-col :span="8">
<el-form-item label="时间范围" prop="timedata">
<el-date-picker
v-model="timedata"
@@ -104,18 +87,18 @@ const timedata = ref([]);
// 打开
const HolderRef = ref<InstanceType<typeof Holder>>();
function OpenUseTablesfun() {
HolderRef.value.open();
HolderRef.value.open(form.value.residents.split(',').filter((item) => item));
}
// 多选
const checkoutUses = ref([]);
function handleSelect(val) {
checkoutUses.value = val;
form.value.residents = val.map((item) => item.id).join(',');
}
// 删除
function delteUse(id: string | number) {
checkoutUses.value = checkoutUses.value.filter((item) => item.id !== id);
form.value.residents = val
.map((item) => item)
.filter((item) => item)
.join(',');
}
// ! 编辑问卷 ------------------------------------------------------------------------
const questionnaireEditRef = ref<InstanceType<typeof QuestionnaireEdit>>();
const editData = ref<QuestionComponentnaireForm>(null);
@@ -155,15 +138,7 @@ async function handleEdit(id: string | number) {
checkoutUses.value = res.data.residents
.split(',')
.filter((item) => item)
.map((item) => {
const find = tableData.value.find((useritem) => useritem.id == item);
if (find) {
return {
name: find.name,
id: item
};
}
});
.map((item) => item);
timedata.value = [res.data.startTime, res.data.endTime];
});
}

View File

@@ -19,7 +19,7 @@
</el-card>
</div>
</transition>
<!-- TODO 删除问题 -->
<el-card class="flex-1" shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
@@ -98,7 +98,7 @@ const questionnaireList = ref<QuestionnaireVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const ids = ref<Array<string>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
@@ -141,10 +141,14 @@ const { queryParams, form, rules } = toRefs(data);
/** 查询问卷调查列表 */
const getList = async () => {
loading.value = true;
const res = await listQuestionnaire(queryParams.value);
listQuestionnaire(queryParams.value)
.then((res) => {
questionnaireList.value = res.rows;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
};
/** 取消按钮 */
@@ -188,7 +192,7 @@ const handleAdd = () => {
/** 修改按钮操作 */
const handleUpdate = async (row?: QuestionnaireVO) => {
const _id = row.id || ids.value[0];
const _id = row?.id || ids.value[0];
router.push({
path: '/repair/editQuestionnaire',
query: {
@@ -226,21 +230,23 @@ const submitForm = () => {
/** 删除按钮操作 */
const handleDelete = async (row?: QuestionnaireVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除问卷调查?').finally(() => (loading.value = false));
await delQuestionnaire(_ids);
await proxy?.$modal
.confirm('是否确认删除问卷调查?')
.then(() => {
delQuestionnaire(_ids, true).catch(async (res) => {
const content = res.msg.replace(/\n/g, '<br/>');
await proxy?.$modal
.confirm(content)
.then(() => {
delQuestionnaire(_ids, false).then(() => {
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'questionnaire/questionnaire/export',
{
...queryParams.value
},
`questionnaire_${new Date().getTime()}.xlsx`
);
getList();
});
})
.finally(() => (loading.value = false));
});
})
.finally(() => (loading.value = false));
};
onMounted(() => {

View File

@@ -27,7 +27,13 @@
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="报修标题" prop="item">
<el-input v-model.trim="form.item" placeholder="请输入报修标题" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="问题描述" prop="problem">
@@ -38,7 +44,19 @@
<el-row>
<el-col :span="20">
<el-form-item label="报修人" prop="residentId">
<Holder :userid="form.residentId" @change="handleChangeResident"></Holder>
<Holder :isMultiple="false" :userid="form.residentId ?? ''" ref="HolderRef" @change="handleChange">
<template #default>
<div class="residentBox" @click="choiceResidentFun">
<div class="defaultbox" v-if="form.residentId === '' || form.residentId === null">
<span>请选择</span>
<el-icon><Search /></el-icon>
</div>
<div v-else class="overflow-auto overflow-hidden w-full text-ellipsis text-nowrap">
<span>姓名{{ form?.residentName }}</span>
</div>
</div>
</template>
</Holder>
</el-form-item>
</el-col>
</el-row>
@@ -88,9 +106,6 @@
<script setup lang="ts">
import { ref } from 'vue';
import PageHeader from '@/components/Pageheader/index.vue';
import { addMonthCard, getMonthCard, updateMonthCard } from '@/api/system/MonthCard';
import { getVillageTree } from '@/api/system/house';
import { getCommunitylistAPI } from '@/api/system/community';
import { convertBuildingToTree, hasFormData } from '@/utils/house';
import Holder from '@/components/Holder/index.vue';
import { UploadProps, UploadUserFile } from 'element-plus';
@@ -114,8 +129,10 @@ const form = ref({
id: null,
houseId: null, // 关联房屋id
maintenanceItemId: null, // 维修项目id
item: '', // 标题项目名称
problem: '', // 问题描述
residentId: null, // 报修人id(住户表id)
residentId: '', // 报修人id(住户表id)
residentName: '', // 报修人姓名
phone: null, // 报手机号码
problemImageUrl: '', // 问题图片地址
problemStatus: '0', // 0 已提交 1已分配 2维修中 3维修完成
@@ -143,7 +160,7 @@ const currentviliage = ref();
const maintenanceItemTreeData = ref();
function init() {
gettreelistRepairProject().then((res) => {
maintenanceItemTreeData.value = res.data;
maintenanceItemTreeData.value = handleTreeDisable(res.data);
if (route.query.id) {
userid.value = route.query.id;
form.value.id = userid.value;
@@ -168,6 +185,21 @@ function init() {
}
init();
function handleTreeDisable(TreeData) {
const arr = TreeData.map((item) => {
if (item.status === '1') {
item.disabled = true;
} else {
item.disabled = false;
}
if (item && item.children && item.children.length > 0) {
item.children = handleTreeDisable(item.children);
}
return item;
});
return arr;
}
// ! 上传图片 =--==================================================================
const fileList = ref<UploadUserFile[]>([]);
@@ -184,8 +216,21 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
};
// !== 选择报修人 ====================================================================
function handleChangeResident(val: string | number) {
form.value.residentId = val;
const holderInfo = ref(null);
const HolderRef = ref<typeof Holder>(null);
function choiceResidentFun() {
HolderRef.value.open([form.value.residentId]);
}
// 选择住户
function handleChange(pop) {
holderInfo.value = null;
holderInfo.value = pop;
form.value.residentId = '';
form.value.residentName = '';
if (pop !== null) {
form.value.residentId = holderInfo.value.id;
form.value.residentName = holderInfo.value.name;
}
}
// !== 房屋 ===========================================================================
@@ -314,4 +359,21 @@ const cancelForm = () => {
}
}
}
.residentBox {
width: 385px;
height: 35px;
line-height: 35px;
border: 1px solid #ccc;
border-radius: 5px;
padding: 0 10px;
.defaultbox {
display: flex;
align-items: center;
justify-content: space-between;
}
cursor: pointer;
&:hover {
background-color: #f4f8ff;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div class="AddBuildingBox">
<PageHeader :title="userid ? '编辑车辆信息' : '添加车辆信息'"></PageHeader>
<PageHeader></PageHeader>
<div class="AddBuildingBody">
<div class="title">基本信息</div>
<div class="addBuildingFormBody">
@@ -41,7 +41,7 @@
<el-col :span="11">
<el-form-item label="隶属父级" prop="email">
<el-select @change="handleChange" v-model="form.parentId" placeholder="请选择隶属父级" style="width: 240px">
<el-option v-for="item in options" :key="item.id" :label="item.projectName" :value="item.id" />
<el-option v-for="item in options" :key="item.id" :disabled="item.disabled" :label="item.projectName" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
@@ -59,7 +59,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import PageHeader from '@/components/Pageheader/index.vue';
import { addRepairProject, listRepairProject, updateRepairProject } from '@/api/system/RepairProject';
import { addRepairProject, getRepairProject, listRepairProject, updateRepairProject } from '@/api/system/RepairProject';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { com_repair_project_types } = toRefs<any>(proxy?.useDict('com_repair_project_types'));
const { com_repair_project_status } = toRefs<any>(proxy?.useDict('com_repair_project_status'));
@@ -88,6 +88,7 @@ const userid = ref(null);
const options = ref([]);
function handleChange(val) {
if (!val) return;
const find = options.value.find((item) => item.id === val);
if (find) {
form.value.projectType = find.projectType;
@@ -95,7 +96,7 @@ function handleChange(val) {
}
// != ==========================================
function init() {
async function init() {
form.value = {
id: null,
projectName: null, // 项目名称
@@ -104,22 +105,26 @@ function init() {
projectLevel: 0, //项目层级
status: '0' //状态
};
listRepairProject().then((res) => {
const res = await listRepairProject();
const arr = res.rows.filter((ite) => ite.projectLevel === 0);
options.value = arr;
if (route.query.id) {
userid.value = route.query.id;
const find = res.rows.find((item) => item.id === userid.value);
if (find) {
form.value = {
...form.value,
...find,
projectLevel: find.projectLevel
};
handleChange(find.parentId);
}
getRepairProject(userid.value).then((res) => {
form.value = res.data;
form.value.status = res.data.status ?? '0' + '';
options.value = options.value.map((ite) => {
if (ite.id === res.data.id) {
ite.disabled = true;
} else {
ite.disabled = false;
}
return ite;
});
handleChange(res.data.parentId);
});
}
}
init();
// !== 提交 =============================================================================

View File

@@ -60,16 +60,17 @@
<el-table v-loading="loading" border :data="repairList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="报修房屋" align="center" prop="houseName" />
<el-table-column label="报修房屋" align="center" show-overflow-tooltip prop="houseName" />
<el-table-column label="维修项目" align="center" prop="maintenanceItemName" />
<el-table-column label="标题" align="center" prop="item" />
<el-table-column label="问题描述" align="center" prop="problem" />
<el-table-column label="报修人" align="center" prop="residentName" />
<el-table-column label="预约日期" align="center" prop="orderDate" width="180">
<el-table-column label="报修人" width="100" align="center" prop="residentName" />
<el-table-column label="预约日期" align="center" prop="orderDate" width="150">
<template #default="scope">
<span>{{ parseTime(scope.row.orderDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="维修状态" align="center" prop="problemStatus">
<el-table-column label="维修状态" align="center" width="100" prop="problemStatus">
<template #default="scope">
<dict-tag :options="com_repair_status" :value="scope.row.problemStatus || ''"></dict-tag>
</template>
@@ -94,8 +95,6 @@
<script setup name="Repair" lang="ts">
import { listRepair, delRepair } from '@/api/system/Repair/index';
import { RepairVO, RepairQuery, RepairForm } from '@/api/system/Repair/type';
import { listRepairProject } from '@/api/system/RepairProject';
import { listResident } from '@/api/system/resident';
import { checkPermi } from '@/utils/permission';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -103,7 +102,7 @@ const { com_repair_status } = toRefs<any>(proxy?.useDict('com_repair_status'));
interface repairlistType extends RepairVO {
maintenanceItemPath?: string;
residentName?: string;
residentName: string;
}
const repairList = ref<repairlistType[]>([]);
const loading = ref(true);
@@ -164,10 +163,14 @@ const { queryParams, form, rules } = toRefs(data);
/** 查询报修管理列表 */
const getList = async () => {
loading.value = true;
const res = await listRepair(queryParams.value);
listRepair(queryParams.value)
.then((res) => {
repairList.value = res.rows;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
};
/** 搜索按钮操作 */
@@ -178,6 +181,8 @@ const handleQuery = () => {
/** 重置按钮操作 */
const resetQuery = () => {
queryParams.value.problemStatus = '';
queryParams.value.orderDate = '';
queryFormRef.value?.resetFields();
handleQuery();
};
@@ -198,7 +203,7 @@ const handleAdd = () => {
/** 修改按钮操作 */
const handleUpdate = async (row?: RepairVO) => {
const _id = row.id || ids.value[0];
const _id = row?.id || ids.value[0];
router.push({
path: '/repair/editRepair',
query: {
@@ -208,7 +213,7 @@ const handleUpdate = async (row?: RepairVO) => {
};
/** 分配人员 */
const handleShare = (row) => {
const _id = row.id || ids.value[0];
const _id = row?.id || ids.value[0];
router.push({
path: '/repair/repairMain',
query: {

View File

@@ -15,28 +15,30 @@
</template>
<div class="w-full h-full CarinfoBox">
<el-row class="mb-20px">
<el-col :span="8">
<el-col :span="10">
<div class="carinfo_item">
<div class="label">报修房屋</div>
<div class="main">{{ form.house }}</div>
<div class="main">{{ form.houseName }}</div>
</div>
</el-col>
<el-col :span="8">
<el-col :span="4"></el-col>
<el-col :span="10">
<div class="carinfo_item">
<div class="label">维修项目</div>
<div class="main">{{ form.maintenanceItem }}</div>
<div class="main">{{ form.maintenanceItemName }}</div>
</div>
</el-col>
</el-row>
<el-row class="mb-20px">
<el-col :span="8">
<el-col :span="10">
<div class="carinfo_item">
<div class="label">报修人</div>
<div class="main">{{ form.residentName }}</div>
</div>
</el-col>
<el-col :span="8">
<el-col :span="4"></el-col>
<el-col :span="10">
<div class="carinfo_item">
<div class="label">手机号码</div>
<div class="main">{{ form.phone }}</div>
@@ -59,7 +61,7 @@
</div>
</el-col>
</el-row>
<el-row>
<el-row v-if="form.problemImageUrl.trim() === ''">
<el-col :span="24">
<div class="carinfo_item">
<div class="label">附件</div>
@@ -69,10 +71,10 @@
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
:preview-src-list="form.problemImageUrlList"
:preview-src-list="form.problemImageUrl.split(',').filter((item) => item)"
show-progress
:initial-index="0"
v-for="(item, index) in form.problemImageUrlList"
v-for="(item, index) in form.problemImageUrl.split(',').filter((item) => item)"
:key="index"
:src="splitImages(item)"
class="carinfoImage"
@@ -93,9 +95,9 @@
<div class="preivewBody w-full h-full flex flex-items-start mt-2">
<div class="title">维修师傅</div>
<div class="preivewBodybox flex-1 ml-5">
<repairUser :userid="form.repairId" @change="handleChange"></repairUser>
<repairUser :is-multiple="false" :username="form.repairName" :userid="form.repairUserId" @change="handleChange"> </repairUser>
<div class="actions mt-5">
<el-button @click="handleSend" type="primary">确定派单</el-button>
<el-button v-if="form.problemStatus === '0'" @click="handleSend" type="primary">确定派单</el-button>
<el-button @click="backRouter">返回</el-button>
</div>
</div>
@@ -150,17 +152,10 @@ import { ref } from 'vue';
import repairUser from '@/components/Holder/repairUser.vue';
import PageHeader from '@/components/Pageheader/index.vue';
import { getRepair, updateRepair } from '@/api/system/Repair';
import { getHousePathById } from '@/utils/house';
import { listRepairProject } from '@/api/system/RepairProject';
import { listResident } from '@/api/system/resident';
import { RepairVO } from '@/api/system/Repair/type';
import { useHouseStore } from '@/store/modules/house';
import { listRepairProcess, updateRepairProcess } from '@/api/system/repairProcess';
import { getRepairProcess, listRepairProcess, updateRepairProcess } from '@/api/system/repairProcess';
import { splitImages } from '@/utils';
const houseStore = useHouseStore();
const { treedata } = storeToRefs(houseStore);
const route = useRoute();
const router = useRouter();
@@ -168,32 +163,41 @@ const active = ref(0);
const activeStop = ref(0);
interface formType extends RepairVO {
maintenanceItem?: string;
residentName?: string;
problemImageUrlList?: string[];
house?: string;
problemImageUrlList: string[];
}
const form = ref<formType>({
'id': null,
'houseId': null,
'id': '',
'houseId': '',
'maintenanceItemId': null,
'maintenanceItemName': null,
'problem': '',
'residentId': null,
'phone': null,
'residentId': '',
'residentName': null,
'phone': '',
'orderDate': '',
'problemImageUrl': '',
'problemStatus': '0',
'problemStatus': '',
'repairDes': '',
'repairImageUrl': '',
'repairId': null
'repairId': '',
'repairName': '',
'houseName': '',
'repairUserId': '',
'item': null,
'projectType': null,
'updateTime': '',
'projectTypeStr': '',
'repairIsCheck': '1',
'manageIsCheck': '1',
'isCheckStr': null,
'repairPhone': null,
'repairRoleName': null,
'repairStatus': null,
'createTime': null,
problemImageUrlList: []
});
const userid = ref();
// 维修项目
const projectlist = ref([]);
// 住户
const residentList = ref([]);
// 审核流程
const reviewInfo = ref({
'id': '',
@@ -207,39 +211,28 @@ const reviewInfo = ref({
});
// ! 房屋 ====================================================================================================
async function init() {
// 项目
const projectres = await listRepairProject();
projectlist.value = projectres.rows;
// 住户
const residentlistres = await listResident();
residentList.value = residentlistres.rows;
// 报修详情
const res = await getRepair(userid.value);
form.value = res.data;
const find = projectlist.value.find((item) => item.id === form.value.maintenanceItemId);
if (find) {
if (find.parentId) {
const find2 = projectlist.value.find((item) => item.id === find.parentId);
form.value.maintenanceItem = find2.projectName + ' - ' + find.projectName;
} else {
form.value.maintenanceItem = find.projectName;
}
}
const houseinfo = getHousePathById(treedata.value, form.value.houseId, 'label');
form.value.house = houseinfo.join(' - ');
const residentinfo = residentList.value.find((item) => item.id === form.value.residentId);
form.value.residentName = residentinfo.name;
getRepair(userid.value).then((res) => {
form.value = {
...form.value,
...res.data
};
try {
form.value.problemImageUrlList = form.value.problemImageUrl.split(',').filter((item) => item);
// 获取保修审核状态
const info = await listRepairProcess({ repairId: form.value.id, pageNum: 1, pageSize: 999999 });
if (info && info.rows.length > 0) {
reviewInfo.value = info.rows.pop();
handleReview();
} catch {
form.value.problemImageUrlList = [];
}
// 获取保修审核状态
listRepairProcess({
repairId: res.data.id,
pageSize: 9999,
pageNum: 1
}).then((info) => {
reviewInfo.value = info.rows[info.rows.length - 1] || null;
console.log(reviewInfo.value);
handleReview();
});
});
}
// 处理审核
@@ -269,10 +262,12 @@ if (route.query.id) {
/** 派单 */
function handleSend() {
// 已分配
form.value.problemStatus = '1';
updateRepair(form.value).then(() => {
form.value.problemStatus = '1';
reviewInfo.value.allocateTime = new Date().toLocaleString().replaceAll('/', '-');
if (form.value.repairId) {
if (form.value.repairUserId) {
reviewInfo.value.allocateStatus = '1';
} else {
reviewInfo.value.allocateStatus = '0';
@@ -285,7 +280,7 @@ function handleSend() {
/** 选择维修人员 */
function handleChange(row) {
form.value.repairId = row;
form.value.repairUserId = row;
}
/** 返回 */
@@ -327,8 +322,9 @@ function handleEdit() {
.label {
font-weight: 600;
width: 80px;
text-align: right;
text-align: left;
padding-right: 10px;
white-space: nowrap;
}
.carinfoImage {
margin: 10px;

View File

@@ -36,7 +36,7 @@
:disable-delete="multiple"
:disable-edit="single"
:show-add="checkPermi(['repairProject:repairProject:add'])"
:show-edit="checkPermi(['repairProject:repairProject:edit'])"
:show-edit="false"
:show-delete="checkPermi(['repairProject:repairProject:remove'])"
@update:add="handleAdd"
@update:edit="handleUpdate()"

View File

@@ -19,17 +19,26 @@
</el-radio-group>
</el-form-item>
<el-form-item label="计算公式" prop="formula">
<el-select v-model="form.formula">
<el-select
@change="
(val) => {
if (val === '2') {
form.unit = null;
}
}
"
v-model="form.formula"
>
<el-option v-for="(item, index) in com_charge_item_formula" :key="index" :value="item.value" :label="item.label"></el-option>
</el-select>
</el-form-item>
<el-form-item label="计量单位" prop="unit">
<el-form-item label="计量单位" prop="unit" v-if="form.formula !== '2'">
<el-select v-model="form.unit">
<el-option v-for="(item, index) in com_charge_item_unit" :key="index" :value="item.value" :label="item.label"></el-option>
</el-select>
</el-form-item>
<el-form-item label="费用单价" prop="fixedPrice">
<el-input v-model.trim="form.fixedPrice" placeholder="请输入费用单价" />
<el-input v-model.trim="form.fixedPrice" :placeholder="placeHolderFun(form.typeId)" />
</el-form-item>
<el-form-item label="计算精度" prop="accuracy">
<el-select v-model="form.accuracy">
@@ -41,16 +50,15 @@
<el-option v-for="(item, index) in com_charge_item_carry_method" :key="index" :value="item.value" :label="item.label"></el-option>
</el-select>
</el-form-item>
<el-form-item label="启用/停用" prop="status">
<!-- <el-form-item label="启用/停用" prop="status">
<el-radio-group v-model="form.status">
<el-radio value="0" label="启用"></el-radio>
<el-radio value="1" label="停用"></el-radio>
</el-radio-group>
</el-form-item>
</el-form-item> -->
<el-form-item label="备注" prop="remark">
<el-input type="textarea" :rows="5" v-model.trim="form.remark" placeholder="请输入" />
</el-form-item>
<el-form-item style="margin-top: 30px">
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="cancelForm">返回</el-button>
@@ -119,6 +127,9 @@ if (route.query.id) {
// !== 提交 =============================================================================
// 提交
const submitForm = () => {
if (form.value.formula === '2') {
form.value.unit = null;
}
formRef.value?.validate(async (valid: boolean) => {
if (valid) {
if (userid.value) {
@@ -139,6 +150,12 @@ const submitForm = () => {
const cancelForm = () => {
router.back();
};
// 车位费 物业费
const typeNamelist = ref(['2049026520314445826']);
function placeHolderFun(typeId: string) {
return typeNamelist.value.includes(typeId) ? '请输入每日费用单价' : '请输入费用单价';
}
</script>
<style scoped lang="scss">

View File

@@ -36,38 +36,39 @@
<el-table v-loading="loading" border :data="chargeItemList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="项目名称" align="center" prop="itemName" />
<el-table-column label="计费方式" align="center" prop="billingType">
<el-table-column label="计费方式" width="100" align="center" prop="billingType">
<template #default="scope">
<dict-tag :options="com_charge_item_method" :value="scope.row.billingType"></dict-tag>
</template>
</el-table-column>
<el-table-column label="计算公式" align="center" prop="formula">
<el-table-column label="计算公式" width="100" align="center" prop="formula">
<template #default="scope">
<dict-tag :options="com_charge_item_formula" :value="scope.row.formula"></dict-tag>
</template>
</el-table-column>
<el-table-column label="单价" align="center" prop="fixedPrice">
<el-table-column label="单价" width="100" align="center" prop="fixedPrice">
<template #default="scope">
{{ scope.row.price }}
<dict-tag :options="com_charge_item_unit" :value="scope.row.fixedPrice"></dict-tag>
</template>
</el-table-column>
<el-table-column label="计量单位" align="center" prop="unit">
<el-table-column label="计量单位" width="100" align="center" prop="unit">
<template #default="scope">
<dict-tag :options="com_charge_item_unit" :value="scope.row.unit"></dict-tag>
<dict-tag v-if="scope.row.unit" :options="com_charge_item_unit" :value="scope.row.unit"></dict-tag>
<span v-else>---</span>
</template>
</el-table-column>
<el-table-column label="计算精度" align="center" prop="accuracy">
<el-table-column label="计算精度" width="120" align="center" prop="accuracy">
<template #default="scope">
<dict-tag :options="com_charge_item_precision" :value="scope.row.accuracy"></dict-tag>
</template>
</el-table-column>
<el-table-column label="进位方式" align="center" prop="carryMethod">
<el-table-column label="进位方式" width="100" align="center" prop="carryMethod">
<template #default="scope">
<dict-tag :options="com_charge_item_carry_method" :value="scope.row.carryMethod"></dict-tag>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<el-table-column label="状态" width="100" align="center" prop="status">
<template #default="scope">
<el-switch
@change="(val: string) => handleSwitch(val, scope.row)"
@@ -77,6 +78,7 @@
></el-switch>
</template>
</el-table-column>
<el-table-column label="备注" show-overflow-tooltip align="center" prop="remark" />
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['chargeItem:chargeItem:edit']">编辑</el-button>
@@ -138,10 +140,14 @@ const { queryParams, form, rules } = toRefs(data);
/** 查询收费项目管理列表 */
const getList = async () => {
loading.value = true;
const res = await listChargeItem(queryParams.value);
listChargeItem(queryParams.value)
.then((res) => {
chargeItemList.value = res.rows;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
};
/** 取消按钮 */
@@ -185,10 +191,11 @@ const handleAdd = () => {
/** 修改按钮操作 */
const handleUpdate = async (row?: ChargeItemVO) => {
const _ids = row?.id || ids.value[0];
router.push({
path: '/sdb/editchargeItem',
query: {
id: row.id
id: _ids
}
});
};

View File

@@ -214,23 +214,12 @@ const submitForm = () => {
/** 删除按钮操作 */
const handleDelete = async (row?: ChargeTypeVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除收费类型编号为"' + _ids + '"的数据项').finally(() => (loading.value = false));
await proxy?.$modal.confirm('是否确认删除收费类型?').finally(() => (loading.value = false));
await delChargeType(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'chargeType/chargeType/export',
{
...queryParams.value
},
`chargeType_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});

Some files were not shown because too many files have changed in this diff Show More