7/10 缴费分析,人员分析 数据对接

This commit is contained in:
Zy
2026-07-10 17:44:28 +08:00
parent 3a808750ac
commit 298f93380c
46 changed files with 1065 additions and 673 deletions

View File

@@ -13,7 +13,8 @@ export interface DecorationVO {
* 所属房屋id
*/
houseId: string | number;
buildingId: string;
unitNoId: string;
/**
* 申请人姓名
*/

View File

@@ -40,3 +40,110 @@ export const income_statsApi = (datetime?: string): AxiosPromise<IncomeStatsResp
}
});
};
export interface sourceType {
'totalPayable': string;
'totalReceived': string;
'totalPending': string;
'parkingFee': string;
'parkingFeePercent': string;
'propertyFee': string;
'propertyFeePercent': string;
'utilityFee': string;
'utilityFeePercent': string;
'shopRent': string;
'shopRentPercent': string;
'repairFee': string;
'repairFeePercent': string;
'garbageFee': string;
'garbageFeePercent': string;
}
// 收入来源 --₧╬.╬
export const income_scourcApi = (datetime?: string): AxiosPromise<sourceType> => {
return request({
url: '/api/analysis/income/source',
method: 'get',
params: {
date: datetime
}
});
};
export interface PayMentMethod {
'wechatAmount': string;
'wechatPercent': string;
'alipayAmount': string;
'alipayPercent': string;
'offlineAmount': string;
'offlinePercent': string;
}
// 缴费方式
export const payment_methodApi = (datetime?: string): AxiosPromise<PayMentMethod> => {
return request({
url: '/api/analysis/income/payment_method',
method: 'get',
params: {
date: datetime
}
});
};
export interface Arrears_distrubution {
month: number;
amount: string;
}
// 欠费分布
export const arrears_distributionApi = (
datetime?: string
): AxiosPromise<{
items: Arrears_distrubution[];
}> => {
return request({
url: '/api/analysis/income/arrears_distribution',
method: 'get',
params: {
date: datetime
}
});
};
export interface payment_sourceType {
'miniProgramAmount': string;
'miniProgramPercent': string;
'mobileAppAmount': string;
'mobileAppPercent': string;
'offlineAmount': string;
'offlinePercent': string;
}
// 缴费来源分析
export const payment_sourceApi = (datetime?: string): AxiosPromise<payment_sourceType> => {
return request({
url: '/api/analysis/income/payment_source',
method: 'get',
params: {
date: datetime
}
});
};
export interface payment_trendType {
'date': string;
'paidAmount': string;
'unpaidAmount': string;
}
// 缴费走势分析
export const payment_trendApi = (startDate?: string, endDate?: string): AxiosPromise<{
items:payment_trendType[]
}> => {
return request({
url: '/api/analysis/income/payment_trend',
method: 'get',
params: {
startDate,
endDate,
}
});
};

View File

@@ -8,7 +8,7 @@ import { AxiosPromise } from 'axios';
*/
export const user_movein_list = (query?: {
datetime: string;
dateTime: string;
}): AxiosPromise<{
'villageId': string;
'villageName': string;
@@ -31,7 +31,7 @@ export const user_movein_list = (query?: {
*/
export const get_movein_user_type = (query?: {
datetime: string;
dateTime: string;
}): Promise<{
'owner': string;
'ownerRate': string;
@@ -56,7 +56,7 @@ export const get_movein_user_type = (query?: {
*/
export const get_movein_user_sex = (query?: {
datetime: string;
dateTime: string;
}): Promise<{
'man': number;
'woman': number;
@@ -92,3 +92,22 @@ export const get_movein_moveout_list = (query?: {
params: query
});
};
export const get_person_age = (query?: {
dateTime: string;
}): AxiosPromise<
{
'date': string;
'moveInCount': number;
'moveOutCount': number;
'totalCount': number;
}[]
> => {
return request({
url: '/gender/personAge',
method: 'get',
params: query
});
};

View File

@@ -12,7 +12,7 @@ export function listOss(query: OssQuery): AxiosPromise<OssVO[]> {
}
// 查询OSS对象基于id串
export function listByIds(ossId: string | number): AxiosPromise<OssVO[]> {
export function listByIds(ossId: string): AxiosPromise<OssVO[]> {
return request({
url: '/resource/oss/listByIds/' + ossId,
method: 'get'

View File

@@ -4,6 +4,7 @@ export interface ResidentVO {
*/
userId: string;
id: string;
unitNoId: string;
/**
* 住户编号
*/
@@ -79,6 +80,7 @@ export interface ResidentForm extends BaseEntity {
* 主键 住户管理id
*/
userId?: string;
unitNoId: string;
/**
* 住户编号

View File

@@ -85,7 +85,7 @@ const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(
watch(
() => props.modelValue,
async (val) => {
async (val:string) => {
if (val) {
let temp = 1;
// 首先将值转为数组

View File

@@ -51,7 +51,7 @@
<el-table-column label="手机号码" align="center" prop="phone" />
<el-table-column label="房间" show-overflow-tooltip align="center">
<template #default="{ row }">
{{ row.housePath }}
{{ row.houseName }}
</template>
</el-table-column>
</el-table>
@@ -78,7 +78,6 @@
<script setup lang="ts">
import { listResident } from '@/api/system/resident';
import { ResidentVO } from '@/api/system/resident/type';
import { getHousePathById } from '@/utils/house';
import { ref, nextTick, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
import { useHouseStore } from '@/store/modules/house';
@@ -235,12 +234,6 @@ function getlist() {
listResident(queryParams.value)
.then((res) => {
tableData.value = res.rows;
tableData.value.forEach((item) => {
if (item.houseId !== null) {
const houselist = getHousePathById(treedata.value, item.houseId, 'label');
item.housePath = houselist?.join('-') || '';
}
});
handleUserSelection();
total.value = res.total;
})

View File

@@ -61,7 +61,6 @@ function isMapAvailable() {
function fitMapView(overlays: any[]) {
if (!isMapAvailable() || overlays.length === 0) return;
// 第二个参数true关闭动画留白80px最大缩放18级
console.log(props.smallScroll);
map.value.setFitView(overlays, props.smallScroll);
}

View File

@@ -36,7 +36,7 @@ import { getNormalPath } from '@/utils/ruoyi';
import { useSettingsStore } from '@/store/modules/settings';
import { usePermissionStore } from '@/store/modules/permission';
import { useTagsViewStore } from '@/store/modules/tagsView';
import { RouteRecordRaw, RouteLocationNormalized } from 'vue-router';
import { RouteRecordRaw, RouteLocationNormalized, RouteLocationRaw } from 'vue-router';
import { Back, CircleClose, Close, RefreshRight, Right } from '@element-plus/icons-vue';
const visible = ref(false);
@@ -181,7 +181,7 @@ const closeLeftTags = () => {
});
};
const closeOthersTags = () => {
router.push(selectedTag.value).catch(() => {});
router.push(selectedTag.value as RouteLocationRaw).catch(() => {});
proxy?.$tab.closeOtherPage(selectedTag.value).then(() => {
moveToCurrentTag();
});

View File

@@ -22,7 +22,7 @@ import SideBar from './components/Sidebar/index.vue';
import { AppMain, Navbar, Settings } from './components';
import { useAppStore } from '@/store/modules/app';
import { useSettingsStore } from '@/store/modules/settings';
import { NavTypeEnum } from '@/enums/NavTypeEnum';
// import { NavTypeEnum } from '@/enums/NavTypeEnum';
import { initWebSocket } from '@/utils/websocket';
import { initSSE } from '@/utils/sse';
@@ -75,13 +75,13 @@ const theme = computed(() => settingsStore.theme);
const sidebar = computed(() => useAppStore().sidebar);
const device = computed(() => useAppStore().device);
const needTagsView = computed(() => settingsStore.tagsView);
const fixedHeader = computed(() => settingsStore.fixedHeader);
const layout = computed(() => settingsStore.navType);
// const fixedHeader = computed(() => settingsStore.fixedHeader);
// const layout = computed(() => settingsStore.navType);
const showSidebar = computed(() => {
if (sidebar.value.hide) return false;
return layout.value === NavTypeEnum.LEFT || layout.value === NavTypeEnum.MIX;
});
// const showSidebar = computed(() => {
// if (sidebar.value.hide) return false;
// return layout.value === NavTypeEnum.LEFT || layout.value === NavTypeEnum.MIX;
// });
const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened,
@@ -108,9 +108,9 @@ watchEffect(() => {
const navbarRef = ref<InstanceType<typeof Navbar>>();
const settingRef = ref<InstanceType<typeof Settings>>();
const handleClickOutside = () => {
useAppStore().closeSideBar({ withoutAnimation: false });
};
// const handleClickOutside = () => {
// useAppStore().closeSideBar({ withoutAnimation: false });
// };
const setLayout = () => {
settingRef.value?.openSetting();
@@ -120,7 +120,7 @@ const setLayout = () => {
<style lang="scss" scoped>
@use '@/assets/styles/mixin.scss';
@use '@/assets/styles/variables.module.scss' as *;
//8^▐╬╬2╬╬>╘
.app-wrapper {
@include mixin.clearfix;
position: relative;

View File

@@ -2,7 +2,7 @@ import { getVillageByIdAPI } from '@/api/system/community';
import { communitylist_rows_type } from '@/api/system/community/type';
import { getBuildinglistAPI, getHouselistAPI, getVillageTree } from '@/api/system/house';
import type { BuildingUnit, BuildingVo } from '@/api/system/house/type';
import { convertBuildingToTree } from '@/utils/house';
import { buildHousePathCache, convertBuildingToTree } from '@/utils/house';
import { defineStore } from 'pinia';
// 数字排序
function sortNumberStr(a: string, b: string) {
@@ -235,6 +235,8 @@ export const useHouseStore = defineStore(
treedata.value = [];
const res = await getVillageTree(villageid);
treedata.value = convertBuildingToTree(res.data.buildings || []);
buildHousePathCache(treedata.value);
return treedata.value;
} catch (err) {
console.error('获取楼栋树失败:', err);

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

@@ -30,7 +30,7 @@ interface ImportMetaEnv {
VITE_LOGIN_BACK_URL: string;
VITE_LOGIN_BACK_URL_WORKBENCH_TRU: boolean;
VITE_APP_SHOW_WORK_PLATFORM: boolean;
VITE_APP_SHOW_WORK_PLATFORM: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;

View File

@@ -68,43 +68,94 @@ interface Tree {
type ValidTreeKey = {
[K in keyof Tree]: Tree[K] extends string | number ? K : never;
}[keyof Tree];
// 全局缓存houseId -> 对应字段路径数组
const pathCache = new Map<ValidTreeKey, Map<string, (string | number)[]>>();
/**
* 根据 houseId 查找完整路径(楼栋 > 单元 > 楼层 > 房屋)
* @param treeData 转换后的树形数据
* @param houseId 目标房屋ID
* @param response
* @returns string[] 路径数组,如 ['1号楼', '1单元', '3层', '301']
* 初始化缓存,树形数据加载完成执行一次
* @param treeData 完整树形数据
*/
export function getHousePathById(treeData: Tree[], houseId: string | number, response: ValidTreeKey = 'value'): (string | number)[] {
const path: (string | number)[] = [];
export function buildHousePathCache(treeData: Tree[]) {
// 清空旧缓存
pathCache.set('label', new Map());
pathCache.set('value', new Map());
// 递归查找
function findNode(nodes: Tree[]): boolean {
for (const node of nodes) {
path.push(node[response]); // 把当前节点加入路径
const stack: [Tree, (string | number)[], (string | number)[]][] = treeData.map(node => [
node,
[node.label],
[node.value]
]);
// 找到目标房屋
if (node.type === 'houseId' && node.value === houseId) {
return true;
}
while (stack.length) {
const [node, labelPath, valPath] = stack.pop()!;
// 递归子节点
if (node.children && node.children.length > 0) {
if (findNode(node.children)) {
return true;
}
}
path.pop(); // 回溯,移除当前节点
// 房屋节点存入两套索引
if (node.type === 'houseId') {
const id = String(node.value);
pathCache.get('label')!.set(id, labelPath);
pathCache.get('value')!.set(id, valPath);
}
return false;
}
findNode(treeData);
return path;
if (node.children?.length) {
const reverseChildren = [...node.children].reverse();
for (const child of reverseChildren) {
stack.push([
child,
[...labelPath, child.label],
[...valPath, child.value]
]);
}
}
}
}
/**
* 缓存版极速查询房屋路径O(1)
* @param houseId 房屋ID
* @param fieldKey 返回字段 label / value
*/
export function getHousePathById(houseId: string | number, fieldKey: ValidTreeKey = 'value'): (string | number)[] {
const id = String(houseId);
console.log(pathCache);
const cacheMap = pathCache.get(fieldKey);
if (!cacheMap) return [];
return cacheMap.get(id) ?? [];
}
// /**
// * 根据 houseId 查找完整路径(楼栋 > 单元 > 楼层 > 房屋)
// * @param treeData 转换后的树形数据
// * @param houseId 目标房屋ID
// * @param response
// * @returns string[] 路径数组,如 ['1号楼', '1单元', '3层', '301']
// */
// export function getHousePathById(treeData: Tree[], houseId: string | number, response: ValidTreeKey = 'value'): (string | number)[] {
// const path: (string | number)[] = [];
// // 递归查找
// function findNode(nodes: Tree[]): boolean {
// for (const node of nodes) {
// path.push(node[response]); // 把当前节点加入路径
//
// // 找到目标房屋
// if (node.type === 'houseId' && node.value === houseId) {
// return true;
// }
//
// // 递归子节点
// if (node.children && node.children.length > 0) {
// if (findNode(node.children)) {
// return true;
// }
// }
//
// path.pop(); // 回溯,移除当前节点
// }
// return false;
// }
//
// findNode(treeData);
// return path;
// }
// 对字符串 图片地址,处理
export function splitStringUrl(str: string, splitLabel: string = ',') {
const list = str.split(splitLabel);

View File

@@ -43,8 +43,8 @@ const whiteVillageId = [
'/payRecordList/payRecordList/list',
'/warehouse',
'/notification/read', // 已读
'/cash/billList',
'/building/list'
'/cash/billList'
// '/building/list'
];
/**

View File

@@ -55,11 +55,13 @@
<script lang="ts" setup>
import defaultVilageinfoImage from '@/assets/images/villageimage.png';
import Navbar from '@/layout/components/Navbar.vue';
import { deleteVillageByIdAPI, switchVillageAPI, getCommunitylistAPI } from '@/api/system/community/index';
import { deleteVillageByIdAPI, getCommunitylistAPI, switchVillageAPI } from '@/api/system/community/index';
import type { communitylist_rows_type } from '@/api/system/community/type';
import { AddLocation, Picture } from '@element-plus/icons-vue';
import { useHouseStore } from '@/store/modules/house';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const houseStore = useHouseStore();
const router = useRouter();
const list = ref<communitylist_rows_type[]>([]);
@@ -85,9 +87,10 @@ function handleAdd() {
/** 删除小区 */
function handleDelete(item: communitylist_rows_type) {
deleteVillageByIdAPI(item.villageId).then(() => {
const newlist = list.value.filter((listitem) => listitem.villageId !== item.villageId);
list.value = newlist;
proxy?.$modal.confirm('该信息删除后无法恢复,确定要删除吗?').then(() => {
deleteVillageByIdAPI(item.villageId).then(() => {
list.value = list.value.filter((listitem) => listitem.villageId !== item.villageId);
});
});
}

View File

@@ -37,7 +37,13 @@
<el-row>
<el-col :span="12">
<el-form-item label="装修押金(元)" prop="decorationDeposit">
<el-input v-model.trim="form.decorationDeposit" placeholder="请输入装修押金" />
<el-input
type="number"
min="0"
step="0.01"
placeholder="请输入装修押金最多保留2位小数"
v-model.trim.number="form.decorationDeposit"
/>
</el-form-item>
</el-col>
</el-row>
@@ -57,7 +63,7 @@ import { ref } from 'vue';
import PageHeader from '@/components/Pageheader/index.vue';
import { addDecoration, getDecoration, updateDecoration } from '@/api/system/Decoration';
import { useHouseStore } from '@/store/modules/house';
import { getHousePathById } from '@/utils/house';
import { validator } from '@/utils/Reg';
const houseStore = useHouseStore();
const treedata = ref([]);
@@ -68,15 +74,56 @@ const formRef = ref();
const form = ref({
'id': null,
'houseId': null,
'buildingId': null,
'unitNoId': null,
'applyName': '',
'decorationCompany': '',
'decorationContent': '',
'decorationDeposit': null
});
/**
* 通用金额校验器
* @param value 输入值
* @param type money金额 / num整数
* @returns 校验提示信息,空则校验通过
*/
const validator = (value: number | null, type: 'money' | 'num') => {
// 空值由 required 规则拦截,这里只处理有值的情况
if (value === null || value === undefined) return '';
// 转字符串处理小数位数
const strVal = String(value);
// 禁止负数
if (value < 0) return '金额不能为负数';
if (type === 'money') {
// 匹配最多两位小数
const reg = /^\d+(\.\d{1,2})?$/;
if (!reg.test(strVal)) return '金额仅支持最多两位小数';
// 限制金额上限(可按需修改)
if (value > 9999999.99) return '押金金额不能超过9999999.99元';
} else if (type === 'num') {
const reg = /^\d+$/;
if (!reg.test(strVal)) return '只能输入正整数';
}
return '';
};
const rules = {
houseId: [{ required: true, message: '请选择具体房屋', trigger: 'blur' }],
applyName: [{ required: true, message: '请输入申请人', trigger: 'blur' }],
decorationDeposit: [{ required: true, message: '请输入装修押金', trigger: 'blur' }],
decorationDeposit: [
{ required: true, message: '请输入装修押金', trigger: 'blur' },
{
validator: (rule, val, callback) => {
const msg = validator(val, 'money');
if (msg) callback(new Error(msg));
else callback();
},
trigger: ['blur', 'change']
}
],
decorationContent: [{ required: true, message: '请输入装修内容', trigger: 'blur' }]
};
const userid = ref();
@@ -85,7 +132,7 @@ const userid = ref();
const userHousepath = ref([]);
function handleChange(val: (string | number)[]) {
if (val.length >= 3) {
form.value.houseId = val.pop() as number;
form.value.houseId = val.at(2) as number;
} else {
form.value.houseId = null;
}
@@ -102,14 +149,14 @@ function getTree() {
});
}
getTree();
// TODO
function init() {
getDecoration(userid.value).then((res) => {
form.value = {
...form.value,
...res.data
};
const arr = getHousePathById(treedata.value, form.value.houseId);
userHousepath.value = arr;
userHousepath.value = [form.value.buildingId, form.value.unitNoId, form.value.houseId];
});
}

View File

@@ -169,7 +169,7 @@ const rules = ref({
}
callback();
},
trigger: 'blur' // ✅ 只在失焦时验证
trigger: 'blur' // ✅ 只在失焦时验证╬N
}
],
startTime: [{ required: true, message: '请选择有效日期', trigger: 'blur' }],

View File

@@ -8,7 +8,7 @@
<el-form-item label="项目名称" prop="itemName">
<el-input v-model.trim="form.itemName" placeholder="请输入项目名称" />
</el-form-item>
<el-form-item label="收费项目" prop="typeId">
<el-form-item label="收费类型" prop="typeId">
<el-select v-model="form.typeId">
<el-option v-for="(item, index) in chargeitemlist" :key="index" :value="item.id" :label="item.name"></el-option>
</el-select>

View File

@@ -108,7 +108,6 @@ const { com_charge_item_carry_method } = toRefs<any>(proxy?.useDict('com_charge_
const { com_charge_item_formula } = toRefs<any>(proxy?.useDict('com_charge_item_formula'));
const chargeItemList = ref<ChargeItemVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
@@ -117,12 +116,6 @@ const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const chargeItemFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: ChargeItemForm = {
id: undefined,
@@ -138,7 +131,7 @@ const data = reactive<PageData<ChargeItemForm, ChargeItemQuery>>({
rules: {}
});
const { queryParams, form, rules } = toRefs(data);
const { queryParams } = toRefs(data);
/** 查询收费项目管理列表 */
const getList = async () => {
@@ -153,18 +146,6 @@ const getList = async () => {
});
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
chargeItemFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
@@ -221,17 +202,6 @@ const handleDelete = async (row?: ChargeItemVO) => {
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'chargeItem/chargeItem/export',
{
...queryParams.value
},
`chargeItem_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});

View File

@@ -6,16 +6,11 @@
<div class="addBuildingFormBody">
<el-form class="formBody" label-position="right" ref="formRef" :model="form" :rules="rules" label-width="130px">
<el-form-item label="选择仪表" prop="deviceType">
<el-radio-group v-model="form.deviceType" @change="handlechange">
<el-radio-group v-model="form.deviceType" @change="handleChange">
<el-radio label="电表" value="1"></el-radio>
<el-radio label="水表" value="2"></el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item label="抄表项目" prop="payType">
<el-radio-group v-model="form.payType" @change="handlechange2">
<el-radio v-for="(item, index) in chargeitemlist" :key="index" :label="item.itemName" :value="item.id"></el-radio>
</el-radio-group>
</el-form-item> -->
<el-form-item label="抄表月份" prop="recordMonth">
<el-date-picker @update:model-value="handleMonth" v-model="form.recordMonth" value-format="YYYY-MM" format="YYYY-MM" type="month" />
</el-form-item>
@@ -47,14 +42,12 @@ import { ref } from 'vue';
import PageHeader from '@/components/Pageheader/index.vue';
import { addPayRecord, getPayRecord, updatePayRecord } from '@/api/system/SdbPayRecord';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const router = useRouter();
const route = useRoute();
const formRef = ref();
const form = ref({
'deviceType': '1', // 设备类型 1电表 2水表
'payType': '1', // 设备类型 1电表 2水表
'deviceType': '1', // 设备类型: 1 电表 2 水表
'payType': '1', // 设备类型: 1 电表 2 水表
'recordMonth': '', // 抄表月份
'startTime': '', // 开始时间
'endTime': '' // 结束时间
@@ -112,11 +105,7 @@ const disabledDate = (time: Date) => {
// 不在 [当月1号 ~ 当月最后1号] 之间 → 禁用
return time < chooseStart.value || time > chooseEnd.value;
};
// const chargeitemlist = ref([]);
function init() {
// listChargeItem().then((res) => {
// chargeitemlist.value = res.rows;
// });
if (route.query.id) {
userid.value = route.query.id;
getPayRecord(userid.value).then((res) => {
@@ -130,7 +119,7 @@ function init() {
init();
function handlechange(val: string) {
function handleChange(val: string) {
form.value.deviceType = val;
form.value.payType = val;
}
@@ -141,12 +130,12 @@ const submitForm = () => {
formRef.value?.validate(async (valid: boolean) => {
if (valid) {
if (userid.value) {
updatePayRecord(form.value).then((res) => {
updatePayRecord(form.value).then(() => {
ElMessage.success('编辑成功');
cancelForm();
});
} else {
addPayRecord(form.value).then((res) => {
addPayRecord(form.value).then(() => {
ElMessage.success('添加成功');
cancelForm();
});
@@ -192,52 +181,6 @@ const cancelForm = () => {
overflow-y: auto;
padding: 10px 5px;
}
.checkoutSideUsesbox {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 5px;
li {
display: flex;
align-items: center;
justify-content: space-between;
min-width: 120px;
height: 35px;
padding: 0 10px;
line-height: 35px;
margin-bottom: 10px;
background-color: #f3f9ff;
border: 1px solid #8fc0f1;
@media (max-width: 2000px) {
font-size: 12px;
}
@media (max-width: 1650px) {
font-size: 10px;
}
@media (max-width: 1650px) {
font-size: 8px;
}
.el-icon {
cursor: pointer;
&:hover {
color: red;
}
}
&.add {
cursor: pointer;
justify-content: center;
color: #409eff;
border: 1px dashed #409eff;
background-color: transparent;
&:hover {
background-color: #40a0ff10;
}
span {
margin-left: 10px;
}
}
}
}
&:deep(.el-form-item__content, .el-select, .el-input) {
width: 350px !important;
margin-right: 10px;

View File

@@ -142,7 +142,7 @@
</template>
<script setup name="PayRecordList" lang="ts">
import { listPayRecordList, getPayRecordList, addPayRecordList, updatePayRecordList } from '@/api/system/SdbPayRecordMain/index';
import { listPayRecordList, getPayRecordList, addPayRecordList, updatePayRecordList } from '@/api/system/SdbPayRecordMain';
import { PayRecordListVO, PayRecordListQuery, PayRecordListForm } from '@/api/system/SdbPayRecordMain/type';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -155,8 +155,6 @@ const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const router = useRouter();
const route = useRoute();
@@ -219,9 +217,9 @@ const deviceCodeList = [
];
const { queryParams, form } = toRefs(data);
const queryid = ref('');
const queryId = ref('');
if (route.query.id) {
queryid.value = route.query.id as string;
queryId.value = route.query.id as string;
}
/** 查询抄表周期明细列表 */
@@ -229,7 +227,7 @@ const getList = async () => {
loading.value = true;
const res = await listPayRecordList({
...queryParams.value,
recordId: queryid.value ?? undefined
recordId: queryId.value ?? undefined
});
payRecordListList.value = res.rows;
total.value = res.total;

View File

@@ -12,7 +12,7 @@
<el-select v-model="form.chargeItemId" @change="(val: string) => handleChange(val, true)">
<el-option
:disabled="item.status === '1'"
v-for="(item, index) in chargeitemlist"
v-for="(item, index) in chargeItemList"
:key="index"
:value="item.id"
:label="item.itemName"
@@ -20,7 +20,7 @@
</el-select>
</el-form-item>
<!-- 水费电费 收费范围为 房屋 -->
<el-form-item label="收费范围" prop="chargeScope" v-if="typeNamelist.includes(typeName)">
<el-form-item label="收费范围" prop="chargeScope" v-if="typeNameList.includes(typeName)">
<div>
<div class="flex flex-row flex-items-end">
<el-radio-group v-model="form.chargeScope">
@@ -36,7 +36,7 @@
</div>
</el-radio-group>
<div style="height: 32px">
<el-button @click="OpenUseTablesfun('house')" link type="primary" v-if="form.chargeScope === '1'" class="ml-10px">请选择</el-button>
<el-button @click="OpenUseTablesFun('house')" link type="primary" v-if="form.chargeScope === '1'" class="ml-10px">请选择</el-button>
</div>
</div>
</div>
@@ -57,21 +57,21 @@
</div>
</el-radio-group>
<div style="height: 32px">
<el-button @click="OpenUseTablesfun('user')" link type="primary" v-if="form.chargeScope === '1'" class="ml-10px">请选择</el-button>
<el-button @click="OpenUseTablesFun('user')" link type="primary" v-if="form.chargeScope === '1'" class="ml-10px">请选择</el-button>
</div>
</div>
</el-form-item>
<!-- 房屋选择 -->
<el-form-item v-if="form.chargeScope === '1' && typeNamelist.includes(typeName)">
<el-form-item v-if="form.chargeScope === '1' && typeNameList.includes(typeName)">
<div class="residentBox">
<House returnvalue="houseId" ref="HolderRef" :isMultiple="true" @change="handleSelect"> </House>
</div>
</el-form-item>
<!-- 业主选择 -->
<el-form-item v-if="form.chargeScope === '1' && !typeNamelist.includes(typeName)">
<el-form-item v-if="form.chargeScope === '1' && !typeNameList.includes(typeName)">
<!-- 业主选择 车位费用 -->
<div class="residentBox" v-if="typeName === HandleTypeId.Resident.Parking">
<CarUser returnvalue="parkingId" ref="ParkinguserRef" :isMultiple="true" @change="handleCheckPaking"></CarUser>
<CarUser returnvalue="parkingId" ref="ParkingUserRef" :isMultiple="true" @change="handleCheckParking"></CarUser>
</div>
<div class="residentBox" v-else>
<UserHolder returnvalue="residentId" ref="userRef" :isMultiple="true" @change="handleCheckUser"></UserHolder>
@@ -192,7 +192,7 @@ function handleClear() {
form.value.endDate = '';
}
// 收费类型
const chargeitemlist = ref([]);
const chargeItemList = ref([]);
// // 房屋列表
// const houselist = ref([]);
@@ -200,7 +200,7 @@ const chargeitemlist = ref([]);
// const userlist = ref([]);
async function init() {
listChargeItem({}).then((res) => {
chargeitemlist.value = res.rows;
chargeItemList.value = res.rows;
// const res = await listBillHouse({ pageNum: 1, pageSize: 999999 });
// houselist.value = res.rows;
@@ -252,11 +252,11 @@ const HandleTypeId = {
}
};
// 符合类型: 显示房屋,选择房屋
const typeNamelist = ref([HandleTypeId.House.Water, HandleTypeId.House.Electricity]);
const typeNameList = ref([HandleTypeId.House.Water, HandleTypeId.House.Electricity]);
const typeName = ref('');
// 获取收费类型id
function handleChange(val: string, callback: boolean = false) {
const find = chargeitemlist.value.find((item) => item.id === val);
const find = chargeItemList.value.find((item) => item.id === val);
if (find) {
form.value.chargeTypeId = find.typeId;
typeName.value = find.typeId;
@@ -282,7 +282,7 @@ function handleCheckUser(val: string[]) {
}
const checkoutParking = ref([]);
/** 添加 车位业主 */
function handleCheckPaking(val: string[]) {
function handleCheckParking(val: string[]) {
checkoutParking.value = val;
console.log(val);
form.value.parkingIds = val;
@@ -299,13 +299,13 @@ function handleClearCheckout() {
// 打开
const HolderRef = ref<InstanceType<typeof House>>();
const userRef = ref<InstanceType<typeof UserHolder>>();
const ParkinguserRef = ref<InstanceType<typeof CarUser>>();
function OpenUseTablesfun(val: string) {
const ParkingUserRef = ref<InstanceType<typeof CarUser>>();
function OpenUseTablesFun(val: string) {
if (val === 'house') {
HolderRef.value.open(checkoutHouses.value);
} else if (val === 'user') {
if (typeName.value === HandleTypeId.Resident.Parking) {
ParkinguserRef.value.open(checkoutParking.value);
ParkingUserRef.value.open(checkoutParking.value);
} else {
userRef.value.open(checkoutUser.value);
}
@@ -370,63 +370,6 @@ const cancelForm = () => {
overflow-y: auto;
padding: 10px 5px;
}
.checkoutSideUsesbox {
// display: grid;
// grid-template-columns: repeat(3, 1fr);
// gap: 5px;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
justify-content: space-between;
padding: 10px 5px;
li {
margin-right: 10px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
min-width: 240px;
height: 35px;
padding: 0 10px;
line-height: 35px;
background-color: #f3f9ff;
border: 1px solid #8fc0f1;
margin-bottom: 10px;
@media (max-width: 2000px) {
font-size: 12px;
}
@media (max-width: 1650px) {
font-size: 10px;
}
@media (max-width: 1650px) {
font-size: 8px;
}
.el-icon {
margin-left: 20px;
cursor: pointer;
&:hover {
color: red;
}
}
&.add {
cursor: pointer;
justify-content: center;
color: #409eff;
border: 1px dashed #409eff;
background-color: transparent;
&:hover {
background-color: #40a0ff10;
}
span {
margin-left: 10px;
}
}
}
}
&:deep(.el-form-item__content, .el-select, .el-input) {
width: 350px !important;
margin-right: 10px;

View File

@@ -11,11 +11,11 @@
<el-form-item label="收费项目" prop="chargeItemId">
<el-select disabled v-model="form.chargeItemId" @change="handleChange">
<el-option v-for="(item, index) in chargeitemlist" :key="index" :value="item.id" :label="item.itemName"></el-option>
<el-option v-for="(item, index) in chargeItemList" :key="index" :value="item.id" :label="item.itemName"></el-option>
</el-select>
</el-form-item>
<el-form-item label="收费范围" prop="chargeScope" v-if="itemTypeincludes">
<el-form-item label="收费范围" prop="chargeScope" v-if="itemTypeIncludes">
<el-radio-group disabled v-model="form.chargeScope">
<el-radio v-for="(item, index) in com_questionnaire_scope" :key="index" :value="item.value" :label="item.label"></el-radio>
</el-radio-group>
@@ -110,23 +110,23 @@ function handleDate(val: string[]) {
form.value.startDate = formatDate2(val[0]);
form.value.endDate = formatDate2(val[1]);
}
const chargeitemlist = ref([]);
const chargeItemList = ref([]);
// 电费 水费
const typeNamelist = ref(['2049026484272791553', '2049026503017136129']);
const typeNameList = ref(['2049026484272791553', '2049026503017136129']);
const itemTypeincludes = computed(() => {
return typeNamelist.value.includes(form.value.chargeItemId);
const itemTypeIncludes = computed(() => {
return typeNameList.value.includes(form.value.chargeItemId);
});
const houselist = ref([]);
const houseList = ref([]);
const checkoutUses = ref([]);
async function init() {
const res1 = await listBillHouse({ pageNum: 1, pageSize: 999999 });
houselist.value = res1.rows;
houseList.value = res1.rows;
listChargeItem().then(async (res) => {
chargeitemlist.value = res.rows;
chargeItemList.value = res.rows;
if (route.query.id) {
userid.value = route.query.id;
@@ -140,7 +140,7 @@ async function init() {
checkoutUses.value = form.value.houseIds
.map((item) => {
if (item) {
const find = houselist.value.find((it) => it.houseId === item);
const find = houseList.value.find((it) => it.houseId === item);
if (find) {
return find;
} else {
@@ -158,14 +158,11 @@ init();
// 获取收费类型id
function handleChange(val) {
const find = chargeitemlist.value.find((item) => item.id === val);
const find = chargeItemList.value.find((item) => item.id === val);
if (find) {
form.value.charegeTypeId = find.typeId;
}
}
// !== 提交 =============================================================================
// 推出
const cancelForm = () => {
router.back();
@@ -228,8 +225,6 @@ const cancelForm = () => {
background-color: #f3f9ff;
border: 1px solid #8fc0f1;
margin-bottom: 10px;
@media (max-width: 2000px) {
font-size: 12px;
}

View File

@@ -121,7 +121,7 @@ import { billlistType, billPrint, BillPrintType, billQuery } from '@/api/system/
import { printTicketDiv } from '@/utils/print';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { com_pay_method, com_pay_status } = toRefs<any>(proxy?.useDict('com_pay_method', 'com_pay_status'));
const { com_pay_status } = toRefs<any>(proxy?.useDict('com_pay_status'));
const { com_bill_charge_period } = toRefs<any>(proxy?.useDict('com_bill_charge_period'));
const { com_charge_item_unit } = toRefs<any>(proxy?.useDict('com_charge_item_unit'));

View File

@@ -312,6 +312,7 @@ function changeTabs(value: number) {
function handleChangeVillage(val: string) {
queryParams.value.unitNoId = undefined;
queryParams.value.buildingId = undefined;
queryParams.value.chargeItemId = undefined;
buildingList.value = [];
unitList.value = [];
if (val) {
@@ -398,14 +399,25 @@ const buttonLoading = ref(false);
const inComeFrom = ref({
allCost: null, // 金额
billResidentId: [], // 账单id
payStatus: '0', // 0:未缴 1:已缴
payStatus: '1', // 0:未缴 1:已缴
payType: '0', // 0:微信 1:支付宝 2 线下
remark: '',
residentName: '' // 缴费人
});
function initInComeFrom() {
inComeFrom.value = {
allCost: null, // 金额
billResidentId: [], // 账单id
payStatus: '1', // 0:未缴 1:已缴
payType: '2', // 0:微信 1:支付宝 2 线下
remark: '',
residentName: '' // 缴费人
};
}
// 收费 --------------------------------------------------------------
/** 收费 */
function handleIncome(row: billlistType | false) {
initInComeFrom();
console.log(ids.value);
inComeFrom.value.allCost = 0;
inComeFrom.value.billResidentId = [];
@@ -436,6 +448,7 @@ function handleIncome(row: billlistType | false) {
const closer = () => {
dialog.visible = false;
dialog.title = '';
initInComeFrom();
};
const submitForm = () => {
buttonLoading.value = true;
@@ -526,6 +539,142 @@ onMounted(() => {
villageList.value = res.rows;
});
});
/*
* 显。     更何况李恩成把自己的要求提得很清楚,一来是【血
* 暾果】,二来是通过【宣元坊】来联系他,不要再去府辰峰。   
*   “此人在宗内独善其身多年,果然有些心思……”     李
* 曦治思忖一阵,先把笔墨搁置,朝着杨宵儿道:     “恐怕还
* 要宵儿去一趟帝云峰,查一查李恩成入宗前后之事,是不是得罪了什
* 么人,在自己峰中都不敢说话。”     李恩成虽然是丹修大师
* ,却处处受制于云丹峰,大部分人情都是求到云丹峰,他苦苦炼成丹
* 药,最后人情与好处大都被云丹峰得去。     只是他一向表现
* 得古怪,众人都当他不在乎,李曦治现下想来也觉得蹊跷了,不敢让
* 自己父亲贸然行事,打算打听个清楚。     ‘杨天衙亲自为我
* 家遮掩,想必与我家有联系或是与萧家有图谋,去帝云峰才能查出真
* 消息,也不至于露出马脚。’     他放了笔墨,取出《六色寻
* 元遁》,创出这遁法的前辈明显是個好面子的,玉简上五彩缤纷,雕
* 刻着各色华纹。     ‘若是论真元华丽,《朝霞采露诀》还真
* 是一等一…’     李曦治看了一夜,便见着杨宵儿驾风回来,
* 皱着眉进来,回答道:     “李恩成…还真得罪过一人!”
*     “谁?”     李曦治连忙抬头,却见杨宵儿面色古怪
* ,低声道:     “迟尉!”     “迟尉?!”    
*  李曦治呆了一瞬,有些难以置信地道:     “怎么可能?”
*     杨宵儿仔细检查门窗,用秘法传音:     “迟尉当
* 年还是筑基,与李恩成外出,两人在一片废墟中寻到了一道丹道传承
* ,李恩成见利起欲,打伤迟尉,将他锁在秘境之中,夺取此道…”
*     “不曾想迟尉后来反而在其中得了大好处,归宗而来,李恩
* 成被拿下审问,只依靠了当年的府辰峰主一力保下,迟尉也大度不再
* 追究。”     “两人化干戈为玉帛,后头李恩成再未出宗,一
* 日日在峰上炼丹,以赎罪过……”     李曦治听得摇头:  
*    “原来还有这种渊源…难怪…”     杨宵儿复又道:
*     “如今迟尉身死,李恩成成了摇钱树,宗内已经抹去这一段
* 不提,乃是老祖派人告诉我的。”     “难怪李恩成很少外出
* …”     李曦治点头,若有所思:     “未必是他不愿
* 出宗,兴许是不敢。”     李曦治从架上取出一枚玉简,仔细
* 一查,若有所悟,低声道:     “果然有这小段记录,所得【
* 密樊宗】一丹道传承…原来其中还有这样一段故事。”     杨
* 宵儿努努嘴,提醒道:     “这事情实在太过久远,除了几个
* 紫府,如今宗内的峰主与弟子皆不晓得,应该没有什么问题。”  
*    “我家老祖当年也不过是个小弟子,这事情几分真几分假,宗
* 内说的和秘境中发生的都不知真假,姑且一听。”     李曦治
* 微微点头,心中暗暗计较,拿起笔来又在信上添了几句,默默折好,
* 用自家秘法封了,准备寄出去。     李渊蛟送来的三枚宝药并
* 没有用上,虽然父亲李渊蛟信中说的是若是结交无望,留以自用,李
* 曦治却不舍得全都塞进自己腰包里。     李曦治仔细看了看,
* 取出其中一枚【云藤灵椒】,准备交给云丹峰炼制一炉丹药,其余两
* 枚还是原封不动地收好,装入小袋之中,随信送回家去。     
* ……     青杜峰。     李曦明端坐在炉火旁,金黄色的
* 火焰照的他面上忽明忽暗,手中轻轻一钩,【玄阳离火】跳动而出,
* 在他指上回荡勾勒。     【玄阳离火】爆裂难缠,却在他手中
* 乖巧犹如精灵,跳跃流动。     这原本是对敌的上好灵火,只
* 可惜家中只有这一份,只能转成【长行元火】,李曦明在手中把玩着
* ,感受着其中爆裂灼热的强大威力,有些惋惜。     李曦明受
* 了符种,修为到达练气五层,距离练气六层本只有一步之遥,闭关四
* 月,轻轻松松地突破练气六层,气息平缓,游刃有余。     
* 箓气如此神妙,可惜只能受一道,若是能服用几道就好了…’   
*   李曦明闭关两月,才出了关,心中痒痒,把院外的下人叫进来,
* 吩咐道:     “把那窦…几个都叫上来…”     这人帮
* 着传唤多次,闻弦歌而知雅意,自然晓得,正拜退下去,李曦明又连
* 忙道:     “等等!等等…有人来了。”     便见着院
* 门嘎吱一声开了,外头进来个白袍少年,剑眉星目,一身真元如寒雪
* ,笑道:     “曦明出关了,倒是来得巧!”     李曦
* 峻的修为也达到了练气五层,两人受的都是筑基级别,一连突破,都
* 比宗内的李曦治高出一筹了,都成了练气中期,唯独山下的李曦峸还
* 是练气二层。     兄弟两坐下,李曦明还有些尴尬,李曦峻不
* 去点破,将他闭关前后的事情一一说了,把两条矿脉一指,李曦明也
* 颇为欣喜,答道:     “是好事!”     李曦峻开口道
*     “仲父如今已经闭关养伤,我这两月外出采气,修炼【
* 屠钧葵光】,顺道走了两个坊市,已经把施【闰阳法】的灵萃灵物为
* 你买齐了。”     说着从储物袋中摆出诸物,凤尾花纹的灵石
* 、入手温热的玉瓶、赤红的灵萃…一系列灵物:     “这是【
* 凤尾石】、这是【灰乌烟】、【元阳灵萃】……”     李曦明
* 一一接过,见着李曦峻风尘仆仆的模样,连忙谢了一句,把东西先放
* 在案上,就不好意思多说什么,只能说着多谢。     李曦峻摆
* 手,不曾放在心上。     他如今修为有所精进,又开始修炼《
* 清目灵瞳》,眼中精光流转,仿佛能把人看个通透,低声道:   
*   “大叔公近来很是憔悴,应该是心魔缠身,只是他从不肯说,你
* 多炼些清心一类的丹药…我找机会再叫几人跑一趟南边大郡,寻一寻
* 这一类的丹药。”     “恰好我有一味灵水也需要外出寻找,
* 把这两件事合在一块做,也方便些。”     李曦明听了他的话
* ,点头应是,李玄宣是他大父,本来是他最该上心的,只是闭关修炼
* ,耽误了时间,愧道:     “我这就去炼丹,我这就去炼丹…
* 大父的事情我今后仔细看着,不必你们操心。”     李曦峻笑
* 着点头,李曦明当下问道:     “【屠钧葵光】竟然这样难修
* 炼?家中已经有一味寒水,一道寒气,袁家那处得了一道,《寒雪集
* 》中有三道寒气,一道寒水寻法…还要外出?”     “就差那
* 一道寒水了。”     李曦峻笑着点头,答道:     “我
* 这【明霜松岭】善于使用法术,【屠钧葵光】在我手中很是厉害。”
*     他伸出两指,将拇指抵在为食指根处,口中念诀,顿时放
* 出一阵刺目的寒光来,寒气凛凛,指尖飘散出一缕缕洁白无瑕的华光
* ,在空中四散飞舞。     李曦明只觉得面上生寒,身后汗毛根
* 根竖起,脚底下的地面浮现出一层白莹莹的霜,体力的真元流转都有
* 些不畅起来。     桌案上放着灵材,李曦峻不敢放开,只略微
* 展示,挥手散去法术,解释道:     “屠钧葵光远不止在寒、
* 还在于阴,等我得了灵水,还能更上一层,到时候在打斗中运起这法
* 术,就算是修为高出我许多,猝不及防也要吃个闷亏。”     
* 李曦明羡慕地看了看:     “只可惜我功法乃是金阳一系,不
* 能修炼,《金殿煌元诀》虽好,却没有配套的法术与遁术。”   
*   两人交谈一阵,青杜山大阵外传来一声娇喝:     “玄岳
* 门孔氏,应约而来,还请开一开山门!”     两人微愣,驾风
* 而起,阵外正站着个素衣女子,腰间佩着金珠,挂着两把弯刀,姿容
* 绰约,后头则是个平平无奇的中年人,一言不发。     ‘好美
* 的女子。’     李曦明性格平和,虽然有些留恋于女色,却还
* 不至于色令智昏,知道是筑基前辈,低垂着头不敢看。     李
* 曦峻则抱拳道:     “来人可是孔婷云前辈?”     “
* 不错,你是…?”     孔婷云看了他一眼,心中暗赞,客气几
* 分,轻声道:     “蛟兄曾经与我约定为贵族修筑火脉,如今
* 我已应约而来。”     “哗啦…”     言谈之间,望月
* 湖深青色的湖面轰然破开,一个黑袍青年驾风上来,阴沉沉地看了一
* 眼孔婷云,目光停留在她腰间配着两把弯刀上,脸色一下难看起来,
* 一言不发地立着。     孔婷云瞥了他一眼,只觉得很是眼熟,
* 震惊了一瞬,失声道:     “钩蛇?!”     李乌梢深
* 深地出了两口气,低声下气地道:     “钩蛇已死,在下是青
* 杜李乌梢,乌梢见过上仙。”     孔婷云很是不自然地应了,
* 埋藏多时的疑惑总算是解决:     ‘我说李渊蛟怎地莫名其妙
* 、不嫌苦累一路把这钩蛇带回李家…原来是有控制妖物的法子…毕竟
* 靠近北方,也不足为奇。’     只是她腰上还挂着钩蛇曾经的
* 尾钩炼制成的一对筑基法器,实在有些尴尬,钩蛇如今是李家的人,
* 该给的面子要给,默默把法器收到储物袋中,孔婷云道:     
* “玄岳孔婷云,见过道友。”     李乌梢面色好看许多,默默
* 退到李曦峻身后。     李渊蛟与李清虹闭关,这担子自然落到
* 李曦峻身上,他客客气气地道:     “还请前辈稍待些时日,
* 长辈正闭关,一时间不能出来迎接…”     孔婷云摆摆手,显
* 现出无所谓的模样,径直道:     “用不着李渊蛟,伱指一座
* 山让我开了火脉,便把那阵盘还我,我便要离去了,没有多少闲功夫
* 。”     言罢,孔婷云还有些提心吊胆地疑问道:     
* “那阵法不会给李渊蛟弄坏了,至今不敢见我?这可是贵重物件,要
* 是弄坏了,你家可赔得多了!”     “前辈放心,自然无事。
* ”     李曦峻点头,火脉的选址本是李家计划好的,选在了乌
* 涂山。     火脉变动大多会影响周边的灵田,其余几峰都矗立
* 在大片灵田之中,唯独乌涂山孤零零地悬在山林中,只能选了这山。
*     孔婷云一示意,那练气巅峰的中年男子已经驾风落向乌涂
* 山,李曦峻带人驾风过去,一齐在空中看着。 亲,点击进去,给个
* 好评呗,分数越高更新越快,据说给香书小说打满分的最后都找到了
* 漂亮的老婆哦! 手机站全新改版升级地址http://wap
* .xbiqugu.la数据和书签与电脑站同步无广告清新阅
* 读!
*/
</script>
<style lang="scss" scoped>

View File

@@ -35,27 +35,17 @@
<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="handleMarkqrcode" v-has-permi="['equipment:addqrcode:electricitydevice']"
>批量生成条形码</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Upload">批量更新固件</el-button>
</el-col> -->
<el-col :span="1.5">
<div style="font-size: 0.8rem">电表列表</div>
</el-col>
<right-toolbar :show-add="false" :show-edit="false" :show-delete="false">
<template #dropdown>
<!-- <el-dropdown-menu> -->
<el-dropdown-item @click="handleMarkqrcode" v-if="checkPermi(['equipment:addqrcode:electricitydevice'])">
<span>批量生成条形码</span>
</el-dropdown-item>
<el-dropdown-item @click="handleMarkQrcodeZip" v-if="checkPermi(['equipment:addqrcode:electricitydevice'])">
<span>批量下载条形码</span>
</el-dropdown-item>
<!-- </el-dropdown-menu> -->
<!-- <el-dropdown-item v-has-permi="['equipment:addqrcode:electricitydevice']">
<div @click="handleMarkqrcode">批量更新固件</div>
</el-dropdown-item> -->
@@ -261,7 +251,7 @@ import {
getElectricityDeviceTimeRangeUsedWater,
listElectricityDevice,
updateElectricityDevice
} from '@/api/system/SdbElectricityDevice/index';
} from '@/api/system/SdbElectricityDevice';
import { ElectricityDeviceVO } from '@/api/system/SdbElectricityDevice/type';
import { useHouseStore } from '@/store/modules/house';
import { useUserStore } from '@/store/modules/user';
@@ -510,7 +500,7 @@ function getTree() {
/** 电表 启停*/
// TODO 修改电表 启停
const handleChangeWater = (val: string, row: ElectricityDeviceVO) => {};
// const handleChangeWater = (val: string, row: ElectricityDeviceVO) => {};
/** 提交按钮 */
const submitForm = () => {
if (dialogType.value === 'usedWater') {

View File

@@ -310,7 +310,7 @@ import {
updateWaterDevice,
WaterDeviceUpdateBin,
latestRecord
} from '@/api/system/SdbWaterDevice/index';
} from '@/api/system/SdbWaterDevice';
import { WaterDeviceVO } from '@/api/system/SdbWaterDevice/type';
import { useHouseStore } from '@/store/modules/house';
import { useUserStore } from '@/store/modules/user';
@@ -614,7 +614,7 @@ function getTree() {
});
}
/** 水表 启停*/
/** 水表 ╬«2222╬2╬22╬╬╬╬2╬22222╬ 启停*/
const handleChangeWater = (val: string, row: WaterDeviceVO) => {};
function handleUpdate(id: string) {

View File

@@ -107,7 +107,7 @@
</template>
<script setup name="WaterRecord" lang="ts">
import { listWaterRecord, getWaterRecord, delWaterRecord, addWaterRecord, updateWaterRecord } from '@/api/system/SdbWaterMeterReading/index';
import { listWaterRecord, getWaterRecord, delWaterRecord, addWaterRecord, updateWaterRecord } from '@/api/system/SdbWaterMeterReading';
import { WaterRecordVO, WaterRecordQuery, WaterRecordForm } from '@/api/system/SdbWaterMeterReading/type';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;

View File

@@ -17,8 +17,7 @@
return;
}
SearchTime = val;
const days = val;
const [star, end] = days.map((item) => item.split(' ')[0]);
const [star, end] = val.map((item) => item.split(' ')[0]);
queryParams.startDate = star + ' 00:00:00';
queryParams.endDate = end + ' 23:59:59';
}

View File

@@ -127,7 +127,6 @@ function init() {
}
}
init();
function back() {
router.back();
}

View File

@@ -267,12 +267,12 @@ function choiceResidentFun() {
function handleChange(pop) {
holderInfo.value = null;
holderInfo.value = pop;
form.value.residentId = '';
form.value.residentName = '';
form.value.userId = '';
form.value.userName = '';
console.log(pop);
if (pop !== null) {
form.value.residentId = pop.user_id;
form.value.residentName = pop.user_name;
form.value.userId = pop.user_id;
form.value.userName = pop.user_name;
}
}
</script>

View File

@@ -33,7 +33,7 @@
</div>
<div class="info_item">
<div class="label">设备状态</div>
<span class="value">电梯</span>
<dict-tag :options="com_equipment_archive_status" :value="form.status"></dict-tag>
</div>
<div class="info_item">
<div class="label">维护人员</div>
@@ -88,6 +88,7 @@ import { delDeviceRepair, listDeviceRepair } from '@/api/system/equipment/Repair
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { com_equipment_repair_status } = toRefs<any>(proxy?.useDict('com_equipment_repair_status'));
const { com_equipment_archive_status } = toRefs<any>(proxy?.useDict('com_equipment_archive_status'));
const router = useRouter();
const route = useRoute();

View File

@@ -220,6 +220,8 @@ const form = ref({
vx: '', //微信
houseUse: '0', //房屋用途
rentalStatus: 0, //租赁状态
buildingId: null, //楼栋id
unitNoId: null, //单元id
email: '' //邮箱
});
const userHousepath = ref<CascaderValue>([]);
@@ -257,11 +259,9 @@ async function gettreedata() {
...form.value,
...res.data
};
housedeviceslist.value = form.value.facilities.split(',').filter((item) => item);
const imgurllist = splitStringUrl(form.value.houseImageUrl);
fileList.value = imgurllist;
const arr = getHousePathById(treedata.value, form.value.houseId);
userHousepath.value = arr;
if (form.value.facilities) housedeviceslist.value = form.value.facilities.split(',').filter((item) => item);
if (form.value.houseImageUrl) fileList.value = splitStringUrl(form.value.houseImageUrl);
handleChange([form.value.buildingId, form.value.unitNoId, form.value.houseId]);
});
}
}

View File

@@ -12,7 +12,7 @@
<div class="addBuildingFormBody">
<div class="descriptionsbox">
<div class="label">房屋:</div>
<div class="value">{{ HouseInfo.house }}</div>
<div class="value">{{ HouseInfo.name }}</div>
</div>
<div class="descriptionsbox">
<div class="label">业主姓名:</div>
@@ -104,10 +104,8 @@ import { getHousePathById } from '@/utils/house';
import { listHouseFacilities } from '@/api/system/HouseFacilities';
const houseStore = useHouseStore();
const { treedata } = storeToRefs(houseStore);
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { com_rental_method } = toRefs<any>(proxy?.useDict('com_rental_method'));
const { com_house_rental_facilities } = toRefs<any>(proxy?.useDict('com_house_rental_facilities'));
const route = useRoute();
const router = useRouter();
@@ -145,15 +143,14 @@ async function init() {
}
const reslist = await listHouseFacilities();
housefacilities.value = reslist.rows;
await houseStore.getCurrentVilageTreeHouse();
getHouseRental(houseRentalid.value).then((res) => {
HouseInfo.value = res.data;
HouseInfo.value.facilitieslist = res.data.facilities.split(',').filter((item) => item);
HouseInfo.value.houseImageUrls = HouseInfo.value.houseImageUrl.split(',').filter((item) => item);
const arr = getHousePathById(treedata.value, HouseInfo.value.houseId, 'label');
HouseInfo.value.house = arr.join(' - ');
});
}
//.╬
init();
function handlefacilities(id: string) {

View File

@@ -125,6 +125,7 @@ const route = useRoute();
const formRef = ref();
const form = ref<ResidentForm>({
houseId: null,
unitNoId: null,
name: '',
gender: '0',
idCard: '',
@@ -178,8 +179,7 @@ function gettreedata() {
form.value = {
...res.data
};
const arr = getHousePathById(treedata.value, form.value.houseId);
userHousepath.value = arr;
userHousepath.value = [form.value.buildingId, form.value.unitNoId, form.value.houseId];
});
}
}

View File

@@ -67,7 +67,7 @@
</el-table-column>
<el-table-column label="证件号" min-width="120" align="center" prop="idCard" />
<el-table-column label="手机号码" min-width="120" align="center" prop="phone" />
<el-table-column label="房间" min-width="120" align="center" prop="houseAddress" />
<el-table-column label="房间" min-width="120" align="center" prop="houseName" />
<el-table-column label="操作" align="center" min-width="120" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<el-button v-if="scope.row.status !== '1'" link type="primary" @click="handleMain(scope.row)" v-has-permi="['resident:resident:edit']"
@@ -98,7 +98,6 @@ import DictTag from '@/components/DictTag/index.vue';
import { listResident, delResident } from '@/api/system/resident';
import { ResidentVO, ResidentQuery, ResidentForm } from '@/api/system/resident/type';
import { getHousePathById } from '@/utils/house';
import { useHouseStore } from '@/store/modules/house';
import { checkPermi } from '@/utils/permission';
@@ -229,10 +228,6 @@ const getList = async () => {
listResident(queryParams.value)
.then((res) => {
residentList.value = res.rows;
residentList.value.forEach((item) => {
const arr = getHousePathById(treated.value, item.houseId, 'label');
item.houseAddress = arr.join(' ');
});
total.value = res.total;
})
.finally(() => {
@@ -324,6 +319,9 @@ onMounted(() => {
width: 250px;
min-width: 250px;
margin-right: 10px;
display: flex;
flex-direction: column;
.treeTitlebox {
height: 60px;
line-height: 60px;
@@ -333,7 +331,11 @@ onMounted(() => {
background-color: rgba(233, 242, 255, 0.5);
color: rgba(16, 16, 16, 1);
}
.treeboxbody {
flex: 1;
overflow: hidden;
overflow-y: auto;
margin-top: 10px;
&:deep(.el-tree-node__content) {

View File

@@ -70,7 +70,7 @@ import { allocatedUserList, authUserCancel, authUserCancelAll } from '@/api/syst
import { UserQuery } from '@/api/system/user/types';
import { UserVO } from '@/api/system/user/types';
import SelectUser from './selectUser.vue';
import { RouteLocationNormalized } from 'vue-router';
import { RouteLocationNormalized, RouteLocationRaw } from 'vue-router';
const route = useRoute();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -115,7 +115,7 @@ const handleClose = () => {
query: undefined,
redirectedFrom: undefined
};
proxy?.$tab.closeOpenPage(obj);
proxy?.$tab.closeOpenPage(obj as RouteLocationRaw);
};
/** 搜索按钮操作 */
const handleQuery = () => {

View File

@@ -0,0 +1,85 @@
<script name="RingProgress" setup lang="ts">
import { EChartsOption } from 'echarts';
import Echarts from '@/components/Echarts/index.vue';
const props = defineProps<{
name: string;
value: string;
color: string;
}>();
console.log(props.name, props.value);
const options = ref<EChartsOption>({
tooltip: {
show: false,
trigger: 'item',
formatter: '{b}: {c}%'
},
series: [
{
type: 'pie',
radius: ['50%', '80%'], // 环形大小
center: ['50%', '50%'], // 每个图表各自居中
labelLine: {
show: false
},
data: [
{
value: 100 - Number(props.value),
itemStyle: {
color: '#ebeef5' // 背景色
},
emphasis: {
itemStyle: {
color: '#ebeef5'
},
scale: false
}
},
{
value: Number(props.value),
name: props.name,
itemStyle: { color: props.color, borderRadius: 8, borderColor: props.color, borderWidth: 0 },
label: {
show: true,
position: 'center', // 显示在圆环中心
formatter: '{c}%', // 显示百分比数值
// fontSize: '50%',
// fontWeight: 'bold',
color: '#4ce19c',
rich: {
num: {
fontSize: '28%', // 富文本内支持百分比,相对圆环直径
fontWeight: 'bold',
color: '#4ce19c'
}
}
},
emphasis: {
scale: true
}
}
]
}
]
});
</script>
<template>
<div class="chart-item flex flex-col items-center" style="flex: 1; height: 100%">
<Echarts :options="options as EChartsOption" style="width: 100%; height: 165px"></Echarts>
<div class="chart-item-name">{{ props.name }}</div>
</div>
</template>
<style scoped lang="scss">
.chart-item {
min-width: 120px;
}
.chart-item-name {
font-size: 0.8rem;
}
</style>

View File

@@ -13,47 +13,42 @@
<el-button type="primary" @click="querySearch">查询</el-button>
<el-button @click="reset">重置</el-button>
</div>
<div style="margin-bottom: 15px; position: relative; z-index: 2">
<div style="position: relative; z-index: 2">
<el-row :gutter="20">
<el-col :lg="14" :xl="14">
<el-col :lg="14" :xl="14" style="margin-bottom: 15px">
<div class="cardbox">
<div>收入来源</div>
<div class="card_infoall">
<div>应缴费用{{ formatMoneyWithUnit(form.totalDue) }}</div>
<div>已收{{ formatMoneyWithUnit(form.totalPaid) }}</div>
<div>待收{{ formatMoneyWithUnit(form.totalUnpaid) }}</div>
<div class="card_InfoAll">
<div>应缴费用{{ formatMoneyWithUnit(sourceFrom.totalPayable) }}</div>
<div>已收{{ formatMoneyWithUnit(sourceFrom.totalReceived) }}</div>
<div>待收{{ formatMoneyWithUnit(sourceFrom.totalPending) }}</div>
</div>
<div class="cardbody" style="height: 165px">
<div
v-for="(opt, index) in incomeChartOptions"
:key="index"
class="chart-item flex flex-col items-center"
style="width: 165px; height: 100%"
>
<Echarts :options="opt.options ?? []" style="width: 100%; height: 165px"></Echarts>
<!-- 如果需要额外显示名称可以在这里加 div但图表中心已显示 -->
<div class="chart-item-name">{{ opt.name }}</div>
</div>
<div class="cardBody" style="display: flex; flex-wrap: wrap" v-if="sourceFromIsComputed">
<RingProgress name="停车费" color="#1a75ff" :value="sourceFrom.parkingFeePercent"></RingProgress>
<RingProgress name="物业费" color="#ffbf05" :value="sourceFrom.propertyFeePercent"></RingProgress>
<RingProgress name="水电费" color="#6c76f4" :value="sourceFrom.utilityFeePercent"></RingProgress>
<RingProgress name="商铺租金" color="#fa746b" :value="sourceFrom.shopRentPercent"></RingProgress>
<RingProgress name="维修费" color="#3fdf95" :value="sourceFrom.repairFeePercent"></RingProgress>
<RingProgress name="垃圾处理费" color="#1e1b39" :value="sourceFrom.garbageFeePercent"></RingProgress>
</div>
</div>
</el-col>
<el-col :lg="10" :xl="10">
<el-col :lg="10" :xl="10" style="margin-bottom: 15px">
<div class="cardbox">
<div>欠费分布</div>
<div class="cardbody w-full" style="height: 150px">
<div>缴费方式</div>
<div class="cardBody w-full" style="height: 100%">
<Echarts :options="options2"></Echarts>
</div>
</div>
</el-col>
</el-row>
</div>
<div style="margin-bottom: 15px; position: relative; z-index: 2">
<div style="position: relative; z-index: 2">
<el-row :gutter="20">
<el-col :lg="12" :xl="14">
<el-col :lg="12" :xl="14" style="margin-bottom: 15px">
<div class="cardbox w-full">
<div>欠费分布</div>
<div class="cardbody w-full" style="height: 330px">
<div class="cardBody w-full" style="height: 330px">
<Echarts :options="options3"></Echarts>
</div>
</div>
@@ -72,7 +67,7 @@
<span class="ml-2">{{ item.label }}</span>
</li>
</ul>
<ul class="circlebox">
<ul class="circleBox">
<li :data-level="index" :style="{ backgroundColor: item.color }" v-for="(item, index) in sortPayList" :key="index">
{{ item.number }}%
</li>
@@ -90,7 +85,7 @@
</div> -->
<div class="cardbox w-full">
<div>缴费走势分析</div>
<div class="cardbody w-full" style="height: 400px">
<div class="cardBody w-full" style="height: 400px">
<Echarts :options="flowLineOption"></Echarts>
</div>
</div>
@@ -110,59 +105,48 @@ import Echarts from '@/components/Echarts/index.vue';
import { formatMoneyWithUnit } from '@/utils/money';
import { EChartsOption } from 'echarts';
import { getLast7Days, getPastDayDate } from '@/utils/time';
import { income_statsApi } from '@/api/system/Payment';
const open = ref(false);
const dataTime = ref('2025-01-01');
const form = ref({
'totalDue': '',
'totalPaid': '',
'totalUnpaid': '',
'paidPercentage': '',
'payWx': null,
'payZfb': null,
'payOffline': null,
'totalPay': null,
'feeTypeStats': [],
'dailyStats': []
});
import {
arrears_distributionApi,
Arrears_distrubution,
income_scourcApi,
payment_methodApi,
payment_sourceApi,
payment_trendApi,
payment_trendType,
PayMentMethod
} from '@/api/system/Payment';
import RingProgress from './compontents/RingProgress.vue';
const defaultDay = 30; // 默认查询近30天数据
const open = ref(false);
const last10Day = getPastDayDate(defaultDay);
const dataTime = ref(last10Day);
const sourceFromIsComputed = ref(true);
/** 收入来源 */
/** 收入来源图表配置 */
const incomeChartOptions = ref<
{
name: string;
options?: EChartsOption;
}[]
>(
[
{
'name': '停车费'
},
{
'name': '物业费'
},
{
'name': '水电费'
},
{
'name': '商铺租金'
},
{
'name': '维修费'
}
].map((i) => {
return {
name: i.name,
options: {
tooltip: {},
series: []
}
};
})
);
const sourceFrom = ref({
'totalPayable': '0',
'totalReceived': '0',
'totalPending': '0',
'parkingFee': '0',
'parkingFeePercent': '0',
'propertyFee': '0',
'propertyFeePercent': '0',
'utilityFee': '0',
'utilityFeePercent': '0',
'shopRent': '0',
'shopRentPercent': '0',
'repairFee': '0',
'repairFeePercent': '0',
'garbageFee': '0',
'garbageFeePercent': '0'
});
/** 缴费方式 */
const options2 = shallowRef<EChartsOption>({
@@ -199,23 +183,54 @@ const options2 = shallowRef<EChartsOption>({
show: false
},
data: [
{ value: 50, name: '支付宝', itemStyle: { color: '#0a74fe' } },
{ value: 30, name: '微信', itemStyle: { color: '#f74043' } },
{ value: 20, name: '线下', itemStyle: { color: '#fcd152' } }
{ value: 0, name: '支付宝', itemStyle: { color: '#0a74fe' } },
{ value: 0, name: '微信', itemStyle: { color: '#f74043' } },
{ value: 0, name: '线下', itemStyle: { color: '#fcd152' } }
]
}
]
});
function updatePayMethod(dataFrom: PayMentMethod): EChartsOption {
return {
legend: {
top: 'center',
left: '0',
orient: 'vertical',
itemGap: 50,
itemWidth: 12, // 图例色块宽度
itemHeight: 12 // 图例色块高度
},
clockwise: true,
series: [
{
name: '缴费方式',
type: 'pie',
startAngle: 280,
radius: ['50%', '100%'],
avoidLabelOverlap: false,
left: '50%',
emphasis: {
scale: true,
scaleSize: 6
},
data: [
{ value: Number(dataFrom.alipayAmount) ?? 0, name: '支付宝', itemStyle: { color: '#0a74fe' } },
{ value: Number(dataFrom.wechatAmount) ?? 0, name: '微信', itemStyle: { color: '#f74043' } },
{ value: Number(dataFrom.offlineAmount) ?? 0, name: '线下', itemStyle: { color: '#fcd152' } }
]
}
]
};
}
// 【新增】定义12个月份
const monthX = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
/** 欠费分布 散点图 */
/** 欠费分布 散点图 (带特效版) */
const options3 = shallowRef<EChartsOption>({
tooltip: {
trigger: 'item',
formatter: (params: any) => {
return `${monthX[params.value[0]]}<br/>欠费金额: <b>${params.value[1]}</b> 万元`;
return `${params.name}<br/>欠费金额: <b>${formatMoneyWithUnit(params.value[1])}</b>`;
},
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#eee',
@@ -304,7 +319,92 @@ const options3 = shallowRef<EChartsOption>({
}
]
});
function UpdateOptions3(data: Arrears_distrubution[]): EChartsOption {
const xData = data.map((item, index) => {
return [index, item.amount];
});
const xMonth = data.map((item) => `${item.month}`);
return {
tooltip: {
trigger: 'item',
formatter: (params: any) => {
return `${params.name}<br/>欠费金额: <b>${formatMoneyWithUnit(params.value[1])}</b>`;
},
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#eee',
textStyle: { color: '#333' }
},
grid: {
left: '1%',
right: '2%',
top: '12%',
bottom: '12%',
containLabel: true
},
xAxis: {
type: 'category',
data: xMonth,
boundaryGap: false,
axisTick: {
alignWithLabel: true,
length: 5, // 刻度线长度
lineStyle: { color: '#ccc' }
},
axisLine: { lineStyle: { color: '#eee' }, onZero: false },
axisLabel: {
color: '#666',
interval: 0, // 强制显示所有月份
margin: 12 // 标签与轴线的距离
},
splitLine: {
show: true,
lineStyle: {
color: '#f0f0f0',
type: 'dashed',
width: 1
}
}
},
yAxis: {
type: 'value',
name: '金额(万元)',
nameTextStyle: { color: '#999', padding: [0, 0, 0, 10] },
splitLine: {
show: true,
lineStyle: {
color: '#f0f0f0',
type: 'dashed' // 虚线网格更轻盈
}
},
axisLabel: { color: '#999', margin: 20 }
},
series: [
{
name: '欠费金额',
type: 'scatter',
symbolSize: (data: any) => {
const value = data[1];
return Math.min(Math.max(value * 1.5, 10), 40);
},
itemStyle: {
color: (params: any) => {
const index = params.dataIndex;
if (index % 2 === 0) {
return '#739ff0';
} else {
return '#86c9d0';
}
}
},
// 【特效 2】高亮状态鼠标悬停
emphasis: {
scale: 1.2 // 悬停时额外放大
},
data: xData
}
]
};
}
// ! 进十日缴费数量情况 -------------------------------------------------------------------------------------
// 近10天日期
const dayX = getLast7Days(10);
@@ -393,50 +493,65 @@ const flowLineOption = shallowRef<EChartsOption>({
}
]
});
// ! 进十日缴费数量情况 -------------------------------------------------------------------------------------
const paytypelist = ref([
{ label: '微信', value: 'payWx', number: 0, color: '#4a3afe' },
{ label: '支付宝', value: 'payZfb', number: 0, color: '#1e1b39' },
{ label: '线下', value: 'payOffline', number: 0, color: '#c893fd' }
const payTypeList = ref([
{ label: '小程序', number: 0, color: '#4a3afe' },
{ label: '线下缴费', number: 0, color: '#1e1b39' },
{ label: '手机APP', number: 0, color: '#c893fd' }
]);
// 排序后
const sortPayList = ref([]);
const sortPayList = ref(payTypeList.value);
const payMethodFrom = ref({
'wechatAmount': '0',
'wechatPercent': '0',
'alipayAmount': '0',
'alipayPercent': '0',
'offlineAmount': '0',
'offlinePercent': '0'
});
function getList() {
const startData = getPastDayDate(defaultDay);
income_statsApi(dataTime.value === '' ? startData : dataTime.value).then((res) => {
form.value = res.data;
const { payOffline, payWx, payZfb, feeTypeStats, dailyStats } = res.data;
const total = (payOffline ?? 0) + (payWx ?? 0) + (payZfb ?? 0);
if (total > 0) {
form.value.payOffline = Number.isNaN((payOffline / total) * 100) ? 0 : ((payOffline / total) * 100).toFixed(2);
form.value.payWx = Number.isNaN((payWx / total) * 100) ? 0 : ((payWx / total) * 100).toFixed(2);
form.value.payZfb = Number.isNaN((payZfb / total) * 100) ? 0 : ((payZfb / total) * 100).toFixed(2);
const payTypeList = JSON.parse(JSON.stringify(paytypelist.value));
payTypeList[0].number = form.value.payWx;
payTypeList[1].number = form.value.payZfb;
payTypeList[2].number = form.value.payOffline;
sortPayList.value = payTypeList.toSorted((a, b) => Number(b.number) - Number(a.number));
}
// 收入来源
// 2. 处理收入来源 (新逻辑)
if (feeTypeStats && Array.isArray(feeTypeStats)) {
updateIncomeChart(feeTypeStats);
}
// 3. 【新增】处理缴费走势 (近十日缴费情况)
if (dailyStats && Array.isArray(dailyStats)) {
updateFlowLineChart(dailyStats);
}
sourceFromIsComputed.value = false;
// 收入来源
income_scourcApi(dataTime.value)
.then((res) => {
sourceFrom.value = res.data;
})
.finally(() => {
setTimeout(() => {
sourceFromIsComputed.value = true;
});
});
// 缴费方式
payment_methodApi(dataTime.value).then((res) => {
payMethodFrom.value = res.data;
options2.value = updatePayMethod(res.data);
});
// 欠费分布
arrears_distributionApi(dataTime.value).then((res) => {
options3.value = UpdateOptions3(res.data.items);
});
//缴费来源
payment_sourceApi(dataTime.value).then((res) => {
const payTypeListCopy = JSON.parse(JSON.stringify(payTypeList.value));
payTypeListCopy[0].number = res.data.miniProgramPercent;
payTypeListCopy[1].number = res.data.offlinePercent;
payTypeListCopy[2].number = res.data.mobileAppPercent;
sortPayList.value = payTypeListCopy.toSorted(
(a: { number: string | number }, b: { number: string | number }) => Number(b.number) - Number(a.number)
);
});
// 缴费走势
payment_trendApi(dataTime.value, getPastDayDate(0)).then((res) => {
updateFlowLineChart(res.data.items);
});
}
/**
* 更新缴费走势图表
* @param stats 后端返回的 dailyStats 数组
* @param stats 后端返回的 dailyStats 数组&
*/
function updateFlowLineChart(stats: any[]) {
function updateFlowLineChart(stats: payment_trendType[]) {
if (!stats.length) {
flowLineOption.value.series = [];
flowLineOption.value.xAxis.data = [];
@@ -444,13 +559,13 @@ function updateFlowLineChart(stats: any[]) {
}
// 提取数据
const dates = stats.map((item) => item.statDate);
const paidData = stats.map((item) => Number(item.totalPaid) || 0);
const unpaidData = stats.map((item) => Number(item.totalUnpaid) || 0);
const dates = stats.map((item) => item.date);
const paidData = stats.map((item) => Number(item.paidAmount) || 0);
const unpaidData = stats.map((item) => Number(item.unpaidAmount) || 0);
// 更新图表配置
// 使用 shallowRef 时,建议替换整个对象或确保深层属性被正确追踪
// 这里我们直接修改 series 和 xAxis 的数据
// 这里我们直接修改 series 和 xAxis 的数据 ╬╬
flowLineOption.value = {
...flowLineOption.value,
xAxis: {
@@ -507,96 +622,12 @@ function updateFlowLineChart(stats: any[]) {
]
};
}
/**
* 更新收入来源图表数组
* @param stats 后端返回的 feeTypeStats 数组
*/
function updateIncomeChart(stats: any[]) {
incomeChartOptions.value = [];
let statslist = stats;
if (statslist.length === 0) {
statslist = [
{
'feeName': '停车费'
},
{
'feeName': '物业费'
},
{
'feeName': '水电费'
},
{
'feeName': '商铺租金'
},
{
'feeName': '维修费'
}
];
}
incomeChartOptions.value = statslist.map((item) => {
const percentage = Number(item.paidPercentage) || 0;
const color = '#409eff';
return {
name: item.feeName, // 图表名称
options: {
tooltip: {
show: false,
trigger: 'item',
formatter: '{b}: {c}%'
},
series: [
{
type: 'pie',
radius: ['50%', '70%'], // 环形大小
center: ['50%', '50%'], // 每个图表各自居中
select: {
scale: false
},
labelLine: {
show: false
},
data: [
{
value: 100 - percentage,
itemStyle: {
color: '#ebeef5' // 背景色
},
emphasis: {
itemStyle: {
color: '#ebeef5'
},
scale: false
}
},
{
value: percentage,
name: item.feeName,
itemStyle: { color: color },
label: {
show: true,
position: 'center', // 显示在圆环中心
formatter: '{c}%', // 显示百分比数值
fontSize: 14,
fontWeight: 'bold',
color: '#4ce19c'
},
emphasis: {
scale: true
}
}
]
}
]
}
};
});
console.log(incomeChartOptions.value);
}
function querySearch() {
getList();
}
function reset() {
dataTime.value = '';
dataTime.value = getPastDayDate(defaultDay);
querySearch();
}
getList();
@@ -632,6 +663,7 @@ onUnmounted(() => {
font-size: 20px;
padding: 20px;
min-height: 275px;
height: 100%;
@media (max-width: 1600px) {
font-size: 18px;
}
@@ -641,7 +673,7 @@ onUnmounted(() => {
@media (max-width: 1200px) {
margin-bottom: 20px;
}
.card_infoall {
.card_InfoAll {
padding-left: 40px;
display: flex;
align-items: center;
@@ -653,7 +685,7 @@ onUnmounted(() => {
margin-top: 15px;
}
.cardbody {
.cardBody {
padding: 0 30px;
display: flex;
align-items: center;
@@ -663,10 +695,10 @@ onUnmounted(() => {
}
.card_leftbox {
text-align: center;
.labelbox {
.labelBox {
font-size: 16px;
}
.valuebox {
.valueBox {
font-weight: 600;
font-size: 36px;
color: #222222;
@@ -683,7 +715,6 @@ onUnmounted(() => {
li {
margin-bottom: 20px;
position: relative;
width: 100px;
height: 50px;
line-height: 50px;
text-align: left;
@@ -700,7 +731,7 @@ onUnmounted(() => {
}
}
}
.circlebox {
.circleBox {
margin-left: 60px;
position: relative;
width: 300px;

View File

@@ -17,13 +17,13 @@
<el-col :lg="8" :xl="8">
<div class="cardbox">
<div>入住情况</div>
<div class="cardbody" style="height: 200px">
<div class="card_leftbox">
<div class="valuebox">{{ movein.totalResident }}</div>
<div class="labelbox">入住人数</div>
<div class="cardBody" style="height: 200px">
<div class="card_LeftBox">
<div class="valueBox">{{ MoveIn.totalResident }}</div>
<div class="labelBox">入住人数</div>
</div>
<div class="echartsbox h-full">
<Echarts :options="options"></Echarts>
<div class="echartsBox h-full">
<Echarts :options="options as EChartsOption"></Echarts>
</div>
</div>
</div>
@@ -31,16 +31,16 @@
<el-col :lg="10" :xl="8">
<div class="cardbox">
<div>住户类型情况</div>
<div class="cardbody w-full" style="height: 150px">
<Echarts :options="options1"></Echarts>
<div class="cardBody w-full" style="height: 180px">
<Echarts :options="options1 as EChartsOption"></Echarts>
</div>
</div>
</el-col>
<el-col :lg="6" :xl="8">
<div class="cardbox">
<div>男女比例</div>
<div class="cardbody w-full" style="height: 150px">
<Echarts :options="options2"></Echarts>
<div class="cardBody w-full" style="height: 180px">
<Echarts :options="options2 as EChartsOption"></Echarts>
</div>
</div>
</el-col>
@@ -56,7 +56,7 @@
<el-row style="margin-top: 20px; position: relative; z-index: 2">
<el-col :span="24">
<div class="cardbox w-full" style="height: 450px">
<Echarts :options="flowLineOption"></Echarts>
<Echarts :options="flowLineOption as EChartsOption"></Echarts>
</div>
</el-col>
</el-row>
@@ -68,12 +68,11 @@
</template>
<script setup lang="ts">
// TODO 人员分析
import { ref } from 'vue';
import Echarts from '@/components/Echarts/index.vue';
import { EChartsOption } from 'echarts';
import { getLastNDays } from '@/utils/time';
import { get_movein_moveout_list, get_movein_user_sex, get_movein_user_type, user_movein_list } from '@/api/system/Personnel';
import { get_movein_moveout_list, get_movein_user_sex, get_movein_user_type, get_person_age, user_movein_list } from '@/api/system/Personnel';
const date = ref('');
const open = ref(true);
@@ -86,25 +85,29 @@ onUnmounted(() => {
});
function reset() {
date.value = '';
date.value = getLastNDays(10)[0];
querySelect();
}
/** 入住情况 */
const options = ref<EChartsOption>({
tooltip: { show: false },
tooltip: { show: true, trigger: 'item' },
series: [
// 1. 底层细圆环(灰色底色)
{
name: '总房屋',
type: 'pie',
radius: ['68%', '90%'], // 细一点
clockwise: true,
data: [100],
itemStyle: { color: '#ebeef5' },
emphasis: { disabled: true },
tooltip: { show: false }, // 关键:不显示提示
label: { show: false }
},
// 2. 上层粗圆环(进度条)
{
name: '入住房屋',
type: 'pie',
radius: ['65%', '98%'], // 粗一点
clockwise: true,
@@ -132,33 +135,39 @@ const options = ref<EChartsOption>({
}
}
},
data: [{ value: 85 }, { value: 15, itemStyle: { color: 'transparent' } }]
data: [
{ value: 85 },
{
value: 15,
itemStyle: { color: 'transparent' }
}
]
}
]
});
/** 住类型 */
/** 住类型 */
const options1 = ref<EChartsOption>({
legend: {
top: 'center',
left: '0',
orient: 'vertical',
itemGap: 50
itemGap: 80
},
tooltip: {
trigger: 'item'
},
series: [
{
name: 'Access From',
name: '住户类型',
type: 'pie',
radius: ['50%', '100%'],
radius: ['45%', '95%'],
avoidLabelOverlap: false,
left: '50%',
label: {
show: false,
position: 'center'
},
tooltip: {
show: false
},
emphasis: {
label: {
show: false,
@@ -185,12 +194,15 @@ const options2 = ref<EChartsOption>({
top: 'center',
left: '0',
orient: 'vertical',
itemGap: 50
itemGap: 80
},
tooltip: {
trigger: 'item'
},
clockwise: true,
series: [
{
name: 'Access From',
name: '男女比例',
type: 'pie',
startAngle: 280,
radius: ['50%', '100%'],
@@ -200,9 +212,6 @@ const options2 = ref<EChartsOption>({
show: false,
position: 'center'
},
tooltip: {
show: false
},
emphasis: {
label: {
show: false,
@@ -221,15 +230,23 @@ const options2 = ref<EChartsOption>({
]
});
// ! 年龄分布 -------------------------------------------------------------------------------------
// ! 年龄分布 -----------------------------------------
const agesX = ['6岁以下', '6-11岁', '11-14岁', '15-24岁', '25-30岁', '31-40岁', '41-50岁', '51-61岁', '61-65岁', '65-70岁', '70岁以上'];
const agesData = [520, 680, 460, 750, 1350, 1860, 1420, 960, 630, 380, 210];
// 🔥 关键:灰色背景数据 = 最大值 - 真实数据互补100%
const maxValue = 2000; // 设定总高度(可自行调整)
const middleHeight = 20;
const barwidth = '10%';
const bgData = agesData.map((item) => maxValue - item - middleHeight);
const bgData2 = agesData.map((item) => middleHeight);
const agesData = ref(new Array(11).fill(0));
const maxValue = ref(10); // 设定总高度
const middleHeight = 0.05; // 中间白条高度
const barWidth = '10%'; // 柱状图粗度
const bgData = computed(() => {
return agesData.value.map((item) => {
const num = maxValue.value - item - middleHeight;
if (num > 0) {
return num;
} else {
return maxValue.value;
}
});
});
const bgData2 = agesX.map(() => middleHeight);
/** 年龄分布 */
const options3 = shallowRef<EChartsOption>({
title: {
@@ -250,19 +267,16 @@ const options3 = shallowRef<EChartsOption>({
confine: true,
axisPointer: { type: 'shadow' }
},
// 鼠标悬浮高亮当前,其他隐藏
xAxis: { type: 'category', data: agesX, axisLabel: { interval: 0 } },
yAxis: {
type: 'value',
splitLine: { show: true, lineStyle: { color: '#eee' } }
},
series: [
// 蓝色真实数据
{
type: 'bar',
barWidth: barwidth,
data: agesData,
barWidth: barWidth,
data: agesData.value,
itemStyle: {
color: '#409eff',
borderRadius: [4, 4, 0, 0]
@@ -271,7 +285,7 @@ const options3 = shallowRef<EChartsOption>({
},
{
type: 'bar',
barWidth: barwidth,
barWidth: barWidth,
data: bgData2,
tooltip: { show: false }, // 关键:不显示提示
itemStyle: {
@@ -281,8 +295,8 @@ const options3 = shallowRef<EChartsOption>({
},
{
type: 'bar',
barWidth: barwidth,
data: bgData,
barWidth: barWidth,
data: bgData.value,
tooltip: { show: false }, // 关键:不显示提示
itemStyle: {
color: 'rgba(180, 180, 180, 0.2)'
@@ -291,6 +305,65 @@ const options3 = shallowRef<EChartsOption>({
}
]
});
function updateOptions3(data: number[]): EChartsOption {
return {
title: {
text: '住户年龄分布',
left: '0',
top: '0',
textStyle: { fontSize: 16, fontWeight: 'bold' }
},
grid: {
left: '3%',
right: '4%',
top: '12%',
bottom: '8%',
containLabel: true
},
tooltip: {
trigger: 'item',
confine: true,
axisPointer: { type: 'shadow' }
},
xAxis: { type: 'category', data: agesX, axisLabel: { interval: 0 } },
yAxis: {
type: 'value',
splitLine: { show: true, lineStyle: { color: '#eee' } }
},
series: [
{
type: 'bar',
barWidth: barWidth,
data: data,
itemStyle: {
color: '#409eff',
borderRadius: [4, 4, 0, 0]
},
stack: 'total'
},
{
type: 'bar',
barWidth: barWidth,
data: bgData2,
tooltip: { show: false }, // 关键:不显示提示
itemStyle: {
color: '#fff'
},
stack: 'total'
},
{
type: 'bar',
barWidth: barWidth,
data: bgData.value,
tooltip: { show: false }, // 关键:不显示提示
itemStyle: {
color: 'rgba(180, 180, 180, 0.2)'
},
stack: 'total'
}
]
};
}
// ! 年龄分布 -------------------------------------------------------------------------------------
// ! 人员流动情况 -------------------------------------------------------------------------------------
@@ -388,7 +461,7 @@ const flowLineOption = ref<EChartsOption>({
}
]
});
function flowLineOptionFun(days: string[], indata: number[], outdata: number[]): EChartsOption {
function flowLineOptionFun(days: string[], InData: number[], OutData: number[]): EChartsOption {
return {
title: {
text: '近十日人员流动趋势',
@@ -430,7 +503,7 @@ function flowLineOptionFun(days: string[], indata: number[], outdata: number[]):
name: '人员流入',
type: 'line',
smooth: true, // 平滑折线 核心
data: indata,
data: InData,
symbol: 'circle',
symbolSize: 6,
itemStyle: { color: '#f94144' },
@@ -453,7 +526,7 @@ function flowLineOptionFun(days: string[], indata: number[], outdata: number[]):
name: '人员流出',
type: 'line',
smooth: true, // 平滑折线 核心
data: outdata,
data: OutData,
symbol: 'circle',
symbolSize: 6,
itemStyle: { color: '#f9c74f' },
@@ -477,34 +550,43 @@ function flowLineOptionFun(days: string[], indata: number[], outdata: number[]):
}
// ! 人员流动情况 -------------------------------------------------------------------------------------
const movein = ref({
const MoveIn = ref({
totalResident: 0,
totalHouse: 0
});
function init() {
// 获取入住情况
moveinFun();
// 入住情况
MoveInFun();
// 入住类型
moveinUserType();
moveinUserSex();
moveinUserMoveinOut();
MoveInUserType();
// 男女类型
MoveInUserSex();
// 迁入迁出
MoveInUserMoveInOut();
// 住户年龄分布
MoveInPersonAge();
}
init();
function querySelect() {
init();
}
function moveinFun() {
// 入住情况
function MoveInFun() {
user_movein_list({
datetime: date.value
dateTime: date.value
}).then((res) => {
// 获取入住情况
movein.value.totalResident = res.data.occupancyCount;
movein.value.totalHouse = res.data.totalHouse;
MoveIn.value.totalResident = res.data.occupancyCount;
MoveIn.value.totalHouse = res.data.totalHouse;
options.value.series[1].data = [
{ value: res.data.occupiedHouse },
{ value: res.data.totalHouse - res.data.occupiedHouse, itemStyle: { color: 'transparent' } }
{
value: res.data.totalHouse - res.data.occupiedHouse,
itemStyle: { color: 'transparent' },
tooltip: {
show: false
}
}
];
options.value.series[1].label.formatter = () => {
const total = res.data.totalHouse;
@@ -518,17 +600,22 @@ function moveinFun() {
});
}
// 获取住户类型
function moveinUserType() {
function MoveInUserType() {
get_movein_user_type({
datetime: date.value
dateTime: date.value
}).then((res) => {
console.log(res);
options1.value.series[0].data = [
{ value: res.owner, name: '业主', itemStyle: { color: '#0a74fe' } },
{ value: res.tenant, name: '租户', itemStyle: { color: '#fe3d4e' } },
{ value: res.relatives, name: '亲属', itemStyle: { color: '#00cf7f' } },
{ value: res.other, name: '其他', itemStyle: { color: '#f48e3b' } }
];
});
}
// 住户性别
function moveinUserSex() {
function MoveInUserSex() {
get_movein_user_sex({
datetime: date.value
dateTime: date.value
}).then((res) => {
options2.value.series[0].data = [
{ value: res.man, name: '男', itemStyle: { color: '#0a74fe' } },
@@ -537,11 +624,7 @@ function moveinUserSex() {
});
}
// 迁入迁出
function moveinUserMoveinOut() {
// 判断 date.value 是否为今天
if (date.value === '') {
date.value = getLastNDays(7)[0];
}
function MoveInUserMoveInOut() {
get_movein_moveout_list({
date: date.value
}).then((res) => {
@@ -551,6 +634,17 @@ function moveinUserMoveinOut() {
flowLineOption.value = flowLineOptionFun(dayX.value, inData.value, outData.value);
});
}
// 住户年龄分布
function MoveInPersonAge() {
get_person_age({
dateTime: date.value
}).then((res) => {
const values = Object.values(res).slice(0, -1);
maxValue.value = res.total;
agesData.value = values;
options3.value = updateOptions3(values);
});
}
</script>
<style scoped lang="scss">
@@ -571,6 +665,7 @@ function moveinUserMoveinOut() {
}
// 入住清空
.cardbox {
box-sizing: border-box;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, #ffffff 70%);
border-radius: 16px 16px 16px 16px;
border: 2px solid #ffffff;
@@ -580,25 +675,25 @@ function moveinUserMoveinOut() {
@media (max-width: 1200px) {
margin-bottom: 20px;
}
.cardbody {
.cardBody {
padding: 0 30px;
display: flex;
align-items: center;
justify-content: space-between;
.card_leftbox {
.card_LeftBox {
text-align: center;
margin-right: 20px;
.labelbox {
.labelBox {
font-size: 16px;
}
.valuebox {
.valueBox {
font-weight: 600;
font-size: 36px;
color: #222222;
width: 100px;
}
}
.echartsbox {
.echartsBox {
width: 400px;
}
}

View File

@@ -68,7 +68,6 @@ const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const router = useRouter();
const storeroomList = ref<StoreroomVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
@@ -77,12 +76,6 @@ const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const storeroomFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: StoreroomForm = {
id: undefined,
@@ -116,7 +109,7 @@ const data = reactive<PageData<StoreroomForm, StoreroomQuery>>({
}
});
const { queryParams, form, rules } = toRefs(data);
const { queryParams } = toRefs(data);
/** 查询储藏室管理列表 */
const getList = async () => {
@@ -127,18 +120,6 @@ const getList = async () => {
loading.value = false;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
storeroomFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
@@ -166,7 +147,7 @@ const handleAdd = () => {
};
/** 修改按钮操作 */
const handleUpdate = async (row?: StoreroomVO) => {
const handleUpdate = (row?: StoreroomVO) => {
const id = row?.id || ids.value[0];
router.push({
path: '/house/EditStoreRoom',

View File

@@ -61,7 +61,7 @@ import pagination from '@/components/Pagination/index.vue';
import { RoleVO } from '@/api/system/role/types';
import { getAuthRole, updateAuthRole } from '@/api/system/user';
import { UserForm } from '@/api/system/user/types';
import { RouteLocationNormalized } from 'vue-router';
import { RouteLocationNormalized, RouteLocationRaw } from 'vue-router';
const route = useRoute();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -112,7 +112,7 @@ const close = () => {
redirectedFrom: undefined,
path: '/system/user'
};
proxy?.$tab.closeOpenPage(obj);
proxy?.$tab.closeOpenPage(obj as RouteLocationRaw);
};
/** 提交按钮 */
const submitForm = async () => {

View File

@@ -482,7 +482,7 @@ const cancel = () => {
};
/** 新增按钮操作 */
const handleAdd = async () => {
const handleAdd = () => {
router.push({
path: '/system/addUser'
});
@@ -490,7 +490,7 @@ const handleAdd = async () => {
};
/** 修改按钮操作 */
const handleUpdate = async (row?: UserForm) => {
const handleUpdate = (row?: UserForm) => {
const userId = row?.userId || ids.value[0];
router.push({
path: '/system/editUser',
@@ -548,10 +548,6 @@ onMounted(() => {
initPassword.value = response.data;
});
});
async function handleDeptChange(value: number | string) {
console.log(value);
}
</script>
<style lang="scss" scoped>

View File

@@ -119,7 +119,7 @@ import { optionselect as getDictOptionselect } from '@/api/system/dict/type';
import { DictTypeVO } from '@/api/system/dict/type/types';
import BasicInfoForm from './basicInfoForm.vue';
import GenInfoForm from './genInfoForm.vue';
import { RouteLocationNormalized } from 'vue-router';
import { RouteLocationNormalized, RouteLocationRaw } from 'vue-router';
const route = useRoute();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -179,7 +179,7 @@ const close = () => {
redirectedFrom: undefined,
query: { t: Date.now().toString(), pageNum: route.query.pageNum }
};
proxy?.$tab.closeOpenPage(obj);
proxy?.$tab.closeOpenPage(obj as RouteLocationRaw);
};
(async () => {