feat(dormitory): 添加宿舍管理系统相关接口和组件

- 新增宿舍审批管理模块,包括申请、审核、合同等流程功能
- 新增宿舍床铺管理模块,支持床位分配、上下铺设置、充电口配置
- 新增宿舍楼栋管理模块,支持楼栋信息维护和状态管理
- 新增宿舍楼层管理模块,支持楼层信息维护和排序功能
- 新增宿舍房间记录管理模块,支持房间基本信息和设施配置
- 新增商城用户管理模块,完善用户信息和权限管理
- 新增学生选择器组件,支持双击选择和单选操作
- 新增宿舍仪表盘搜索组件,集成字段扩展、字典管理等快捷入口
- 新增小程序编辑组件,支持Logo上传、域名配置、SEO设置等功能
This commit is contained in:
2025-10-04 15:14:03 +08:00
parent 398737d46d
commit 650c80f5fe
49 changed files with 6835 additions and 39 deletions

View File

@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { DormitoryApply, DormitoryApplyParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询审批管理
*/
export async function pageDormitoryApply(params: DormitoryApplyParam) {
const res = await request.get<ApiResult<PageResult<DormitoryApply>>>(
MODULES_API_URL + '/dormitory/dormitory-apply/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询审批管理列表
*/
export async function listDormitoryApply(params?: DormitoryApplyParam) {
const res = await request.get<ApiResult<DormitoryApply[]>>(
MODULES_API_URL + '/dormitory/dormitory-apply',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加审批管理
*/
export async function addDormitoryApply(data: DormitoryApply) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-apply',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改审批管理
*/
export async function updateDormitoryApply(data: DormitoryApply) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-apply',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除审批管理
*/
export async function removeDormitoryApply(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-apply/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除审批管理
*/
export async function removeBatchDormitoryApply(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-apply/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询审批管理
*/
export async function getDormitoryApply(id: number) {
const res = await request.get<ApiResult<DormitoryApply>>(
MODULES_API_URL + '/dormitory/dormitory-apply/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,57 @@
import type { PageParam } from '@/api';
/**
* 审批管理
*/
export interface DormitoryApply {
// 主键ID
id?: number;
// 类型
type?: number;
// 用户ID
userId?: number;
// 姓名
realName?: string;
// 手机号
mobile?: string;
// 客户名称
dealerName?: string;
// 客户编号
dealerCode?: string;
// 详细地址
address?: string;
// 签约价格
money?: string;
// 推荐人用户ID
refereeId?: number;
// 申请方式(10需后台审核 20无需审核)
applyType?: number;
// 审核状态 (10待审核 20审核通过 30驳回)
applyStatus?: number;
// 申请时间
applyTime?: string;
// 审核时间
auditTime?: string;
// 合同时间
contractTime?: string;
// 过期时间
expirationTime?: string;
// 驳回原因
rejectReason?: string;
// 备注
comments?: string;
// 商城ID
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 审批管理搜索条件
*/
export interface DormitoryApplyParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { DormitoryBed, DormitoryBedParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询宿舍床铺
*/
export async function pageDormitoryBed(params: DormitoryBedParam) {
const res = await request.get<ApiResult<PageResult<DormitoryBed>>>(
MODULES_API_URL + '/dormitory/dormitory-bed/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询宿舍床铺列表
*/
export async function listDormitoryBed(params?: DormitoryBedParam) {
const res = await request.get<ApiResult<DormitoryBed[]>>(
MODULES_API_URL + '/dormitory/dormitory-bed',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加宿舍床铺
*/
export async function addDormitoryBed(data: DormitoryBed) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-bed',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改宿舍床铺
*/
export async function updateDormitoryBed(data: DormitoryBed) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-bed',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除宿舍床铺
*/
export async function removeDormitoryBed(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-bed/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除宿舍床铺
*/
export async function removeBatchDormitoryBed(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-bed/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询宿舍床铺
*/
export async function getDormitoryBed(id: number) {
const res = await request.get<ApiResult<DormitoryBed>>(
MODULES_API_URL + '/dormitory/dormitory-bed/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,55 @@
import type { PageParam } from '@/api';
/**
* 宿舍床铺
*/
export interface DormitoryBed {
// ID
id?: number;
// 宿舍名称
name?: string;
// 编号
code?: string;
// 楼栋ID
buildingId?: number;
// 楼栋名称
buildingName?: string;
// 楼层ID
floorId?: number;
// 楼层名称
floorName?: string;
// 房间ID
recordId?: number;
// 房间名称
recordName?: string;
// 上下铺 1下铺 2上铺 0无
bunk?: number;
// 充电口
chargingPort?: string;
// 用户ID
userId?: number;
// 真实姓名
realName?: string;
// 手机号码
phone?: string;
// 头像
avatar?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1报修
status?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
}
/**
* 宿舍床铺搜索条件
*/
export interface DormitoryBedParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { DormitoryBuilding, DormitoryBuildingParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询宿舍楼栋
*/
export async function pageDormitoryBuilding(params: DormitoryBuildingParam) {
const res = await request.get<ApiResult<PageResult<DormitoryBuilding>>>(
MODULES_API_URL + '/dormitory/dormitory-building/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询宿舍楼栋列表
*/
export async function listDormitoryBuilding(params?: DormitoryBuildingParam) {
const res = await request.get<ApiResult<DormitoryBuilding[]>>(
MODULES_API_URL + '/dormitory/dormitory-building',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加宿舍楼栋
*/
export async function addDormitoryBuilding(data: DormitoryBuilding) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-building',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改宿舍楼栋
*/
export async function updateDormitoryBuilding(data: DormitoryBuilding) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-building',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除宿舍楼栋
*/
export async function removeDormitoryBuilding(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-building/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除宿舍楼栋
*/
export async function removeBatchDormitoryBuilding(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-building/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询宿舍楼栋
*/
export async function getDormitoryBuilding(id: number) {
const res = await request.get<ApiResult<DormitoryBuilding>>(
MODULES_API_URL + '/dormitory/dormitory-building/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,31 @@
import type { PageParam } from '@/api';
/**
* 宿舍楼栋
*/
export interface DormitoryBuilding {
// ID
id?: number;
// 楼栋名称
name?: string;
// 楼栋编号
code?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
}
/**
* 宿舍楼栋搜索条件
*/
export interface DormitoryBuildingParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { DormitoryFloor, DormitoryFloorParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询宿舍楼层
*/
export async function pageDormitoryFloor(params: DormitoryFloorParam) {
const res = await request.get<ApiResult<PageResult<DormitoryFloor>>>(
MODULES_API_URL + '/dormitory/dormitory-floor/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询宿舍楼层列表
*/
export async function listDormitoryFloor(params?: DormitoryFloorParam) {
const res = await request.get<ApiResult<DormitoryFloor[]>>(
MODULES_API_URL + '/dormitory/dormitory-floor',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加宿舍楼层
*/
export async function addDormitoryFloor(data: DormitoryFloor) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-floor',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改宿舍楼层
*/
export async function updateDormitoryFloor(data: DormitoryFloor) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-floor',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除宿舍楼层
*/
export async function removeDormitoryFloor(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-floor/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除宿舍楼层
*/
export async function removeBatchDormitoryFloor(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-floor/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询宿舍楼层
*/
export async function getDormitoryFloor(id: number) {
const res = await request.get<ApiResult<DormitoryFloor>>(
MODULES_API_URL + '/dormitory/dormitory-floor/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,35 @@
import type { PageParam } from '@/api';
/**
* 宿舍楼层
*/
export interface DormitoryFloor {
// ID
id?: number;
// 楼层
name?: string;
// 编号
code?: string;
// 楼栋ID
buildingId?: number;
// 楼栋名称
buildingName?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
}
/**
* 宿舍楼层搜索条件
*/
export interface DormitoryFloorParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { DormitoryRecord, DormitoryRecordParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询宿舍记录
*/
export async function pageDormitoryRecord(params: DormitoryRecordParam) {
const res = await request.get<ApiResult<PageResult<DormitoryRecord>>>(
MODULES_API_URL + '/dormitory/dormitory-record/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询宿舍记录列表
*/
export async function listDormitoryRecord(params?: DormitoryRecordParam) {
const res = await request.get<ApiResult<DormitoryRecord[]>>(
MODULES_API_URL + '/dormitory/dormitory-record',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加宿舍记录
*/
export async function addDormitoryRecord(data: DormitoryRecord) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-record',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改宿舍记录
*/
export async function updateDormitoryRecord(data: DormitoryRecord) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-record',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除宿舍记录
*/
export async function removeDormitoryRecord(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-record/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除宿舍记录
*/
export async function removeBatchDormitoryRecord(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/dormitory/dormitory-record/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询宿舍记录
*/
export async function getDormitoryRecord(id: number) {
const res = await request.get<ApiResult<DormitoryRecord>>(
MODULES_API_URL + '/dormitory/dormitory-record/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,43 @@
import type { PageParam } from '@/api';
/**
* 宿舍记录
*/
export interface DormitoryRecord {
// ID
id?: number;
// 宿舍名称
name?: string;
// 编号
code?: string;
// 楼栋ID
buildingId?: number;
// 楼栋名称
buildingName?: string;
// 楼层ID
floorId?: number;
// 楼层名称
floorName?: string;
// 床铺数
beds?: number;
// 独立卫生间
toilet?: boolean;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
}
/**
* 宿舍记录搜索条件
*/
export interface DormitoryRecordParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopUser, ShopUserParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询用户记录表
*/
export async function pageShopUser(params: ShopUserParam) {
const res = await request.get<ApiResult<PageResult<ShopUser>>>(
MODULES_API_URL + '/shop/shop-user/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询用户记录表列表
*/
export async function listShopUser(params?: ShopUserParam) {
const res = await request.get<ApiResult<ShopUser[]>>(
MODULES_API_URL + '/shop/shop-user',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加用户记录表
*/
export async function addShopUser(data: ShopUser) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-user',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改用户记录表
*/
export async function updateShopUser(data: ShopUser) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-user',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除用户记录表
*/
export async function removeShopUser(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-user/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除用户记录表
*/
export async function removeBatchShopUser(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-user/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询用户记录表
*/
export async function getShopUser(id: number) {
const res = await request.get<ApiResult<ShopUser>>(
MODULES_API_URL + '/shop/shop-user/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,163 @@
import type { PageParam } from '@/api';
/**
* 用户记录表
*/
export interface ShopUser {
// 用户id
userId?: number;
// 用户类型 0个人用户 1企业用户 2其他
type?: number;
// 账号
username?: string;
// 密码
password?: string;
// 昵称
nickname?: string;
// 手机号
phone?: string;
// 性别 1男 2女
sex?: number;
// 职务
position?: string;
// 注册来源客户端 (APP、H5、MP-WEIXIN等)
platform?: string;
// 邮箱
email?: string;
// 邮箱是否验证, 0否, 1是
emailVerified?: number;
// 别名
alias?: string;
// 真实姓名
realName?: string;
// 证件号码
idCard?: string;
// 出生日期
birthday?: string;
// 所在国家
country?: string;
// 所在省份
province?: string;
// 所在城市
city?: string;
// 所在辖区
region?: string;
// 街道地址
address?: string;
// 经度
longitude?: string;
// 纬度
latitude?: string;
// 用户可用余额
balance?: string;
// 已提现金额
cashedMoney?: string;
// 用户可用积分
points?: number;
// 用户总支付的金额
payMoney?: string;
// 实际消费的金额(不含退款)
expendMoney?: string;
// 密码
payPassword?: string;
// 会员等级ID
gradeId?: number;
// 行业分类
category?: string;
// 个人简介
introduction?: string;
// 机构id
organizationId?: number;
// 会员分组ID
groupId?: number;
// 头像
avatar?: string;
// 背景图
bgImage?: string;
// 用户编码
userCode?: string;
// 是否已实名认证
certification?: number;
// 年龄
age?: number;
// 是否线下会员
offline?: string;
// 关注数
followers?: number;
// 粉丝数
fans?: number;
// 点赞数
likes?: number;
// 评论数
commentNumbers?: number;
// 是否推荐
recommend?: number;
// 微信openid
openid?: string;
// 微信公众号openid
officeOpenid?: string;
// 微信unionID
unionid?: string;
// 客户端ID
clientId?: string;
// 不允许办卡
notAllowVip?: string;
// 是否管理员
isAdmin?: string;
// 是否企业管理员
isOrganizationAdmin?: string;
// 累计登录次数
loginNum?: number;
// 企业ID
companyId?: number;
// 可管理的场馆
merchants?: string;
// 商户ID
merchantId?: number;
// 商户名称
merchantName?: string;
// 商户头像
merchantAvatar?: string;
// 第三方系统的用户ID
uid?: number;
// 专家角色
expertType?: string;
// 过期时间
expireTime?: number;
// 最后结算时间
settlementTime?: string;
// 资质
aptitude?: string;
// 行业类型(父级)
industryParent?: string;
// 行业类型(子级)
industryChild?: string;
// 头衔
title?: string;
// 安装的产品ID
templateId?: number;
// 插件安装状态(仅对超超管判断) 0未安装 1已安装
installed?: number;
// 特长
speciality?: string;
// 备注
comments?: string;
// 状态, 0在线, 1离线
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 用户记录表搜索条件
*/
export interface ShopUserParam extends PageParam {
userId?: number;
keywords?: string;
}

View File

@@ -0,0 +1,137 @@
<template>
<ele-modal
:width="750"
:visible="visible"
:maskClosable="false"
:title="title"
:footer="null"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
>
<ele-pro-table
ref="tableRef"
row-key="userId"
:datasource="datasource"
:columns="columns"
:customRow="customRow"
:pagination="false"
>
<template #toolbar>
<a-space>
<span class="text-orange-400 px-3">操作提示双击选择</span>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'websiteLogo'">
<a-avatar :src="record.websiteLogo" />
</template>
<template v-if="column.key === 'action'">
<a-radio @click="onRadio(record)" />
</template>
</template>
</ele-pro-table>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import {
ColumnItem,
DatasourceFunction
} from 'ele-admin-pro/es/ele-pro-table/types';
import { EleProTable } from 'ele-admin-pro';
import {User, UserParam} from "@/api/system/user/model";
import {pageUsers} from "@/api/system/user";
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 标题
title?: string;
// 修改回显的数据
data?: User | null;
}>();
const emit = defineEmits<{
(e: 'done', data: User): void;
(e: 'update:visible', visible: boolean): void;
}>();
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 搜索内容
const searchText = ref(null);
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格配置
const columns = ref<ColumnItem[]>([
{
title: 'LOGO',
dataIndex: 'websiteLogo',
key: 'websiteLogo',
width: 90,
align: 'center'
},
{
title: '网站名称',
dataIndex: 'websiteName',
key: 'websiteName'
},
{
title: '域名',
dataIndex: 'domain',
key: 'domain'
},
// {
// title: '状态',
// dataIndex: 'status',
// key: 'status',
// align: 'center'
// }
]);
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
if(searchText.value){
where.keywords = searchText.value;
}
return pageUsers({
...where,
...orders,
page,
limit
});
};
/* 搜索 */
const reload = (where?: UserParam) => {
tableRef?.value?.reload({ page: 1, ...where });
};
const onRadio = (record: User) => {
updateVisible(false);
emit('done', record);
};
/* 自定义行属性 */
const customRow = (record: User) => {
return {
// 行点击事件
// onClick: () => {
// updateVisible(false);
// emit('done', record);
// },
// 行双击事件
onDblclick: () => {
updateVisible(false);
emit('done', record);
}
};
};
</script>
<style lang="less"></style>

View File

@@ -0,0 +1,53 @@
<template>
<div>
<a-button @click="openEdit">
<template #icon><BulbOutlined v-if="value > 20" class="ele-text-heading" /><BulbOutlined v-else class="ele-text-danger" /></template>
</a-button>
<!-- 选择弹窗 -->
<SelectData
v-model:visible="showEdit"
:data="current"
:title="placeholder"
:customer-type="customerType"
@done="onChange"
/>
</div>
</template>
<script lang="ts" setup>
import { BulbOutlined } from '@ant-design/icons-vue';
import { ref } from 'vue';
import SelectData from './components/select-data.vue';
import {User} from "@/api/system/user/model";
const props = withDefaults(
defineProps<{
value?: any;
customerType?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择数据'
}
);
const emit = defineEmits<{
(e: 'done', data: User): void;
(e: 'clear'): void;
}>();
// 是否显示编辑弹窗
const showEdit = ref(false);
// 当前编辑数据
const current = ref<User | null>(null);
/* 打开编辑弹窗 */
const openEdit = (row?: User) => {
current.value = row ?? null;
showEdit.value = true;
};
const onChange = (row: User) => {
emit('done', row);
};
</script>

View File

@@ -0,0 +1,52 @@
<!-- 搜索表单 -->
<template>
<a-space style="flex-wrap: wrap; display: none;" v-if="hasRole('superAdmin') || hasRole('admin') || hasRole('foundation')">
<a-button
type="text"
@click="openUrl('/dormitory/building')"
>楼栋管理
</a-button
>
<a-button
type="text"
@click="openUrl('/dormitory/floor')"
>楼层管理
</a-button
>
<a-button
type="text"
@click="openUrl('/dormitory/record')"
>宿舍管理
</a-button
>
</a-space>
</template>
<script lang="ts" setup>
import {watch, nextTick} from 'vue';
import {CmsWebsite} from '@/api/cms/cmsWebsite/model';
import {openUrl} from "@/utils/common";
import {hasAnyRole, hasPermission, hasRole} from "@/utils/permission";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
website?: CmsWebsite;
count?: 0;
}>(),
{}
);
nextTick(() => {
if (localStorage.getItem('NotActive')) {
// IsActive.value = false
}
})
watch(
() => props.selection,
() => {
}
);
</script>

View File

@@ -0,0 +1,100 @@
<!-- 搜索表单 -->
<template>
<a-space style="flex-wrap: wrap">
<a-button
type="text"
@click="openUrl(`/website/field`)"
>字段扩展
</a-button>
<a-button
type="text"
@click="openUrl('/website/dict')"
>字典管理
</a-button>
<a-button
type="text"
@click="openUrl('/website/domain')"
>域名管理
</a-button
>
<a-button
type="text"
@click="openUrl('/website/model')"
>模型管理
</a-button
>
<a-button
type="text"
@click="openUrl('/website/form')"
>表单管理
</a-button
>
<a-button
type="text"
@click="openUrl('/website/lang')"
>国际化
</a-button
>
<a-button
type="text"
@click="openUrl('/website/setting')"
>网站设置
</a-button
>
<a-button
type="text"
class="ele-btn-icon"
@click="clearSiteInfoCache">
清除缓存
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import {watch,nextTick} from 'vue';
import {CmsWebsite} from '@/api/cms/cmsWebsite/model';
import {openUrl} from "@/utils/common";
import {message} from 'ant-design-vue';
import {removeSiteInfoCache} from "@/api/cms/cmsWebsite";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
website?: CmsWebsite;
count?: 0;
}>(),
{}
);
const emit = defineEmits<{
(e: 'add'): void;
}>();
const add = () => {
emit('add');
};
// 清除缓存
const clearSiteInfoCache = () => {
removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId') + "*").then(
(msg) => {
if (msg) {
message.success(msg);
}
}
);
};
nextTick(() => {
if(localStorage.getItem('NotActive')){
// IsActive.value = false
}
})
watch(
() => props.selection,
() => {
}
);
</script>

View File

@@ -0,0 +1,413 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑小程序' : '创建小程序'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
:confirm-loading="loading"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="Logo" name="avatar">
<SelectFile
:placeholder="`请选择图片`"
:limit="1"
:data="images"
@done="chooseImage"
@del="onDeleteItem"
/>
</a-form-item>
<a-form-item label="账号类型" name="type">
{{ form.type }}
</a-form-item>
<a-form-item label="小程序名称" name="websiteName">
<a-input
allow-clear
placeholder="请输入小程序名称"
v-model:value="form.websiteName"
/>
</a-form-item>
<a-form-item label="网站域名" name="domain" v-if="form.type == 10">
<a-input
v-model:value="form.domain"
placeholder="huawei.com"
>
<template #addonBefore>
<a-select v-model:value="form.prefix" style="width: 90px">
<a-select-option value="http://">http://</a-select-option>
<a-select-option value="https://">https://</a-select-option>
</a-select>
</template>
</a-input>
</a-form-item>
<a-form-item label="AppId" name="websiteCode" v-if="form.type == 20">
<a-input
allow-clear
placeholder="请输入AppId"
v-model:value="form.websiteCode"
/>
</a-form-item>
<a-form-item label="小程序描述" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入小程序描述"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="SEO关键词" name="keywords">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入SEO关键词"
v-model:value="form.keywords"
/>
</a-form-item>
<!-- <a-form-item label="全局样式" name="style">-->
<!-- <a-textarea-->
<!-- :rows="4"-->
<!-- :maxlength="200"-->
<!-- placeholder="全局样式"-->
<!-- v-model:value="form.style"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="小程序类型" name="websiteType">-->
<!-- <a-select-->
<!-- :options="websiteType"-->
<!-- :value="form.websiteType"-->
<!-- placeholder="请选择主体类型"-->
<!-- @change="onCmsWebsiteType"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="当前版本" name="version">-->
<!-- <a-tag color="red" v-if="form.version === 10">标准版</a-tag>-->
<!-- <a-tag color="green" v-if="form.version === 20">专业版</a-tag>-->
<!-- <a-tag color="cyan" v-if="form.version === 30">永久授权</a-tag>-->
<!-- </a-form-item>-->
<a-form-item label="状态" name="running">
<a-radio-group
v-model:value="form.running"
:disabled="form.running == 4 || form.running == 5"
>
<a-radio :value="1">运行中</a-radio>
<a-radio :value="2">维护中</a-radio>
<a-radio :value="3">已关闭</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item v-if="form.running == 2" label="维护说明" name="statusText">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="状态说明"
v-model:value="form.statusText"
/>
</a-form-item>
<!-- <a-divider style="margin-bottom: 24px" />-->
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref, reactive, watch} from 'vue';
import {Form, message} from 'ant-design-vue';
import {assignObject, uuid} from 'ele-admin-pro';
import {addCmsWebsite, updateCmsWebsite} from '@/api/cms/cmsWebsite';
import {CmsWebsite} from '@/api/cms/cmsWebsite/model';
import {useThemeStore} from '@/store/modules/theme';
import {storeToRefs} from 'pinia';
import {FormInstance, type Rule} from 'ant-design-vue/es/form';
import {ItemType} from 'ele-admin-pro/es/ele-image-upload/types';
import {FileRecord} from '@/api/system/file/model';
import {checkExistence} from '@/api/cms/cmsDomain';
import {updateCmsDomain} from '@/api/cms/cmsDomain';
import {updateTenant} from "@/api/system/tenant";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const {styleResponsive} = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: CmsWebsite | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
const websiteQrcode = ref<ItemType[]>([]);
const oldDomain = ref();
const files = ref<ItemType[]>([]);
// 用户信息
const form = reactive<CmsWebsite>({
websiteId: undefined,
websiteLogo: undefined,
websiteName: undefined,
websiteCode: undefined,
type: 20,
files: undefined,
keywords: '',
prefix: '',
domain: '',
adminUrl: '',
style: '',
icpNo: undefined,
email: undefined,
version: undefined,
websiteType: '',
running: 1,
expirationTime: undefined,
sortNumber: undefined,
comments: undefined,
status: undefined,
statusText: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
// comments: [
// {
// required: true,
// type: 'string',
// message: '请填写小程序描述',
// trigger: 'blur'
// }
// ],
keywords: [
{
required: true,
type: 'string',
message: '请填写SEO关键词',
trigger: 'blur'
}
],
running: [
{
required: true,
type: 'number',
message: '请选择小程序状态',
trigger: 'change'
}
],
domain: [
{
required: true,
type: 'string',
message: '请填写小程序域名',
trigger: 'blur'
}
],
websiteCode: [
{
required: true,
type: 'string',
message: '请填写小程序码',
trigger: 'blur'
}
],
// websiteCode: [
// {
// required: true,
// type: 'string',
// message: '该域名已被使用',
// validator: (_rule: Rule, value: string) => {
// return new Promise<void>((resolve, reject) => {
// if (!value) {
// return reject('请输入二级域名');
// }
// checkExistence('domain', `${value}.wsdns.cn`)
// .then(() => {
// if (value === oldDomain.value) {
// return resolve();
// }
// reject('已存在');
// })
// .catch(() => {
// resolve();
// });
// });
// },
// trigger: 'blur'
// }
// ],
adminUrl: [
{
required: true,
type: 'string',
message: '请填写小程序后台管理地址',
trigger: 'blur'
}
],
icpNo: [
{
required: true,
type: 'string',
message: '请填写ICP备案号',
trigger: 'blur'
}
],
appSecret: [
{
required: true,
type: 'string',
message: '请填写小程序秘钥',
trigger: 'blur'
}
],
websiteName: [
{
required: true,
type: 'string',
message: '请填写小程序信息名称',
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.websiteLogo = data.downloadUrl;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.websiteLogo = '';
};
const chooseFile = (data: FileRecord) => {
form.websiteCode = data.url;
files.value.push({
uid: data.id,
url: data.url,
status: 'done'
});
};
const onDeleteFile = (index: number) => {
files.value.splice(index, 1);
};
// const onWebsiteType = (text: string) => {
// form.websiteType = text;
// };
const {resetFields} = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateCmsWebsite : addCmsWebsite;
if (!isUpdate.value) {
updateVisible(false);
message.loading('创建过程中请勿刷新页面!', 0)
}
const formData = {
...form,
type: 20,
adminUrl: `mp.websoft.top`,
files: JSON.stringify(files.value),
};
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
updateVisible(false);
updateCmsDomain({
websiteId: form.websiteId,
domain: `${localStorage.getItem('TenantId')}.shoplnk.cn`
});
updateTenant({
tenantName: `${form.websiteName}`
}).then(() => {
})
localStorage.setItem('Domain', `${form.websiteCode}.shoplnk.cn`);
localStorage.setItem('WebsiteId', `${form.websiteId}`);
localStorage.setItem('WebsiteName', `${form.websiteName}`);
message.destroy();
message.success(msg);
// window.location.reload();
emit('done');
})
.catch((e) => {
loading.value = false;
message.destroy();
message.error(e.message);
});
})
.catch(() => {
});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
files.value = [];
websiteQrcode.value = [];
if (props.data?.websiteId) {
assignObject(form, props.data);
if (props.data.websiteLogo) {
images.value.push({
uid: uuid(),
url: props.data.websiteLogo,
status: 'done'
});
}
if (props.data.files) {
files.value = JSON.parse(props.data.files);
}
if (props.data.websiteCode) {
oldDomain.value = props.data.websiteCode;
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{immediate: true}
);
</script>

View File

@@ -0,0 +1,295 @@
<template>
<a-page-header :show-back="false">
<a-row :gutter="16">
<!-- 应用基本信息卡片 -->
<a-col :span="24" style="margin-bottom: 16px">
<a-card title="概况" :bordered="false">
<a-row :gutter="16">
<a-col :span="6">
<a-image
:width="80"
:height="80"
:preview="false"
style="border-radius: 8px"
:src="siteStore.logo"
fallback="/logo.png"
/>
</a-col>
<a-col :span="14">
<div class="system-info">
<h2 class="ele-text-heading cursor-pointer" @click="$router.push('/website/index')">{{ siteStore.appName }}</h2>
<p class="ele-text-secondary">{{ siteStore.description }}</p>
<a-space>
<a-tag color="green">{{ siteStore.statusText }}</a-tag>
<a-tag color="blue" v-if="siteStore.version">{{ siteStore.version }}</a-tag>
<a-popover title="小程序码" v-if="siteStore.mpQrCode">
<template #content>
<p><img :src="siteStore.mpQrCode" alt="小程序码" width="300" height="300"></p>
</template>
<a-tag>
<QrcodeOutlined/>
</a-tag>
</a-popover>
</a-space>
</div>
</a-col>
<a-col :span="3">
<div class="flex justify-center items-center h-full w-full">
</div>
</a-col>
</a-row>
</a-card>
</a-col>
<!-- 统计数据卡片 -->
<a-col :span="6">
<a-card :bordered="false" class="stat-card">
<a-statistic
title="楼栋"
:value="userCount"
:value-style="{ color: '#3f8600' }"
:loading="loading"
>
<template #prefix>
<UserOutlined/>
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card :bordered="false" class="stat-card">
<a-statistic
title="宿舍"
:value="orderCount"
:value-style="{ color: '#1890ff' }"
:loading="loading"
>
<template #prefix>
<AccountBookOutlined/>
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card :bordered="false" class="stat-card">
<a-statistic
title="学生"
:value="totalSales"
:value-style="{ color: '#cf1322' }"
:loading="loading"
>
<template #prefix>
<MoneyCollectOutlined/>
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card :bordered="false" class="stat-card">
<a-statistic
title="待审批"
:value="runDays"
suffix="天"
:value-style="{ color: '#722ed1' }"
:loading="loading"
>
<template #prefix>
<ClockCircleOutlined/>
</template>
</a-statistic>
</a-card>
</a-col>
<!-- 系统基本信息 -->
<a-col :span="12">
<a-card title="基本信息" :bordered="false">
<a-descriptions :column="1" size="small">
<a-descriptions-item label="系统名称">
{{ systemInfo.name }}
</a-descriptions-item>
<a-descriptions-item label="版本号">
{{ systemInfo.version }}
</a-descriptions-item>
<a-descriptions-item label="部署环境">
{{ systemInfo.environment }}
</a-descriptions-item>
<a-descriptions-item label="数据库">
{{ systemInfo.database }}
</a-descriptions-item>
<a-descriptions-item label="服务器">
{{ systemInfo.server }}
</a-descriptions-item>
<a-descriptions-item label="创建时间">
{{ siteInfo?.createTime }}
</a-descriptions-item>
<a-descriptions-item label="到期时间">
{{ siteInfo?.expirationTime }}
</a-descriptions-item>
<a-descriptions-item label="技术支持">
<span class="cursor-pointer" @click="openNew(`https://websoft.top`)">网宿软件</span>
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
<!-- 快捷操作 -->
<a-col :span="12">
<a-card title="快捷操作" :bordered="false" style="min-height: 353px">
<a-space direction="vertical" style="width: 100%">
<a-button type="primary" block @click="$router.push('/website/field')" :loading="loading">
<UngroupOutlined/>
参数配置
</a-button>
<a-button block @click="$router.push('/shopOrder')">
<CalendarOutlined/>
订单管理
</a-button>
<a-button block @click="$router.push('/system/user')">
<UserOutlined/>
用户管理
</a-button>
<a-button block @click="$router.push('/website/index')">
<ShopOutlined/>
站点管理
</a-button>
<!-- <a-button block @click="refreshStatistics" :loading="loading">-->
<!-- <ReloadOutlined/>-->
<!-- 刷新统计-->
<!-- </a-button>-->
<a-button block @click="$router.push('/system/login-record')">
<FileTextOutlined/>
登录日志
</a-button>
<a-button block @click="clearSiteInfoCache">
<ClearOutlined/>
清除缓存
</a-button>
<!-- <a-button block @click="$router.push('/system/setting')">-->
<!-- <SettingOutlined/>-->
<!-- 系统设置-->
<!-- </a-button>-->
</a-space>
</a-card>
</a-col>
</a-row>
</a-page-header>
</template>
<script lang="ts" setup>
import {ref, onMounted, onUnmounted, computed} from 'vue';
import {
UserOutlined,
CalendarOutlined,
QrcodeOutlined,
ShopOutlined,
ClockCircleOutlined,
AccountBookOutlined,
FileTextOutlined,
ClearOutlined,
UngroupOutlined,
MoneyCollectOutlined
} from '@ant-design/icons-vue';
import {message} from 'ant-design-vue/es';
import {openNew} from "@/utils/common";
import {useSiteStore} from '@/store/modules/site';
import {useStatisticsStore} from '@/store/modules/statistics';
import {storeToRefs} from 'pinia';
import {removeSiteInfoCache} from "@/api/cms/cmsWebsite";
// 使用状态管理
const siteStore = useSiteStore();
const statisticsStore = useStatisticsStore();
// 从 store 中获取响应式数据
const {siteInfo, loading: siteLoading} = storeToRefs(siteStore);
const {loading: statisticsLoading} = storeToRefs(statisticsStore);
// 系统信息
const systemInfo = ref({
name: '小程序开发',
description: '基于Spring、SpringBoot、SpringMVC等技术栈构建的前后端分离开发平台',
version: '2.0.0',
status: '运行中',
logo: '/logo.png',
environment: '生产环境',
database: 'MySQL 8.0',
server: 'Linux CentOS 7.9',
expirationTime: '2024-01-01 09:00:00'
});
// 计算属性
const runDays = computed(() => siteStore.runDays);
const userCount = computed(() => statisticsStore.userCount);
const orderCount = computed(() => statisticsStore.orderCount);
const totalSales = computed(() => statisticsStore.totalSales);
// 加载状态
const loading = computed(() => siteLoading.value || statisticsLoading.value);
// 清除缓存
const clearSiteInfoCache = () => {
removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId')).then(
(msg) => {
if (msg) {
message.success(msg);
}
}
);
};
// 刷新统计数据
const refreshStatistics = async () => {
try {
await statisticsStore.forceRefresh();
message.success('统计数据刷新成功');
} catch (error) {
console.error('刷新统计数据失败:', error);
message.error('刷新统计数据失败');
}
};
onMounted(async () => {
// 加载网站信息和统计数据
try {
await Promise.all([
siteStore.fetchSiteInfo(),
statisticsStore.fetchStatistics()
]);
// 开始自动刷新统计数据每5分钟
statisticsStore.startAutoRefresh();
} catch (error) {
console.error('加载数据失败:', error);
}
});
onUnmounted(() => {
// 组件卸载时停止自动刷新
statisticsStore.stopAutoRefresh();
});
</script>
<style scoped>
.system-info h2 {
margin-bottom: 8px;
}
.stat-card {
text-align: center;
margin-bottom: 16px;
}
.stat-card :deep(.ant-statistic-title) {
font-size: 14px;
color: #666;
}
.stat-card :deep(.ant-statistic-content) {
font-size: 24px;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,307 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '审批管理' : '办理入住'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="类型" name="type">
<a-input
allow-clear
placeholder="请输入类型"
v-model:value="form.type"
/>
</a-form-item>
<a-form-item label="用户ID" name="userId">
<a-input
allow-clear
placeholder="请输入用户ID"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="姓名" name="realName">
<a-input
allow-clear
placeholder="请输入姓名"
v-model:value="form.realName"
/>
</a-form-item>
<a-form-item label="手机号" name="mobile">
<a-input
allow-clear
placeholder="请输入手机号"
v-model:value="form.mobile"
/>
</a-form-item>
<a-form-item label="客户名称" name="dealerName">
<a-input
allow-clear
placeholder="请输入客户名称"
v-model:value="form.dealerName"
/>
</a-form-item>
<a-form-item label="客户编号" name="dealerCode">
<a-input
allow-clear
placeholder="请输入客户编号"
v-model:value="form.dealerCode"
/>
</a-form-item>
<a-form-item label="详细地址" name="address">
<a-input
allow-clear
placeholder="请输入详细地址"
v-model:value="form.address"
/>
</a-form-item>
<a-form-item label="签约价格" name="money">
<a-input
allow-clear
placeholder="请输入签约价格"
v-model:value="form.money"
/>
</a-form-item>
<a-form-item label="推荐人用户" name="refereeId">
<a-input
allow-clear
placeholder="请输入推荐人用户ID"
v-model:value="form.refereeId"
/>
</a-form-item>
<a-form-item label="申请方式" name="applyType">
<a-input
allow-clear
placeholder="请输入申请方式"
v-model:value="form.applyType"
/>
</a-form-item>
<a-form-item label="审核状态" name="applyStatus">
<a-input
allow-clear
placeholder="请输入审核状态"
v-model:value="form.applyStatus"
/>
</a-form-item>
<a-form-item label="申请时间" name="applyTime">
<a-input
allow-clear
placeholder="请输入申请时间"
v-model:value="form.applyTime"
/>
</a-form-item>
<a-form-item label="审核时间" name="auditTime">
<a-input
allow-clear
placeholder="请输入审核时间"
v-model:value="form.auditTime"
/>
</a-form-item>
<a-form-item label="合同时间" name="contractTime">
<a-input
allow-clear
placeholder="请输入合同时间"
v-model:value="form.contractTime"
/>
</a-form-item>
<a-form-item label="过期时间" name="expirationTime">
<a-input
allow-clear
placeholder="请输入过期时间"
v-model:value="form.expirationTime"
/>
</a-form-item>
<a-form-item label="驳回原因" name="rejectReason">
<a-input
allow-clear
placeholder="请输入驳回原因"
v-model:value="form.rejectReason"
/>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="修改时间" name="updateTime">
<a-input
allow-clear
placeholder="请输入修改时间"
v-model:value="form.updateTime"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { addDormitoryApply, updateDormitoryApply } from '@/api/dormitory/dormitoryApply';
import { DormitoryApply } from '@/api/dormitory/dormitoryApply/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: DormitoryApply | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
const form = reactive<DormitoryApply>({
id: undefined,
type: undefined,
userId: undefined,
realName: undefined,
mobile: undefined,
dealerName: undefined,
dealerCode: undefined,
address: undefined,
money: undefined,
refereeId: undefined,
applyType: undefined,
applyStatus: undefined,
applyTime: undefined,
auditTime: undefined,
contractTime: undefined,
expirationTime: undefined,
rejectReason: undefined,
comments: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
dormitoryApplyId: undefined,
dormitoryApplyName: '',
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
dormitoryApplyName: [
{
required: true,
type: 'string',
message: '请填写审批管理名称',
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
};
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateDormitoryApply : addDormitoryApply;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,292 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50"/>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<DormitoryApplyEdit v-model:visible="showEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ExclamationCircleOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import {toDateString} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import DormitoryApplyEdit from './components/dormitoryApplyEdit.vue';
import {pageDormitoryApply, removeDormitoryApply, removeBatchDormitoryApply} from '@/api/dormitory/dormitoryApply';
import type {DormitoryApply, DormitoryApplyParam} from '@/api/dormitory/dormitoryApply/model';
import {getPageTitle} from "@/utils/common";
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<DormitoryApply[]>([]);
// 当前编辑数据
const current = ref<DormitoryApply | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageDormitoryApply({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '学号',
dataIndex: 'userId',
key: 'userId',
align: 'center',
width: 90,
},
{
title: '姓名',
dataIndex: 'realName',
key: 'realName',
align: 'center',
},
{
title: '手机号',
dataIndex: 'mobile',
key: 'mobile',
align: 'center',
},
{
title: '宿舍号',
dataIndex: 'recordName',
key: 'recordName',
align: 'center',
},
{
title: '床铺号',
dataIndex: 'bedName',
key: 'bedName',
align: 'center',
},
// {
// title: '操作人员',
// dataIndex: 'refereeName',
// key: 'refereeName',
// align: 'center',
// },
// {
// title: '申请方式(10需后台审核 20无需审核)',
// dataIndex: 'applyType',
// key: 'applyType',
// align: 'center',
// },
{
title: '审核状态',
dataIndex: 'applyStatus',
key: 'applyStatus',
align: 'center',
},
// {
// title: '申请时间',
// dataIndex: 'applyTime',
// key: 'applyTime',
// align: 'center',
// },
// {
// title: '审核时间',
// dataIndex: 'auditTime',
// key: 'auditTime',
// align: 'center',
// },
// {
// title: '合同时间',
// dataIndex: 'contractTime',
// key: 'contractTime',
// align: 'center',
// },
// {
// title: '过期时间',
// dataIndex: 'expirationTime',
// key: 'expirationTime',
// align: 'center',
// },
{
title: '驳回原因',
dataIndex: 'rejectReason',
key: 'rejectReason',
align: 'center',
},
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// align: 'center',
// },
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: DormitoryApplyParam) => {
selection.value = [];
tableRef?.value?.reload({where: where});
};
/* 打开编辑弹窗 */
const openEdit = (row?: DormitoryApply) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: DormitoryApply) => {
const hide = message.loading('请求中..', 0);
removeDormitoryApply(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchDormitoryApply(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: DormitoryApply) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'DormitoryApply'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,244 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑床铺' : '添加床铺'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="床铺编号" name="name">
<a-input
allow-clear
placeholder="请输入宿舍名称"
v-model:value="form.name"
/>
</a-form-item>
<!-- <a-form-item label="编号" name="code">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入编号"-->
<!-- v-model:value="form.code"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="上下铺" name="bunk">
<a-radio-group v-model:value="form.bunk">
<a-radio :value="0">下铺</a-radio>
<a-radio :value="1">上铺</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="学生姓名" name="realName">
<div class="flex w-full justify-between gap-2">
<a-input
allow-clear
placeholder="请选择学生"
disabled
v-model:value="form.realName"
/>
<!-- <a-tooltip :title="`选择学生`">-->
<!-- <SelectStudent-->
<!-- :placeholder="`选择学生`"-->
<!-- class="form-item"-->
<!-- v-model:value="form.realName"-->
<!-- @done="chooseStudent"-->
<!-- />-->
<!-- </a-tooltip>-->
</div>
</a-form-item>
<a-form-item label="联系电话" name="phone">
<a-input
allow-clear
disabled
placeholder="请输入学生联系电话"
v-model:value="form.phone"
/>
</a-form-item>
<a-form-item label="照片" name="avatar">
<a-image :src="form.avatar || 'https://oss.wsdns.cn/20251003/bf67047afe244ab0b159d836704baf1e.png'" :preview="false" :width="120" :height="160" />
</a-form-item>
<!-- <a-form-item label="充电口" name="chargingPort">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入充电口"-->
<!-- v-model:value="form.chargingPort"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="排序" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="0">正常</a-radio>
<a-radio :value="1">损坏</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addDormitoryBed, updateDormitoryBed } from '@/api/dormitory/dormitoryBed';
import { DormitoryBed } from '@/api/dormitory/dormitoryBed/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import {User} from "@/api/system/user/model";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: DormitoryBed | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
const form = reactive<DormitoryBed>({
id: undefined,
name: undefined,
code: undefined,
buildingId: undefined,
buildingName: undefined,
floorId: undefined,
floorName: undefined,
bunk: undefined,
userId: undefined,
realName: undefined,
phone: undefined,
avatar: undefined,
chargingPort: undefined,
tenantId: undefined,
createTime: undefined,
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
type: 'string',
message: '请填写宿舍床铺编号',
trigger: 'blur'
}
],
bunk: [
{
required: true,
type: 'string',
message: '选择上铺下铺',
trigger: 'blur'
}
]
});
const chooseStudent = (item: User) => {
form.userId = item.userId;
form.realName = item.realName;
form.phone = item.phone;
}
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateDormitoryBed : addDormitoryBed;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,288 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<template #extra>
<Extra/>
</template>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="dormitoryBedId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<DormitoryBedEdit v-model:visible="showEdit" :data="current" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import DormitoryBedEdit from './components/dormitoryBedEdit.vue';
import { pageDormitoryBed, removeDormitoryBed, removeBatchDormitoryBed } from '@/api/dormitory/dormitoryBed';
import type { DormitoryBed, DormitoryBedParam } from '@/api/dormitory/dormitoryBed/model';
import {getPageTitle} from "@/utils/common";
import Extra from "@/views/dormitory/Extra.vue";
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<DormitoryBed[]>([]);
// 当前编辑数据
const current = ref<DormitoryBed | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageDormitoryBed({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '床铺ID',
dataIndex: 'id',
key: 'userId',
align: 'center',
width: 120
},
{
title: '床铺编号',
dataIndex: 'name',
key: 'name',
align: 'center',
},
{
title: '学生姓名',
dataIndex: 'realName',
key: 'realName',
align: 'center',
width: 300,
},
// {
// title: '手机号码',
// dataIndex: 'phone',
// key: 'phone',
// align: 'center'
// },
// {
// title: '学号',
// dataIndex: 'userId',
// key: 'userId',
// align: 'center'
// },
// {
// title: '床铺号',
// dataIndex: 'name',
// key: 'name',
// align: 'center',
// },
{
title: '宿舍号',
dataIndex: 'recordName',
key: 'recordName',
align: 'center',
width: 300,
},
{
title: '上下铺',
dataIndex: 'bunk',
key: 'bunk',
align: 'center',
customRender: ({ record }) => {
return record.bunk === 0 ? '下铺' : '上铺';
}
},
// {
// title: '充电口',
// dataIndex: 'chargingPort',
// key: 'chargingPort',
// align: 'center',
// },
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
},
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// align: 'center',
// },
// {
// title: '状态',
// dataIndex: 'status',
// key: 'status',
// align: 'center',
// },
// {
// title: '创建时间',
// dataIndex: 'createTime',
// key: 'createTime',
// align: 'center',
// sorter: true,
// ellipsis: true,
// customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
// },
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: DormitoryBedParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: DormitoryBed) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: DormitoryBed) => {
const hide = message.loading('请求中..', 0);
removeDormitoryBed(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchDormitoryBed(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: DormitoryBed) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'DormitoryBed'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,177 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑楼栋' : '添加楼栋'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="楼栋名称" name="name">
<a-input
allow-clear
placeholder="请输入楼栋名称"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="楼栋编号" name="code">
<a-input
allow-clear
placeholder="请输入楼栋编号"
v-model:value="form.code"
/>
</a-form-item>
<a-form-item label="排序" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</a-form-item>
<!-- <a-form-item label="状态" name="status">-->
<!-- <a-radio-group v-model:value="form.status">-->
<!-- <a-radio :value="0">启用</a-radio>-->
<!-- <a-radio :value="1">禁用</a-radio>-->
<!-- </a-radio-group>-->
<!-- </a-form-item>-->
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { addDormitoryBuilding, updateDormitoryBuilding } from '@/api/dormitory/dormitoryBuilding';
import { DormitoryBuilding } from '@/api/dormitory/dormitoryBuilding/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: DormitoryBuilding | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
const form = reactive<DormitoryBuilding>({
id: undefined,
name: undefined,
code: undefined,
tenantId: undefined,
createTime: undefined,
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
type: 'string',
message: '请填写宿舍楼栋名称',
trigger: 'blur'
}
]
});
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateDormitoryBuilding : addDormitoryBuilding;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,242 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<template #extra>
<Extra/>
</template>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<DormitoryBuildingEdit v-model:visible="showEdit" :data="current" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import DormitoryBuildingEdit from './components/dormitoryBuildingEdit.vue';
import { pageDormitoryBuilding, removeDormitoryBuilding, removeBatchDormitoryBuilding } from '@/api/dormitory/dormitoryBuilding';
import type { DormitoryBuilding, DormitoryBuildingParam } from '@/api/dormitory/dormitoryBuilding/model';
import {getPageTitle} from "@/utils/common";
import Extra from "@/views/dormitory/Extra.vue";
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<DormitoryBuilding[]>([]);
// 当前编辑数据
const current = ref<DormitoryBuilding | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageDormitoryBuilding({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '楼栋名称',
dataIndex: 'name',
key: 'name',
align: 'center',
},
// {
// title: '楼栋编号',
// dataIndex: 'code',
// key: 'code',
// align: 'center',
// },
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
},
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// align: 'center',
// },
// {
// title: '创建时间',
// dataIndex: 'createTime',
// key: 'createTime',
// align: 'center',
// sorter: true,
// ellipsis: true,
// customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
// },
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: DormitoryBuildingParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: DormitoryBuilding) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: DormitoryBuilding) => {
const hide = message.loading('请求中..', 0);
removeDormitoryBuilding(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchDormitoryBuilding(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: DormitoryBuilding) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'DormitoryBuilding'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,194 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑楼层' : '添加楼层'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="选择楼栋" name="buildingId">
<a-input
allow-clear
placeholder="请输入楼栋ID"
v-model:value="form.buildingId"
/>
</a-form-item>
<a-form-item label="楼层" name="name">
<a-input
allow-clear
placeholder="请输入楼层"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="编号" name="code">
<a-input
allow-clear
placeholder="请输入编号"
v-model:value="form.code"
/>
</a-form-item>
<a-form-item label="排序" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<!-- <a-form-item label="备注" name="comments">-->
<!-- <a-textarea-->
<!-- :rows="4"-->
<!-- :maxlength="200"-->
<!-- placeholder="请输入描述"-->
<!-- v-model:value="form.comments"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="状态" name="status">-->
<!-- <a-radio-group v-model:value="form.status">-->
<!-- <a-radio :value="0">启用</a-radio>-->
<!-- <a-radio :value="1">禁用</a-radio>-->
<!-- </a-radio-group>-->
<!-- </a-form-item>-->
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addDormitoryFloor, updateDormitoryFloor } from '@/api/dormitory/dormitoryFloor';
import { DormitoryFloor } from '@/api/dormitory/dormitoryFloor/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: DormitoryFloor | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
const form = reactive<DormitoryFloor>({
id: undefined,
name: undefined,
code: undefined,
buildingId: undefined,
buildingName: undefined,
tenantId: undefined,
createTime: undefined,
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
buildingId: [
{
required: true,
type: 'string',
message: '请填写宿舍楼层名称',
trigger: 'blur'
}
],
name: [
{
required: true,
type: 'string',
message: '请填写楼层名称',
trigger: 'blur'
}
]
});
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateDormitoryFloor : addDormitoryFloor;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,248 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<template #extra>
<Extra/>
</template>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50"/>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">启用</a-tag>
<a-tag v-if="record.status === 1" color="red">禁用</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<DormitoryFloorEdit v-model:visible="showEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ExclamationCircleOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import DormitoryFloorEdit from './components/dormitoryFloorEdit.vue';
import {pageDormitoryFloor, removeDormitoryFloor, removeBatchDormitoryFloor} from '@/api/dormitory/dormitoryFloor';
import type {DormitoryFloor, DormitoryFloorParam} from '@/api/dormitory/dormitoryFloor/model';
import {getPageTitle} from "@/utils/common";
import Extra from "@/views/dormitory/Extra.vue";
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<DormitoryFloor[]>([]);
// 当前编辑数据
const current = ref<DormitoryFloor | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageDormitoryFloor({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '楼栋',
dataIndex: 'buildingName',
key: 'buildingName',
align: 'center',
width: 300,
},
{
title: '楼层',
dataIndex: 'name',
key: 'name',
align: 'center',
},
// {
// title: '编号',
// dataIndex: 'code',
// key: 'code',
// align: 'center',
// },
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
},
// {
// title: '状态',
// dataIndex: 'status',
// key: 'status',
// align: 'center',
// },
// {
// title: '创建时间',
// dataIndex: 'createTime',
// key: 'createTime',
// align: 'center',
// sorter: true,
// ellipsis: true,
// customRender: ({text}) => toDateString(text, 'yyyy-MM-dd')
// },
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: DormitoryFloorParam) => {
selection.value = [];
tableRef?.value?.reload({where: where});
};
/* 打开编辑弹窗 */
const openEdit = (row?: DormitoryFloor) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: DormitoryFloor) => {
const hide = message.loading('请求中..', 0);
removeDormitoryFloor(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchDormitoryFloor(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: DormitoryFloor) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'DormitoryFloor'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,205 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑宿舍' : '添加宿舍'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="宿舍号" name="name">
<a-input
allow-clear
placeholder="请输入宿舍号"
v-model:value="form.name"
/>
</a-form-item>
<!-- <a-form-item label="编号" name="code">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入编号"-->
<!-- v-model:value="form.code"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="楼栋ID" name="buildingId">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入楼栋ID"-->
<!-- v-model:value="form.buildingId"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="楼层ID" name="floorId">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入楼层ID"-->
<!-- v-model:value="form.floorId"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="床铺数" name="beds">
<a-input
allow-clear
placeholder="请输入床铺数"
v-model:value="form.beds"
/>
</a-form-item>
<a-form-item label="独立卫生间" name="toilet">
<a-switch v-model:checked="form.toilet"/>
</a-form-item>
<a-form-item label="排序" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<!-- <a-form-item label="备注" name="comments">-->
<!-- <a-textarea-->
<!-- :rows="4"-->
<!-- :maxlength="200"-->
<!-- placeholder="请输入描述"-->
<!-- v-model:value="form.comments"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="状态" name="status">-->
<!-- <a-radio-group v-model:value="form.status">-->
<!-- <a-radio :value="0">启用</a-radio>-->
<!-- <a-radio :value="1">禁用</a-radio>-->
<!-- </a-radio-group>-->
<!-- </a-form-item>-->
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addDormitoryRecord, updateDormitoryRecord } from '@/api/dormitory/dormitoryRecord';
import { DormitoryRecord } from '@/api/dormitory/dormitoryRecord/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: DormitoryRecord | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
const form = reactive<DormitoryRecord>({
id: undefined,
name: undefined,
code: undefined,
buildingId: undefined,
floorId: undefined,
beds: undefined,
toilet: undefined,
tenantId: undefined,
createTime: undefined,
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
type: 'string',
message: '请填写宿舍记录名称',
trigger: 'blur'
}
]
});
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateDormitoryRecord : addDormitoryRecord;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,278 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<template #extra>
<Extra/>
</template>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50"/>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 1" color="green">已使用</a-tag>
<a-tag v-if="record.status === 0">未使用</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<DormitoryRecordEdit v-model:visible="showEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ExclamationCircleOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import DormitoryRecordEdit from './components/dormitoryRecordEdit.vue';
import {pageDormitoryRecord, removeDormitoryRecord, removeBatchDormitoryRecord} from '@/api/dormitory/dormitoryRecord';
import type {DormitoryRecord, DormitoryRecordParam} from '@/api/dormitory/dormitoryRecord/model';
import {getPageTitle} from "@/utils/common";
import Extra from "@/views/dormitory/Extra.vue";
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<DormitoryRecord[]>([]);
// 当前编辑数据
const current = ref<DormitoryRecord | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageDormitoryRecord({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '楼栋',
dataIndex: 'buildingName',
key: 'buildingName',
align: 'center',
width: 300,
},
{
title: '宿舍号',
dataIndex: 'name',
key: 'name',
align: 'center',
},
// {
// title: '编号',
// dataIndex: 'code',
// key: 'code',
// align: 'center',
// },
// {
// title: '楼栋ID',
// dataIndex: 'buildingId',
// key: 'buildingId',
// align: 'center',
// },
{
title: '楼层',
dataIndex: 'floorName',
key: 'floorName',
align: 'center',
},
{
title: '床铺数',
dataIndex: 'beds',
key: 'beds',
align: 'center',
},
// {
// title: '独立卫生间',
// dataIndex: 'toilet',
// key: 'toilet',
// align: 'center',
// },
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
},
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// align: 'center',
// },
// {
// title: '使用状态',
// dataIndex: 'status',
// key: 'status',
// align: 'center',
// },
// {
// title: '创建时间',
// dataIndex: 'createTime',
// key: 'createTime',
// align: 'center',
// sorter: true,
// ellipsis: true,
// customRender: ({text}) => toDateString(text, 'yyyy-MM-dd')
// },
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: DormitoryRecordParam) => {
selection.value = [];
tableRef?.value?.reload({where: where});
};
/* 打开编辑弹窗 */
const openEdit = (row?: DormitoryRecord) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: DormitoryRecord) => {
const hide = message.loading('请求中..', 0);
removeDormitoryRecord(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchDormitoryRecord(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: DormitoryRecord) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'DormitoryRecord'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,55 @@
<!-- 搜索表单 -->
<template>
<a-space style="flex-wrap: wrap" v-if="hasRole('superAdmin') || hasRole('admin') || hasRole('foundation')">
<a-button
type="text"
v-if="hasPermission('sys:org:list')"
@click="openUrl('/staff')"
>人员管理
</a-button
>
<a-button
type="text"
v-if="hasPermission('sys:userVerify:list')"
@click="openUrl('/system/user-verify')"
>实名认证
</a-button
>
<a-button
type="text"
v-if="hasAnyRole(['superAdmin','admin'])"
@click="openUrl('/system/admin')"
>管理员列表
</a-button
>
</a-space>
</template>
<script lang="ts" setup>
import {watch, nextTick} from 'vue';
import {CmsWebsite} from '@/api/cms/cmsWebsite/model';
import {openUrl} from "@/utils/common";
import {hasAnyRole, hasPermission, hasRole} from "@/utils/permission";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
website?: CmsWebsite;
count?: 0;
}>(),
{}
);
nextTick(() => {
if (localStorage.getItem('NotActive')) {
// IsActive.value = false
}
})
watch(
() => props.selection,
() => {
}
);
</script>

View File

@@ -0,0 +1,39 @@
<!-- 机构选择下拉框 -->
<template>
<a-tree-select
allow-clear
tree-default-expand-all
:placeholder="placeholder"
:value="value || undefined"
:tree-data="data"
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
@update:value="updateValue"
/>
</template>
<script lang="ts" setup>
import type { Organization } from '@/api/system/organization/model';
const emit = defineEmits<{
(e: 'update:value', value?: number): void;
}>();
withDefaults(
defineProps<{
// 选中的数据(v-modal)
value?: number;
// 提示信息
placeholder?: string;
// 机构数据
data: Organization[];
}>(),
{
placeholder: '请选择角色'
}
);
/* 更新选中数据 */
const updateValue = (value?: number) => {
emit('update:value', value);
};
</script>

View File

@@ -0,0 +1,71 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
allow-clear
mode="multiple"
:value="roleIds"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
>
<a-select-option
v-for="item in data"
:key="item.roleId"
:value="item.roleId"
>
{{ item.roleName }}
</a-select-option>
</a-select>
</template>
<script lang="ts" setup>
import {ref, computed} from 'vue';
import {message} from 'ant-design-vue/es';
import {listRoles} from '@/api/system/role';
import type {Role} from '@/api/system/role/model';
const emit = defineEmits<{
(e: 'update:value', value: Role[]): void;
(e: 'blur'): void;
}>();
const props = withDefaults(
defineProps<{
// 选中的角色
value?: Role[];
//
placeholder?: string;
}>(),
{
placeholder: '请选择角色'
}
);
// 选中的角色id
const roleIds = computed(() => props.value?.map((d) => d.roleId as number));
// 角色数据
const data = ref<Role[]>([]);
/* 更新选中数据 */
const updateValue = (value: number[]) => {
emit(
'update:value',
value.map((v) => ({roleId: v}))
);
};
/* 获取角色数据 */
listRoles()
.then((list) => {
data.value = list;
})
.catch((e) => {
message.error(e.message);
});
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,45 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
show-search
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
/>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择性别'
}
);
// 字典数据
const data = getDictionaryOptions('sex');
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>

View File

@@ -0,0 +1,363 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改用户' : '新建用户'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 5, sm: 4, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 17, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="账号" name="username">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入账号"
:disabled="isUpdate"
v-model:value="form.username"
/>
</a-form-item>
<a-form-item label="昵称" name="nickname">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入昵称"
v-model:value="form.nickname"
/>
</a-form-item>
<a-form-item label="角色" name="roles">
<role-select v-model:value="form.roles" />
</a-form-item>
<a-form-item label="会员等级">
<SelectGrade
:placeholder="`请选择会员等级`"
v-model:value="form.gradeName"
:disabled="isUpdate"
@done="chooseGradeId"
/>
</a-form-item>
<a-form-item label="性别" name="sex">
<DictSelect
dict-code="sex"
:placeholder="`请选择性别`"
v-model:value="form.sexName"
@done="chooseSex"
/>
</a-form-item>
<a-form-item label="姓名" name="realName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入真实姓名"
v-model:value="form.realName"
/>
</a-form-item>
<a-form-item label="所属机构" name="type">
<org-select
:data="organizationList"
placeholder="请选择所属机构"
v-model:value="form.organizationId"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="手机号" v-if="isUpdate" name="mobile">
<a-input
allow-clear
:maxlength="11"
placeholder="请输入手机号"
v-model:value="form.mobile"
disabled
/>
</a-form-item>
<a-form-item label="手机号" v-else name="phone">
<a-input
allow-clear
:maxlength="11"
placeholder="请输入手机号"
v-model:value="form.phone"
/>
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input
allow-clear
:maxlength="100"
placeholder="请输入邮箱"
v-model:value="form.email"
/>
</a-form-item>
<a-form-item label="出生日期">
<a-date-picker
class="ele-fluid"
value-format="YYYY-MM-DD"
placeholder="请选择出生日期"
v-model:value="form.birthday"
/>
</a-form-item>
<a-form-item v-if="!isUpdate" label="登录密码" name="password">
<a-input-password
:maxlength="20"
v-model:value="form.password"
placeholder="请输入登录密码"
/>
</a-form-item>
<a-form-item label="个人简介">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入个人简介"
v-model:value="form.introduction"
/>
</a-form-item>
<a-form-item label="管理员">
<a-switch
checked-children=""
un-checked-children=""
:checked="form.isAdmin"
@update:checked="updateIsAdmin"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { emailReg, phoneReg } from 'ele-admin-pro/es';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import RoleSelect from './role-select.vue';
import { addUser, updateUser, checkExistence } from '@/api/system/user';
import type { User } from '@/api/system/user/model';
import OrgSelect from './org-select.vue';
// import { getDictionaryOptions } from '@/utils/common';
import { Organization } from '@/api/system/organization/model';
import { Grade } from '@/api/user/grade/model';
import {TEMPLATE_ID} from "@/config/setting";
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
// 获取字典数据
// const userTypeData = getDictionaryOptions('userType');
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: User | null;
// 全部机构
organizationList: Organization[];
}>();
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const { form, resetFields, assignFields } = useFormData<User>({
type: undefined,
userId: undefined,
username: '',
nickname: '',
realName: '',
companyName: '',
sex: undefined,
sexName: '',
roles: [],
email: '',
phone: '',
mobile: '',
password: '',
introduction: '',
organizationId: undefined,
birthday: '',
idCard: '',
comments: '',
gradeName: '',
isAdmin: undefined,
gradeId: undefined,
templateId: TEMPLATE_ID
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
username: [
{
required: true,
type: 'string',
validator: (_rule: Rule, value: string) => {
return new Promise<void>((resolve, reject) => {
if (!value) {
return reject('请输入用户账号');
}
checkExistence('username', value, props.data?.userId)
.then(() => {
reject('账号已经存在');
})
.catch(() => {
resolve();
});
});
},
trigger: 'blur'
}
],
nickname: [
{
required: true,
message: '请输入昵称',
type: 'string',
trigger: 'blur'
}
],
// realName: [
// {
// required: true,
// message: '请输入真实姓名',
// type: 'string',
// trigger: 'blur'
// }
// ],
// sex: [
// {
// required: true,
// message: '请选择性别',
// type: 'string',
// trigger: 'blur'
// }
// ],
roles: [
{
required: true,
message: '请选择角色',
type: 'array',
trigger: 'blur'
}
],
email: [
{
pattern: emailReg,
message: '邮箱格式不正确',
type: 'string',
trigger: 'blur'
}
],
password: [
{
required: true,
type: 'string',
validator: async (_rule: Rule, value: string) => {
if (isUpdate.value || /^[\S]{5,18}$/.test(value)) {
return Promise.resolve();
}
return Promise.reject('密码必须为5-18位非空白字符');
},
trigger: 'blur'
}
],
phone: [
{
required: true,
pattern: phoneReg,
message: '手机号格式不正确',
type: 'string',
trigger: 'blur'
}
]
});
const chooseGradeId = (data: Grade) => {
form.gradeName = data.name;
form.gradeId = data.gradeId;
};
const chooseSex = (data: any) => {
form.sex = data.key;
form.sexName = data.label;
};
const updateIsAdmin = (value: boolean) => {
form.isAdmin = value;
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateUser : addUser;
saveOrUpdate(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignFields({
...props.data,
password: ''
});
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>

View File

@@ -0,0 +1,88 @@
<!-- 用户导入弹窗 -->
<template>
<ele-modal
:width="520"
:footer="null"
title="导入用户"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<a-upload-dragger
accept=".xls,.xlsx"
:show-upload-list="false"
:customRequest="doUpload"
style="padding: 24px 0; margin-bottom: 16px"
>
<p class="ant-upload-drag-icon">
<cloud-upload-outlined />
</p>
<p class="ant-upload-hint">将文件拖到此处或点击上传</p>
</a-upload-dragger>
</a-spin>
<div class="ele-text-center">
<span>只能上传xlsxlsx文件</span>
<a
href="https://oss.wsdns.cn/20200610/用户导入模板.xlsx"
download="用户导入模板.xlsx"
>
下载模板
</a>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { importUsers } from '@/api/system/user';
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
// 是否打开弹窗
visible: boolean;
}>();
// 导入请求状态
const loading = ref(false);
/* 上传 */
const doUpload = ({ file }) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 10) {
message.error('大小不能超过 10MB');
return false;
}
loading.value = true;
importUsers(file)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
return false;
};
/* 更新 visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>

View File

@@ -0,0 +1,143 @@
<!-- 用户编辑弹窗 -->
<template>
<a-drawer
:width="680"
:visible="visible"
:confirm-loading="loading"
:title="'基本信息'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
:footer="null"
>
<a-form
:label-col="{ md: { span: 6 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 19 }, sm: { span: 24 } }"
>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="账号">
<span class="ele-text">{{ user.username }}</span>
</a-form-item>
<a-form-item label="昵称">
<span class="ele-text">{{ user.nickname }}</span>
</a-form-item>
<a-form-item label="性别">
<span class="ele-text">{{ user.sexName }}</span>
</a-form-item>
<a-form-item label="手机号">
<span class="ele-text">{{ user.phone }}</span>
</a-form-item>
<a-form-item label="角色">
<a-tag v-for="item in user.roles" :key="item.roleId" color="blue">
{{ item.roleName }}
</a-tag>
</a-form-item>
<a-form-item label="状态">
<a-badge
v-if="typeof user.status === 'number'"
:status="(['processing', 'error'][user.status] as any)"
:text="['正常', '冻结'][user.status]"
/>
</a-form-item>
<a-form-item label="地址">
<span class="ele-text">{{ user.address }}</span>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="可用余额">
<span class="ele-text-success">{{ formatNumber(user.balance) }}</span>
</a-form-item>
<a-form-item label="可用积分">
<span class="ele-text">{{ user.points }}</span>
</a-form-item>
<a-form-item label="实际消费">
<span class="ele-text">{{ user.payMoney }}</span>
</a-form-item>
<a-form-item label="机构/部门">
<span class="ele-text">{{ user.organizationName }}</span>
</a-form-item>
<a-form-item label="头像">
<a-image :src="user.avatar" :width="36" />
</a-form-item>
<a-form-item label="生日">
<span class="ele-text">{{ user.birthday }}</span>
</a-form-item>
<a-form-item label="创建时间">
<span class="ele-text">{{ user.createTime }}</span>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import type { User } from '@/api/system/user/model';
import { useThemeStore } from '@/store/modules/theme';
import { formatNumber } from 'ele-admin-pro/es';
import { storeToRefs } from 'pinia';
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: User | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 用户信息
const user = reactive<User>({
username: '',
nickname: '',
sexName: '',
phone: '',
avatar: '',
balance: undefined,
points: 0,
payMoney: 0,
birthday: '',
address: '',
roles: [],
createTime: undefined,
status: undefined
});
// 请求状态
const loading = ref(true);
const { resetFields } = useForm(user);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
assignObject(user, props.data);
}
} else {
resetFields();
}
}
);
</script>

View File

@@ -0,0 +1,111 @@
<!-- 搜索表单 -->
<template>
<a-form
:label-col="
styleResponsive ? { xl: 7, lg: 5, md: 7, sm: 4 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { xl: 17, lg: 19, md: 17, sm: 20 } : { flex: '1' }
"
>
<a-row :gutter="8">
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="用户账号">
<a-input
v-model:value.trim="form.username"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="昵称">
<a-input
v-model:value.trim="form.nickname"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="性别">
<a-select v-model:value="form.sex" placeholder="请选择" allow-clear>
<a-select-option value="1">男</a-select-option>
<a-select-option value="2">女</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item class="ele-text-right" :wrapper-col="{ span: 24 }">
<a-space>
<a-button type="primary" @click="search">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { UserParam } from '@/api/system/user/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 默认搜索条件
where?: UserParam;
}>();
const emit = defineEmits<{
(e: 'search', where?: UserParam): void;
}>();
// 表单数据
const { form, resetFields } = useFormData<UserParam>({
username: '',
nickname: '',
sex: undefined,
...props.where
});
/* 搜索 */
const search = () => {
emit('search', form);
};
/* 重置 */
const reset = () => {
resetFields();
search();
};
</script>

View File

@@ -0,0 +1,274 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑' : '添加'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="用户唯一小程序id" name="openId">
<a-input
allow-clear
placeholder="请输入用户唯一小程序id"
v-model:value="form.openId"
/>
</a-form-item>
<a-form-item label="小程序用户秘钥" name="sessionKey">
<a-input
allow-clear
placeholder="请输入小程序用户秘钥"
v-model:value="form.sessionKey"
/>
</a-form-item>
<a-form-item label="用户名" name="username">
<a-input
allow-clear
placeholder="请输入用户名"
v-model:value="form.username"
/>
</a-form-item>
<a-form-item label="头像地址" name="avatarUrl">
<a-input
allow-clear
placeholder="请输入头像地址"
v-model:value="form.avatarUrl"
/>
</a-form-item>
<a-form-item label="1男2女" name="gender">
<a-input
allow-clear
placeholder="请输入1男2女"
v-model:value="form.gender"
/>
</a-form-item>
<a-form-item label="国家" name="country">
<a-input
allow-clear
placeholder="请输入国家"
v-model:value="form.country"
/>
</a-form-item>
<a-form-item label="省份" name="province">
<a-input
allow-clear
placeholder="请输入省份"
v-model:value="form.province"
/>
</a-form-item>
<a-form-item label="城市" name="city">
<a-input
allow-clear
placeholder="请输入城市"
v-model:value="form.city"
/>
</a-form-item>
<a-form-item label="手机号码" name="phone">
<a-input
allow-clear
placeholder="请输入手机号码"
v-model:value="form.phone"
/>
</a-form-item>
<a-form-item label="积分" name="integral">
<a-input
allow-clear
placeholder="请输入积分"
v-model:value="form.integral"
/>
</a-form-item>
<a-form-item label="余额" name="money">
<a-input
allow-clear
placeholder="请输入余额"
v-model:value="form.money"
/>
</a-form-item>
<a-form-item label="" name="idcard">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.idcard"
/>
</a-form-item>
<a-form-item label="" name="truename">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.truename"
/>
</a-form-item>
<a-form-item label="是否管理员1是2否" name="isAdmin">
<a-input
allow-clear
placeholder="请输入是否管理员1是2否"
v-model:value="form.isAdmin"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { addUser, updateUser } from '@/api/system/user';
import { User } from '@/api/system/user/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: User | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
const form = reactive<User>({
id: undefined,
openId: undefined,
sessionKey: undefined,
username: undefined,
avatarUrl: undefined,
gender: undefined,
country: undefined,
province: undefined,
city: undefined,
phone: undefined,
integral: undefined,
money: undefined,
createTime: undefined,
idcard: undefined,
truename: undefined,
isAdmin: undefined,
tenantId: undefined,
userId: undefined,
userName: '',
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
userName: [
{
required: true,
type: 'string',
message: '请填写名称',
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
};
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateUser : addUser;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,130 @@
<template>
<div class="ele-body">
<a-card title="基本信息" :bordered="false">
<a-form
class="ele-form-detail"
:label-col="
styleResponsive ? { md: 2, sm: 4, xs: 6 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { md: 22, sm: 20, xs: 18 } : { flex: '1' }
"
>
<a-form-item label="账号">
<div class="ele-text-secondary">{{ form.username }}</div>
</a-form-item>
<a-form-item label="昵称">
<div class="ele-text-secondary">{{ form.nickname }}</div>
</a-form-item>
<a-form-item label="性别">
<div class="ele-text-secondary">{{ form.sexName }}</div>
</a-form-item>
<a-form-item label="手机号">
<div class="ele-text-secondary">{{ form.phone }}</div>
</a-form-item>
<a-form-item label="真实姓名">
<div class="ele-text-secondary">{{ form.realName }}</div>
</a-form-item>
<a-form-item label="别名">
<div class="ele-text-secondary">{{ form.alias }}</div>
</a-form-item>
<a-form-item label="角色">
<a-tag v-for="item in form.roles" :key="item.roleId" color="blue">
{{ item.roleName }}
</a-tag>
</a-form-item>
<a-form-item label="创建时间">
<div class="ele-text-secondary">{{ form.createTime }}</div>
</a-form-item>
<a-form-item label="状态">
<a-badge
v-if="typeof form.status === 'number'"
:status="(['processing', 'error'][form.status] as any)"
:text="['正常', '冻结'][form.status]"
/>
</a-form-item>
</a-form>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, unref } from 'vue';
import { useRouter } from 'vue-router';
import { message } from 'ant-design-vue/es';
import { toDateString } from 'ele-admin-pro/es';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import { setPageTabTitle } from '@/utils/page-tab-util';
import { getUser } from '@/api/system/user';
import type { User } from '@/api/system/user/model';
const ROUTE_PATH = '/system/user/details';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const { currentRoute } = useRouter();
// 用户信息
const { form, assignFields } = useFormData<User>({
userId: undefined,
alias: '',
realName: '',
username: '',
nickname: '',
sexName: '',
phone: '',
roles: [],
createTime: undefined,
status: undefined
});
// 请求状态
const loading = ref(true);
/* */
const query = () => {
const { query } = unref(currentRoute);
const id = query.id;
if (!id || form.userId === Number(id)) {
return;
}
loading.value = true;
getUser(Number(id))
.then((data) => {
loading.value = false;
assignFields({
...data,
createTime: toDateString(data.createTime)
});
// 修改页签标题
if (unref(currentRoute).path === ROUTE_PATH) {
setPageTabTitle(data.nickname + '的信息');
}
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
};
watch(
currentRoute,
(route) => {
const { path } = unref(route);
if (path !== ROUTE_PATH) {
return;
}
query();
},
{ immediate: true }
);
</script>
<script lang="ts">
export default {
name: 'SystemUserDetails'
};
</script>

View File

@@ -0,0 +1,681 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<!-- <template #extra>-->
<!-- <Extra/>-->
<!-- </template>-->
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="userId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
:scroll="{ x: 1300 }"
:where="defaultWhere"
:customRow="customRow"
cache-key="proSystemUserTable"
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon>
<plus-outlined/>
</template>
<span>新建</span>
</a-button>
<a-button
type="primary"
class="ele-btn-icon"
@click="openEdit(selection[0])"
:disabled="selection.length === 0"
>
<template #icon>
<EditOutlined/>
</template>
<span>修改</span>
</a-button>
<a-button
type="primary"
danger
@click="resetPsw(selection[0])"
:disabled="selection.length === 0"
>重置密码
</a-button
>
<a-button
danger
type="primary"
class="ele-btn-icon"
@click="removeBatch"
>
<template #icon>
<delete-outlined/>
</template>
<span>批量删除</span>
</a-button>
<template v-if="hasRole('superAdmin')">
<a-button type="dashed" class="ele-btn-icon" @click="openImport">
<template #icon>
<upload-outlined/>
</template>
<span>导入</span>
</a-button>
<a-button type="dashed" @click="handleExport">导出xls</a-button>
</template>
<a-input-search
allow-clear
v-model:value="searchText"
placeholder="请输入关键词"
@search="handleSearch"
@pressEnter="handleSearch"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'avatar'">
<a-avatar
:size="30"
:src="`${record.avatar}`"
style="margin-right: 4px"
>
<template #icon>
<UserOutlined/>
</template>
</a-avatar>
</template>
<template v-if="column.key === 'nickname'">
<span>{{ record.nickname }}</span>
</template>
<template v-if="column.key === 'mobile'">
<span v-if="hasRole('superAdmin')">{{ record.phone }}</span>
<span v-else>{{ record.mobile }}</span>
</template>
<template v-if="column.key === 'roles'">
<a-tag v-for="item in record.roles" :key="item.roleId" color="blue">
{{ item.roleName }}
</a-tag>
</template>
<template v-if="column.key === 'platform'">
<WechatOutlined v-if="record.platform === 'MP-WEIXIN'"/>
<Html5Outlined v-if="record.platform === 'H5'"/>
<ChromeOutlined v-if="record.platform === 'WEB'"/>
</template>
<template v-if="column.key === 'balance'">
<span class="ele-text-success">
{{ formatNumber(record.balance) }}
</span>
</template>
<template v-if="column.key === 'expendMoney'">
<span class="ele-text-warning">
{{ formatNumber(record.expendMoney) }}
</span>
</template>
<template v-if="column.key === 'isAdmin'">
<a-tag v-if="record.isAdmin"></a-tag>
<a-tag v-else></a-tag>
<!-- <a-switch-->
<!-- :checked="record.isAdmin == 1"-->
<!-- @change="updateIsAdmin(record)"-->
<!-- />-->
</template>
<template v-if="column.key === 'action'">
<div>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<!-- <a @click="resetPsw(record)">重置</a>-->
<!-- <a-divider type="vertical" />-->
<a-popconfirm
placement="topRight"
title="确定要删除此用户吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</div>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<user-edit
v-model:visible="showEdit"
:data="current"
:organization-list="data"
@done="reload"
/>
<!-- 导入弹窗 -->
<user-import v-model:visible="showImport" @done="reload"/>
<!-- 用户详情 -->
<user-info v-model:visible="showInfo" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref,computed, reactive} from 'vue';
import {message, Modal} from 'ant-design-vue/es';
import {
PlusOutlined,
DeleteOutlined,
UploadOutlined,
EditOutlined,
UserOutlined,
Html5Outlined,
ChromeOutlined,
WechatOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro/es';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import {messageLoading, formatNumber} from 'ele-admin-pro/es';
import UserEdit from './components/user-edit.vue';
import UserImport from './components/user-import.vue';
import UserInfo from './components/user-info.vue';
import {toDateString} from 'ele-admin-pro';
import {
pageUsers,
removeUser,
removeUsers,
updateUserPassword,
updateUser,
listUsers
} from '@/api/system/user';
import type {User, UserParam} from '@/api/system/user/model';
import {toTreeData, uuid} from 'ele-admin-pro';
import {utils, writeFile} from 'xlsx';
import dayjs from 'dayjs';
import {listRoles} from '@/api/system/role';
import {listOrganizations} from '@/api/system/organization';
import {Organization} from '@/api/system/organization/model';
import {hasRole} from '@/utils/permission';
import {getPageTitle} from "@/utils/common";
import Extra from "./components/Extra.vue";
import {useUserStore} from '@/store/modules/user';
// 加载状态
const loading = ref(true);
// 树形数据
const data = ref<Organization[]>([]);
// 树展开的key
const expandedRowKeys = ref<number[]>([]);
// 树选中的key
const selectedRowKeys = ref<number[]>([]);
// 表格选中数据
const selection = ref<User[]>([]);
// 当前编辑数据
const current = ref<User | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示用户详情
const showInfo = ref(false);
// 是否显示用户导入弹窗
const showImport = ref(false);
const userType = ref<number>();
const searchText = ref('');
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
// 加载角色
const roles = ref<any[]>([]);
const filters = () => {
listRoles().then((result) => {
result.map((d) => {
roles.value.push({
text: d.roleName,
value: d.roleId
});
});
});
};
filters();
// 加载机构
listOrganizations()
.then((list) => {
loading.value = false;
const eks: number[] = [];
list.forEach((d) => {
d.key = d.organizationId;
d.value = d.organizationId;
d.title = d.organizationName;
if (typeof d.key === 'number') {
eks.push(d.key);
}
});
expandedRowKeys.value = eks;
data.value = toTreeData({
data: list,
idField: 'organizationId',
parentIdField: 'parentId'
});
if (list.length) {
if (typeof list[0].key === 'number') {
selectedRowKeys.value = [list[0].key];
}
// current.value = list[0];
} else {
selectedRowKeys.value = [];
// current.value = null;
}
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'userId',
width: 90,
showSorterTooltip: false
},
{
title: '头像',
key: 'avatar',
dataIndex: 'avatar',
align: 'center',
width: 90
},
{
title: '真实姓名',
dataIndex: 'realName',
align: 'center',
showSorterTooltip: false
},
{
title: '角色',
dataIndex: 'roles',
key: 'roles',
align: 'center'
},
{
title: '手机号码',
dataIndex: 'mobile',
align: 'center',
key: 'mobile',
showSorterTooltip: false
},
// {
// title: '昵称',
// key: 'nickname',
// align: 'center',
// dataIndex: 'nickname'
// },
{
title: '性别',
dataIndex: 'sex',
align: 'center',
showSorterTooltip: false,
customRender: ({text}) => ['', '男', '女'][text]
},
{
title: '邮箱',
dataIndex: 'email',
align: 'center',
hideInTable: true,
showSorterTooltip: false
},
// {
// title: '可用余额',
// dataIndex: 'balance',
// align: 'center'
// },
// {
// title: '可用积分',
// dataIndex: 'points',
// align: 'center',
// sorter: true
// },
{
title: '实际消费金额',
dataIndex: 'expendMoney',
key: 'expendMoney',
sorter: true,
align: 'center',
hideInTable: true,
showSorterTooltip: false
},
// {
// title: '注册来源',
// key: 'platform',
// align: 'center',
// dataIndex: 'platform',
// width: 120
// },
{
title: '证件号码',
dataIndex: 'idCard',
align: 'center',
hideInTable: true
},
{
title: '出生日期',
dataIndex: 'birthday',
key: 'birthday',
hideInTable: true
},
{
title: '省份',
dataIndex: 'province',
key: 'province',
hideInTable: true
},
{
title: '城市',
dataIndex: 'city',
key: 'city',
hideInTable: true,
showSorterTooltip: false
},
{
title: '地区',
dataIndex: 'region',
key: 'region',
hideInTable: true,
showSorterTooltip: false
},
{
title: '邮箱认证',
dataIndex: 'emailVerified',
hideInTable: true,
showSorterTooltip: false,
customRender: ({text}) => ['未认证', '已认证'][text]
},
{
title: '实名认证',
dataIndex: 'certification',
sorter: true,
hideInTable: true,
customRender: ({text}) => ['未认证', '已认证'][text]
},
{
title: '管理员',
key: 'isAdmin',
dataIndex: 'isAdmin',
sorter: true,
hideInTable: true,
showSorterTooltip: false,
align: 'center'
},
{
title: '注册时间',
dataIndex: 'createTime',
sorter: true,
showSorterTooltip: false,
ellipsis: true,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right',
align: 'center'
}
]);
// 默认搜索条件
const defaultWhere = reactive({
username: '',
nickname: ''
});
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
where = {};
where.roleId = filters.roles;
where.keywords = searchText.value;
return pageUsers({page, limit, ...where, ...orders});
};
/* 搜索 */
const reload = (where?: UserParam) => {
selection.value = [];
tableRef?.value?.reload({where});
};
/* 打开编辑弹窗 */
const openEdit = (row?: User) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开用户详情弹窗 */
const openInfo = (row?: User) => {
current.value = row ?? null;
showInfo.value = true;
};
/* 打开编辑弹窗 */
const openImport = () => {
showImport.value = true;
};
const handleTabs = (e) => {
userType.value = Number(e.target.value);
reload();
};
/* 删除单个 */
const remove = (row: User) => {
const hide = messageLoading('请求中..', 0);
removeUser(row.userId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的用户吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = messageLoading('请求中..', 0);
removeUsers(selection.value.map((d) => d.userId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 重置用户密码 */
const resetPsw = (row: User) => {
Modal.confirm({
title: '提示',
content: '确定要重置此用户的密码吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
const password = uuid(8);
updateUserPassword(row.userId, password)
.then((msg) => {
hide();
message.success(msg + ',新密码:' + password);
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 修改用户状态 */
const updateIsAdmin = (row: User) => {
row.isAdmin = !row.isAdmin;
updateUser(row)
.then((msg) => {
message.success(msg);
})
.catch((e) => {
message.error(e.message);
});
};
/* 搜索处理 */
const handleSearch = () => {
reload();
};
/* 导出功能 */
const handleExport = async () => {
if (loading.value) {
return;
}
loading.value = true;
message.loading('正在准备导出数据...', 0);
try {
const array: (string | number)[][] = [
[
'用户ID',
'头像',
'手机号码',
'真实姓名',
'昵称',
'性别',
'邮箱',
'可用余额',
'可用积分',
'注册来源',
'注册时间'
]
];
// 构建查询参数
const queryParams = {
keywords: searchText.value
};
// 按搜索结果导出
const list = await listUsers(queryParams);
if (!list || list.length === 0) {
message.warning('没有数据可以导出');
loading.value = false;
return;
}
list.forEach((user: User) => {
array.push([
`${user.userId || ''}`,
`${user.avatar ? '有头像' : '无头像'}`,
`${user.phone || ''}`,
`${user.realName || ''}`,
`${user.nickname || ''}`,
`${user.sex === '1' ? '男' : user.sex === '2' ? '女' : ''}`,
`${user.email || ''}`,
`${user.balance || '0'}`,
`${user.points || '0'}`,
`${user.platform || ''}`,
`${user.createTime || ''}`
]);
});
const sheetName = `导出用户列表${dayjs(new Date()).format('YYYYMMDD')}`;
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽
sheet['!cols'] = [
{wch: 10}, // 用户ID
{wch: 10}, // 头像
{wch: 15}, // 手机号码
{wch: 15}, // 真实姓名
{wch: 15}, // 昵称
{wch: 8}, // 性别
{wch: 20}, // 邮箱
{wch: 12}, // 可用余额
{wch: 12}, // 可用积分
{wch: 12}, // 注册来源
{wch: 20} // 注册时间
];
message.destroy();
message.loading('正在生成Excel文件...', 0);
setTimeout(() => {
writeFile(workbook, `${sheetName}.xlsx`);
loading.value = false;
message.destroy();
message.success(`成功导出 ${list.length} 条记录`);
}, 1000);
} catch (error: any) {
loading.value = false;
message.destroy();
message.error(error.message || '导出失败,请重试');
}
};
/* 自定义行属性 */
const customRow = (record: User) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
</script>
<script lang="ts">
export default {
name: 'SystemUser'
};
</script>
<style lang="less" scoped>
.user-box {
display: flex;
align-items: center;
.user-info {
display: flex;
flex-direction: column;
align-items: start;
}
}
</style>

View File

@@ -392,7 +392,7 @@
dealerId: form.applyId,
userId: form.userId,
status: 1, // 默认设置为已完成
sortNumber: 0
sortNumber: 100
};
// 新增逻辑

View File

@@ -21,8 +21,8 @@
<p class="ele-text-secondary">{{ siteStore.description }}</p>
<a-space>
<a-tag color="green">{{ siteStore.statusText }}</a-tag>
<a-tag color="blue">{{ siteStore.version }}</a-tag>
<a-popover title="小程序码">
<a-tag color="blue" v-if="siteStore.version">{{ siteStore.version }}</a-tag>
<a-popover title="小程序码" v-if="siteStore.mpQrCode">
<template #content>
<p><img :src="siteStore.mpQrCode" alt="小程序码" width="300" height="300"></p>
</template>

View File

@@ -142,14 +142,13 @@ import {toDateString} from 'ele-admin-pro';
import { utils, writeFile } from 'xlsx';
import dayjs from 'dayjs';
import {
pageUsers,
removeUser,
removeUsers,
updateUserPassword,
updateUser,
listUsers
} from '@/api/system/user';
import type {User, UserParam} from '@/api/system/user/model';
pageShopUser,
removeShopUser,
removeBatchShopUser,
updateShopUser,
listShopUser
} from '@/api/shop/shopUser';
import type {ShopUser, ShopUserParam} from '@/api/shop/shopUser/model';
import {toTreeData, uuid} from 'ele-admin-pro';
import {listRoles} from '@/api/system/role';
import {listOrganizations} from '@/api/system/organization';
@@ -167,9 +166,9 @@ const expandedRowKeys = ref<number[]>([]);
// 树选中的key
const selectedRowKeys = ref<number[]>([]);
// 表格选中数据
const selection = ref<User[]>([]);
const selection = ref<ShopUser[]>([]);
// 当前编辑数据
const current = ref<User | null>(null);
const current = ref<ShopUser | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示用户详情
@@ -326,24 +325,23 @@ const datasource: DatasourceFunction = ({
where = {};
where.roleId = filters.roles;
where.keywords = searchText.value;
where.isAdmin = 0;
return pageUsers({page, limit, ...where, ...orders});
return pageShopUser({page, limit, ...where, ...orders});
};
/* 搜索 */
const reload = (where?: UserParam) => {
const reload = (where?: ShopUserParam) => {
selection.value = [];
tableRef?.value?.reload({where});
};
/* 打开编辑弹窗 */
const openEdit = (row?: User) => {
const openEdit = (row?: ShopUser) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开用户详情弹窗 */
const openInfo = (row?: User) => {
const openInfo = (row?: ShopUser) => {
current.value = row ?? null;
showInfo.value = true;
};
@@ -368,8 +366,6 @@ const exportData = async () => {
'手机号',
'邮箱',
'性别',
'所属部门',
'角色',
'状态',
'注册时间'
]
@@ -382,7 +378,7 @@ const exportData = async () => {
};
// 获取用户列表数据
const list = await listUsers(params);
const list = await listShopUser(params);
if (!list || list.length === 0) {
message.warning('没有数据可以导出');
@@ -391,7 +387,7 @@ const exportData = async () => {
}
// 将数据转换为Excel行
list.forEach((user: User) => {
list.forEach((user: ShopUser) => {
array.push([
`${user.userId || ''}`,
`${user.username || ''}`,
@@ -399,9 +395,7 @@ const exportData = async () => {
`${user.realName || ''}`,
`${user.phone || ''}`,
`${user.email || ''}`,
`${user.sexName || ''}`,
`${user.organizationName || ''}`,
`${user.roles?.map(r => r.roleName).join(',') || ''}`,
`${user.sex == 1 ? '男' : ''}`,
`${user.status === 0 ? '正常' : '冻结'}`,
`${user.createTime || ''}`
]);
@@ -452,9 +446,9 @@ const handleTabs = (e) => {
};
/* 删除单个 */
const remove = (row: User) => {
const remove = (row: ShopUser) => {
const hide = messageLoading('请求中..', 0);
removeUser(row.userId)
removeShopUser(row.userId)
.then((msg) => {
hide();
message.success(msg);
@@ -479,7 +473,7 @@ const removeBatch = () => {
maskClosable: true,
onOk: () => {
const hide = messageLoading('请求中..', 0);
removeUsers(selection.value.map((d) => d.userId))
removeShopUser(selection.value.map((d) => d.userId))
.then((msg) => {
hide();
message.success(msg);
@@ -494,7 +488,7 @@ const removeBatch = () => {
};
/* 重置用户密码 */
const resetPsw = (row: User) => {
const resetPsw = (row: ShopUser) => {
Modal.confirm({
title: '提示',
content: '确定要重置此用户的密码吗?',
@@ -503,7 +497,10 @@ const resetPsw = (row: User) => {
onOk: () => {
const hide = message.loading('请求中..', 0);
const password = uuid(8);
updateUserPassword(row.userId, password)
updateShopUser({
...row,
password: password
})
.then((msg) => {
hide();
message.success(msg + ',新密码:' + password);
@@ -517,9 +514,8 @@ const resetPsw = (row: User) => {
};
/* 修改用户状态 */
const updateIsAdmin = (row: User) => {
row.isAdmin = !row.isAdmin;
updateUser(row)
const updateIsAdmin = (row: ShopUser) => {
updateShopUser(row)
.then((msg) => {
message.success(msg);
})
@@ -529,7 +525,7 @@ const updateIsAdmin = (row: User) => {
};
/* 自定义行属性 */
const customRow = (record: User) => {
const customRow = (record: ShopUser) => {
return {
// 行点击事件
onClick: () => {
@@ -560,7 +556,7 @@ watch(
<script lang="ts">
export default {
name: 'SystemAdmin'
name: 'ShopUser'
};
</script>

View File

@@ -31,6 +31,9 @@
<a-button type="dashed" class="ele-btn-icon" @click="foldAll">
折叠全部
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="removeBatch">
批量删除
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="cloneMenu()">
一键克隆
</a-button>
@@ -198,10 +201,10 @@
showSorterTooltip: false,
ellipsis: true
},
{
title: 'API节点',
dataIndex: 'modules'
},
// {
// title: 'API节点',
// dataIndex: 'modules'
// },
{
title: '菜单编号',
dataIndex: 'menuId',