feat(shop): 添加小区、门店、配送员、店员、仓库管理功能及相关API

- 新增小区管理模块,包括查询、添加、修改、删除等功能
- 新增门店管理模块,包含门店基础信息管理功能
- 新增配送员管理模块,支持骑手信息配置及工作状态管理
- 新增店员管理模块,用于门店人员信息维护
- 新增仓库管理模块,支持不同类型的仓库信息管理
- 添加电子围栏功能,支持圆形和方形围栏设置
- 更新经销商用户模型,增加类型、头像、店铺名称等字段
- 更新订单模型,添加物流单号、昵称等字段
- 更新商品模型,调整分销佣金相关字段设计
- 修复信用模块中部分字段映射错误问题
- 优化邀请链接生成逻辑,增加租户ID参数传递
- 移除部分不必要部门字段显示,简化管理员界面
This commit is contained in:
2026-03-04 14:04:57 +08:00
parent fe20f0f0b3
commit a9d513fca4
77 changed files with 9319 additions and 2023 deletions

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopCommunity, ShopCommunityParam } from './model';
/**
* 分页查询小区
*/
export async function pageShopCommunity(params: ShopCommunityParam) {
const res = await request.get<ApiResult<PageResult<ShopCommunity>>>(
'/shop/shop-community/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询小区列表
*/
export async function listShopCommunity(params?: ShopCommunityParam) {
const res = await request.get<ApiResult<ShopCommunity[]>>(
'/shop/shop-community',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加小区
*/
export async function addShopCommunity(data: ShopCommunity) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-community',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改小区
*/
export async function updateShopCommunity(data: ShopCommunity) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-community',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除小区
*/
export async function removeShopCommunity(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-community/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除小区
*/
export async function removeBatchShopCommunity(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-community/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询小区
*/
export async function getShopCommunity(id: number) {
const res = await request.get<ApiResult<ShopCommunity>>(
'/shop/shop-community/' + 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 ShopCommunity {
// ID
id?: number;
// 小区名称
name?: string;
// 小区编号
code?: string;
// 详细地址
address?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
}
/**
* 小区搜索条件
*/
export interface ShopCommunityParam extends PageParam {
id?: number;
name?: string;
code?: string;
keywords?: string;
}

View File

@@ -8,6 +8,8 @@ export interface ShopDealerCapital {
id?: number; id?: number;
// 分销商用户ID // 分销商用户ID
userId?: number; userId?: number;
// 分销商昵称
nickName?: string;
// 订单ID // 订单ID
orderId?: number; orderId?: number;
// 订单编号 // 订单编号

View File

@@ -12,6 +12,8 @@ export interface ShopDealerOrder {
title?: string; title?: string;
// 买家用户昵称 // 买家用户昵称
nickname?: string; nickname?: string;
// 真实姓名
realName?: string;
// 订单编号 // 订单编号
orderNo?: string; orderNo?: string;
// 订单总金额(不含运费) // 订单总金额(不含运费)
@@ -52,6 +54,10 @@ export interface ShopDealerOrder {
isSettled?: number; isSettled?: number;
// 结算时间 // 结算时间
settleTime?: number; settleTime?: number;
// 是否解冻(0否 1是)
isUnfreeze?: number;
// 解冻时间
unfreezeTime?: number;
// 订单备注 // 订单备注
comments?: string; comments?: string;
// 商城ID // 商城ID

View File

@@ -6,8 +6,16 @@ import type { PageParam } from '@/api';
export interface ShopDealerUser { export interface ShopDealerUser {
// 主键ID // 主键ID
id?: number; id?: number;
// 类型 0经销商 1企业 2集团
type?: number;
// 自增ID // 自增ID
userId?: number; userId?: number;
// 头像
avatar?: string;
// 店铺名称
dealerName?: string;
// 小区名称
community?: string;
// 姓名 // 姓名
realName?: string; realName?: string;
// 手机号 // 手机号
@@ -34,6 +42,8 @@ export interface ShopDealerUser {
thirdNum?: number; thirdNum?: number;
// 专属二维码 // 专属二维码
qrcode?: string; qrcode?: string;
// 配送员所属门店
shopName?: string;
// 是否删除 // 是否删除
isDelete?: number; isDelete?: number;
// 租户id // 租户id
@@ -56,5 +66,7 @@ export interface ShopDealerUser {
*/ */
export interface ShopDealerUserParam extends PageParam { export interface ShopDealerUserParam extends PageParam {
id?: number; id?: number;
realName?: string;
mobile?: string;
keywords?: string; keywords?: string;
} }

View File

@@ -94,7 +94,7 @@ export interface ShopGoods {
supplierMerchantId?: number; supplierMerchantId?: number;
supplierName?: string; supplierName?: string;
// 状态0未上架1上架 // 状态0未上架1上架
isShow?: number; isShow?: boolean;
// 状态, 0上架 1待上架 2待审核 3审核不通过 // 状态, 0上架 1待上架 2待审核 3审核不通过
status?: number; status?: number;
// 备注 // 备注
@@ -124,6 +124,21 @@ export interface ShopGoods {
canUseDate?: string; canUseDate?: string;
ensureTag?: string; ensureTag?: string;
expiredDay?: number; expiredDay?: number;
// --- 分销/佣金(新字段,后端保持 snake_case---
// 是否开启分销佣金0关闭 1开启
isOpenCommission?: number;
// 分佣类型10固定金额 20百分比
commissionType?: number;
// 一级/二级/三级分销佣金(单位以服务端为准)
firstMoney?: number;
secondMoney?: number;
thirdMoney?: number;
// 一级/二级分红(单位以服务端为准)
firstDividend?: number;
secondDividend?: number;
// 配送奖金
deliveryMoney?: number;
} }
export interface BathSet { export interface BathSet {

View File

@@ -135,3 +135,18 @@ export async function shopOrderTotal(params?: ShopOrderParam) {
} }
return Promise.reject(new Error(res.data.message)); return Promise.reject(new Error(res.data.message));
} }
/**
* 申请|同意退款
*/
export async function refundShopOrder(data: ShopOrder) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-order/refund',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -37,6 +37,10 @@ export interface ShopOrder {
icCard?: string; icCard?: string;
// 头像 // 头像
avatar?: string; avatar?: string;
// 昵称(部分接口会返回)
nickname?: string;
// 兼容字段:部分接口可能返回 name
name?: string;
// 真实姓名 // 真实姓名
realName?: string; realName?: string;
// 手机号码 // 手机号码
@@ -99,6 +103,8 @@ export interface ShopOrder {
expressId?: number; expressId?: number;
// 快递公司名称 // 快递公司名称
expressName?: string; expressName?: string;
// 物流单号
expressNo?: string;
// 发货人 // 发货人
sendName?: string; sendName?: string;
// 发货人联系方式 // 发货人联系方式

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopStore, ShopStoreParam } from './model';
/**
* 分页查询门店
*/
export async function pageShopStore(params: ShopStoreParam) {
const res = await request.get<ApiResult<PageResult<ShopStore>>>(
'/shop/shop-store/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询门店列表
*/
export async function listShopStore(params?: ShopStoreParam) {
const res = await request.get<ApiResult<ShopStore[]>>(
'/shop/shop-store',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加门店
*/
export async function addShopStore(data: ShopStore) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-store',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改门店
*/
export async function updateShopStore(data: ShopStore) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-store',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除门店
*/
export async function removeShopStore(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除门店
*/
export async function removeBatchShopStore(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询门店
*/
export async function getShopStore(id: number) {
const res = await request.get<ApiResult<ShopStore>>(
'/shop/shop-store/' + 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,59 @@
import type { PageParam } from '@/api';
/**
* 门店
*/
export interface ShopStore {
// 自增ID
id?: number;
// 店铺名称
name?: string;
// 门店地址
address?: string;
// 手机号码
phone?: string;
// 邮箱
email?: string;
// 门店经理
managerName?: string;
// 门店banner
shopBanner?: string;
// 所在省份
province?: string;
// 所在城市
city?: string;
// 所在辖区
region?: string;
// 经度和纬度
lngAndLat?: string;
// 位置
location?:string;
// 区域
district?: string;
// 轮廓
points?: string;
// 用户ID
userId?: number;
// 状态
status?: number;
// 备注
comments?: string;
// 排序号
sortNumber?: number;
// 是否删除
isDelete?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 门店搜索条件
*/
export interface ShopStoreParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopStoreFence, ShopStoreFenceParam } from './model';
/**
* 分页查询黄家明_电子围栏
*/
export async function pageShopStoreFence(params: ShopStoreFenceParam) {
const res = await request.get<ApiResult<PageResult<ShopStoreFence>>>(
'/shop/shop-store-fence/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询黄家明_电子围栏列表
*/
export async function listShopStoreFence(params?: ShopStoreFenceParam) {
const res = await request.get<ApiResult<ShopStoreFence[]>>(
'/shop/shop-store-fence',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加黄家明_电子围栏
*/
export async function addShopStoreFence(data: ShopStoreFence) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-store-fence',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改黄家明_电子围栏
*/
export async function updateShopStoreFence(data: ShopStoreFence) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-store-fence',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除黄家明_电子围栏
*/
export async function removeShopStoreFence(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-fence/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除黄家明_电子围栏
*/
export async function removeBatchShopStoreFence(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-fence/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询黄家明_电子围栏
*/
export async function getShopStoreFence(id: number) {
const res = await request.get<ApiResult<ShopStoreFence>>(
'/shop/shop-store-fence/' + 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 ShopStoreFence {
// 自增ID
id?: number;
// 围栏名称
name?: string;
// 类型 0圆形 1方形
type?: number;
// 定位
location?: string;
// 经度
longitude?: string;
// 纬度
latitude?: string;
// 区域
district?: string;
// 电子围栏轮廓
points?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 黄家明_电子围栏搜索条件
*/
export interface ShopStoreFenceParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopStoreRider, ShopStoreRiderParam } from './model';
/**
* 分页查询配送员
*/
export async function pageShopStoreRider(params: ShopStoreRiderParam) {
const res = await request.get<ApiResult<PageResult<ShopStoreRider>>>(
'/shop/shop-store-rider/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询配送员列表
*/
export async function listShopStoreRider(params?: ShopStoreRiderParam) {
const res = await request.get<ApiResult<ShopStoreRider[]>>(
'/shop/shop-store-rider',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加配送员
*/
export async function addShopStoreRider(data: ShopStoreRider) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-store-rider',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改配送员
*/
export async function updateShopStoreRider(data: ShopStoreRider) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-store-rider',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除配送员
*/
export async function removeShopStoreRider(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-rider/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除配送员
*/
export async function removeBatchShopStoreRider(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-rider/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询配送员
*/
export async function getShopStoreRider(id: number) {
const res = await request.get<ApiResult<ShopStoreRider>>(
'/shop/shop-store-rider/' + 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,65 @@
import type { PageParam } from '@/api';
/**
* 配送员
*/
export interface ShopStoreRider {
// 主键ID
id?: string;
// 门店IDshop_store.id
storeId?: number;
// 门店名称(后端联表返回,提交时可不传)
storeName?: string;
// 配送点IDshop_dealer.id
dealerId?: number;
// 骑手编号(可选)
riderNo?: string;
// 姓名
realName?: string;
// 手机号
mobile?: string;
// 头像
avatar?: string;
// 身份证号(可选)
idCardNo?: string;
// 状态1启用0禁用
status?: number;
// 接单状态0休息/下线1在线2忙碌
workStatus?: number;
// 是否开启自动派单1是0否
autoDispatchEnabled?: number;
// 派单优先级(同小区多骑手时可用,值越大越优先)
dispatchPriority?: number;
// 最大同时配送单数0表示不限制
maxOnhandOrders?: number;
// 是否计算工资(提成)1计算0不计算如三方配送点可设0
commissionCalcEnabled?: number;
// 水每桶提成金额(元/桶)
waterBucketUnitFee?: string;
// 其他商品提成方式1按订单固定金额2按订单金额比例3按商品规则(另表)
otherGoodsCommissionType?: number;
// 其他商品提成值:固定金额(元)或比例(%)
otherGoodsCommissionValue?: string;
// 用户ID
userId?: number;
// 备注
comments?: string;
// 排序号
sortNumber?: number;
// 是否删除
isDelete?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 配送员搜索条件
*/
export interface ShopStoreRiderParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopStoreUser, ShopStoreUserParam } from './model';
/**
* 分页查询店员
*/
export async function pageShopStoreUser(params: ShopStoreUserParam) {
const res = await request.get<ApiResult<PageResult<ShopStoreUser>>>(
'/shop/shop-store-user/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询店员列表
*/
export async function listShopStoreUser(params?: ShopStoreUserParam) {
const res = await request.get<ApiResult<ShopStoreUser[]>>(
'/shop/shop-store-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 addShopStoreUser(data: ShopStoreUser) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-store-user',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改店员
*/
export async function updateShopStoreUser(data: ShopStoreUser) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-store-user',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除店员
*/
export async function removeShopStoreUser(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-user/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除店员
*/
export async function removeBatchShopStoreUser(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-user/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询店员
*/
export async function getShopStoreUser(id: number) {
const res = await request.get<ApiResult<ShopStoreUser>>(
'/shop/shop-store-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,33 @@
import type { PageParam } from '@/api';
/**
* 店员
*/
export interface ShopStoreUser {
// 主键ID
id?: number;
// 配送点IDshop_dealer.id
storeId?: number;
// 用户ID
userId?: number;
// 备注
comments?: string;
// 排序号
sortNumber?: number;
// 是否删除
isDelete?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 店员搜索条件
*/
export interface ShopStoreUserParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopStoreWarehouse, ShopStoreWarehouseParam } from './model';
/**
* 分页查询仓库
*/
export async function pageShopStoreWarehouse(params: ShopStoreWarehouseParam) {
const res = await request.get<ApiResult<PageResult<ShopStoreWarehouse>>>(
'/shop/shop-store-warehouse/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询仓库列表
*/
export async function listShopStoreWarehouse(params?: ShopStoreWarehouseParam) {
const res = await request.get<ApiResult<ShopStoreWarehouse[]>>(
'/shop/shop-store-warehouse',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加仓库
*/
export async function addShopStoreWarehouse(data: ShopStoreWarehouse) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-store-warehouse',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改仓库
*/
export async function updateShopStoreWarehouse(data: ShopStoreWarehouse) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-store-warehouse',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除仓库
*/
export async function removeShopStoreWarehouse(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-warehouse/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除仓库
*/
export async function removeBatchShopStoreWarehouse(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-warehouse/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询仓库
*/
export async function getShopStoreWarehouse(id: number) {
const res = await request.get<ApiResult<ShopStoreWarehouse>>(
'/shop/shop-store-warehouse/' + 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,51 @@
import type { PageParam } from '@/api';
/**
* 仓库
*/
export interface ShopStoreWarehouse {
// 自增ID
id?: number;
// 仓库名称
name?: string;
// 唯一标识
code?: string;
// 类型 中心仓,区域仓,门店仓
type?: string;
// 仓库地址
address?: string;
// 真实姓名
realName?: string;
// 联系电话
phone?: string;
// 所在省份
province?: string;
// 所在城市
city?: string;
// 所在辖区
region?: string;
// 经纬度
lngAndLat?: string;
// 用户ID
userId?: number;
// 备注
comments?: string;
// 排序号
sortNumber?: number;
// 是否删除
isDelete?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 仓库搜索条件
*/
export interface ShopStoreWarehouseParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -168,8 +168,8 @@
}, },
{ {
title: '发生时间', title: '发生时间',
dataIndex: 'occurrenceTime', dataIndex: 'releaseDate',
key: 'occurrenceTime', key: 'releaseDate',
width: 120 width: 120
}, },
{ {

View File

@@ -989,7 +989,7 @@
title: '其他当事人/第三人', title: '其他当事人/第三人',
key: 'otherPartiesThirdParty' key: 'otherPartiesThirdParty'
}, },
{ dataIndex: 'occurrenceTime', title: '发生时间' }, { dataIndex: 'finalDate', title: '发生时间' },
{ dataIndex: 'caseNumber', title: '案号' }, { dataIndex: 'caseNumber', title: '案号' },
{ dataIndex: 'involvedAmount', title: '涉案金额' }, { dataIndex: 'involvedAmount', title: '涉案金额' },
{ dataIndex: 'courtName', title: '法院' }, { dataIndex: 'courtName', title: '法院' },
@@ -1051,7 +1051,7 @@
}, },
{ dataIndex: 'plaintiffAppellant', title: '被告/被上诉人', key: 'plaintiffAppellant' }, { dataIndex: 'plaintiffAppellant', title: '被告/被上诉人', key: 'plaintiffAppellant' },
{ dataIndex: 'otherPartiesThirdParty', title: '其他当事人/第三人' }, { dataIndex: 'otherPartiesThirdParty', title: '其他当事人/第三人' },
{ dataIndex: 'occurrenceTime', title: '发生时间' }, { dataIndex: 'releaseDate', title: '发生时间' },
{ dataIndex: 'caseNumber', title: '案号' }, { dataIndex: 'caseNumber', title: '案号' },
{ dataIndex: 'involvedAmount', title: '涉案金额' }, { dataIndex: 'involvedAmount', title: '涉案金额' },
{ dataIndex: 'courtName', title: '法院' }, { dataIndex: 'courtName', title: '法院' },

View File

@@ -160,8 +160,8 @@
}, },
{ {
title: '发生时间', title: '发生时间',
dataIndex: 'occurrenceTime', dataIndex: 'finalDate',
key: 'occurrenceTime', key: 'finalDate',
width: 120 width: 120
}, },
{ {

View File

@@ -183,8 +183,8 @@
}, },
{ {
title: '冻结结束日期', title: '冻结结束日期',
dataIndex: 'freezeDateStart', dataIndex: 'freezeDateEnd',
key: 'freezeDateStart' key: 'freezeDateEnd'
}, },
{ {
title: '状态', title: '状态',

View File

@@ -143,8 +143,7 @@
<a-descriptions-item label="技术支持"> <a-descriptions-item label="技术支持">
<span <span
class="cursor-pointer" class="cursor-pointer"
@click="openNew(`https://websoft.top`)" >麦芽知电子商务</span
>网宿软件</span
> >
</a-descriptions-item> </a-descriptions-item>
</a-descriptions> </a-descriptions>
@@ -214,7 +213,6 @@
MoneyCollectOutlined MoneyCollectOutlined
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue/es'; import { message } from 'ant-design-vue/es';
import { openNew } from '@/utils/common';
import { useSiteStore } from '@/store/modules/site'; import { useSiteStore } from '@/store/modules/site';
import { useStatisticsStore } from '@/store/modules/statistics'; import { useStatisticsStore } from '@/store/modules/statistics';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';

View File

@@ -169,10 +169,21 @@
return props.inviterId || Number(localStorage.getItem('UserId')); return props.inviterId || Number(localStorage.getItem('UserId'));
}); });
// 邀请链接需要带上 tenantId避免未登录用户打开链接时后端无法识别租户导致角色/权限初始化失败
const tenantId = computed(() => {
const tid = localStorage.getItem('TenantId');
return tid ? Number(tid) : undefined;
});
// 生成邀请链接 // 生成邀请链接
const invitationLink = computed(() => { const invitationLink = computed(() => {
const baseUrl = window.location.origin; const baseUrl = window.location.origin;
return `${baseUrl}/dealer/register?inviter=${inviterId.value}`; const params = new URLSearchParams();
params.set('inviter', String(inviterId.value));
if (tenantId.value) {
params.set('tenantId', String(tenantId.value));
}
return `${baseUrl}/dealer/register?${params.toString()}`;
}); });
// 复制链接 // 复制链接

View File

@@ -232,12 +232,12 @@
align: 'center', align: 'center',
showSorterTooltip: false showSorterTooltip: false
}, },
{ // {
title: '所属部门', // title: '所属部门',
dataIndex: 'organizationName', // dataIndex: 'organizationName',
key: 'organizationName', // key: 'organizationName',
align: 'center' // align: 'center'
}, // },
{ {
title: '角色', title: '角色',
dataIndex: 'roles', dataIndex: 'roles',

View File

@@ -0,0 +1,62 @@
<!-- 搜索表单 -->
<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-input-search
allow-clear
placeholder="小区名称"
style="width: 240px"
v-model:value="where.keywords"
@search="reload"
/>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import { watch } from 'vue';
import useSearch from "@/utils/use-search";
import {ShopCommunityParam} from "@/api/shop/shopCommunity/model";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: ShopCommunityParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
const reload = () => {
emit('search', where);
};
// 表单数据
const { where } = useSearch<ShopCommunityParam>({
keywords: '',
name: undefined,
code: undefined
});
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,209 @@
<!-- 编辑弹窗 -->
<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="address">
<a-input
allow-clear
placeholder="请输入详细地址"
v-model:value="form.address"
/>
</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 { addShopCommunity, updateShopCommunity } from '@/api/shop/shopCommunity';
import { ShopCommunity } from '@/api/shop/shopCommunity/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?: ShopCommunity | 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<ShopCommunity>({
id: undefined,
name: undefined,
code: undefined,
address: undefined,
tenantId: undefined,
createTime: undefined,
shopCommunityId: undefined,
shopCommunityName: '',
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopCommunityName: [
{
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 ? updateShopCommunity : addShopCommunity;
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,252 @@
<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>
<!-- 编辑弹窗 -->
<ShopCommunityEdit v-model:visible="showEdit" :data="current" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed } 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 {getPageTitle} from '@/utils/common';
import ShopCommunityEdit from './components/shopCommunityEdit.vue';
import { pageShopCommunity, removeShopCommunity, removeBatchShopCommunity } from '@/api/shop/shopCommunity';
import type { ShopCommunity, ShopCommunityParam } from '@/api/shop/shopCommunity/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopCommunity[]>([]);
// 当前编辑数据
const current = ref<ShopCommunity | 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 pageShopCommunity({
...where,
...orders,
page,
limit
});
};
// 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 90,
},
{
title: '小区名称',
dataIndex: 'name',
key: 'name',
ellipsis: true
},
{
title: '小区编号',
dataIndex: 'code',
key: 'code',
ellipsis: true
},
{
title: '详细地址',
dataIndex: 'address',
key: 'address',
ellipsis: true
},
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
width: 120
},
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120,
align: 'center'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
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?: ShopCommunityParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopCommunity) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopCommunity) => {
const hide = message.loading('请求中..', 0);
removeShopCommunity(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);
removeBatchShopCommunity(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: ShopCommunity) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopCommunity'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,221 @@
<!-- 搜索表单 -->
<template>
<div class="search-container">
<!-- 搜索表单 -->
<a-form
:model="searchForm"
layout="inline"
class="search-form"
@finish="handleSearch"
>
<a-form-item label="申请人姓名">
<a-input
v-model:value="searchForm.realName"
placeholder="请输入申请人姓名"
allow-clear
style="width: 160px"
/>
</a-form-item>
<a-form-item label="手机号码">
<a-input
v-model:value="searchForm.mobile"
placeholder="请输入手机号码"
allow-clear
style="width: 160px"
/>
</a-form-item>
<a-form-item label="申请方式">
<a-select
v-model:value="searchForm.applyType"
placeholder="全部方式"
allow-clear
style="width: 120px"
>
<a-select-option :value="10">需要审核</a-select-option>
<a-select-option :value="20">免审核</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="审核状态">
<a-select
v-model:value="searchForm.applyStatus"
placeholder="全部状态"
allow-clear
style="width: 120px"
>
<a-select-option :value="10">待审核</a-select-option>
<a-select-option :value="20">审核通过</a-select-option>
<a-select-option :value="30">审核驳回</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="申请时间">
<a-range-picker
v-model:value="searchForm.dateRange"
style="width: 240px"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" html-type="submit" class="ele-btn-icon">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button @click="resetSearch"> 重置 </a-button>
</a-space>
</a-form-item>
</a-form>
<!-- 操作按钮 -->
<div class="action-buttons">
<a-space>
<!-- <a-button type="primary" @click="add" class="ele-btn-icon">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- 新增申请-->
<!-- </a-button>-->
<a-button
type="primary"
ghost
:disabled="!selection?.length"
@click="batchApprove"
class="ele-btn-icon"
>
<template #icon>
<CheckOutlined />
</template>
批量通过
</a-button>
<a-button
:disabled="!selection?.length"
@click="exportData"
class="ele-btn-icon"
>
<template #icon>
<ExportOutlined />
</template>
导出数据
</a-button>
</a-space>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import {
PlusOutlined,
SearchOutlined,
CheckOutlined,
ExportOutlined
} from '@ant-design/icons-vue';
import type { ShopDealerApplyParam } from '@/api/shop/shopDealerApply/model';
import dayjs from 'dayjs';
const props = withDefaults(
defineProps<{
// 选中的数据
selection?: any[];
}>(),
{
selection: () => []
}
);
const emit = defineEmits<{
(e: 'search', where?: ShopDealerApplyParam): void;
(e: 'add'): void;
(e: 'batchApprove'): void;
(e: 'export'): void;
}>();
// 搜索表单
const searchForm = reactive<any>({
realName: '',
mobile: '',
applyType: undefined,
applyStatus: undefined,
dateRange: undefined
});
// 搜索
const handleSearch = () => {
const searchParams: ShopDealerApplyParam = {};
if (searchForm.realName) {
searchParams.realName = searchForm.realName;
}
if (searchForm.mobile) {
searchParams.mobile = searchForm.mobile;
}
if (searchForm.applyType) {
searchParams.applyType = searchForm.applyType;
}
if (searchForm.applyStatus) {
searchParams.applyStatus = searchForm.applyStatus;
}
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
searchParams.startTime = dayjs(searchForm.dateRange[0]).format(
'YYYY-MM-DD'
);
searchParams.endTime = dayjs(searchForm.dateRange[1]).format(
'YYYY-MM-DD'
);
}
emit('search', searchParams);
};
// 重置搜索
const resetSearch = () => {
searchForm.realName = '';
searchForm.mobile = '';
searchForm.applyType = undefined;
searchForm.applyStatus = undefined;
searchForm.dateRange = undefined;
emit('search', {});
};
// 新增
const add = () => {
emit('add');
};
// 批量通过
const batchApprove = () => {
emit('batchApprove');
};
// 导出数据
const exportData = () => {
emit('export');
};
</script>
<style lang="less" scoped>
.search-container {
background: #fff;
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
.search-form {
margin-bottom: 16px;
:deep(.ant-form-item) {
margin-bottom: 8px;
}
}
.action-buttons {
border-top: 1px solid #f0f0f0;
padding-top: 16px;
}
}
</style>

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://cms-api.websoft.top/api/shop/shop-dealer-apply/import/template"
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 { importShopDealerApplies } from '@/api/shop/shopDealerApply';
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;
importShopDealerApplies(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,298 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="600"
: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="{ span: 4 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="企业名称" name="dealerName">
<a-input placeholder="请输入企业名称" v-model:value="form.dealerName" />
</a-form-item>
<a-form-item label="入市状态" name="applyStatus">
<a-select
v-model:value="form.applyStatus"
placeholder="请选择入市状态"
@change="handleStatusChange"
>
<a-select-option :value="10">
<a-tag>未入市</a-tag>
<span style="margin-left: 8px">未入市</span>
</a-select-option>
<a-select-option :value="20">
<a-tag color="success">已入市</a-tag>
<span style="margin-left: 8px">已入市</span>
</a-select-option>
</a-select>
</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 dayjs from 'dayjs';
import { assignObject } from 'ele-admin-pro';
import {
addShopDealerApply,
updateShopDealerApply
} from '@/api/shop/shopDealerApply';
import { ShopDealerApply } from '@/api/shop/shopDealerApply/model';
import { FormInstance } from 'ant-design-vue/es/form';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerApply | 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 form = reactive<ShopDealerApply>({
applyId: undefined,
type: 3,
userId: undefined,
dealerName: '',
realName: '',
mobile: '',
refereeId: undefined,
applyType: 10,
applyTime: undefined,
applyStatus: 10,
auditTime: undefined,
rejectReason: '',
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
dealerName: [
{
required: true,
message: '请输入经销商名称',
trigger: 'blur'
}
],
realName: [
{
required: true,
message: '请输入企业名称',
trigger: 'blur'
}
],
applyStatus: [
{
required: true,
message: '请选择审核状态',
trigger: 'change'
}
]
});
const { resetFields } = useForm(form, rules);
/* 处理审核状态变化 */
const handleStatusChange = (value: number) => {
// 当状态改为审核通过或驳回时,自动设置审核时间为当前时间
if ((value === 20 || value === 30) && !form.auditTime) {
form.auditTime = dayjs();
}
// 当状态改为待审核时,清空审核时间和驳回原因
if (value === 10) {
form.auditTime = undefined;
form.rejectReason = '';
}
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
// 动态验证规则
const validateFields: string[] = [
'userId',
'realName',
'mobile',
'applyStatus'
];
// 如果是驳回状态,需要验证驳回原因
if (form.applyStatus === 30) {
validateFields.push('rejectReason');
}
// 如果是审核通过或驳回状态,需要验证审核时间
if (form.applyStatus === 20 || form.applyStatus === 30) {
validateFields.push('auditTime');
}
formRef.value
.validate(validateFields)
.then(() => {
loading.value = true;
const formData = {
...form
};
// 处理时间字段转换 - 转换为ISO字符串格式
if (formData.applyTime) {
if (dayjs.isDayjs(formData.applyTime)) {
formData.applyTime = formData.applyTime.format(
'YYYY-MM-DD HH:mm:ss'
);
} else if (typeof formData.applyTime === 'number') {
formData.applyTime = dayjs(formData.applyTime).format(
'YYYY-MM-DD HH:mm:ss'
);
}
}
if (formData.auditTime) {
if (dayjs.isDayjs(formData.auditTime)) {
formData.auditTime = formData.auditTime.format(
'YYYY-MM-DD HH:mm:ss'
);
} else if (typeof formData.auditTime === 'number') {
formData.auditTime = dayjs(formData.auditTime).format(
'YYYY-MM-DD HH:mm:ss'
);
}
}
// 当审核状态为通过或驳回时,确保有审核时间
if (
(formData.applyStatus === 20 || formData.applyStatus === 30) &&
!formData.auditTime
) {
formData.auditTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
}
// 当状态为待审核时,清空审核时间
if (formData.applyStatus === 10) {
formData.auditTime = undefined;
}
const saveOrUpdate = isUpdate.value
? updateShopDealerApply
: addShopDealerApply;
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) {
if (props.data) {
assignObject(form, props.data);
// 处理时间字段 - 确保转换为dayjs对象
if (props.data.applyTime) {
form.applyTime = dayjs(props.data.applyTime);
}
if (props.data.auditTime) {
form.auditTime = dayjs(props.data.auditTime);
}
isUpdate.value = true;
} else {
// 重置为默认值
Object.assign(form, {
applyId: undefined,
userId: undefined,
realName: '',
mobile: '',
refereeId: undefined,
applyType: 10,
applyTime: dayjs(),
applyStatus: 10,
auditTime: undefined,
rejectReason: '',
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
<style lang="less" scoped>
:deep(.ant-divider-horizontal.ant-divider-with-text-left) {
margin: 24px 0 16px 0;
.ant-divider-inner-text {
padding: 0 16px 0 0;
}
}
:deep(.ant-form-item) {
margin-bottom: 16px;
}
:deep(.ant-radio) {
display: flex;
align-items: center;
margin-bottom: 8px;
.ant-radio-inner {
margin-right: 8px;
}
}
:deep(.ant-select-selection-item) {
display: flex;
align-items: center;
}
</style>

View File

@@ -0,0 +1,301 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="applyId"
:columns="columns"
:datasource="datasource"
class="sys-org-table"
:scroll="{ x: 1300 }"
:where="defaultWhere"
:customRow="customRow"
cache-key="proSystemShopDealerApplyTable"
>
<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 class="ele-btn-icon" @click="openImport()">
<template #icon>
<cloud-upload-outlined />
</template>
<span>导入</span>
</a-button>
<!-- <a-button class="ele-btn-icon" @click="exportData()" :loading="exportLoading">-->
<!-- <template #icon>-->
<!-- <download-outlined/>-->
<!-- </template>-->
<!-- <span>导出</span>-->
<!-- </a-button>-->
<a-input-search
allow-clear
v-model:value="searchText"
placeholder="请输入关键词"
@search="reload"
@pressEnter="reload"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'applyStatus'">
<span class="text-green-500" v-if="record.applyStatus == 20"
>已入市</span
>
<span class="text-gray-300" v-else>未入市</span>
</template>
<template v-if="column.key === 'action'">
<div>
<a @click="openEdit(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>
<!-- 编辑弹窗 -->
<ShopDealerApplyEdit
v-model:visible="showEdit"
:data="current"
:organization-list="data"
@done="reload"
/>
<!-- 导入弹窗 -->
<ShopDealerApplyImport v-model:visible="showImport" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import { PlusOutlined, CloudUploadOutlined } 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 } from 'ele-admin-pro/es';
import ShopDealerApplyEdit from './components/shopDealerApplyEdit.vue';
import ShopDealerApplyImport from './components/shop-dealer-apply-import.vue';
import { toDateString } from 'ele-admin-pro';
import { utils, writeFile } from 'xlsx';
import dayjs from 'dayjs';
import { Organization } from '@/api/system/organization/model';
import { getPageTitle } from '@/utils/common';
import router from '@/router';
import {
listShopDealerApply,
pageShopDealerApply,
removeShopDealerApply
} from '@/api/shop/shopDealerApply';
import {
ShopDealerApply,
ShopDealerApplyParam
} from '@/api/shop/shopDealerApply/model';
// 加载状态
const loading = ref(true);
// 树形数据
const data = ref<Organization[]>([]);
// 树展开的key
const expandedRowKeys = ref<number[]>([]);
// 树选中的key
const selectedRowKeys = ref<number[]>([]);
// 表格选中数据
const selection = ref<ShopDealerApply[]>([]);
// 当前编辑数据
const current = ref<ShopDealerApply | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示用户导入弹窗
const showImport = ref(false);
// 导出加载状态
const exportLoading = ref(false);
const searchText = ref('');
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
// {
// title: 'ID',
// dataIndex: 'userId',
// width: 90,
// showSorterTooltip: false
// },
{
title: '企业名称',
dataIndex: 'dealerName',
align: 'dealerName',
showSorterTooltip: false
},
{
title: '入市情况',
dataIndex: 'applyStatus',
key: 'applyStatus',
align: 'center',
sorter: true
},
{
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
align: 'center',
showSorterTooltip: false,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center'
}
]);
// 默认搜索条件
const defaultWhere = reactive({
username: '',
nickname: ''
});
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
where = {};
where.keywords = searchText.value;
where.type = 3;
return pageShopDealerApply({ page, limit, ...where, ...orders });
};
/* 搜索 */
const reload = (where?: ShopDealerApplyParam) => {
selection.value = [];
tableRef?.value?.reload({ where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerApply) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开编辑弹窗 */
const openImport = () => {
showImport.value = true;
};
/* 导出数据 */
const exportData = async () => {
exportLoading.value = true;
try {
// 定义表头
const array: (string | number)[][] = [
['ID', '企业名称', '联系方式', '入市状态', '创建时间']
];
// 构建查询参数,使用当前搜索条件
const params = {
keywords: searchText.value,
isAdmin: 0
};
// 获取用户列表数据
const list = await listShopDealerApply(params);
if (!list || list.length === 0) {
message.warning('没有数据可以导出');
exportLoading.value = false;
return;
}
// 将数据转换为Excel行
list.forEach((user: ShopDealerApply) => {
array.push([
`${user.applyId || ''}`,
`${user.realName || ''}`,
`${user.mobile || ''}`,
`${user.applyStatus === 20 ? '通过' : '未通过'}`,
`${user.createTime || ''}`
]);
});
// 生成Excel文件
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'] = [];
message.loading('正在生成Excel文件...', 0);
setTimeout(() => {
writeFile(workbook, `${sheetName}.xlsx`);
exportLoading.value = false;
message.destroy();
message.success(`成功导出 ${list.length} 条记录`);
}, 1000);
} catch (error: any) {
exportLoading.value = false;
message.error(error.message || '导出失败');
}
};
/* 删除单个 */
const remove = (row: ShopDealerApply) => {
const hide = messageLoading('请求中..', 0);
removeShopDealerApply(row.applyId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 自定义行属性 */
const customRow = (record: ShopDealerApply) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
watch(
() => router.currentRoute.value.query,
() => {},
{ immediate: true }
);
</script>
<script lang="ts">
export default {
name: 'ShopDealerApplyRs'
};
</script>

View File

@@ -1,19 +1,28 @@
<!-- 搜索表单 --> <!-- 搜索表单 -->
<template> <template>
<a-space :size="10" style="flex-wrap: wrap"> <a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add"> <a-input-search
<template #icon> allow-clear
<PlusOutlined /> placeholder="用户ID|订单编号"
</template> style="width: 240px"
<span>添加</span> v-model:value="where.keywords"
</a-button> @search="reload"
/>
<a-button type="dashed" @click="handleExport">导出xls</a-button>
</a-space> </a-space>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue'; import { ref, watch } from 'vue';
import type { GradeParam } from '@/api/user/grade/model'; import { utils, writeFile } from 'xlsx';
import { watch } from 'vue'; import { message } from 'ant-design-vue';
import { pageShopDealerCapital } from '@/api/shop/shopDealerCapital';
import {
ShopDealerCapital,
ShopDealerCapitalParam
} from '@/api/shop/shopDealerCapital/model';
import { getTenantId } from '@/utils/domain';
import useSearch from '@/utils/use-search';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -24,15 +33,81 @@
); );
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'search', where?: GradeParam): void; (e: 'search', where?: ShopDealerCapitalParam): void;
(e: 'add'): void; (e: 'add'): void;
(e: 'remove'): void; (e: 'remove'): void;
(e: 'batchMove'): void; (e: 'batchMove'): void;
}>(); }>();
// 新增 const reload = () => {
const add = () => { emit('search', where);
emit('add'); };
// 表单数据
const { where } = useSearch<ShopDealerCapitalParam>({
keywords: '',
userId: undefined,
toUserId: undefined,
limit: 5000
});
const list = ref<ShopDealerCapital[]>([]);
// 导出
const handleExport = async () => {
const array: (string | number)[][] = [
[
'订单号',
'用户',
'收益类型',
'金额',
'描述',
'创建时间',
'租户ID'
]
];
// 按搜索结果导出
await pageShopDealerCapital(where)
.then((data) => {
list.value = data?.list || [];
list.value?.forEach((d: ShopDealerCapital) => {
array.push([
`${d.orderNo}`,
`${d.nickName}(${d.userId})`,
`${d.flowType == 10 ? '佣金收入' : ''}`,
`${d.money}`,
`${d.comments}`,
`${d.createTime}`,
`${d.tenantId}`
]);
});
const sheetName = `bak_shop_dealer_capital_${getTenantId()}`;
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽
sheet['!cols'] = [
{ wch: 10 },
{ wch: 20 },
{ wch: 20 },
{ wch: 15 },
{ wch: 10 },
{ wch: 10 },
{ wch: 20 }
];
message.loading('正在导出...');
setTimeout(() => {
writeFile(workbook, `${sheetName}.xlsx`);
}, 1000);
})
.catch((msg) => {
message.error(msg);
})
.finally(() => {});
}; };
watch( watch(

View File

@@ -23,6 +23,9 @@
<template v-if="column.key === 'image'"> <template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" /> <a-image :src="record.image" :width="50" />
</template> </template>
<template v-if="column.key === 'userId'">
<div>{{ record.nickName }}({{ record.userId }})</div>
</template>
<template v-if="column.key === 'status'"> <template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag> <a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag> <a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
@@ -57,7 +60,6 @@
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'; import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro'; import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type { import type {
DatasourceFunction, DatasourceFunction,
ColumnItem ColumnItem
@@ -111,33 +113,39 @@
// 表格列配置 // 表格列配置
const columns = ref<ColumnItem[]>([ const columns = ref<ColumnItem[]>([
{ {
title: 'ID', key: 'index',
dataIndex: 'id', width: 48,
key: 'id',
align: 'center', align: 'center',
width: 80, fixed: 'left',
fixed: 'left' hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
}, },
{ {
title: '用户ID', title: '订单号',
dataIndex: 'orderNo',
key: 'orderNo',
align: 'center',
customRender: ({ text }) => text || '-'
},
{
title: '用户',
dataIndex: 'userId', dataIndex: 'userId',
key: 'userId', key: 'userId',
align: 'center', align: 'center',
width: 100,
fixed: 'left' fixed: 'left'
}, },
{ {
title: '流动类型', title: '收益类型',
dataIndex: 'flowType', dataIndex: 'flowType',
key: 'flowType', key: 'flowType',
align: 'center', align: 'center',
width: 120,
customRender: ({ text }) => { customRender: ({ text }) => {
const typeMap = { const typeMap = {
10: { text: '佣金收入', color: 'success' }, 10: { text: '佣金收入', color: 'success' },
20: { text: '提现支出', color: 'warning' }, 20: { text: '提现支出', color: 'warning' },
30: { text: '转账支出', color: 'error' }, 30: { text: '转账支出', color: 'error' },
40: { text: '转账收入', color: 'processing' } 40: { text: '转账收入', color: 'processing' },
50: { text: '佣金解冻', color: 'processing' }
}; };
const type = typeMap[text] || { text: '未知', color: 'default' }; const type = typeMap[text] || { text: '未知', color: 'default' };
return { return {
@@ -152,43 +160,35 @@
dataIndex: 'money', dataIndex: 'money',
key: 'money', key: 'money',
align: 'center', align: 'center',
width: 120,
customRender: ({ text, record }) => { customRender: ({ text, record }) => {
const amount = parseFloat(text || '0').toFixed(2); const amount = parseFloat(text || '0').toFixed(2);
const isIncome = record.flowType === 10 || record.flowType === 40; const isIncome =
record.flowType === 10 ||
record.flowType === 40 ||
record.flowType === 50;
return { return {
type: 'span', type: 'span',
props: { props: {
style: { style: {
color: isIncome ? '#52c41a' : '#ff4d4f', color: isIncome ? '#424242' : '#ff4d4f'
fontWeight: 'bold'
} }
}, },
children: `${isIncome ? '+' : '-'}¥${amount}` children: `${isIncome ? '' : '-'} ${amount}`
}; };
} }
}, },
{ // {
title: '关联订单', // title: '对方用户',
dataIndex: 'orderNo', // dataIndex: 'toUserId',
key: 'orderNo', // key: 'toUserId',
align: 'center', // align: 'center',
customRender: ({ text }) => text || '-' // customRender: ({ text }) => (text ? `ID: ${text}` : '-')
}, // },
{
title: '对方用户',
dataIndex: 'toUserId',
key: 'toUserId',
align: 'center',
width: 100,
customRender: ({ text }) => (text ? `ID: ${text}` : '-')
},
{ {
title: '描述', title: '描述',
dataIndex: 'describe', dataIndex: 'comments',
key: 'describe', key: 'comments',
align: 'left', align: 'left',
width: 200,
ellipsis: true, ellipsis: true,
customRender: ({ text }) => text || '-' customRender: ({ text }) => text || '-'
}, },
@@ -197,24 +197,16 @@
dataIndex: 'createTime', dataIndex: 'createTime',
key: 'createTime', key: 'createTime',
align: 'center', align: 'center',
sorter: true, sorter: true
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '修改时间',
dataIndex: 'updateTime',
key: 'updateTime',
align: 'center'
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
} }
// {
// title: '操作',
// key: 'action',
// width: 180,
// fixed: 'right',
// align: 'center',
// hideInSetting: true
// }
]); ]);
/* 搜索 */ /* 搜索 */

View File

@@ -19,16 +19,6 @@
</p> </p>
<p class="ant-upload-hint">将文件拖到此处或点击上传</p> <p class="ant-upload-hint">将文件拖到此处或点击上传</p>
</a-upload-dragger> </a-upload-dragger>
<div class="ant-upload-text text-gray-400">
<div
>1必须按<a
href="https://oss.wsdns.cn/20251018/408b805ec3cd4084a4dc686e130af578.xlsx"
target="_blank"
>导入模版</a
>的格式上传</div
>
<div>2导入成功确认结算完成佣金的发放</div>
</div>
</a-spin> </a-spin>
</ele-modal> </ele-modal>
</template> </template>
@@ -37,7 +27,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { message } from 'ant-design-vue/es'; import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue'; import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { importSdyDealerOrder } from '@/api/sdy/sdyDealerOrder'; import { importShopDealerOrder } from '@/api/shop/shopDealerOrder';
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'done'): void; (e: 'done'): void;
@@ -68,7 +58,7 @@
return false; return false;
} }
loading.value = true; loading.value = true;
importSdyDealerOrder(file) importShopDealerOrder(file)
.then((msg) => { .then((msg) => {
loading.value = false; loading.value = false;
message.success(msg); message.success(msg);

View File

@@ -1,182 +1,147 @@
<template>
<div class="flex items-center gap-20">
<!-- 搜索表单 --> <!-- 搜索表单 -->
<a-form <template>
:model="where" <a-space :size="10" style="flex-wrap: wrap">
layout="inline"
class="search-form"
@finish="handleSearch"
>
<a-form-item>
<a-space>
<a-button
danger
class="ele-btn-icon"
v-if="selection.length > 0"
:disabled="selection?.length === 0"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
</a-space>
</a-form-item>
<!-- <a-form-item label="订单状态">-->
<!-- <a-select-->
<!-- v-model:value="where.isInvalid"-->
<!-- placeholder="全部"-->
<!-- allow-clear-->
<!-- style="width: 120px"-->
<!-- >-->
<!-- <a-select-option :value="0">有效</a-select-option>-->
<!-- <a-select-option :value="1">失效</a-select-option>-->
<!-- </a-select>-->
<!-- </a-form-item>-->
<!-- <a-form-item label="结算状态">-->
<!-- <a-select-->
<!-- v-model:value="where.isSettled"-->
<!-- placeholder="全部"-->
<!-- allow-clear-->
<!-- style="width: 120px"-->
<!-- >-->
<!-- <a-select-option :value="0">未结算</a-select-option>-->
<!-- <a-select-option :value="1">已结算</a-select-option>-->
<!-- </a-select>-->
<!-- </a-form-item>-->
<a-form-item>
<a-space>
<a-input-search <a-input-search
allow-clear allow-clear
placeholder="请输入关键词" placeholder="客户名称|订单编号"
style="width: 240px" style="width: 240px"
v-model:value="where.keywords" v-model:value="where.keywords"
@search="handleSearch" @search="reload"
/> />
<!-- <a-button type="primary" html-type="submit" class="ele-btn-icon">--> <a-button type="dashed" @click="handleExport">导出xls</a-button>
<!-- <template #icon>-->
<!-- <SearchOutlined/>-->
<!-- </template>-->
<!-- 搜索-->
<!-- </a-button>-->
<a-button @click="resetSearch"> 重置 </a-button>
</a-space> </a-space>
</a-form-item>
</a-form>
<a-divider type="vertical" />
<a-space>
<!-- <a-button @click="exportData" class="ele-btn-icon">-->
<!-- <template #icon>-->
<!-- <ExportOutlined />-->
<!-- </template>-->
<!-- 导出数据-->
<!-- </a-button>-->
<a-button @click="openImport" class="ele-btn-icon">
<template #icon>
<UploadOutlined />
</template>
导入数据
</a-button>
<a-button
type="primary"
danger
@click="batchSettle"
:disabled="selection?.length === 0"
>
<template #icon>
<DollarOutlined />
</template>
批量结算
</a-button>
</a-space>
</div>
<!-- 导入弹窗 -->
<Import v-model:visible="showImport" @done="emit('importDone')" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { import { utils, writeFile } from 'xlsx';
DollarOutlined, import { message } from 'ant-design-vue';
UploadOutlined, import { getTenantId } from '@/utils/domain';
DeleteOutlined
} from '@ant-design/icons-vue';
import type { ShopDealerOrderParam } from '@/api/shop/shopDealerOrder/model';
import Import from './Import.vue';
import useSearch from '@/utils/use-search'; import useSearch from '@/utils/use-search';
import type {
ShopDealerOrder,
ShopDealerOrderParam
} from '@/api/shop/shopDealerOrder/model';
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder';
withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
// 选中的数据 // 选中的角色
selection?: any[]; selection?: [];
}>(), }>(),
{ {}
selection: () => []
}
); );
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'search', where?: ShopDealerOrderParam): void; (e: 'search', where?: ShopDealerOrderParam): void;
(e: 'batchSettle'): void;
(e: 'export'): void;
(e: 'importDone'): void;
(e: 'remove'): void;
}>(); }>();
// 是否显示导入弹窗 const reload = () => {
const showImport = ref(false); emit('search', where);
};
// 搜索表单 // 表单数据
const { where, resetFields } = useSearch<ShopDealerOrderParam>({ const { where } = useSearch<ShopDealerOrderParam>({
orderNo: '', keywords: '',
productName: '', userId: undefined,
isInvalid: undefined, orderNo: undefined,
isSettled: undefined isSettled: 1, // 与列表页一致:只展示/导出已结算订单
page: 1,
limit: 5000
}); });
// 搜索 const list = ref<ShopDealerOrder[]>([]);
const handleSearch = () => {
const searchParams = { ...where }; const toMoney = (val: unknown) => {
// 清除空值 const n = Number.parseFloat(String(val ?? '0'));
Object.keys(searchParams).forEach((key) => { return Number.isFinite(n) ? n.toFixed(2) : '0.00';
if (searchParams[key] === '' || searchParams[key] === undefined) { };
delete searchParams[key];
} const toDateTime = (val: unknown) => {
if (val === null || val === undefined || val === '') return '';
if (typeof val === 'number') return new Date(val).toLocaleString();
return String(val);
};
// 导出
const handleExport = async () => {
const array: (string | number)[][] = [
[
// 与 `src/views/shop/shopDealerOrder/index.vue` 表头保持一致
'订单编号',
'买家',
'订单金额',
'一级佣金(10%)',
'二级佣金(5%)',
'一级门店分红(2%/3%)',
'二级门店分红(1%)',
'结算状态',
'创建时间'
]
];
// 按搜索结果导出
await pageShopDealerOrder(where)
.then((data) => {
list.value = data?.list || [];
list.value?.forEach((d: ShopDealerOrder) => {
const buyer =
(d.title ? String(d.title) : '') +
(d.nickname || d.userId
? `\n${d.nickname ?? '-'}(${d.userId ?? '-'})`
: '');
const firstDividendUserName = (d as any)?.firstDividendUserName ?? '-';
const firstDividend = (d as any)?.firstDividend ?? 0;
const secondDividendUserName =
(d as any)?.secondDividendUserName ?? '-';
const secondDividend = (d as any)?.secondDividend ?? 0;
array.push([
d.orderNo ?? '',
buyer,
toMoney(d.orderPrice),
`${toMoney(d.firstMoney)}\n${d.firstNickname ?? '-'}`,
`${toMoney(d.secondMoney)}\n${d.secondNickname ?? '-'}`,
`${toMoney(firstDividend)}\n${firstDividendUserName}`,
`${toMoney(secondDividend)}\n${secondDividendUserName}`,
d.isSettled === 1 ? '已结算' : '未结算',
`${d.createTime ?? ''}${
d.settleTime ? `\n${toDateTime(d.settleTime)}` : ''
}`
]);
}); });
emit('search', searchParams); const sheetName = `bak_shop_dealer_order_${getTenantId()}`;
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽
sheet['!cols'] = [
{ wch: 20 },
{ wch: 20 },
{ wch: 12 },
{ wch: 16 },
{ wch: 16 },
{ wch: 18 },
{ wch: 16 },
{ wch: 10 },
{ wch: 20 }
];
message.loading('正在导出...', 0);
setTimeout(() => {
writeFile(workbook, `${sheetName}.xlsx`);
message.destroy();
}, 1000);
})
.catch((msg) => {
message.destroy();
message.error(msg);
})
.finally(() => {});
}; };
// 重置搜索 void props;
const resetSearch = () => {
// Object.keys(searchForm).forEach(key => {
// searchForm[key] = key === 'orderId' ? undefined : '';
// });
resetFields();
emit('search', {});
};
// 批量删除
const removeBatch = () => {
emit('remove');
};
// 批量结算
const batchSettle = () => {
emit('batchSettle');
};
// 导出数据
const exportData = () => {
emit('export');
};
// 打开导入弹窗
const openImport = () => {
showImport.value = true;
};
</script> </script>

View File

@@ -24,66 +24,36 @@
</a-divider> </a-divider>
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12">
<a-form-item label="客户名称" name="title">
{{ form.title }}
</a-form-item>
</a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item label="订单编号" name="orderNo"> <a-form-item label="订单编号" name="orderNo">
{{ form.orderNo }} {{ form.orderNo }}
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item label="结算电量" name="orderPrice"> <a-form-item label="订单金额" name="payPrice">
{{ parseFloat(form.orderPrice || 0).toFixed(2) }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="换算成度" name="dealerPrice">
{{ parseFloat(form.degreePrice || 0).toFixed(2) }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="税率" name="rate">
{{ form.rate }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="单价" name="price">
{{ form.price }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="结算金额" name="payPrice">
{{ parseFloat(form.settledPrice || 0).toFixed(2) }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="实发金额" name="payPrice">
{{ parseFloat(form.payPrice || 0).toFixed(2) }} {{ parseFloat(form.payPrice || 0).toFixed(2) }}
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<div class="font-bold text-gray-400 bg-gray-50">开发调试</div> <!-- <div class="font-bold text-gray-400 bg-gray-50">开发调试</div>-->
<div class="text-gray-400 bg-gray-50"> <!-- <div class="text-gray-400 bg-gray-50">-->
<div>业务员({{ form.userId }}){{ form.nickname }}</div> <!-- <div>业务员({{ form.userId }}){{ form.nickname }}</div>-->
<div <!-- <div-->
>一级分销商({{ form.firstUserId }}){{ <!-- >一级分销商({{ form.firstUserId }}){{-->
form.firstNickname <!-- form.firstNickname-->
}}一级佣金30%{{ form.firstMoney }}</div <!-- }}一级佣金30%{{ form.firstMoney }}</div-->
> <!-- >-->
<div <!-- <div-->
>二级分销商({{ form.secondUserId }}){{ <!-- >二级分销商({{ form.secondUserId }}){{-->
form.secondNickname <!-- form.secondNickname-->
}}二级佣金10%{{ form.secondMoney }}</div <!-- }}二级佣金10%{{ form.secondMoney }}</div-->
> <!-- >-->
<div <!-- <div-->
>三级分销商({{ form.thirdUserId }}){{ <!-- >三级分销商({{ form.thirdUserId }}){{-->
form.thirdNickname <!-- form.thirdNickname-->
}}三级佣金60%{{ form.thirdMoney }}</div <!-- }}三级佣金60%{{ form.thirdMoney }}</div-->
> <!-- >-->
</div> <!-- </div>-->
<!-- 分销商信息 --> <!-- 分销商信息 -->
<a-divider orientation="left"> <a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600">收益计算</span> <span style="color: #1890ff; font-weight: 600">收益计算</span>
@@ -92,7 +62,7 @@
<!-- 一级分销商 --> <!-- 一级分销商 -->
<div class="dealer-section"> <div class="dealer-section">
<h4 class="dealer-title"> <h4 class="dealer-title">
<a-tag color="orange">一级佣金30%</a-tag> <a-tag color="orange">一级佣金10%</a-tag>
</h4> </h4>
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
@@ -105,7 +75,7 @@
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item label="占比" name="rate"> <a-form-item label="占比" name="rate">
{{ '30%' }} {{ '10%' }}
</a-form-item> </a-form-item>
<a-form-item label="获取收益" name="firstMoney"> <a-form-item label="获取收益" name="firstMoney">
{{ form.firstMoney }} {{ form.firstMoney }}
@@ -179,7 +149,7 @@
import { assignObject } from 'ele-admin-pro'; import { assignObject } from 'ele-admin-pro';
import { ShopDealerOrder } from '@/api/shop/shopDealerOrder/model'; import { ShopDealerOrder } from '@/api/shop/shopDealerOrder/model';
import { FormInstance } from 'ant-design-vue/es/form'; import { FormInstance } from 'ant-design-vue/es/form';
import { updateSdyDealerOrder } from '@/api/sdy/sdyDealerOrder'; import { updateShopDealerOrder } from '@/api/shop/shopDealerOrder';
// 是否是修改 // 是否是修改
const isUpdate = ref(false); const isUpdate = ref(false);
@@ -275,7 +245,7 @@
...form, ...form,
isSettled: 1 isSettled: 1
}; };
updateSdyDealerOrder(formData) updateShopDealerOrder(formData)
.then((msg) => { .then((msg) => {
loading.value = false; loading.value = false;
message.success(msg); message.success(msg);

View File

@@ -7,28 +7,24 @@
:columns="columns" :columns="columns"
:datasource="datasource" :datasource="datasource"
:customRow="customRow" :customRow="customRow"
v-model:selection="selection"
tool-class="ele-toolbar-form" tool-class="ele-toolbar-form"
class="sys-org-table" class="sys-org-table"
> >
<template #toolbar> <template #toolbar>
<search <search
@search="reload" @search="reload"
:selection="selection"
@batchSettle="batchSettle"
@export="handleExport"
@remove="removeBatch"
@importDone="reload"
/> />
</template> </template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'title'"> <template v-if="column.key === 'title'">
<div>{{ record.title }}</div> <div>{{ record.title }}</div>
<div class="text-gray-400">用户ID{{ record.userId }}</div> <div class="text-gray-400"
>{{ record.nickname }}({{ record.userId }})</div
>
</template> </template>
<template v-if="column.key === 'orderPrice'"> <template v-if="column.key === 'orderPrice'">
{{ record.orderPrice.toFixed(2) }} {{ parseFloat(record.orderPrice).toFixed(2) }}
</template> </template>
<template v-if="column.key === 'degreePrice'"> <template v-if="column.key === 'degreePrice'">
@@ -36,7 +32,7 @@
</template> </template>
<template v-if="column.key === 'price'"> <template v-if="column.key === 'price'">
{{ record.price }} {{ record.price || 0 }}
</template> </template>
<template v-if="column.key === 'settledPrice'"> <template v-if="column.key === 'settledPrice'">
@@ -47,6 +43,30 @@
{{ record.payPrice.toFixed(2) }} {{ record.payPrice.toFixed(2) }}
</template> </template>
<template v-if="column.key === 'firstNickname'">
<div>{{ record.firstMoney }}</div>
<div class="text-gray-400">{{ record.firstNickname || '-' }}</div>
</template>
<template v-if="column.key === 'secondNickname'">
<div>{{ record.secondMoney }}</div>
<div class="text-gray-400">{{ record.secondNickname || '-' }}</div>
</template>
<template v-if="column.key === 'firstDividendUserName'">
<div>{{ record.firstDividend }}</div>
<div class="text-gray-400"
>{{ record.firstDividendUserName || '-' }}</div
>
</template>
<template v-if="column.key === 'secondDividendUserName'">
<div>{{ record.secondDividend }}</div>
<div class="text-gray-400"
>{{ record.secondDividendUserName || '-' }}</div
>
</template>
<template v-if="column.key === 'dealerInfo'"> <template v-if="column.key === 'dealerInfo'">
<div class="dealer-info"> <div class="dealer-info">
<div v-if="record.firstUserId" class="dealer-level"> <div v-if="record.firstUserId" class="dealer-level">
@@ -76,8 +96,11 @@
</template> </template>
<template v-if="column.key === 'isSettled'"> <template v-if="column.key === 'isSettled'">
<div class="flex flex-col">
<a-tag v-if="record.isSettled === 0" color="orange">未结算</a-tag> <a-tag v-if="record.isSettled === 0" color="orange">未结算</a-tag>
<a-tag v-if="record.isSettled === 1" color="success">已结算</a-tag> <a-tag v-if="record.isSettled === 1" color="purple">已结算</a-tag>
<a-tag v-if="record.isUnfreeze === 1" color="success">已解冻</a-tag>
</div>
</template> </template>
<template v-if="column.key === 'createTime'"> <template v-if="column.key === 'createTime'">
@@ -88,14 +111,30 @@
<a-tooltip title="结算时间"> <a-tooltip title="结算时间">
<span class="text-purple-500">{{ record.settleTime }}</span> <span class="text-purple-500">{{ record.settleTime }}</span>
</a-tooltip> </a-tooltip>
<a-tooltip title="解冻时间">
<span class="text-green-500">{{ record.unfreezeTime }}</span>
</a-tooltip>
</div> </div>
</template> </template>
<template v-if="column.key === 'action'"> <template v-if="column.key === 'action'">
<template v-if="record.isSettled === 0 && record.isInvalid === 0"> <template v-if="record.isSettled === 0 && record.isInvalid === 0">
<a @click="openEdit(record)" class="ele-text-success"> 结算 </a> <a @click="settleOrder(record)" class="ele-text-success">
结算
</a>
<a-divider type="vertical" /> <a-divider type="vertical" />
</template> </template>
<!-- <template v-if="record.isInvalid === 0">-->
<!-- <a-popconfirm-->
<!-- title="确定要标记此订单为失效吗?"-->
<!-- @confirm="invalidateOrder(record)"-->
<!-- placement="topRight"-->
<!-- >-->
<!-- <a class="text-purple-500">-->
<!-- 验证-->
<!-- </a>-->
<!-- </a-popconfirm>-->
<!-- </template>-->
<a-popconfirm <a-popconfirm
v-if="record.isSettled === 0" v-if="record.isSettled === 0"
title="确定要删除吗?" title="确定要删除吗?"
@@ -121,7 +160,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { createVNode, ref } from 'vue'; import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'; import {
ExclamationCircleOutlined,
DollarOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro'; import type { EleProTable } from 'ele-admin-pro';
import type { import type {
DatasourceFunction, DatasourceFunction,
@@ -139,7 +181,9 @@
ShopDealerOrder, ShopDealerOrder,
ShopDealerOrderParam ShopDealerOrderParam
} from '@/api/shop/shopDealerOrder/model'; } from '@/api/shop/shopDealerOrder/model';
import { exportSdyDealerOrder } from '@/api/sdy/sdyDealerOrder'; import {
updateShopDealerOrder
} from '@/api/shop/shopDealerOrder';
// 表格实例 // 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null); const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
@@ -153,9 +197,6 @@
// 加载状态 // 加载状态
const loading = ref(true); const loading = ref(true);
// 当前搜索条件
const currentWhere = ref<ShopDealerOrderParam>({});
// 表格数据源 // 表格数据源
const datasource: DatasourceFunction = ({ const datasource: DatasourceFunction = ({
page, page,
@@ -167,11 +208,8 @@
if (filters) { if (filters) {
where.status = filters.status; where.status = filters.status;
} }
// 保存当前搜索条件用于导出 // 已结算订单
currentWhere.value = { ...where }; where.isSettled = 1;
// 未结算订单
where.isSettled = 0;
where.myOrder = 1;
return pageShopDealerOrder({ return pageShopDealerOrder({
...where, ...where,
...orders, ...orders,
@@ -182,88 +220,77 @@
// 表格列配置 // 表格列配置
const columns = ref<ColumnItem[]>([ const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{ {
title: '订单编号', title: '订单编号',
dataIndex: 'orderNo', dataIndex: 'orderNo',
key: 'orderNo' key: 'orderNo',
align: 'center',
width: 200
}, },
{ {
title: '客户名称', title: '买家',
dataIndex: 'title', dataIndex: 'title',
key: 'title', key: 'title'
width: 220
}, },
{ {
title: '结算电量', title: '订单金额',
dataIndex: 'orderPrice', dataIndex: 'orderPrice',
key: 'orderPrice', key: 'orderPrice',
align: 'center' align: 'center'
}, },
{ {
title: '换算成度', title: '一级佣金',
dataIndex: 'degreePrice', dataIndex: 'firstNickname',
key: 'degreePrice', key: 'firstNickname',
align: 'center' align: 'center'
}, },
{ {
title: '结算单价', title: '二级佣金',
dataIndex: 'price', dataIndex: 'secondNickname',
key: 'price', key: 'secondNickname',
align: 'center' align: 'center'
}, },
{ {
title: '结算金额', title: '一级门店分红',
dataIndex: 'settledPrice', dataIndex: 'firstDividendUserName',
key: 'settledPrice', key: 'firstDividendUserName',
align: 'center' align: 'center'
}, },
{ {
title: '税费', title: '二级门店分红',
dataIndex: 'rate', dataIndex: 'secondDividendUserName',
key: 'rate', key: 'secondDividendUserName',
align: 'center' align: 'center'
}, },
{
title: '实发金额',
dataIndex: 'payPrice',
key: 'payPrice',
align: 'center'
},
{
title: '签约状态',
dataIndex: 'isInvalid',
key: 'isInvalid',
align: 'center',
width: 100
},
{
title: '月份',
dataIndex: 'month',
key: 'month',
align: 'center',
width: 100
},
{ {
title: '结算状态', title: '结算状态',
dataIndex: 'isSettled', dataIndex: 'isSettled',
key: 'isSettled', key: 'isSettled',
align: 'center', align: 'center'
width: 100
}, },
{ {
title: '创建时间', title: '创建时间',
dataIndex: 'createTime', dataIndex: 'createTime',
key: 'createTime', key: 'createTime',
align: 'center'
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center', align: 'center',
hideInSetting: true width: 180
} }
// {
// title: '操作',
// key: 'action',
// width: 180,
// fixed: 'right',
// align: 'center',
// hideInSetting: true
// }
]); ]);
/* 搜索 */ /* 搜索 */
@@ -272,6 +299,37 @@
tableRef?.value?.reload({ where: where }); tableRef?.value?.reload({ where: where });
}; };
/* 结算单个订单 */
const settleOrder = (row: ShopDealerOrder) => {
const totalCommission = (
parseFloat(row.firstMoney || '0') +
parseFloat(row.secondMoney || '0') +
parseFloat(row.thirdMoney || '0')
).toFixed(2);
Modal.confirm({
title: '确认结算',
content: `确定要结算此订单吗?总佣金金额:¥${totalCommission}`,
icon: createVNode(DollarOutlined),
okText: '确认结算',
okType: 'primary',
cancelText: '取消',
onOk: () => {
const hide = message.loading('正在结算...', 0);
// 这里调用结算API
updateShopDealerOrder({
...row,
isSettled: 1
});
setTimeout(() => {
hide();
message.success('结算成功');
reload();
}, 1000);
}
});
};
/* 批量结算 */ /* 批量结算 */
const batchSettle = () => { const batchSettle = () => {
if (!selection.value.length) { if (!selection.value.length) {
@@ -318,16 +376,10 @@
}); });
}; };
/* 导出数据 */
const handleExport = () => {
// 调用导出API传入当前搜索条件
exportSdyDealerOrder(currentWhere.value);
};
/* 打开编辑弹窗 */ /* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerOrder) => { const openEdit = (row?: ShopDealerOrder) => {
current.value = row ?? null; current.value = row ?? null;
showEdit.value = true; // showEdit.value = true;
}; };
/* 删除单个 */ /* 删除单个 */

View File

@@ -24,7 +24,7 @@
<a-radio-group v-model:value="basicSettings.distributionLevel"> <a-radio-group v-model:value="basicSettings.distributionLevel">
<a-radio :value="1">一级</a-radio> <a-radio :value="1">一级</a-radio>
<a-radio :value="2">二级</a-radio> <a-radio :value="2">二级</a-radio>
<a-radio :value="3">三级</a-radio> <!-- <a-radio :value="3">三级</a-radio>-->
</a-radio-group> </a-radio-group>
<div class="setting-desc">设置分销商推荐层级关系</div> <div class="setting-desc">设置分销商推荐层级关系</div>
</a-form-item> </a-form-item>
@@ -235,14 +235,26 @@
import { PlusOutlined } from '@ant-design/icons-vue'; import { PlusOutlined } from '@ant-design/icons-vue';
import { getPageTitle } from '@/utils/common'; import { getPageTitle } from '@/utils/common';
import { import {
addShopDealerSetting,
updateShopDealerSetting, updateShopDealerSetting,
getShopDealerSetting listShopDealerSetting
} from '@/api/shop/shopDealerSetting'; } from '@/api/shop/shopDealerSetting';
import type { ShopDealerSetting } from '@/api/shop/shopDealerSetting/model';
// 当前激活的标签页 // 当前激活的标签页
const activeTab = ref('basic'); const activeTab = ref('basic');
// 保存状态 // 保存状态
const saving = ref(false); const saving = ref(false);
const settingMap = ref<Record<string, ShopDealerSetting>>({});
const settingValuesMap = ref<Record<string, any>>({});
const settingMeta = {
basic: '基础设置',
condition: '分销商条件',
settlement: '结算',
license: '申请协议',
words: '自定义文字',
background: '页面背景图'
};
// 基础设置 // 基础设置
const basicSettings = reactive({ const basicSettings = reactive({
@@ -283,6 +295,63 @@
backgroundImages: [] backgroundImages: []
}); });
const getBooleanFlag = (value: any) => value === 1 || value === true;
const getNumberFlag = (value: any) => (value ? 1 : 0);
const pickEnumValue = (value: any, options: number[], fallback: number) =>
options.includes(value) ? value : fallback;
const ensurePath = (root: Record<string, any>, path: string[]) => {
let current = root;
path.forEach((key) => {
if (!current[key] || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
});
return current;
};
const setWordValue = (
root: Record<string, any>,
path: string[],
value: string
) => {
const node = ensurePath(root, path);
if (typeof node.default !== 'string') {
node.default = value;
}
node.value = value;
};
const getUploadUrl = (fileList: any[]) => {
if (!Array.isArray(fileList) || fileList.length === 0) {
return '';
}
const file = fileList[0];
return (
file?.url ||
file?.thumbUrl ||
file?.response?.url ||
file?.response?.data?.url ||
file?.response?.data?.path ||
''
);
};
const toUploadFileList = (url?: string) => {
if (!url) {
return [];
}
return [
{
uid: 'background-0',
name: '背景图',
status: 'done',
url
}
];
};
/* 图片预览 */ /* 图片预览 */
const handlePreview = (file: any) => { const handlePreview = (file: any) => {
console.log('预览图片:', file); console.log('预览图片:', file);
@@ -291,37 +360,178 @@
/* 加载设置 */ /* 加载设置 */
const loadSettings = async () => { const loadSettings = async () => {
try { try {
// 这里应该调用API获取设置数据 const list = await listShopDealerSetting();
// const settings = await getShopDealerSetting(); const nextMap: Record<string, ShopDealerSetting> = {};
// 然后将数据分配到各个设置对象中 const nextValues: Record<string, any> = {};
console.log('加载设置数据'); list.forEach((item) => {
if (!item.key) {
return;
}
nextMap[item.key] = item;
if (item.values) {
try {
nextValues[item.key] = JSON.parse(item.values);
} catch (error) {
console.warn('解析设置失败:', item.key, error);
}
}
});
settingMap.value = nextMap;
settingValuesMap.value = nextValues;
const basicValue = nextValues.basic || {};
basicSettings.enableDistribution = getBooleanFlag(basicValue.is_open);
basicSettings.distributionLevel = basicValue.level ?? 3;
basicSettings.dealerSelfBuy = getBooleanFlag(basicValue.self_buy);
const conditionValue = nextValues.condition || {};
commissionSettings.applyType = pickEnumValue(
conditionValue.become,
[10, 20],
commissionSettings.applyType
);
const settlementValue = nextValues.settlement || {};
commissionSettings.settlementType = pickEnumValue(
settlementValue.settle_days ?? settlementValue.settlement_type,
[10, 20],
commissionSettings.settlementType
);
commissionSettings.minWithdrawAmount =
settlementValue.min_money ?? commissionSettings.minWithdrawAmount;
withdrawSettings.withdrawMethods = Array.isArray(settlementValue.pay_type)
? settlementValue.pay_type
: withdrawSettings.withdrawMethods;
withdrawSettings.withdrawFeeRate =
typeof settlementValue.fee_rate === 'number'
? settlementValue.fee_rate
: withdrawSettings.withdrawFeeRate;
withdrawSettings.withdrawAudit =
settlementValue.audit === undefined
? withdrawSettings.withdrawAudit
: getBooleanFlag(settlementValue.audit);
const licenseValue = nextValues.license || {};
if (licenseValue.license) {
agreementSettings.dealerAgreement = licenseValue.license;
}
const wordsValue = nextValues.words || {};
if (wordsValue.index?.title?.value) {
pageSettings.centerTitle = wordsValue.index.title.value;
}
if (wordsValue.apply?.words?.wait_audit?.value) {
notificationSettings.applySuccessText =
wordsValue.apply.words.wait_audit.value;
}
if (wordsValue.apply?.words?.fail?.value) {
notificationSettings.applyFailText = wordsValue.apply.words.fail.value;
}
if (wordsValue.withdraw_apply?.words?.success?.value) {
notificationSettings.withdrawSuccessText =
wordsValue.withdraw_apply.words.success.value;
}
const backgroundValue = nextValues.background || {};
const backgroundUrl =
backgroundValue.index ||
backgroundValue.apply ||
backgroundValue.withdraw_apply;
pageSettings.backgroundImages = toUploadFileList(backgroundUrl);
} catch (error) { } catch (error) {
console.error('加载设置失败:', error); console.error('加载设置失败:', error);
message.error('加载设置失败'); message.error('加载设置失败');
} }
}; };
const saveSettingItem = async (
key: keyof typeof settingMeta,
values: Record<string, any>
) => {
const existSetting = settingMap.value[key];
const payload: ShopDealerSetting = {
...(existSetting || {}),
key,
describe: existSetting?.describe ?? settingMeta[key],
values: JSON.stringify(values),
updateTime: Date.now()
};
const saveOrUpdate = existSetting
? updateShopDealerSetting
: addShopDealerSetting;
await saveOrUpdate(payload);
settingMap.value[key] = payload;
settingValuesMap.value[key] = values;
};
/* 保存设置 */ /* 保存设置 */
const saveSettings = async () => { const saveSettings = async () => {
saving.value = true; saving.value = true;
try { try {
// 收集所有设置数据 const basicValues = {
const allSettings = { is_open: getNumberFlag(basicSettings.enableDistribution),
basic: basicSettings, level: basicSettings.distributionLevel,
commission: commissionSettings, self_buy: getNumberFlag(basicSettings.dealerSelfBuy)
withdraw: withdrawSettings,
agreement: agreementSettings,
notification: notificationSettings,
page: pageSettings
}; };
console.log('保存设置:', allSettings); const conditionValues = {
...(settingValuesMap.value.condition || {}),
become: commissionSettings.applyType
};
// 这里应该调用API保存设置 const settlementValues = {
// await updateShopDealerSetting(allSettings); ...(settingValuesMap.value.settlement || {}),
pay_type: [...withdrawSettings.withdrawMethods],
min_money: commissionSettings.minWithdrawAmount,
settle_days: commissionSettings.settlementType,
fee_rate: withdrawSettings.withdrawFeeRate,
audit: getNumberFlag(withdrawSettings.withdrawAudit)
};
// 模拟保存 const licenseValues = {
await new Promise((resolve) => setTimeout(resolve, 1000)); license: agreementSettings.dealerAgreement
};
const wordsValues = {
...(settingValuesMap.value.words || {})
};
setWordValue(wordsValues, ['index', 'title'], pageSettings.centerTitle);
setWordValue(
wordsValues,
['apply', 'words', 'wait_audit'],
notificationSettings.applySuccessText
);
setWordValue(
wordsValues,
['apply', 'words', 'fail'],
notificationSettings.applyFailText
);
setWordValue(
wordsValues,
['withdraw_apply', 'words', 'success'],
notificationSettings.withdrawSuccessText
);
const backgroundUrl = getUploadUrl(pageSettings.backgroundImages);
const backgroundValues = {
...(settingValuesMap.value.background || {}),
index: backgroundUrl || settingValuesMap.value.background?.index || '',
apply: backgroundUrl || settingValuesMap.value.background?.apply || '',
withdraw_apply:
backgroundUrl || settingValuesMap.value.background?.withdraw_apply || ''
};
await saveSettingItem('basic', basicValues);
await saveSettingItem('condition', conditionValues);
await saveSettingItem('settlement', settlementValues);
await saveSettingItem('license', licenseValues);
await saveSettingItem('words', wordsValues);
await saveSettingItem('background', backgroundValues);
await loadSettings();
message.success('设置保存成功'); message.success('设置保存成功');
} catch (error) { } catch (error) {

View File

@@ -1,221 +1,51 @@
<!-- 搜索表单 --> <!-- 搜索表单 -->
<template> <template>
<div class="search-container"> <a-space :size="10" style="flex-wrap: wrap">
<!-- 搜索表单 --> <a-input-search
<a-form
:model="searchForm"
layout="inline"
class="search-form"
@finish="handleSearch"
>
<a-form-item label="申请人姓名">
<a-input
v-model:value="searchForm.realName"
placeholder="请输入申请人姓名"
allow-clear allow-clear
style="width: 160px" placeholder="姓名|电话"
/>
</a-form-item>
<a-form-item label="手机号码">
<a-input
v-model:value="searchForm.mobile"
placeholder="请输入手机号码"
allow-clear
style="width: 160px"
/>
</a-form-item>
<a-form-item label="申请方式">
<a-select
v-model:value="searchForm.applyType"
placeholder="全部方式"
allow-clear
style="width: 120px"
>
<a-select-option :value="10">需要审核</a-select-option>
<a-select-option :value="20">免审核</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="审核状态">
<a-select
v-model:value="searchForm.applyStatus"
placeholder="全部状态"
allow-clear
style="width: 120px"
>
<a-select-option :value="10">待审核</a-select-option>
<a-select-option :value="20">审核通过</a-select-option>
<a-select-option :value="30">审核驳回</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="申请时间">
<a-range-picker
v-model:value="searchForm.dateRange"
style="width: 240px" style="width: 240px"
v-model:value="where.keywords"
@search="reload"
/> />
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" html-type="submit" class="ele-btn-icon">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button @click="resetSearch"> 重置 </a-button>
</a-space> </a-space>
</a-form-item>
</a-form>
<!-- 操作按钮 -->
<div class="action-buttons">
<a-space>
<!-- <a-button type="primary" @click="add" class="ele-btn-icon">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- 新增申请-->
<!-- </a-button>-->
<a-button
type="primary"
ghost
:disabled="!selection?.length"
@click="batchApprove"
class="ele-btn-icon"
>
<template #icon>
<CheckOutlined />
</template>
批量通过
</a-button>
<a-button
:disabled="!selection?.length"
@click="exportData"
class="ele-btn-icon"
>
<template #icon>
<ExportOutlined />
</template>
导出数据
</a-button>
</a-space>
</div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive } from 'vue'; import type { GradeParam } from '@/api/user/grade/model';
import { import { watch } from 'vue';
PlusOutlined, import useSearch from "@/utils/use-search";
SearchOutlined, import {ShopDealerUserParam} from "@/api/shop/shopDealerUser/model";
CheckOutlined,
ExportOutlined
} from '@ant-design/icons-vue';
import type { ShopDealerApplyParam } from '@/api/shop/shopDealerApply/model';
import dayjs from 'dayjs';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
// 选中的数据 // 选中的角色
selection?: any[]; selection?: [];
}>(), }>(),
{ {}
selection: () => []
}
); );
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'search', where?: ShopDealerApplyParam): void; (e: 'search', where?: GradeParam): void;
(e: 'add'): void; (e: 'add'): void;
(e: 'batchApprove'): void; (e: 'remove'): void;
(e: 'export'): void; (e: 'batchMove'): void;
}>(); }>();
// 搜索表单 const reload = () => {
const searchForm = reactive<any>({ emit('search', where);
realName: '', };
mobile: '',
applyType: undefined, // 表单数据
applyStatus: undefined, const { where } = useSearch<ShopDealerUserParam>({
dateRange: undefined keywords: '',
userId: undefined,
mobile: undefined,
realName: undefined
}); });
// 搜索 watch(
const handleSearch = () => { () => props.selection,
const searchParams: ShopDealerApplyParam = {}; () => {}
if (searchForm.realName) {
searchParams.realName = searchForm.realName;
}
if (searchForm.mobile) {
searchParams.mobile = searchForm.mobile;
}
if (searchForm.applyType) {
searchParams.applyType = searchForm.applyType;
}
if (searchForm.applyStatus) {
searchParams.applyStatus = searchForm.applyStatus;
}
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
searchParams.startTime = dayjs(searchForm.dateRange[0]).format(
'YYYY-MM-DD'
); );
searchParams.endTime = dayjs(searchForm.dateRange[1]).format(
'YYYY-MM-DD'
);
}
emit('search', searchParams);
};
// 重置搜索
const resetSearch = () => {
searchForm.realName = '';
searchForm.mobile = '';
searchForm.applyType = undefined;
searchForm.applyStatus = undefined;
searchForm.dateRange = undefined;
emit('search', {});
};
// 新增
const add = () => {
emit('add');
};
// 批量通过
const batchApprove = () => {
emit('batchApprove');
};
// 导出数据
const exportData = () => {
emit('export');
};
</script> </script>
<style lang="less" scoped>
.search-container {
background: #fff;
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
.search-form {
margin-bottom: 16px;
:deep(.ant-form-item) {
margin-bottom: 8px;
}
}
.action-buttons {
border-top: 1px solid #f0f0f0;
padding-top: 16px;
}
}
</style>

View File

@@ -5,12 +5,9 @@
:visible="visible" :visible="visible"
:maskClosable="false" :maskClosable="false"
:maxable="maxable" :maxable="maxable"
:confirm-loading="loading"
:title="isUpdate ? '编辑分销商用户' : '添加分销商用户'" :title="isUpdate ? '编辑分销商用户' : '添加分销商用户'"
:body-style="{ :body-style="{ paddingBottom: '28px' }"
paddingBottom: '28px',
maxHeight: '70vh',
overflowY: 'auto'
}"
@update:visible="updateVisible" @update:visible="updateVisible"
@ok="save" @ok="save"
> >
@@ -18,265 +15,189 @@
ref="formRef" ref="formRef"
:model="form" :model="form"
:rules="rules" :rules="rules"
:label-col="{ span: 6 }" :label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="{ span: 18 }" :wrapper-col="
layout="horizontal" styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
> >
<!-- 基本信息 --> <a-divider orientation="left">
<a-divider orientation="left">信息</a-divider> <span style="color: #1890ff; font-weight: 600">信息</span>
</a-divider>
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="24" v-if="!isUpdate">
<a-form-item <a-form-item label="关联用户" name="userId">
label="用户ID" <SelectUser
name="userId" :key="selectedUserText"
:label-col="{ span: 8 }" :value="selectedUserText"
:wrapper-col="{ span: 16 }" :placeholder="`选择用户`"
> @done="onChooseUser"
<a-input-number
:min="1"
placeholder="请输入用户ID"
v-model:value="form.userId"
style="width: 100%"
:disabled="isUpdate"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item <a-form-item label="类型" name="type">
label="姓名" <a-select v-model:value="form.type" placeholder="请选择类型">
name="realName" <a-select-option :value="0">分销商</a-select-option>
:label-col="{ span: 8 }" <a-select-option :value="1">门店</a-select-option>
:wrapper-col="{ span: 16 }" <a-select-option :value="2">总分销商</a-select-option>
> </a-select>
<a-input
allow-clear
placeholder="请输入真实姓名"
v-model:value="form.realName"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
<a-form-item <a-form-item label="手机号" name="mobile">
label="手机号"
name="mobile"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<a-input <a-input
allow-clear allow-clear
:maxlength="11"
placeholder="请输入手机号" placeholder="请输入手机号"
:disabled="true"
v-model:value="form.mobile" v-model:value="form.mobile"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item <a-form-item label="真实姓名" name="realName">
label="支付密码"
name="payPassword"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<a-input-password
allow-clear
placeholder="请输入支付密码"
v-model:value="form.payPassword"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 佣金信息 -->
<a-divider orientation="left">佣金信息</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item
label="可提现佣金"
name="money"
:label-col="{ span: 12 }"
:wrapper-col="{ span: 12 }"
>
<a-input-number
:min="0"
:precision="2"
placeholder="0.00"
v-model:value="form.money"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item
label="冻结佣金"
name="freezeMoney"
:label-col="{ span: 12 }"
:wrapper-col="{ span: 12 }"
>
<a-input-number
:min="0"
:precision="2"
placeholder="0.00"
v-model:value="form.freezeMoney"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item
label="累计提现"
name="totalMoney"
:label-col="{ span: 12 }"
:wrapper-col="{ span: 12 }"
>
<a-input-number
:min="0"
:precision="2"
placeholder="0.00"
v-model:value="form.totalMoney"
style="width: 100%"
:disabled="true"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<!-- 推荐关系 -->
<a-divider orientation="left">推荐关系</a-divider>
<a-form-item label="推荐人" name="refereeId">
<a-input-group compact>
<a-input-number
:min="1"
placeholder="请输入推荐人用户ID"
v-model:value="form.refereeId"
style="width: calc(100% - 80px)"
/>
<a-button type="primary" @click="selectReferee">选择</a-button>
</a-input-group>
</a-form-item>
<!-- 团队信息 -->
<a-divider orientation="left">团队信息</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item
label="一级成员"
name="firstNum"
:label-col="{ span: 12 }"
:wrapper-col="{ span: 12 }"
>
<a-input-number
:min="0"
placeholder="0"
v-model:value="form.firstNum"
style="width: 100%"
:disabled="true"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item
label="二级成员"
name="secondNum"
:label-col="{ span: 12 }"
:wrapper-col="{ span: 12 }"
>
<a-input-number
:min="0"
placeholder="0"
v-model:value="form.secondNum"
style="width: 100%"
:disabled="true"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item
label="三级成员"
name="thirdNum"
:label-col="{ span: 12 }"
:wrapper-col="{ span: 12 }"
>
<a-input-number
:min="0"
placeholder="0"
v-model:value="form.thirdNum"
style="width: 100%"
:disabled="true"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<!-- 其他设置 -->
<a-divider orientation="left">其他设置</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item
label="专属二维码"
name="qrcode"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<a-input-group compact>
<a-input <a-input
placeholder="系统自动生成" allow-clear
v-model:value="form.qrcode" :maxlength="20"
style="width: calc(100% - 80px)" placeholder="请输入真实姓名"
:disabled="true" v-model:value="form.realName"
/> />
<a-button type="primary" @click="generateQrcode">生成</a-button>
</a-input-group>
</a-form-item> </a-form-item>
</a-col> </a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item-->
<!-- label="支付密码"-->
<!-- name="payPassword"-->
<!-- :help="isUpdate ? '留空表示不修改' : undefined"-->
<!-- >-->
<!-- <a-input-password-->
<!-- allow-clear-->
<!-- :maxlength="20"-->
<!-- placeholder="请输入支付密码"-->
<!-- v-model:value="form.payPassword"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<a-col :span="12"> <a-col :span="12">
<a-form-item <a-form-item label="头像" name="image">
label="账户状态" <a-image :src="form.avatar" :width="50" :preview="false" />
name="isDelete"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<a-radio-group v-model:value="form.isDelete">
<a-radio :value="0">正常</a-radio>
<a-radio :value="1">已删除</a-radio>
</a-radio-group>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600">佣金信息</span>
</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="可提现" name="money">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.money"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="已冻结" name="freezeMoney">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.freezeMoney"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="累积提现" name="totalMoney">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.totalMoney"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="佣金比例" name="rate">
<a-input-number
class="ele-fluid"
:min="0"
:max="1"
:precision="3"
stringMode
:disabled="true"
placeholder="例如 0.007"
v-model:value="form.rate"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item label="单价" name="price">-->
<!-- <a-input-number-->
<!-- class="ele-fluid"-->
<!-- :min="0"-->
<!-- :precision="2"-->
<!-- stringMode-->
<!-- placeholder="0.00"-->
<!-- v-model:value="form.price"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
</a-row>
<!-- <a-row :gutter="16">-->
<!-- <a-col :span="12">-->
<!-- <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-col>-->
<!-- <a-col :span="12">-->
<!-- <a-form-item label="备注" name="comments">-->
<!-- <a-textarea-->
<!-- :rows="3"-->
<!-- :maxlength="200"-->
<!-- show-count-->
<!-- placeholder="请输入备注"-->
<!-- v-model:value="form.comments"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<!-- </a-row>-->
</a-form> </a-form>
</ele-modal> </ele-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, watch } from 'vue'; import { computed, ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue'; import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro'; import { assignObject, toDateString, uuid } from 'ele-admin-pro';
import { import { addShopDealerUser, updateShopDealerUser } from '@/api/shop/shopDealerUser';
addShopDealerUser,
updateShopDealerUser
} from '@/api/shop/shopDealerUser';
import { ShopDealerUser } from '@/api/shop/shopDealerUser/model'; import { ShopDealerUser } from '@/api/shop/shopDealerUser/model';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types'; import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form'; import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model'; import { FileRecord } from '@/api/system/file/model';
import type { User } from '@/api/system/user/model';
// 是否是修改 // 是否是修改
const isUpdate = ref(false); const isUpdate = ref(false);
@@ -309,12 +230,16 @@
const form = reactive<ShopDealerUser>({ const form = reactive<ShopDealerUser>({
id: undefined, id: undefined,
userId: undefined, userId: undefined,
avatar: undefined,
type: 0,
realName: undefined, realName: undefined,
mobile: undefined, mobile: undefined,
payPassword: undefined, payPassword: '',
money: undefined, money: undefined,
freezeMoney: undefined, freezeMoney: undefined,
totalMoney: undefined, totalMoney: undefined,
rate: undefined,
price: undefined,
refereeId: undefined, refereeId: undefined,
firstNum: undefined, firstNum: undefined,
secondNum: undefined, secondNum: undefined,
@@ -324,8 +249,6 @@
tenantId: undefined, tenantId: undefined,
createTime: undefined, createTime: undefined,
updateTime: undefined, updateTime: undefined,
shopDealerUserId: undefined,
shopDealerUserName: '',
status: 0, status: 0,
comments: '', comments: '',
sortNumber: 100 sortNumber: 100
@@ -336,58 +259,90 @@
emit('update:visible', value); emit('update:visible', value);
}; };
const createTimeText = computed(() => {
return form.createTime ? toDateString(form.createTime, 'yyyy-MM-dd HH:mm:ss') : '';
});
const updateTimeText = computed(() => {
return form.updateTime ? toDateString(form.updateTime, 'yyyy-MM-dd HH:mm:ss') : '';
});
const selectedUserText = ref<string>('');
// 表单验证规则 // 表单验证规则
const rules = reactive({ const rules = reactive({
userId: [ userId: [
{
validator: (_rule: unknown, value: number | undefined) => {
if (!isUpdate.value && !value) {
return Promise.reject(new Error('请选择关联用户'));
}
return Promise.resolve();
},
trigger: 'change'
}
],
type: [
{ {
required: true, required: true,
message: '请输入用户ID', type: 'number',
trigger: 'blur' message: '请选择类型',
trigger: 'change'
} }
], ],
realName: [ realName: [
{ {
required: true, required: true,
message: '请输入真实姓名', type: 'string',
message: '请填写真实姓名',
trigger: 'blur' trigger: 'blur'
}
],
// mobile: [
// {
// required: true,
// type: 'string',
// message: '请填写手机号',
// trigger: 'blur'
// },
// {
// pattern: /^1\\d{10}$/,
// message: '手机号格式不正确',
// trigger: 'blur'
// }
// ],
payPassword: [
{
validator: (_rule: unknown, value: string) => {
if (!isUpdate.value && !value) {
return Promise.reject(new Error('请输入支付密码'));
}
return Promise.resolve();
}, },
{
min: 2,
max: 20,
message: '姓名长度应在2-20个字符之间',
trigger: 'blur'
}
],
mobile: [
{
required: true,
message: '请输入手机号',
trigger: 'blur'
},
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号格式',
trigger: 'blur'
}
],
money: [
{
type: 'number',
min: 0,
message: '可提现佣金不能小于0',
trigger: 'blur'
}
],
freezeMoney: [
{
type: 'number',
min: 0,
message: '冻结佣金不能小于0',
trigger: 'blur' trigger: 'blur'
} }
] ]
}); });
const onChooseUser = (user?: User) => {
if (!user) {
selectedUserText.value = '';
form.userId = undefined;
return;
}
form.userId = user.userId;
// 新增时尽量帮用户填充,避免重复输入
if (!form.realName) {
form.realName = user.realName ?? user.nickname;
}
if (!form.mobile) {
form.mobile = user.phone ?? user.mobile;
}
const name = user.realName ?? user.nickname ?? '';
const phone = user.phone ?? user.mobile ?? '';
selectedUserText.value = phone ? `${name}${phone}` : name;
};
const chooseImage = (data: FileRecord) => { const chooseImage = (data: FileRecord) => {
images.value.push({ images.value.push({
uid: data.id, uid: data.id,
@@ -404,25 +359,6 @@
const { resetFields } = useForm(form, rules); const { resetFields } = useForm(form, rules);
/* 选择推荐人 */
const selectReferee = () => {
message.info('推荐人选择功能待开发');
// 这里可以打开用户选择器
};
/* 生成二维码 */
const generateQrcode = () => {
if (!form.userId) {
message.error('请先填写用户ID');
return;
}
// 生成二维码逻辑
const qrcode = `DEALER_${form.userId}_${Date.now()}`;
form.qrcode = qrcode;
message.success('二维码生成成功');
};
/* 保存编辑 */ /* 保存编辑 */
const save = () => { const save = () => {
if (!formRef.value) { if (!formRef.value) {
@@ -432,46 +368,34 @@
.validate() .validate()
.then(() => { .then(() => {
loading.value = true; loading.value = true;
// 不在弹窗里编辑的字段不提交避免误更新如自增ID、删除标识等
// 数据处理 const {
const formData = { isDelete,
...form tenantId,
}; createTime,
updateTime,
// 确保数值类型正确 ...rest
if (formData.userId) formData.userId = Number(formData.userId); } = form;
if (formData.refereeId) formData.refereeId = Number(formData.refereeId); const formData: ShopDealerUser = { ...rest };
if (formData.money !== undefined) // userId 新增需要,编辑不允许修改
formData.money = Number(formData.money); if (isUpdate.value) {
if (formData.freezeMoney !== undefined) delete formData.userId;
formData.freezeMoney = Number(formData.freezeMoney); }
if (formData.totalMoney !== undefined) // 编辑时留空表示不修改密码
formData.totalMoney = Number(formData.totalMoney); if (isUpdate.value && !formData.payPassword) {
if (formData.firstNum !== undefined) delete formData.payPassword;
formData.firstNum = Number(formData.firstNum); }
if (formData.secondNum !== undefined) const saveOrUpdate = isUpdate.value ? updateShopDealerUser : addShopDealerUser;
formData.secondNum = Number(formData.secondNum);
if (formData.thirdNum !== undefined)
formData.thirdNum = Number(formData.thirdNum);
if (formData.isDelete !== undefined)
formData.isDelete = Number(formData.isDelete);
console.log('提交的数据:', formData);
const saveOrUpdate = isUpdate.value
? updateShopDealerUser
: addShopDealerUser;
saveOrUpdate(formData) saveOrUpdate(formData)
.then((msg) => { .then((msg) => {
loading.value = false; loading.value = false;
message.success(msg || '操作成功'); message.success(msg);
updateVisible(false); updateVisible(false);
emit('done'); emit('done');
}) })
.catch((e) => { .catch((e) => {
loading.value = false; loading.value = false;
console.error('保存失败:', e); message.error(e.message);
message.error(e.message || '操作失败');
}); });
}) })
.catch(() => {}); .catch(() => {});
@@ -481,52 +405,39 @@
() => props.visible, () => props.visible,
(visible) => { (visible) => {
if (visible) { if (visible) {
formRef.value?.clearValidate();
images.value = []; images.value = [];
if (props.data) { if (props.data) {
assignObject(form, props.data); assignObject(form, props.data);
// 不回显密码,避免误操作
form.payPassword = '';
selectedUserText.value = '';
if(props.data.image){ if(props.data.image){
images.value.push({ images.value.push({
uid: uuid(), uid: uuid(),
url: props.data.image, url: props.data.image,
status: 'done' status: 'done'
}); })
} }
isUpdate.value = true; isUpdate.value = true;
} else { } else {
// 新增时确保表单是干净的默认值
resetFields();
form.payPassword = '';
form.type = 0;
form.status = 0;
form.comments = '';
form.sortNumber = 100;
selectedUserText.value = '';
isUpdate.value = false; isUpdate.value = false;
} }
} else { } else {
resetFields(); resetFields();
formRef.value?.clearValidate();
images.value = [];
selectedUserText.value = '';
} }
}, },
{ immediate: true } { immediate: true }
); );
</script> </script>
<style lang="less" scoped>
:deep(.ant-form-item-label) {
text-align: left !important;
}
:deep(.ant-form-item-label > label) {
text-align: left !important;
}
:deep(.ant-divider-horizontal.ant-divider-with-text-left) {
margin: 24px 0 16px 0;
.ant-divider-inner-text {
color: #1890ff;
font-weight: 600;
}
}
:deep(.ant-input-group-addon) {
padding: 0 8px;
}
:deep(.ant-input-number-disabled) {
background-color: #f5f5f5;
color: #999;
}
</style>

View File

@@ -3,116 +3,146 @@
<a-card :bordered="false" :body-style="{ padding: '16px' }"> <a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table <ele-pro-table
ref="tableRef" ref="tableRef"
row-key="applyId" row-key="id"
:columns="columns" :columns="columns"
:datasource="datasource" :datasource="datasource"
:customRow="customRow" :customRow="customRow"
tool-class="ele-toolbar-form" tool-class="ele-toolbar-form"
class="sys-org-table" class="sys-org-table"
v-model:selection="selection"
> >
<template #toolbar> <template #toolbar>
<search <search
@search="reload" @search="reload"
:selection="selection" :selection="selection"
@add="openEdit" @add="openEdit"
@batchApprove="batchApprove" @remove="removeBatch"
@export="exportData" @batchMove="openMove"
/> />
</template> </template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'applyStatus'"> <template v-if="column.key === 'image'">
<a-tag v-if="record.applyStatus === 10" color="orange" <a-image :src="record.image" :width="50" />
>待审核</a-tag </template>
> <template v-if="column.key === 'type'">
<a-tag v-if="record.applyStatus === 20" color="green">已通过</a-tag> <a-tag v-if="record.type === 0">分销商</a-tag>
<a-tag v-if="record.applyStatus === 30" color="red">已驳回</a-tag> <a-tag v-if="record.type === 1" color="orange">门店</a-tag>
<a-tag v-if="record.type === 2" color="purple">总分销商</a-tag>
</template>
<template v-if="column.key === 'qrcode'">
<QrcodeOutlined :style="{fontSize: '24px'}" @click="openQrCode(record)" />
</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>
<template v-if="column.key === 'action'"> <template v-if="column.key === 'action'">
<a @click="openEdit(record)" class="ele-text-primary"> <a-space>
<EditOutlined /> <a @click="openEdit(record)">修改</a>
编辑
</a>
<template v-if="record.applyStatus !== 20">
<a-divider type="vertical" />
<a @click="approveApply(record)" class="ele-text-success">
<CheckOutlined />
通过
</a>
<a-divider type="vertical" />
<a @click="rejectApply(record)" class="ele-text-warning">
<CloseOutlined />
驳回
</a>
<a-divider type="vertical" /> <a-divider type="vertical" />
<a-popconfirm <a-popconfirm
v-if="record.applyStatus != 20" title="确定要删除此记录吗?"
title="确定要删除此申请记录吗?"
@confirm="remove(record)" @confirm="remove(record)"
placement="topRight"
> >
<a class="ele-text-danger"> <a class="ele-text-danger">删除</a>
<DeleteOutlined />
删除
</a>
</a-popconfirm> </a-popconfirm>
</template> </a-space>
</template> </template>
</template> </template>
</ele-pro-table> </ele-pro-table>
</a-card> </a-card>
<!-- 编辑弹窗 --> <!-- 编辑弹窗 -->
<ShopDealerApplyEdit <ShopDealerUserEdit v-model:visible="showEdit" :data="current" @done="reload" />
v-model:visible="showEdit"
:data="current" <!-- 二维码预览 -->
@done="reload" <a-modal
/> v-model:visible="showQrModal"
:title="qrModalTitle"
:footer="null"
:width="380"
centered
destroy-on-close
>
<div style="display: flex; justify-content: center">
<a-image v-if="qrModalUrl" :src="qrModalUrl" :width="280" :preview="false" />
</div>
<div style="display: flex; justify-content: center; margin-top: 12px">
<a-space>
<a-button @click="copyQrUrl">复制链接</a-button>
<a-button type="primary" @click="openQrInNewTab">打开原图</a-button>
</a-space>
</div>
</a-modal>
</a-page-header> </a-page-header>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { createVNode, ref } from 'vue'; import { createVNode, ref, computed } from 'vue';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { import { ExclamationCircleOutlined, QrcodeOutlined } from '@ant-design/icons-vue';
ExclamationCircleOutlined,
CheckOutlined,
CloseOutlined,
EditOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro'; import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type { import type {
DatasourceFunction, DatasourceFunction,
ColumnItem ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types'; } from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue'; import Search from './components/search.vue';
import {getPageTitle} from '@/utils/common'; import {getPageTitle} from '@/utils/common';
import ShopDealerApplyEdit from './components/shopDealerApplyEdit.vue'; import ShopDealerUserEdit from './components/shopDealerUserEdit.vue';
import { import { pageShopDealerUser, removeShopDealerUser, removeBatchShopDealerUser } from '@/api/shop/shopDealerUser';
pageShopDealerApply, import type { ShopDealerUser, ShopDealerUserParam } from '@/api/shop/shopDealerUser/model';
removeShopDealerApply,
removeBatchShopDealerApply,
batchApproveShopDealerApply,
updateShopDealerApply
} from '@/api/shop/shopDealerApply';
import type {
ShopDealerApply,
ShopDealerApplyParam
} from '@/api/shop/shopDealerApply/model';
// 表格实例 // 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null); const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据 // 表格选中数据
const selection = ref<ShopDealerApply[]>([]); const selection = ref<ShopDealerUser[]>([]);
// 当前编辑数据 // 当前编辑数据
const current = ref<ShopDealerApply | null>(null); const current = ref<ShopDealerUser | null>(null);
// 是否显示编辑弹窗 // 是否显示编辑弹窗
const showEdit = ref(false); const showEdit = ref(false);
// 是否显示二维码弹窗
const showQrModal = ref(false);
const qrModalUrl = ref<string>('');
const qrModalTitle = ref<string>('二维码');
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态 // 加载状态
const loading = ref(true); const loading = ref(true);
const getQrCodeUrl = (userId?: number) => {
return `https://glt-api.websoft.top/api/wx-login/getOrderQRCodeUnlimited/uid_${userId ?? ''}`;
};
const openQrCode = (row: ShopDealerUser) => {
if (!row.userId) {
message.warning('缺少用户ID无法生成二维码');
return;
}
qrModalUrl.value = getQrCodeUrl(row.userId);
qrModalTitle.value = row.realName ? `${row.realName} 的二维码` : `UID_${row.userId} 二维码`;
showQrModal.value = true;
};
const copyQrUrl = async () => {
if (!qrModalUrl.value) {
return;
}
try {
await navigator.clipboard.writeText(qrModalUrl.value);
message.success('已复制');
} catch (e) {
message.error('复制失败,请手动复制');
}
};
const openQrInNewTab = () => {
if (!qrModalUrl.value) {
return;
}
window.open(qrModalUrl.value, '_blank');
};
// 表格数据源 // 表格数据源
const datasource: DatasourceFunction = ({ const datasource: DatasourceFunction = ({
page, page,
@@ -124,9 +154,7 @@
if (filters) { if (filters) {
where.status = filters.status; where.status = filters.status;
} }
where.type = 4; return pageShopDealerUser({
where.applyStatus = 20;
return pageShopDealerApply({
...where, ...where,
...orders, ...orders,
page, page,
@@ -134,201 +162,140 @@
}); });
}; };
// 表格列配置 // 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([ const columns = ref<ColumnItem[]>([
{ {
title: 'ID', title: '用户ID',
dataIndex: 'applyId', dataIndex: 'userId',
key: 'applyId', key: 'userId',
align: 'center', width: 90,
width: 80,
fixed: 'left'
}, },
{ {
title: '申请人信息', title: '类型',
key: 'applicantInfo', dataIndex: 'type',
align: 'left', key: 'type',
fixed: 'left',
customRender: ({ record }) => {
return `${record.realName || '-'} (${record.mobile || '-'})`;
}
},
{
title: '申请方式',
dataIndex: 'applyType',
key: 'applyType',
align: 'center',
width: 120,
customRender: ({ text }) => {
const typeMap = {
10: { text: '需审核', color: 'orange' },
20: { text: '免审核', color: 'green' }
};
const type = typeMap[text] || { text: '未知', color: 'default' };
return {
type: 'tag',
props: { color: type.color },
children: type.text
};
}
},
{
title: '审核状态',
dataIndex: 'applyStatus',
key: 'applyStatus',
align: 'center', align: 'center',
width: 120 width: 120
}, },
{ {
title: '推荐人', title: '真实姓名',
dataIndex: 'refereeId', dataIndex: 'realName',
key: 'refereeId', key: 'realName'
align: 'center', },
width: 100, {
customRender: ({ text }) => (text ? `ID: ${text}` : '') title: '手机号',
dataIndex: 'mobile',
key: 'mobile'
},
{
title: '可提现',
dataIndex: 'money',
key: 'money',
width: 120
},
{
title: '已冻结',
dataIndex: 'freezeMoney',
key: 'freezeMoney',
width: 120
},
{
title: '累积提现',
dataIndex: 'totalMoney',
key: 'totalMoney',
width: 120
}, },
// { // {
// title: '申请时间', // title: '推荐人用户ID',
// dataIndex: 'applyTime', // dataIndex: 'refereeId',
// key: 'applyTime', // key: 'refereeId',
// align: 'center', // width: 120
// width: 120,
// customRender: ({ text }) => text ? toDateString(new Date(text), 'yyyy-MM-dd HH:mm') : '-'
// }, // },
// { // {
// title: '审核时间', // title: '成员数量(一级)',
// dataIndex: 'auditTime', // dataIndex: 'firstNum',
// key: 'auditTime', // key: 'firstNum',
// align: 'center', // width: 120
// customRender: ({ text }) => text ? toDateString(new Date(text), 'yyyy-MM-dd HH:mm') : '-' // },
// {
// title: '成员数量(二级)',
// dataIndex: 'secondNum',
// key: 'secondNum',
// width: 120
// },
// {
// title: '成员数量(三级)',
// dataIndex: 'thirdNum',
// key: 'thirdNum',
// width: 120
// }, // },
{ {
title: '驳回原因', title: '专属二维码',
dataIndex: 'rejectReason', dataIndex: 'qrcode',
key: 'rejectReason', key: 'qrcode',
align: 'left', align: 'center'
ellipsis: true,
customRender: ({ text }) => text || '-'
}, },
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '排序号',
// dataIndex: 'sortNumber',
// key: 'sortNumber',
// width: 120
// },
{ {
title: '创建时间', title: '创建时间',
dataIndex: 'createTime', dataIndex: 'createTime',
key: 'createTime', key: 'createTime',
width: 200,
align: 'center', align: 'center',
sorter: true, sorter: true,
ellipsis: true ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 180,
fixed: 'right', fixed: 'right',
align: 'center', align: 'center',
width: 380,
hideInSetting: true hideInSetting: true
} }
]); ]);
/* 搜索 */ /* 搜索 */
const reload = (where?: ShopDealerApplyParam) => { const reload = (where?: ShopDealerUserParam) => {
selection.value = []; selection.value = [];
tableRef?.value?.reload({ where: where }); tableRef?.value?.reload({ where: where });
}; };
/* 审核通过 */
const approveApply = (row: ShopDealerApply) => {
Modal.confirm({
title: '审核通过确认',
content: `确定要通过 ${row.realName} 的经销商申请吗?`,
icon: createVNode(CheckOutlined),
okText: '确认通过',
okType: 'primary',
cancelText: '取消',
onOk: async () => {
const hide = message.loading('正在处理审核...', 0);
try {
await updateShopDealerApply({
...row,
applyId: row.applyId,
applyStatus: 20
});
hide();
message.success('审核通过成功');
reload();
} catch (error: any) {
hide();
message.error(error.message || '审核失败,请重试');
}
}
});
};
/* 审核驳回 */
const rejectApply = (row: ShopDealerApply) => {
let rejectReason = '';
Modal.confirm({
title: '审核驳回',
content: createVNode('div', null, [
createVNode('p', null, `申请人: ${row.realName} (${row.mobile})`),
createVNode('p', { style: 'margin-top: 12px;' }, '请输入驳回原因:'),
createVNode('textarea', {
placeholder: '请输入驳回原因...',
style:
'width: 100%; height: 80px; margin-top: 8px; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;',
onInput: (e: any) => {
rejectReason = e.target.value;
}
})
]),
icon: createVNode(CloseOutlined),
okText: '确认驳回',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
if (!rejectReason.trim()) {
message.error('请输入驳回原因');
return Promise.reject();
}
const hide = message.loading('正在处理审核...', 0);
try {
await updateShopDealerApply({
...row,
applyStatus: 30,
rejectReason: rejectReason.trim()
});
hide();
message.success('审核驳回成功');
reload();
} catch (error: any) {
hide();
message.error(error.message || '审核失败,请重试');
}
}
});
};
/* 打开编辑弹窗 */ /* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerApply) => { const openEdit = (row?: ShopDealerUser) => {
current.value = row ?? null; current.value = row ?? null;
showEdit.value = true; showEdit.value = true;
}; };
/* 删除单个 */ /* 打开批量移动弹窗 */
const remove = (row: ShopDealerApply) => { const openMove = () => {
if (!row.applyId) { showMove.value = true;
message.error('删除失败:缺少必要参数'); };
return;
}
const hide = message.loading('正在删除申请记录...', 0); /* 删除单个 */
removeShopDealerApply(row.applyId) const remove = (row: ShopDealerUser) => {
const hide = message.loading('请求中..', 0);
removeShopDealerUser(row.id)
.then((msg) => { .then((msg) => {
hide(); hide();
message.success(msg || '删除成功'); message.success(msg);
reload(); reload();
}) })
.catch((e) => { .catch((e) => {
hide(); hide();
message.error(e.message || '删除失败'); message.error(e.message);
}); });
}; };
@@ -338,99 +305,34 @@
message.error('请至少选择一条数据'); message.error('请至少选择一条数据');
return; return;
} }
const validIds = selection.value
.filter((d) => d.applyId)
.map((d) => d.applyId);
if (!validIds.length) {
message.error('选中的数据中没有有效的ID');
return;
}
Modal.confirm({ Modal.confirm({
title: '批量删除确认', title: '提示',
content: `确定要删除选中的 ${validIds.length} 条申请记录吗?此操作不可恢复。`, content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined), icon: createVNode(ExclamationCircleOutlined),
maskClosable: true, maskClosable: true,
okText: '确认删除',
okType: 'danger',
cancelText: '取消',
onOk: () => { onOk: () => {
const hide = message.loading( const hide = message.loading('请求中..', 0);
`正在删除 ${validIds.length} 条记录...`, removeBatchShopDealerUser(selection.value.map((d) => d.id))
0
);
removeBatchShopDealerApply(validIds)
.then((msg) => { .then((msg) => {
hide(); hide();
message.success(msg || `成功删除 ${validIds.length} 条记录`); message.success(msg);
selection.value = [];
reload(); reload();
}) })
.catch((e) => { .catch((e) => {
hide(); hide();
message.error(e.message || '批量删除失败'); message.error(e.message);
}); });
} }
}); });
}; };
/* 批量通过 */
const batchApprove = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
const pendingApplies = selection.value.filter(
(item) => item.applyStatus === 10
);
if (!pendingApplies.length) {
message.error('所选申请中没有待审核的记录');
return;
}
Modal.confirm({
title: '批量通过确认',
content: `确定要通过选中的 ${pendingApplies.length} 个申请吗?`,
icon: createVNode(ExclamationCircleOutlined),
okText: '确认通过',
okType: 'primary',
cancelText: '取消',
onOk: async () => {
const hide = message.loading('正在批量通过...', 0);
try {
const ids = pendingApplies.map((item) => item.applyId);
await batchApproveShopDealerApply(ids);
hide();
message.success(`成功通过 ${pendingApplies.length} 个申请`);
selection.value = [];
reload();
} catch (error: any) {
hide();
message.error(error.message || '批量审核失败,请重试');
}
}
});
};
/* 导出数据 */
const exportData = () => {
const hide = message.loading('正在导出申请数据...', 0);
// 这里调用导出API
setTimeout(() => {
hide();
message.success('申请数据导出成功');
}, 2000);
};
/* 查询 */ /* 查询 */
const query = () => { const query = () => {
loading.value = true; loading.value = true;
}; };
/* 自定义行属性 */ /* 自定义行属性 */
const customRow = (record: ShopDealerApply) => { const customRow = (record: ShopDealerUser) => {
return { return {
// 行点击事件 // 行点击事件
onClick: () => { onClick: () => {
@@ -447,71 +349,8 @@
<script lang="ts"> <script lang="ts">
export default { export default {
name: 'ShopDealerApply' name: 'ShopDealerUser'
}; };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped></style>
.sys-org-table {
:deep(.ant-table-thead > tr > th) {
background-color: #fafafa;
font-weight: 600;
}
:deep(.ant-table-tbody > tr:hover > td) {
background-color: #f8f9fa;
}
}
.detail-item {
p {
margin: 4px 0;
color: #666;
}
strong {
color: #1890ff;
font-size: 14px;
}
}
.ele-text-primary {
color: #1890ff;
&:hover {
color: #40a9ff;
}
}
.ele-text-info {
color: #13c2c2;
&:hover {
color: #36cfc9;
}
}
.ele-text-success {
color: #52c41a;
&:hover {
color: #73d13d;
}
}
.ele-text-warning {
color: #faad14;
&:hover {
color: #ffc53d;
}
}
.ele-text-danger {
color: #ff4d4f;
&:hover {
color: #ff7875;
}
}
</style>

View File

@@ -0,0 +1,51 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-input-search
allow-clear
placeholder="姓名|电话"
style="width: 240px"
v-model:value="where.keywords"
@search="reload"
/>
</a-space>
</template>
<script lang="ts" setup>
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
import useSearch from "@/utils/use-search";
import {ShopDealerUserParam} from "@/api/shop/shopDealerUser/model";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
const reload = () => {
emit('search', where);
};
// 表单数据
const { where } = useSearch<ShopDealerUserParam>({
keywords: '',
userId: undefined,
mobile: undefined,
realName: undefined
});
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,468 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="1000"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:confirm-loading="loading"
: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-row :gutter="16">
<a-col :span="12">
<a-form-item label="绑定门店" name="shopName">
<a-input
allow-clear
placeholder="店铺名称"
:disabled="true"
v-model:value="form.shopName"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item label="小区名称" name="community">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- :maxlength="20"-->
<!-- placeholder="请输入小区名称"-->
<!-- v-model:value="form.community"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
</a-row>
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600">基础信息</span>
</a-divider>
<a-row :gutter="16">
<a-col :span="24" v-if="!isUpdate">
<a-form-item label="关联用户" name="userId">
<SelectUser
:key="selectedUserText"
:value="selectedUserText"
:placeholder="`选择用户`"
@done="onChooseUser"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="类型" name="type">
<a-select v-model:value="form.type" :disabled="true" placeholder="请选择类型">
<a-select-option :value="0">分销商</a-select-option>
<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 :span="12">
<a-form-item label="手机号" name="mobile">
<a-input
allow-clear
:maxlength="11"
placeholder="请输入手机号"
:disabled="true"
v-model:value="form.mobile"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="真实姓名" name="realName">
<a-input
allow-clear
:maxlength="20"
:disabled="true"
placeholder="请输入真实姓名"
v-model:value="form.realName"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item-->
<!-- label="支付密码"-->
<!-- name="payPassword"-->
<!-- :help="isUpdate ? '留空表示不修改' : undefined"-->
<!-- >-->
<!-- <a-input-password-->
<!-- allow-clear-->
<!-- :maxlength="20"-->
<!-- placeholder="请输入支付密码"-->
<!-- v-model:value="form.payPassword"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<a-col :span="12">
<a-form-item label="头像" name="image">
<a-image :src="form.avatar" :width="50" :preview="false" />
</a-form-item>
</a-col>
</a-row>
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600">佣金信息</span>
</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="可提现" name="money">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.money"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="已冻结" name="freezeMoney">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.freezeMoney"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="累积提现" name="totalMoney">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.totalMoney"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="佣金比例" name="rate">
<a-input-number
class="ele-fluid"
:min="0"
:max="1"
:precision="4"
stringMode
:disabled="true"
placeholder="例如 0.007"
v-model:value="form.rate"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item label="单价" name="price">-->
<!-- <a-input-number-->
<!-- class="ele-fluid"-->
<!-- :min="0"-->
<!-- :precision="2"-->
<!-- stringMode-->
<!-- placeholder="0.00"-->
<!-- v-model:value="form.price"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
</a-row>
<!-- <a-row :gutter="16">-->
<!-- <a-col :span="12">-->
<!-- <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-col>-->
<!-- <a-col :span="12">-->
<!-- <a-form-item label="备注" name="comments">-->
<!-- <a-textarea-->
<!-- :rows="3"-->
<!-- :maxlength="200"-->
<!-- show-count-->
<!-- placeholder="请输入备注"-->
<!-- v-model:value="form.comments"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<!-- </a-row>-->
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, toDateString, uuid } from 'ele-admin-pro';
import { addShopDealerUser, updateShopDealerUser } from '@/api/shop/shopDealerUser';
import { ShopDealerUser } from '@/api/shop/shopDealerUser/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';
import type { 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?: ShopDealerUser | 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<ShopDealerUser>({
id: undefined,
userId: undefined,
avatar: undefined,
type: 0,
realName: undefined,
mobile: undefined,
payPassword: '',
money: undefined,
freezeMoney: undefined,
totalMoney: undefined,
rate: undefined,
price: undefined,
refereeId: undefined,
firstNum: undefined,
secondNum: undefined,
thirdNum: undefined,
shopName: undefined,
qrcode: undefined,
isDelete: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const createTimeText = computed(() => {
return form.createTime ? toDateString(form.createTime, 'yyyy-MM-dd HH:mm:ss') : '';
});
const updateTimeText = computed(() => {
return form.updateTime ? toDateString(form.updateTime, 'yyyy-MM-dd HH:mm:ss') : '';
});
const selectedUserText = ref<string>('');
// 表单验证规则
const rules = reactive({
userId: [
{
validator: (_rule: unknown, value: number | undefined) => {
if (!isUpdate.value && !value) {
return Promise.reject(new Error('请选择关联用户'));
}
return Promise.resolve();
},
trigger: 'change'
}
],
type: [
{
required: true,
type: 'number',
message: '请选择类型',
trigger: 'change'
}
],
dealerName: [
{
required: true,
type: 'string',
message: '请填写真实姓名',
trigger: 'blur'
}
],
// mobile: [
// {
// required: true,
// type: 'string',
// message: '请填写手机号',
// trigger: 'blur'
// },
// {
// pattern: /^1\\d{10}$/,
// message: '手机号格式不正确',
// trigger: 'blur'
// }
// ],
payPassword: [
{
validator: (_rule: unknown, value: string) => {
if (!isUpdate.value && !value) {
return Promise.reject(new Error('请输入支付密码'));
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
const onChooseUser = (user?: User) => {
if (!user) {
selectedUserText.value = '';
form.userId = undefined;
return;
}
form.userId = user.userId;
// 新增时尽量帮用户填充,避免重复输入
if (!form.realName) {
form.realName = user.realName ?? user.nickname;
}
if (!form.mobile) {
form.mobile = user.phone ?? user.mobile;
}
const name = user.realName ?? user.nickname ?? '';
const phone = user.phone ?? user.mobile ?? '';
selectedUserText.value = phone ? `${name}${phone}` : name;
};
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;
// 不在弹窗里编辑的字段不提交避免误更新如自增ID、删除标识等
const {
isDelete,
tenantId,
createTime,
updateTime,
...rest
} = form;
const formData: ShopDealerUser = { ...rest };
// userId 新增需要,编辑不允许修改
if (isUpdate.value) {
delete formData.userId;
}
// 编辑时留空表示不修改密码
if (isUpdate.value && !formData.payPassword) {
delete formData.payPassword;
}
const saveOrUpdate = isUpdate.value ? updateShopDealerUser : addShopDealerUser;
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) {
formRef.value?.clearValidate();
images.value = [];
if (props.data) {
assignObject(form, props.data);
// 不回显密码,避免误操作
form.payPassword = '';
selectedUserText.value = '';
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
// 新增时确保表单是干净的默认值
resetFields();
form.payPassword = '';
form.type = 0;
form.status = 0;
form.comments = '';
form.sortNumber = 100;
selectedUserText.value = '';
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
images.value = [];
selectedUserText.value = '';
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,359 @@
<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 === 'shopName'">
<div class="text-purple-500">{{ record.shopName || '-' }}</div>
</template>
<template v-if="column.key === 'type'">
<a-tag v-if="record.type === 0">分销商</a-tag>
<a-tag v-if="record.type === 1" color="orange">门店</a-tag>
<a-tag v-if="record.type === 2" color="purple">配送员</a-tag>
</template>
<template v-if="column.key === 'qrcode'">
<QrcodeOutlined :style="{fontSize: '24px'}" @click="openQrCode(record)" />
</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>
<!-- 编辑弹窗 -->
<ShopDealerUserEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 二维码预览 -->
<a-modal
v-model:visible="showQrModal"
:title="qrModalTitle"
:footer="null"
:width="380"
centered
destroy-on-close
>
<div style="display: flex; justify-content: center">
<a-image v-if="qrModalUrl" :src="qrModalUrl" :width="280" :preview="false" />
</div>
<div style="display: flex; justify-content: center; margin-top: 12px">
<a-space>
<a-button @click="copyQrUrl">复制链接</a-button>
<a-button type="primary" @click="openQrInNewTab">打开原图</a-button>
</a-space>
</div>
</a-modal>
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined, QrcodeOutlined } 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 {getPageTitle} from '@/utils/common';
import ShopDealerUserEdit from './components/shopDealerUserEdit.vue';
import { pageShopDealerUser, removeShopDealerUser, removeBatchShopDealerUser } from '@/api/shop/shopDealerUser';
import type { ShopDealerUser, ShopDealerUserParam } from '@/api/shop/shopDealerUser/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopDealerUser[]>([]);
// 当前编辑数据
const current = ref<ShopDealerUser | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示二维码弹窗
const showQrModal = ref(false);
const qrModalUrl = ref<string>('');
const qrModalTitle = ref<string>('二维码');
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
const getQrCodeUrl = (userId?: number) => {
return `https://glt-api.websoft.top/api/wx-login/getOrderQRCodeUnlimited/uid_${userId ?? ''}`;
};
const openQrCode = (row: ShopDealerUser) => {
if (!row.userId) {
message.warning('缺少用户ID无法生成二维码');
return;
}
qrModalUrl.value = getQrCodeUrl(row.userId);
qrModalTitle.value = row.realName ? `${row.realName} 的二维码` : `UID_${row.userId} 二维码`;
showQrModal.value = true;
};
const copyQrUrl = async () => {
if (!qrModalUrl.value) {
return;
}
try {
await navigator.clipboard.writeText(qrModalUrl.value);
message.success('已复制');
} catch (e) {
message.error('复制失败,请手动复制');
}
};
const openQrInNewTab = () => {
if (!qrModalUrl.value) {
return;
}
window.open(qrModalUrl.value, '_blank');
};
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
where.type = 2;
return pageShopDealerUser({
...where,
...orders,
page,
limit
});
};
// 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
width: 90,
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
align: 'center',
width: 120
},
{
title: '所属门店',
dataIndex: 'shopName',
key: 'shopName'
},
{
title: '真实姓名',
dataIndex: 'realName',
key: 'realName'
},
{
title: '手机号',
dataIndex: 'mobile',
key: 'mobile'
},
// {
// title: '工资',
// dataIndex: 'money',
// key: 'money',
// width: 120
// },
// {
// title: '已冻结',
// dataIndex: 'freezeMoney',
// key: 'freezeMoney',
// width: 120
// },
{
title: '工资统计',
dataIndex: 'totalMoney',
key: 'totalMoney',
width: 120
},
// {
// title: '推荐人用户ID',
// dataIndex: 'refereeId',
// key: 'refereeId',
// width: 120
// },
// {
// title: '成员数量(一级)',
// dataIndex: 'firstNum',
// key: 'firstNum',
// width: 120
// },
// {
// title: '成员数量(二级)',
// dataIndex: 'secondNum',
// key: 'secondNum',
// width: 120
// },
// {
// title: '成员数量(三级)',
// dataIndex: 'thirdNum',
// key: 'thirdNum',
// width: 120
// },
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '排序号',
// dataIndex: 'sortNumber',
// key: 'sortNumber',
// width: 120
// },
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
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?: ShopDealerUserParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerUser) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerUser) => {
const hide = message.loading('请求中..', 0);
removeShopDealerUser(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);
removeBatchShopDealerUser(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: ShopDealerUser) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopDealerUser'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,51 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-input-search
allow-clear
placeholder="姓名|电话"
style="width: 240px"
v-model:value="where.keywords"
@search="reload"
/>
</a-space>
</template>
<script lang="ts" setup>
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
import useSearch from "@/utils/use-search";
import {ShopDealerUserParam} from "@/api/shop/shopDealerUser/model";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
const reload = () => {
emit('search', where);
};
// 表单数据
const { where } = useSearch<ShopDealerUserParam>({
keywords: '',
userId: undefined,
mobile: undefined,
realName: undefined
});
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,463 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="1000"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:confirm-loading="loading"
: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-divider orientation="left">
<span style="color: #1890ff; font-weight: 600">店铺信息</span>
</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="店铺名称" name="dealerName">
<a-input
allow-clear
placeholder="店铺名称"
v-model:value="form.dealerName"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="小区名称" name="community">
<SelectCommunity
:key="`community`"
:value="form.community"
:placeholder="`选择小区`"
@done="onCommunity"
/>
</a-form-item>
</a-col>
</a-row>
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600">基础信息</span>
</a-divider>
<a-row :gutter="16">
<a-col :span="24" v-if="!isUpdate">
<a-form-item label="关联用户" name="userId">
<SelectUser
:key="selectedUserText"
:value="selectedUserText"
:placeholder="`选择用户`"
@done="onChooseUser"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="类型" name="type">
<a-select v-model:value="form.type" :disabled="true" placeholder="请选择类型">
<a-select-option :value="0">经销商</a-select-option>
<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 :span="12">
<a-form-item label="手机号" name="mobile">
<a-input
allow-clear
:maxlength="11"
placeholder="请输入手机号"
:disabled="true"
v-model:value="form.mobile"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="真实姓名" name="realName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入真实姓名"
:disabled="true"
v-model:value="form.realName"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item-->
<!-- label="支付密码"-->
<!-- name="payPassword"-->
<!-- :help="isUpdate ? '留空表示不修改' : undefined"-->
<!-- >-->
<!-- <a-input-password-->
<!-- allow-clear-->
<!-- :maxlength="20"-->
<!-- placeholder="请输入支付密码"-->
<!-- v-model:value="form.payPassword"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<a-col :span="12">
<a-form-item label="头像" name="image">
<a-image :src="form.avatar" :width="50" :preview="false" />
</a-form-item>
</a-col>
</a-row>
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600">佣金信息</span>
</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="可提现" name="money">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.money"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="已冻结" name="freezeMoney">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.freezeMoney"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="累积提现" name="totalMoney">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.totalMoney"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="佣金比例" name="rate">
<a-input-number
class="ele-fluid"
:min="0"
:max="1"
:precision="4"
stringMode
:disabled="true"
placeholder="例如 0.007"
v-model:value="form.rate"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item label="单价" name="price">-->
<!-- <a-input-number-->
<!-- class="ele-fluid"-->
<!-- :min="0"-->
<!-- :precision="2"-->
<!-- stringMode-->
<!-- placeholder="0.00"-->
<!-- v-model:value="form.price"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
</a-row>
<!-- <a-row :gutter="16">-->
<!-- <a-col :span="12">-->
<!-- <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-col>-->
<!-- <a-col :span="12">-->
<!-- <a-form-item label="备注" name="comments">-->
<!-- <a-textarea-->
<!-- :rows="3"-->
<!-- :maxlength="200"-->
<!-- show-count-->
<!-- placeholder="请输入备注"-->
<!-- v-model:value="form.comments"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<!-- </a-row>-->
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, toDateString, uuid } from 'ele-admin-pro';
import { addShopDealerUser, updateShopDealerUser } from '@/api/shop/shopDealerUser';
import { ShopDealerUser } from '@/api/shop/shopDealerUser/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 type { User } from '@/api/system/user/model';
import SelectCommunity from '@/components/SelectCommunity/index.vue';
import {ShopCommunity} from "@/api/shop/shopCommunity/model";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerUser | 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<ShopDealerUser>({
id: undefined,
userId: undefined,
avatar: undefined,
type: 0,
dealerName: undefined,
community: undefined,
realName: undefined,
mobile: undefined,
payPassword: '',
money: undefined,
freezeMoney: undefined,
totalMoney: undefined,
rate: undefined,
price: undefined,
refereeId: undefined,
firstNum: undefined,
secondNum: undefined,
thirdNum: undefined,
qrcode: undefined,
isDelete: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const selectedUserText = ref<string>('');
// 表单验证规则
const rules = reactive({
userId: [
{
validator: (_rule: unknown, value: number | undefined) => {
if (!isUpdate.value && !value) {
return Promise.reject(new Error('请选择关联用户'));
}
return Promise.resolve();
},
trigger: 'change'
}
],
type: [
{
required: true,
type: 'number',
message: '请选择类型',
trigger: 'change'
}
],
dealerName: [
{
required: true,
type: 'string',
message: '请填写店铺名称',
trigger: 'blur'
}
],
community: [
{
required: true,
type: 'string',
message: '请选择所在小区',
trigger: 'blur'
}
],
// mobile: [
// {
// required: true,
// type: 'string',
// message: '请填写手机号',
// trigger: 'blur'
// },
// {
// pattern: /^1\\d{10}$/,
// message: '手机号格式不正确',
// trigger: 'blur'
// }
// ],
payPassword: [
{
validator: (_rule: unknown, value: string) => {
if (!isUpdate.value && !value) {
return Promise.reject(new Error('请输入支付密码'));
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
const onChooseUser = (user?: User) => {
if (!user) {
selectedUserText.value = '';
form.userId = undefined;
return;
}
form.userId = user.userId;
// 新增时尽量帮用户填充,避免重复输入
if (!form.realName) {
form.realName = user.realName ?? user.nickname;
}
if (!form.mobile) {
form.mobile = user.phone ?? user.mobile;
}
const name = user.realName ?? user.nickname ?? '';
const phone = user.phone ?? user.mobile ?? '';
selectedUserText.value = phone ? `${name}${phone}` : name;
};
const onCommunity = (data: ShopCommunity) => {
form.community = data.name;
}
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
// 不在弹窗里编辑的字段不提交避免误更新如自增ID、删除标识等
const {
isDelete,
tenantId,
createTime,
updateTime,
...rest
} = form;
const formData: ShopDealerUser = { ...rest };
// userId 新增需要,编辑不允许修改
if (isUpdate.value) {
delete formData.userId;
}
// 编辑时留空表示不修改密码
if (isUpdate.value && !formData.payPassword) {
delete formData.payPassword;
}
const saveOrUpdate = isUpdate.value ? updateShopDealerUser : addShopDealerUser;
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) {
formRef.value?.clearValidate();
images.value = [];
if (props.data) {
assignObject(form, props.data);
// 不回显密码,避免误操作
form.payPassword = '';
selectedUserText.value = '';
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
// 新增时确保表单是干净的默认值
resetFields();
form.payPassword = '';
form.type = 0;
form.status = 0;
form.comments = '';
form.sortNumber = 100;
selectedUserText.value = '';
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
images.value = [];
selectedUserText.value = '';
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,370 @@
<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 === 'dealerName'">
<div class="text-orange-500">{{ record.dealerName || '-' }}</div>
</template>
<template v-if="column.key === 'type'">
<a-tag v-if="record.type === 0">分销商</a-tag>
<a-tag v-if="record.type === 1" color="orange">门店</a-tag>
<a-tag v-if="record.type === 2" color="purple">配送员</a-tag>
</template>
<template v-if="column.key === 'qrcode'">
<QrcodeOutlined :style="{fontSize: '24px'}" @click="openQrCode(record)" />
</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>
<!-- 编辑弹窗 -->
<ShopDealerUserEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 二维码预览 -->
<a-modal
v-model:visible="showQrModal"
:title="qrModalTitle"
:footer="null"
:width="380"
centered
destroy-on-close
>
<div style="display: flex; justify-content: center">
<a-image v-if="qrModalUrl" :src="qrModalUrl" :width="280" :preview="false" />
</div>
<div style="display: flex; justify-content: center; margin-top: 12px">
<a-space>
<a-button @click="copyQrUrl">复制链接</a-button>
<a-button type="primary" @click="openQrInNewTab">打开原图</a-button>
</a-space>
</div>
</a-modal>
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined, QrcodeOutlined } 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 {getPageTitle} from '@/utils/common';
import ShopDealerUserEdit from './components/shopDealerUserEdit.vue';
import { pageShopDealerUser, removeShopDealerUser, removeBatchShopDealerUser } from '@/api/shop/shopDealerUser';
import type { ShopDealerUser, ShopDealerUserParam } from '@/api/shop/shopDealerUser/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopDealerUser[]>([]);
// 当前编辑数据
const current = ref<ShopDealerUser | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示二维码弹窗
const showQrModal = ref(false);
const qrModalUrl = ref<string>('');
const qrModalTitle = ref<string>('二维码');
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
const getQrCodeUrl = (userId?: number) => {
return `https://glt-api.websoft.top/api/wx-login/getOrderQRCodeUnlimited/uid_${userId ?? ''}`;
};
const openQrCode = (row: ShopDealerUser) => {
if (!row.userId) {
message.warning('缺少用户ID无法生成二维码');
return;
}
qrModalUrl.value = getQrCodeUrl(row.userId);
qrModalTitle.value = row.realName ? `${row.realName} 的二维码` : `UID_${row.userId} 二维码`;
showQrModal.value = true;
};
const copyQrUrl = async () => {
if (!qrModalUrl.value) {
return;
}
try {
await navigator.clipboard.writeText(qrModalUrl.value);
message.success('已复制');
} catch (e) {
message.error('复制失败,请手动复制');
}
};
const openQrInNewTab = () => {
if (!qrModalUrl.value) {
return;
}
window.open(qrModalUrl.value, '_blank');
};
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
where.type = 1;
return pageShopDealerUser({
...where,
...orders,
page,
limit
});
};
// 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
width: 90,
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
align: 'center',
width: 120
},
{
title: '店铺名称',
dataIndex: 'dealerName',
key: 'dealerName'
},
{
title: '所属小区',
dataIndex: 'community',
key: 'community'
},
{
title: '真实姓名',
dataIndex: 'realName',
key: 'realName'
},
{
title: '手机号',
dataIndex: 'mobile',
key: 'mobile'
},
{
title: '可提现',
dataIndex: 'money',
key: 'money',
width: 120
},
{
title: '已冻结',
dataIndex: 'freezeMoney',
key: 'freezeMoney',
width: 120
},
{
title: '累积提现',
dataIndex: 'totalMoney',
key: 'totalMoney',
width: 120
},
// {
// title: '推荐人用户ID',
// dataIndex: 'refereeId',
// key: 'refereeId',
// width: 120
// },
// {
// title: '成员数量(一级)',
// dataIndex: 'firstNum',
// key: 'firstNum',
// width: 120
// },
// {
// title: '成员数量(二级)',
// dataIndex: 'secondNum',
// key: 'secondNum',
// width: 120
// },
// {
// title: '成员数量(三级)',
// dataIndex: 'thirdNum',
// key: 'thirdNum',
// width: 120
// },
{
title: '专属二维码',
dataIndex: 'qrcode',
key: 'qrcode',
align: 'center'
},
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '排序号',
// dataIndex: 'sortNumber',
// key: 'sortNumber',
// width: 120
// },
// {
// title: '创建时间',
// dataIndex: 'createTime',
// key: 'createTime',
// width: 200,
// 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?: ShopDealerUserParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerUser) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerUser) => {
const hide = message.loading('请求中..', 0);
removeShopDealerUser(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);
removeBatchShopDealerUser(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: ShopDealerUser) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopDealerUser'
};
</script>
<style lang="less" scoped></style>

View File

@@ -37,7 +37,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { message } from 'ant-design-vue/es'; import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue'; import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { importSdyDealerOrder } from '@/api/sdy/sdyDealerOrder'; import { importShopDealerOrder } from '@/api/shop/shopDealerOrder';
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'done'): void; (e: 'done'): void;
@@ -68,7 +68,7 @@
return false; return false;
} }
loading.value = true; loading.value = true;
importSdyDealerOrder(file) importShopDealerOrder(file)
.then((msg) => { .then((msg) => {
loading.value = false; loading.value = false;
message.success(msg); message.success(msg);

View File

@@ -33,6 +33,7 @@
v-model:value="where.keywords" v-model:value="where.keywords"
@search="handleSearch" @search="handleSearch"
/> />
<a-button type="dashed" @click="handleExport">导出xls</a-button>
<a-button @click="resetSearch"> 重置 </a-button> <a-button @click="resetSearch"> 重置 </a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
@@ -46,9 +47,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { DeleteOutlined } from '@ant-design/icons-vue'; import { DeleteOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { utils, writeFile } from 'xlsx';
import Import from './Import.vue'; import Import from './Import.vue';
import useSearch from '@/utils/use-search'; import useSearch from '@/utils/use-search';
import { ShopDealerWithdrawParam } from '@/api/shop/shopDealerWithdraw/model'; import { getTenantId } from '@/utils/domain';
import { pageShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw';
import type {
ShopDealerWithdraw,
ShopDealerWithdrawParam
} from '@/api/shop/shopDealerWithdraw/model';
withDefaults( withDefaults(
defineProps<{ defineProps<{
@@ -73,9 +81,13 @@
// 搜索表单 // 搜索表单
const { where, resetFields } = useSearch<ShopDealerWithdrawParam>({ const { where, resetFields } = useSearch<ShopDealerWithdrawParam>({
keywords: '' keywords: '',
page: 1,
limit: 5000
}); });
const list = ref<ShopDealerWithdraw[]>([]);
// 搜索 // 搜索
const handleSearch = () => { const handleSearch = () => {
const searchParams = { ...where }; const searchParams = { ...where };
@@ -88,6 +100,102 @@
emit('search', searchParams); emit('search', searchParams);
}; };
const toMoney = (val: unknown) => {
const n = Number.parseFloat(String(val ?? '0'));
return Number.isFinite(n) ? n.toFixed(2) : '0.00';
};
const toApplyStatus = (val: unknown) => {
const n = Number(val);
if (n === 10) return '待审核';
if (n === 20) return '审核通过';
if (n === 30) return '驳回/取消';
if (n === 40) return '已打款';
return '';
};
const toPayType = (val: unknown) => {
const n = Number(val);
if (n === 10) return '微信/商家转账';
if (n === 20) return '支付宝';
if (n === 30) return '银行卡';
return '';
};
// 导出
const handleExport = async () => {
const array: (string | number)[][] = [
[
'订单号',
'用户',
'手机号',
'提现金额',
'打款方式',
'申请状态',
'审核/打款时间',
'创建时间',
'备注',
'租户ID'
]
];
const params = { ...where };
// 清除空值,避免把空字符串传给后端
Object.keys(params).forEach((key) => {
if (params[key] === '' || params[key] === undefined) {
delete params[key];
}
});
params.page = 1;
params.limit = 5000;
await pageShopDealerWithdraw(params)
.then((data) => {
list.value = data?.list || [];
list.value.forEach((d: ShopDealerWithdraw) => {
array.push([
d.id ?? '',
`${d.realName || d.nickname || '-'}(${d.userId ?? '-'})`,
d.phone ?? '',
toMoney(d.money),
toPayType(d.payType),
toApplyStatus(d.applyStatus),
d.auditTime ?? '',
d.createTime ?? '',
d.comments ?? '',
d.tenantId ?? ''
]);
});
const sheetName = `bak_shop_dealer_withdraw_${getTenantId()}`;
const workbook = { SheetNames: [sheetName], Sheets: {} as any };
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
sheet['!cols'] = [
{ wch: 10 },
{ wch: 20 },
{ wch: 14 },
{ wch: 12 },
{ wch: 14 },
{ wch: 12 },
{ wch: 20 },
{ wch: 20 },
{ wch: 24 },
{ wch: 10 }
];
message.loading('正在导出...', 0);
setTimeout(() => {
writeFile(workbook, `${sheetName}.xlsx`);
message.destroy();
}, 1000);
})
.catch((e: any) => {
message.destroy();
message.error(e?.message ?? String(e));
});
};
// 重置搜索 // 重置搜索
const resetSearch = () => { const resetSearch = () => {
resetFields(); resetFields();

View File

@@ -38,23 +38,23 @@
<div v-if="form.payType === 10" class="payment-info wechat-info"> <div v-if="form.payType === 10" class="payment-info wechat-info">
<a-alert <a-alert
message="微信收款信息" message="微信收款信息"
description="请确保微信账号信息准确,以免影响到账" description="选择审核通过/已打款,立即向用户发起转账"
type="success" type="success"
show-icon show-icon
style="margin-bottom: 16px" style="margin-bottom: 16px"
/> />
<a-form-item label="微信号" name="wechatAccount"> <!-- <a-form-item label="微信号" name="wechatAccount">-->
<a-input <!-- <a-input-->
placeholder="请输入微信号" <!-- placeholder="请输入微信号"-->
v-model:value="form.wechatAccount" <!-- v-model:value="form.wechatAccount"-->
/> <!-- />-->
</a-form-item> <!-- </a-form-item>-->
<a-form-item label="微信昵称" name="wechatName"> <!-- <a-form-item label="微信昵称" name="wechatName">-->
<a-input <!-- <a-input-->
placeholder="请输入微信昵称" <!-- placeholder="请输入微信昵称"-->
v-model:value="form.wechatName" <!-- v-model:value="form.wechatName"-->
/> <!-- />-->
</a-form-item> <!-- </a-form-item>-->
</div> </div>
<!-- 支付宝收款信息 --> <!-- 支付宝收款信息 -->
@@ -171,7 +171,7 @@
<a-form-item <a-form-item
label="上传支付凭证" label="上传支付凭证"
name="image" name="image"
v-if="form.applyStatus === 40" v-if="form.applyStatus === 40 && form.payType !== 10"
> >
<SelectFile <SelectFile
:placeholder="`请选择图片`" :placeholder="`请选择图片`"

View File

@@ -29,37 +29,24 @@
> >
<a-tag v-if="record.applyStatus === 30" color="error">已驳回</a-tag> <a-tag v-if="record.applyStatus === 30" color="error">已驳回</a-tag>
<a-tag v-if="record.applyStatus === 40">已打款</a-tag> <a-tag v-if="record.applyStatus === 40">已打款</a-tag>
<a-tag v-if="record.applyStatus === 50" color="error">已取消</a-tag>
</template> </template>
<template v-if="column.key === 'userInfo'"> <template v-if="column.key === 'userInfo'">
<a-space> <a-space>
<a-avatar :src="record.avatar" /> <a-avatar :src="record.avatar" />
<div class="flex flex-col"> <div class="flex flex-col">
<div>
<span>{{ record.realName || '未实名认证' }}</span> <span>{{ record.realName || '未实名认证' }}</span>
<span class="text-gray-400">{{ record.phone }}</span> <span class="text-gray-400">ID{{ record.userId }}</span>
</div>
<div><span class="text-gray-400">{{ record.phone }}</span></div>
</div> </div>
</a-space> </a-space>
</template> </template>
<template v-if="column.key === 'paymentInfo'"> <template v-if="column.key === 'paymentInfo'">
<template v-if="record.payType === 10"> <template v-if="record.payType === 10">
<a-space direction="vertical"> <a-space direction="vertical">
<a-tag color="blue">微信</a-tag> <a-tag color="blue">商家转账</a-tag>
<span>{{ record.wechatName }}</span>
<span>{{ record.wechatName }}</span>
</a-space>
</template>
<template v-if="record.payType === 20">
<a-space direction="vertical">
<a-tag color="blue">支付宝</a-tag>
<span>{{ record.alipayName }}</span>
<span>{{ record.alipayAccount }}</span>
</a-space>
</template>
<template v-if="record.payType === 30">
<a-space direction="vertical">
<a-tag color="blue">银行卡</a-tag>
<span>{{ record.bankName }}</span>
<span>{{ record.bankAccount }}</span>
<span>{{ record.bankCard }}</span>
</a-space> </a-space>
</template> </template>
</template> </template>
@@ -186,26 +173,27 @@
// 表格列配置 // 表格列配置
const columns = ref<ColumnItem[]>([ const columns = ref<ColumnItem[]>([
{ {
title: '用户ID', title: '订单号',
dataIndex: 'userId', dataIndex: 'id',
key: 'userId', key: 'id',
align: 'center', align: 'center',
width: 90, width: 90,
fixed: 'left' fixed: 'left'
}, },
// {
// title: '用户ID',
// dataIndex: 'userId',
// key: 'userId',
// align: 'center',
// width: 90,
// fixed: 'left'
// },
{ {
title: '提现金额', title: '收款方式',
dataIndex: 'money', dataIndex: 'paymentInfo',
key: 'money', key: 'paymentInfo',
align: 'center', align: 'center',
width: 150, width: 180
customRender: ({ text }) => {
const amount = parseFloat(text || '0').toFixed(2);
return {
type: 'span',
children: `¥${amount}`
};
}
}, },
{ {
title: '用户信息', title: '用户信息',
@@ -213,9 +201,17 @@
key: 'userInfo' key: 'userInfo'
}, },
{ {
title: '收款信息', title: '转账金额',
dataIndex: 'paymentInfo', dataIndex: 'money',
key: 'paymentInfo' key: 'money',
align: 'center',
customRender: ({ text }) => {
const amount = parseFloat(text || '0').toFixed(2);
return {
type: 'span',
children: `¥${amount}`
};
}
}, },
// { // {
// title: '审核时间', // title: '审核时间',
@@ -247,7 +243,7 @@
key: 'comments' key: 'comments'
}, },
{ {
title: '申请状态', title: '状态',
dataIndex: 'applyStatus', dataIndex: 'applyStatus',
key: 'applyStatus', key: 'applyStatus',
align: 'center', align: 'center',

View File

@@ -1,7 +1,7 @@
<!-- 编辑弹窗 --> <!-- 编辑弹窗 -->
<template> <template>
<ele-modal <ele-modal
:width="800" :width="700"
:visible="visible" :visible="visible"
:maskClosable="false" :maskClosable="false"
:maxable="maxable" :maxable="maxable"
@@ -14,7 +14,7 @@
ref="formRef" ref="formRef"
:model="form" :model="form"
:rules="rules" :rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }" :label-col="styleResponsive ? { md: 6, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col=" :wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' } styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
" "
@@ -56,20 +56,6 @@
v-model:value="form.sortNumber" v-model:value="form.sortNumber"
/> />
</a-form-item> </a-form-item>
<a-form-item label="是否删除, 0否, 1是" name="deleted">
<a-input
allow-clear
placeholder="请输入是否删除, 0否, 1是"
v-model:value="form.deleted"
/>
</a-form-item>
<a-form-item label="修改时间" name="updateTime">
<a-input
allow-clear
placeholder="请输入修改时间"
v-model:value="form.updateTime"
/>
</a-form-item>
</a-form> </a-form>
</ele-modal> </ele-modal>
</template> </template>

View File

@@ -123,24 +123,24 @@
key: 'expressName', key: 'expressName',
align: 'center' align: 'center'
}, },
{ // {
title: '物流公司编码 (微信)', // title: '物流公司编码 (微信)',
dataIndex: 'wxCode', // dataIndex: 'wxCode',
key: 'wxCode', // key: 'wxCode',
align: 'center' // align: 'center'
}, // },
{ // {
title: '物流公司编码 (快递100)', // title: '物流公司编码 (快递100)',
dataIndex: 'kuaidi100Code', // dataIndex: 'kuaidi100Code',
key: 'kuaidi100Code', // key: 'kuaidi100Code',
align: 'center' // align: 'center'
}, // },
{ // {
title: '物流公司编码 (快递鸟)', // title: '物流公司编码 (快递鸟)',
dataIndex: 'kdniaoCode', // dataIndex: 'kdniaoCode',
key: 'kdniaoCode', // key: 'kdniaoCode',
align: 'center' // align: 'center'
}, // },
{ {
title: '排序号', title: '排序号',
dataIndex: 'sortNumber', dataIndex: 'sortNumber',

View File

@@ -19,7 +19,7 @@
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' } styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
" "
> >
<a-form-item label="礼品" name="name"> <a-form-item label="礼品" name="name">
<a-input <a-input
allow-clear allow-clear
placeholder="请输入礼品卡名称" placeholder="请输入礼品卡名称"

View File

@@ -24,17 +24,9 @@
> >
<a-tabs type="card" v-model:active-key="active" @change="onChange"> <a-tabs type="card" v-model:active-key="active" @change="onChange">
<a-tab-pane tab="基本信息" key="base"> <a-tab-pane tab="基本信息" key="base">
<a-form-item label="商品ID" name="goodsId"> <!-- <a-form-item label="商品ID" name="goodsId">-->
{{ form.goodsId }} <!-- {{ form.goodsId }}-->
</a-form-item> <!-- </a-form-item>-->
<a-form-item label="商品名称" name="name">
<a-input
allow-clear
style="width: 558px"
placeholder="请输入商品名称"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="所属栏目" name="categoryId"> <a-form-item label="所属栏目" name="categoryId">
<a-tree-select <a-tree-select
allow-clear allow-clear
@@ -49,6 +41,14 @@
@change="onCategoryId" @change="onCategoryId"
/> />
</a-form-item> </a-form-item>
<a-form-item label="商品名称" name="name">
<a-input
allow-clear
style="width: 558px"
placeholder="请输入商品名称"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="商品卖点" name="comments"> <a-form-item label="商品卖点" name="comments">
<a-textarea <a-textarea
:rows="1" :rows="1"
@@ -177,13 +177,64 @@
</a-button> </a-button>
</a-upload> </a-upload>
</a-form-item> </a-form-item>
<a-form-item label="状态" name="isShow"> <a-form-item label="状态" name="status">
<a-radio-group v-model:value="form.isShow"> <a-radio-group v-model:value="form.status">
<a-radio :value="1">上架</a-radio> <a-radio :value="0">上架</a-radio>
<a-radio :value="0">下架</a-radio> <a-radio :value="1">下架</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
</a-tab-pane> </a-tab-pane>
<a-tab-pane tab="商品详情" key="content">
<!-- 富文本编辑器 -->
<div v-if="editor == 1">
<tinymce-editor
ref="editorRef"
class="editor-content"
v-model:value="content"
:disabled="disabled"
:init="config"
placeholder="支持直接粘贴或拖拽图片,也可点击工具栏图片按钮从文件库选择"
/>
<div class="file-selector-tip">
💡 提示工具栏"图片"按钮从图片库选择"上传"按钮快速上传图片"视频"按钮从视频库选择"上传视频"按钮快速上传视频"一键排版"按钮自动优化文章格式"首行缩进"按钮切换段落缩进
</div>
</div>
<!-- Markdown编辑器 -->
<div v-if="editor == 2">
<!-- 📝 Markdown编辑器工具栏扩展 -->
<div class="markdown-toolbar-extension">
<a-button
type="primary"
size="small"
@click="openMarkdownImageSelector"
style="margin-right: 8px;"
>
📷 从图片库选择
</a-button>
<a-button
type="default"
size="small"
@click="openMarkdownVideoSelector"
style="margin-right: 8px;"
>
🎬 从视频库选择
</a-button>
</div>
<MdEditor
v-model="content"
:disabled="disabled"
height="500px"
:placeholder="'请输入Markdown内容...'"
:toolbars="markdownToolbars"
:onUploadImg="onMarkdownUploadImg"
/>
<div class="file-selector-tip">
💡 提示支持Markdown语法可以使用工具栏按钮从文件库选择图片/视频也可以直接拖拽上传文件
</div>
</div>
</a-tab-pane>
<a-tab-pane tab="商品规格" key="spec"> <a-tab-pane tab="商品规格" key="spec">
<a-form-item label="规格类型" name="specs"> <a-form-item label="规格类型" name="specs">
<a-radio-group v-model:value="form.specs"> <a-radio-group v-model:value="form.specs">
@@ -326,57 +377,6 @@
</div> </div>
</a-form-item> </a-form-item>
</a-tab-pane> </a-tab-pane>
<a-tab-pane tab="商品详情" key="content">
<!-- 富文本编辑器 -->
<div v-if="editor == 1">
<tinymce-editor
ref="editorRef"
class="editor-content"
v-model:value="content"
:disabled="disabled"
:init="config"
placeholder="支持直接粘贴或拖拽图片,也可点击工具栏图片按钮从文件库选择"
/>
<div class="file-selector-tip">
💡 提示工具栏"图片"按钮从图片库选择"上传"按钮快速上传图片"视频"按钮从视频库选择"上传视频"按钮快速上传视频"一键排版"按钮自动优化文章格式"首行缩进"按钮切换段落缩进
</div>
</div>
<!-- Markdown编辑器 -->
<div v-if="editor == 2">
<!-- 📝 Markdown编辑器工具栏扩展 -->
<div class="markdown-toolbar-extension">
<a-button
type="primary"
size="small"
@click="openMarkdownImageSelector"
style="margin-right: 8px;"
>
📷 从图片库选择
</a-button>
<a-button
type="default"
size="small"
@click="openMarkdownVideoSelector"
style="margin-right: 8px;"
>
🎬 从视频库选择
</a-button>
</div>
<MdEditor
v-model="content"
:disabled="disabled"
height="500px"
:placeholder="'请输入Markdown内容...'"
:toolbars="markdownToolbars"
:onUploadImg="onMarkdownUploadImg"
/>
<div class="file-selector-tip">
💡 提示支持Markdown语法可以使用工具栏按钮从文件库选择图片/视频也可以直接拖拽上传文件
</div>
</div>
</a-tab-pane>
<a-tab-pane tab="营销设置" key="coupon"> <a-tab-pane tab="营销设置" key="coupon">
<a-form-item label="商品重量" name="goodsWeight"> <a-form-item label="商品重量" name="goodsWeight">
<a-input <a-input
@@ -423,6 +423,15 @@
v-model:value="form.position" v-model:value="form.position"
/> />
</a-form-item> </a-form-item>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
style="width: 250px"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="是否可以快递配送"> <a-form-item label="是否可以快递配送">
<a-switch <a-switch
size="small" size="small"
@@ -439,26 +448,152 @@
:un-checked-value="0" :un-checked-value="0"
/> />
</a-form-item> </a-form-item>
<a-form-item label="是否开启分红角色功能" v-if="!merchantId"> <!-- <a-form-item label="是否开启分红功能" v-if="!merchantId">-->
<!-- <a-switch-->
<!-- size="small"-->
<!-- v-model:checked="form.commissionRole"-->
<!-- :checked-value="1"-->
<!-- :un-checked-value="0"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="角色分红配置" v-if="form.commissionRole === 1">-->
<!-- <a-space>-->
<!-- <a-input-->
<!-- v-model:value="form.goodsRoleCommission[index].amount"-->
<!-- v-for="(item, index) in form.goodsRoleCommission"-->
<!-- :key="index"-->
<!-- >-->
<!-- <template #addonBefore>{{ item.roleName }}</template>-->
<!-- <template #addonAfter></template>-->
<!-- </a-input>-->
<!-- </a-space>-->
<!-- </a-form-item>-->
<a-divider orientation="left">分销设置</a-divider>
<a-form-item label="是否开启分销" name="isOpenCommission">
<a-switch <a-switch
size="small" size="small"
v-model:checked="form.commissionRole" v-model:checked="form.isOpenCommission"
:checked-value="1" :checked-value="1"
:un-checked-value="0" :un-checked-value="0"
/> />
</a-form-item> </a-form-item>
<a-form-item label="角色分红配置" v-if="form.commissionRole === 1"> <template v-if="form.isOpenCommission === 1">
<a-space> <a-form-item label="分佣类型" name="commissionType">
<a-input <a-radio-group v-model:value="form.commissionType">
v-model:value="form.goodsRoleCommission[index].amount" <a-radio :value="10">固定金额</a-radio>
v-for="(item, index) in form.goodsRoleCommission" <a-radio :value="20">百分比</a-radio>
:key="index" </a-radio-group>
>
<template #addonBefore>{{ item.roleName }}</template>
<template #addonAfter></template>
</a-input>
</a-space>
</a-form-item> </a-form-item>
<a-form-item
:label="form.commissionType === 20 ? '一级佣金(%)' : '一级佣金(元)'"
name="firstMoney"
>
<a-input-number
v-model:value="form.firstMoney"
:min="0"
:max="form.commissionType === 20 ? 100 : undefined"
:precision="2"
style="width: 250px"
:placeholder="
form.commissionType === 20
? '请输入一级佣金百分比'
: '请输入一级佣金金额'
"
>
<template #addonAfter>{{
form.commissionType === 20 ? '%' : '元'
}}
</template>
</a-input-number>
</a-form-item>
<a-form-item
:label="form.commissionType === 20 ? '二级佣金(%)' : '二级佣金(元)'"
name="secondMoney"
>
<a-input-number
v-model:value="form.secondMoney"
:min="0"
:max="form.commissionType === 20 ? 100 : undefined"
:precision="2"
style="width: 250px"
:placeholder="
form.commissionType === 20
? '请输入二级佣金百分比'
: '请输入二级佣金金额'
"
>
<template #addonAfter>{{
form.commissionType === 20 ? '%' : '元'
}}
</template>
</a-input-number>
</a-form-item>
<!-- <a-form-item-->
<!-- :label="form.commissionType === 20 ? '三级佣金(%)' : '三级佣金(元)'"-->
<!-- name="thirdMoney"-->
<!-- >-->
<!-- <a-input-number-->
<!-- v-model:value="form.thirdMoney"-->
<!-- :min="0"-->
<!-- :max="form.commissionType === 20 ? 100 : undefined"-->
<!-- :precision="2"-->
<!-- style="width: 250px"-->
<!-- :placeholder="-->
<!-- form.commissionType === 20-->
<!-- ? '请输入三级佣金百分比'-->
<!-- : '请输入三级佣金金额'-->
<!-- "-->
<!-- >-->
<!-- <template #addonAfter>{{-->
<!-- form.commissionType === 20 ? '%' : '元'-->
<!-- }}</template>-->
<!-- </a-input-number>-->
<!-- </a-form-item>-->
<a-form-item label="一级分红" name="firstDividend">
<a-input-number
v-model:value="form.firstDividend"
:min="0"
:max="form.commissionType === 20 ? 100 : undefined"
:precision="2"
style="width: 250px"
placeholder="请输入一级分红"
>
<template #addonAfter>{{
form.commissionType === 20 ? '%' : '元'
}}
</template>
</a-input-number>
</a-form-item>
<a-form-item label="二级分红" name="secondDividend">
<a-input-number
v-model:value="form.secondDividend"
:min="0"
:max="form.commissionType === 20 ? 100 : undefined"
:precision="2"
style="width: 250px"
placeholder="请输入二级分红"
>
<template #addonAfter>{{
form.commissionType === 20 ? '%' : '元'
}}
</template>
</a-input-number>
</a-form-item>
<a-form-item label="配送费奖金" name="deliveryMoney">
<a-input-number
v-model:value="form.deliveryMoney"
:min="0"
:max="0.2"
:precision="2"
:step="0.01"
style="width: 250px"
placeholder="请输入配送费奖金"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</template>
<template v-if="form.type === 1 || merchantId"> <template v-if="form.type === 1 || merchantId">
<a-form-item label="可用日期"> <a-form-item label="可用日期">
<a-select v-model:value="canUseDate" mode="multiple"> <a-select v-model:value="canUseDate" mode="multiple">
@@ -505,15 +640,6 @@
/> />
</a-form-item> </a-form-item>
</template> </template>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
style="width: 250px"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</a-form> </a-form>
@@ -988,6 +1114,15 @@ const form = reactive<ShopGoods>({
categoryName: undefined, categoryName: undefined,
specs: 0, specs: 0,
commissionRole: 0, commissionRole: 0,
// 分销佣金(新字段,后端保持 snake_case
isOpenCommission: 0,
commissionType: 10,
firstMoney: 0,
secondMoney: 0,
thirdMoney: 0,
firstDividend: 0,
secondDividend: 0,
deliveryMoney: 0,
position: undefined, position: undefined,
price: undefined, price: undefined,
originPrice: undefined, originPrice: undefined,
@@ -1710,6 +1845,33 @@ const onPaste = (e) => {
const {resetFields} = useForm(form, rules); const {resetFields} = useForm(form, rules);
const COMMISSION_PERCENT_FIELDS = [
'firstMoney',
'secondMoney',
'thirdMoney',
'firstDividend',
'secondDividend'
] as const;
type CommissionPercentField = (typeof COMMISSION_PERCENT_FIELDS)[number];
const toDbPercent = (val: unknown) => {
if (val === null || val === undefined) return val as any;
const num = Number(val);
if (!Number.isFinite(num)) return val as any;
return num / 100;
};
// Back-end stores percent as a fraction (e.g. 0.1 = 10%); UI inputs percent (e.g. 10 = 10%).
const toUiPercent = (val: unknown) => {
if (val === null || val === undefined) return val as any;
const num = Number(val);
if (!Number.isFinite(num)) return val as any;
// If historical data is already stored as 0~1, convert to 0~100 for display.
if (num <= 1) return Number((num * 100).toFixed(2));
return num;
};
/* 保存编辑 */ /* 保存编辑 */
const save = () => { const save = () => {
if (!formRef.value) { if (!formRef.value) {
@@ -1724,6 +1886,27 @@ const save = () => {
if (ensureTag.value.length) form.ensureTag = ensureTag.value.join(); if (ensureTag.value.length) form.ensureTag = ensureTag.value.join();
if (canUseDate.value.length) form.canUseDate = canUseDate.value.join(); if (canUseDate.value.length) form.canUseDate = canUseDate.value.join();
// 分销关闭时,避免把历史值一并保存到后端
if (form.isOpenCommission !== 1) {
form.commissionType = 10;
form.firstMoney = 0;
form.secondMoney = 0;
form.thirdMoney = 0;
form.firstDividend = 0;
form.secondDividend = 0;
form.deliveryMoney = 0;
}
if (form.isOpenCommission === 1 && form.commissionType === 20) {
// UI 输入0~100保存到后端0~1例如 10% => 0.1
const invalidPercent = COMMISSION_PERCENT_FIELDS.some((k) => {
const v = (form as any)[k] ?? 0;
return v < 0 || v > 100;
});
if (invalidPercent) {
return message.error('佣金百分比需在 0~100 之间');
}
}
// if (form.dealerGift && !form.dealerGiftNum) return message.error('请输入经销商赠品数量'); // if (form.dealerGift && !form.dealerGiftNum) return message.error('请输入经销商赠品数量');
if (form.commissionRole === 1) { if (form.commissionRole === 1) {
for (let i = 0; i < form.goodsRoleCommission.length; i++) { for (let i = 0; i < form.goodsRoleCommission.length; i++) {
@@ -1750,6 +1933,14 @@ const save = () => {
if (isUpdate.value) { if (isUpdate.value) {
formData.type = props.data.type; formData.type = props.data.type;
} }
if (formData.isOpenCommission === 1 && formData.commissionType === 20) {
// Convert percent fields for persistence: 10 => 0.1
COMMISSION_PERCENT_FIELDS.forEach((k) => {
(formData as any)[k as CommissionPercentField] = toDbPercent(
(formData as any)[k]
);
});
}
const saveOrUpdate = isUpdate.value ? updateShopGoods : addShopGoods; const saveOrUpdate = isUpdate.value ? updateShopGoods : addShopGoods;
saveOrUpdate(formData) saveOrUpdate(formData)
.then((msg) => { .then((msg) => {
@@ -1875,6 +2066,17 @@ watch(
ensureTagItem.value = ''; ensureTagItem.value = '';
if (props.data) { if (props.data) {
assignObject(form, props.data); assignObject(form, props.data);
if (form.commissionType === undefined || form.commissionType === null) {
form.commissionType = 10;
}
if (form.isOpenCommission === 1 && form.commissionType === 20) {
// Convert historical DB values (0~1) to UI percent (0~100).
COMMISSION_PERCENT_FIELDS.forEach((k) => {
(form as any)[k as CommissionPercentField] = toUiPercent(
(form as any)[k]
);
});
}
if (props.data.image) { if (props.data.image) {
images.value.push({ images.value.push({
uid: uuid(), uid: uuid(),

View File

@@ -49,7 +49,7 @@
</a-space> </a-space>
</template> </template>
<template v-if="column.key === 'status'"> <template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">出售中</a-tag> <a-tag v-if="record.status === 0" color="green">已上架</a-tag>
<a-tag v-if="record.status === 1" color="orange">待上架</a-tag> <a-tag v-if="record.status === 1" color="orange">待上架</a-tag>
<a-tag v-if="record.status === 2" color="purple">待审核</a-tag> <a-tag v-if="record.status === 2" color="purple">待审核</a-tag>
<a-tag v-if="record.status === 3" color="red">审核不通过</a-tag> <a-tag v-if="record.status === 3" color="red">审核不通过</a-tag>

View File

@@ -3,7 +3,7 @@
<a-modal <a-modal
:visible="visible" :visible="visible"
title="订单发送货" title="订单发送货"
width="600px" width="50%"
:confirm-loading="loading" :confirm-loading="loading"
@update:visible="updateVisible" @update:visible="updateVisible"
@ok="handleSubmit" @ok="handleSubmit"
@@ -73,6 +73,19 @@
</a-row> </a-row>
</a-form-item> </a-form-item>
<!-- 物流单号 -->
<a-form-item
label="物流单号"
name="expressNo"
v-if="form.deliveryType === 0"
>
<a-input
v-model:value="form.expressNo"
placeholder="请输入物流单号"
:maxlength="50"
/>
</a-form-item>
<template v-if="form.deliveryType !== 1"> <template v-if="form.deliveryType !== 1">
<a-form-item label="发货人" name="sendName"> <a-form-item label="发货人" name="sendName">
<a-input <a-input
@@ -96,19 +109,6 @@
</a-form-item> </a-form-item>
</template> </template>
<!-- 快递单号 -->
<!-- <a-form-item-->
<!-- label="快递单号"-->
<!-- name="trackingNumber"-->
<!-- v-if="form.deliveryType === 0"-->
<!-- >-->
<!-- <a-input-->
<!-- v-model:value="form.trackingNumber"-->
<!-- placeholder="请输入快递单号"-->
<!-- :maxlength="50"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- &lt;!&ndash; 分单发货 &ndash;&gt;--> <!-- &lt;!&ndash; 分单发货 &ndash;&gt;-->
<!-- <a-form-item label="分单发货" v-if="form.deliveryType === 0">--> <!-- <a-form-item label="分单发货" v-if="form.deliveryType === 0">-->
<!-- <a-switch--> <!-- <a-switch-->
@@ -178,7 +178,7 @@
deliveryMethod: 'manual', // manual手动填写 print电子面单打印 deliveryMethod: 'manual', // manual手动填写 print电子面单打印
expressId: undefined as number | undefined, expressId: undefined as number | undefined,
expressName: '', expressName: '',
trackingNumber: '', expressNo: '',
partialDelivery: false, partialDelivery: false,
deliveryNote: '', deliveryNote: '',
sendName: '', sendName: '',
@@ -210,18 +210,16 @@
} }
} }
], ],
// trackingNumber: [ expressNo: [
// { {
// required: true, validator: (_: any, value: any) => {
// message: '请输入快递单号', if (form.deliveryType === 0 && !value) {
// validator: (_: any, value: any) => { return Promise.reject('请输入物流单号');
// if (form.deliveryType === 0 && !value) { }
// return Promise.reject('请输入快递单号'); return Promise.resolve();
// } }
// return Promise.resolve(); }
// } ],
// }
// ],
deliveryTime: [{ required: true, message: '请选择发货时间' }], deliveryTime: [{ required: true, message: '请选择发货时间' }],
sendName: [ sendName: [
{ {
@@ -298,6 +296,7 @@
if (type !== 0) { if (type !== 0) {
form.expressId = undefined; form.expressId = undefined;
form.expressName = ''; form.expressName = '';
form.expressNo = '';
form.deliveryMethod = 'manual'; form.deliveryMethod = 'manual';
} }
if (type === 1) { if (type === 1) {
@@ -339,24 +338,31 @@
// 如果是快递配送,添加快递信息 // 如果是快递配送,添加快递信息
if (form.deliveryType === 0) { if (form.deliveryType === 0) {
const express =
expressList.value.find((item) => item.expressId === form.expressId) ||
undefined;
updateData.expressId = form.expressId; updateData.expressId = form.expressId;
updateData.sendName = form.sendName; updateData.sendName = form.sendName;
updateData.sendPhone = form.sendPhone; updateData.sendPhone = form.sendPhone;
updateData.sendAddress = form.sendAddress; updateData.sendAddress = form.sendAddress;
// updateData.expressName = form.expressName; updateData.expressName = form.expressName || express?.expressName;
// updateData.trackingNumber = form.trackingNumber; updateData.expressNo = form.expressNo;
} else if (form.deliveryType === 2) { } else if (form.deliveryType === 2) {
// 商家送货需要记录发货人信息,但不需要快递公司 // 商家送货需要记录发货人信息,但不需要快递公司
updateData.sendName = form.sendName; updateData.sendName = form.sendName;
updateData.sendPhone = form.sendPhone; updateData.sendPhone = form.sendPhone;
updateData.sendAddress = form.sendAddress; updateData.sendAddress = form.sendAddress;
updateData.expressId = undefined; updateData.expressId = undefined;
updateData.expressName = undefined;
updateData.expressNo = undefined;
} else { } else {
// 无需发货,清理快递/发货信息 // 无需发货,清理快递/发货信息
updateData.expressId = undefined; updateData.expressId = undefined;
updateData.sendName = undefined; updateData.sendName = undefined;
updateData.sendPhone = undefined; updateData.sendPhone = undefined;
updateData.sendAddress = undefined; updateData.sendAddress = undefined;
updateData.expressName = undefined;
updateData.expressNo = undefined;
} }
// 分单发货 // 分单发货
@@ -387,7 +393,7 @@
form.deliveryMethod = 'manual'; form.deliveryMethod = 'manual';
form.expressId = undefined; form.expressId = undefined;
form.expressName = ''; form.expressName = '';
form.trackingNumber = ''; form.expressNo = '';
form.partialDelivery = false; form.partialDelivery = false;
form.deliveryNote = ''; form.deliveryNote = '';
form.sendName = ''; form.sendName = '';

View File

@@ -1,7 +1,7 @@
<!-- 用户编辑弹窗 --> <!-- 用户编辑弹窗 -->
<template> <template>
<a-drawer <a-drawer
:width="`65%`" width="70%"
:visible="visible" :visible="visible"
:confirm-loading="loading" :confirm-loading="loading"
:maxable="maxAble" :maxable="maxAble"
@@ -11,110 +11,6 @@
:footer="null" :footer="null"
@ok="save" @ok="save"
> >
<template #extra>
<a-space>
<!-- 未付款状态的操作 -->
<template v-if="!form.payStatus">
<!-- 取消订单未完成且未付款 -->
<a-button
v-if="form.orderStatus === 0"
@click="handleCancelOrder"
danger
:loading="loading"
>
取消订单
</a-button>
<!-- 修改订单未完成且未付款 -->
<a-button
v-if="form.orderStatus === 0"
@click="handleEditOrder"
:loading="loading"
>
修改订单
</a-button>
</template>
<!-- 已付款状态的操作 -->
<template v-if="form.payStatus">
<!-- 发货按钮已付款且未发货且未取消 -->
<a-button
v-if="
form.deliveryStatus === 10 && !isCancelledStatus(form.orderStatus)
"
type="primary"
@click="handleDelivery"
:loading="loading"
>
发货
</a-button>
<!-- 确认收货:已发货且未完成 -->
<a-button
v-if="form.deliveryStatus === 20 && form.orderStatus === 0"
type="primary"
@click="handleConfirmReceive"
:loading="loading"
>
确认收货
</a-button>
</template>
<!-- 退款相关操作 -->
<template v-if="isRefundStatus(form.orderStatus)">
<!-- 同意退款:退款申请中或客户端申请退款 -->
<a-button
v-if="form.orderStatus === 4 || form.orderStatus === 7"
type="primary"
@click="handleApproveRefund"
:loading="loading"
>
同意退款
</a-button>
<!-- 拒绝退款:退款申请中或客户端申请退款 -->
<a-button
v-if="form.orderStatus === 4 || form.orderStatus === 7"
danger
@click="handleRejectRefund"
:loading="loading"
>
拒绝退款
</a-button>
<!-- 重新处理:退款被拒绝 -->
<a-button
v-if="form.orderStatus === 5"
@click="handleRetryRefund"
:loading="loading"
>
重新处理
</a-button>
</template>
<!-- 申请退款:已完成或已发货的订单 -->
<a-button
v-if="canApplyRefund(form)"
@click="handleApplyRefund"
:loading="loading"
>
申请退款
</a-button>
<!-- 删除订单:已完成、已取消、退款成功 -->
<a-button
v-if="canDeleteOrder(form)"
@click="handleDeleteOrder"
danger
:loading="loading"
>
删除订单
</a-button>
<!-- 关闭按钮 -->
<a-button @click="updateVisible(false)"> 关闭 </a-button>
</a-space>
</template>
<a-card title="基本信息" style="margin-bottom: 20px" :bordered="false"> <a-card title="基本信息" style="margin-bottom: 20px" :bordered="false">
<a-descriptions :column="3"> <a-descriptions :column="3">
<!-- 第一排--> <!-- 第一排-->
@@ -300,6 +196,12 @@
<a-tag v-if="form.isInvoice == 2" color="blue">不能开具</a-tag> <a-tag v-if="form.isInvoice == 2" color="blue">不能开具</a-tag>
</a-descriptions-item> </a-descriptions-item>
<!-- 第六排--> <!-- 第六排-->
<a-descriptions-item
label="买家备注"
:labelStyle="{ width: '90px', color: '#808080' }"
>
{{ form.buyerRemarks }}
</a-descriptions-item>
<!-- <a-descriptions-item--> <!-- <a-descriptions-item-->
<!-- label="结算状态"--> <!-- label="结算状态"-->
<!-- :labelStyle="{ width: '90px', color: '#808080' }"--> <!-- :labelStyle="{ width: '90px', color: '#808080' }"-->
@@ -318,6 +220,32 @@
</a-descriptions> </a-descriptions>
</a-card> </a-card>
<a-card title="商品信息" style="margin-bottom: 20px" :bordered="false">
<a-spin :spinning="loading">
<a-table
:data-source="orderGoods"
:columns="columns"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'goodsName'">
<div style="display: flex; align-items: center; gap: 12px">
<a-avatar
:src="record.image || record.goodsImage"
shape="square"
:size="50"
style="flex-shrink: 0"
/>
<span style="flex: 1">{{
record.goodsName || '未知商品'
}}</span>
</div>
</template>
</template>
</a-table>
</a-spin>
</a-card>
<a-card title="收货信息" style="margin-bottom: 20px" :bordered="false"> <a-card title="收货信息" style="margin-bottom: 20px" :bordered="false">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<a-descriptions :column="2"> <a-descriptions :column="2">
@@ -433,48 +361,22 @@
label="快递公司" label="快递公司"
:labelStyle="{ width: '90px', color: '#808080' }" :labelStyle="{ width: '90px', color: '#808080' }"
> >
{{ form.shopOrderDelivery.expressName || '未填写' }} {{
form.expressName ||
form.shopOrderDelivery?.expressName ||
'未填写'
}}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item <a-descriptions-item
label="物流单号" label="物流单号"
:labelStyle="{ width: '90px', color: '#808080' }" :labelStyle="{ width: '90px', color: '#808080' }"
> >
{{ form.shopOrderDelivery.expressNo || '未填写' }} {{ form.expressNo || form.shopOrderDelivery?.expressNo || '未填写' }}
</a-descriptions-item> </a-descriptions-item>
</a-descriptions> </a-descriptions>
</a-spin> </a-spin>
</a-card> </a-card>
<a-card title="商品信息" style="margin-bottom: 20px" :bordered="false">
<a-spin :spinning="loading">
<a-table
:data-source="orderGoods"
:columns="columns"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'goodsName'">
<div style="display: flex; align-items: center; gap: 12px">
<a-avatar
:src="record.image || record.goodsImage"
shape="square"
:size="50"
style="flex-shrink: 0"
/>
<span style="flex: 1">{{
record.goodsName || '未知商品'
}}</span>
</div>
</template>
</template>
</a-table>
</a-spin>
</a-card>
<a-card title="买家留言" style="margin-bottom: 20px" :bordered="false">
<a-spin :spinning="loading">
{{ form.buyerRemarks || '-' }}
</a-spin>
</a-card>
<a-card title="商家备注" style="margin-bottom: 20px" :bordered="false"> <a-card title="商家备注" style="margin-bottom: 20px" :bordered="false">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
{{ form.merchantRemarks || '-' }} {{ form.merchantRemarks || '-' }}
@@ -653,6 +555,10 @@
deliveryTime: undefined, deliveryTime: undefined,
// 无需发货备注 // 无需发货备注
deliveryNote: undefined, deliveryNote: undefined,
// 快递公司名称
expressName: undefined,
// 物流单号
expressNo: undefined,
// 优惠类型0无、1抵扣优惠券、2折扣优惠券、3、VIP月卡、4VIP年卡5VIP次卡、6VIP会员卡、7IC月卡、8IC年卡、9IC次卡、10IC会员卡、11免费订单、12VIP充值卡、13IC充值卡、14VIP季卡、15IC季卡 // 优惠类型0无、1抵扣优惠券、2折扣优惠券、3、VIP月卡、4VIP年卡5VIP次卡、6VIP会员卡、7IC月卡、8IC年卡、9IC次卡、10IC会员卡、11免费订单、12VIP充值卡、13IC充值卡、14VIP季卡、15IC季卡
couponType: undefined, couponType: undefined,
// 优惠说明 // 优惠说明
@@ -736,10 +642,10 @@
}, },
{ {
title: '数量', title: '数量',
dataIndex: 'quantity', dataIndex: 'totalNum',
align: 'center' as const, align: 'center' as const,
customRender: ({ record }: { record: any }) => { customRender: ({ record }: { record: any }) => {
return record.quantity || 1; return record.totalNum || 1;
} }
}, },
{ {

View File

@@ -17,53 +17,53 @@
allow-clear allow-clear
v-model:value="where.orderNo" v-model:value="where.orderNo"
placeholder="订单编号" placeholder="订单编号"
style="width: 240px" style="width: 260px"
@search="reload" @search="reload"
@pressEnter="reload" @pressEnter="reload"
/> />
<a-select
v-model:value="where.type"
style="width: 150px"
placeholder="订单类型"
@change="search"
>
<a-select-option value="">全部</a-select-option>
<a-select-option :value="1">普通订单</a-select-option>
<a-select-option :value="2">秒杀订单</a-select-option>
<a-select-option :value="3">拼团订单</a-select-option>
</a-select>
<a-select
v-model:value="where.payStatus"
style="width: 150px"
placeholder="付款状态"
@change="search"
>
<a-select-option value="">全部</a-select-option>
<a-select-option :value="1">已付款</a-select-option>
<a-select-option :value="0">未付款</a-select-option>
</a-select>
<!-- <a-select--> <!-- <a-select-->
<!-- v-model:value="where.orderStatus"--> <!-- v-model:value="where.type"-->
<!-- style="width: 150px"--> <!-- style="width: 150px"-->
<!-- placeholder="订单状态"--> <!-- placeholder="订单类型"-->
<!-- @change="search"--> <!-- @change="search"-->
<!-- >--> <!-- >-->
<!-- <a-select-option value="">全部</a-select-option>--> <!-- <a-select-option value="">全部</a-select-option>-->
<!-- <a-select-option :value="1">已完成</a-select-option>--> <!-- <a-select-option :value="1">普通订单</a-select-option>-->
<!-- <a-select-option :value="0">未完成</a-select-option>--> <!-- <a-select-option :value="2">秒杀订单</a-select-option>-->
<!-- <a-select-option :value="2">未使用</a-select-option>--> <!-- <a-select-option :value="3">拼团订单</a-select-option>-->
<!-- <a-select-option :value="3">已取消</a-select-option>--> <!-- </a-select>-->
<!-- <a-select-option :value="4">退款中</a-select-option>--> <!-- <a-select-->
<!-- <a-select-option :value="5">退款被拒</a-select-option>--> <!-- v-model:value="where.payStatus"-->
<!-- <a-select-option :value="6">退款成功</a-select-option>--> <!-- style="width: 150px"-->
<!-- placeholder="付款状态"-->
<!-- @change="search"-->
<!-- >-->
<!-- <a-select-option value="">全部</a-select-option>-->
<!-- <a-select-option :value="1">已付款</a-select-option>-->
<!-- <a-select-option :value="0">未付款</a-select-option>-->
<!-- </a-select>--> <!-- </a-select>-->
<a-select <a-select
:options="getPayType()" v-model:value="where.orderStatus"
v-model:value="where.payType"
style="width: 150px" style="width: 150px"
placeholder="付款方式" placeholder="订单状态"
@change="search" @change="search"
/> >
<a-select-option value="">全部</a-select-option>
<a-select-option :value="1">已完成</a-select-option>
<!-- <a-select-option :value="0">未完成</a-select-option>-->
<!-- <a-select-option :value="2">未使用</a-select-option>-->
<a-select-option :value="3">已取消</a-select-option>
<a-select-option :value="4">退款中</a-select-option>
<a-select-option :value="5">退款被拒</a-select-option>
<a-select-option :value="6">退款成功</a-select-option>
</a-select>
<!-- <a-select-->
<!-- :options="getPayType()"-->
<!-- v-model:value="where.payType"-->
<!-- style="width: 150px"-->
<!-- placeholder="付款方式"-->
<!-- @change="search"-->
<!-- />-->
<a-range-picker <a-range-picker
v-model:value="dateRange" v-model:value="dateRange"
@@ -255,11 +255,13 @@
message.error(msg); message.error(msg);
loading.value = false; loading.value = false;
}) })
.finally(() => {}); .finally(() => {
});
}; };
watch( watch(
() => props.selection, () => props.selection,
() => {} () => {
}
); );
</script> </script>

View File

@@ -12,10 +12,10 @@
<a-card :bordered="false" :body-style="{ padding: '16px' }"> <a-card :bordered="false" :body-style="{ padding: '16px' }">
<a-tabs type="card" v-model:activeKey="activeKey" @change="onTabs"> <a-tabs type="card" v-model:activeKey="activeKey" @change="onTabs">
<a-tab-pane key="all" tab="全部" /> <a-tab-pane key="all" tab="全部" />
<a-tab-pane key="unpaid" tab="待付款" />
<a-tab-pane key="undelivered" tab="待发货" /> <a-tab-pane key="undelivered" tab="待发货" />
<a-tab-pane key="unreceived" tab="待收货" /> <a-tab-pane key="unreceived" tab="待收货" />
<a-tab-pane key="completed" tab="已完成" /> <a-tab-pane key="completed" tab="已完成" />
<!-- <a-tab-pane key="unpaid" tab="待付款" />-->
<a-tab-pane key="refunded" tab="退货/售后" /> <a-tab-pane key="refunded" tab="退货/售后" />
<a-tab-pane key="cancelled" tab="已关闭" /> <a-tab-pane key="cancelled" tab="已关闭" />
</a-tabs> </a-tabs>
@@ -25,16 +25,103 @@
:columns="columns" :columns="columns"
:datasource="datasource" :datasource="datasource"
:customRow="customRow" :customRow="customRow"
cache-key="proShopOrderTable"
:toolbar="false" :toolbar="false"
tool-class="ele-toolbar-form" tool-class="ele-toolbar-form"
class="sys-org-table" class="sys-org-table"
> >
<template #toolbar> </template> <template #toolbar> </template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'"> <template v-if="column.key === 'userInfo'">
<div @click="onSearch(record)" class="cursor-pointer">{{ <a-space :size="8">
record.name || '匿名' <a-avatar
}}</div> v-if="record.avatar"
:src="record.avatar"
shape="square"
/>
<div class="leading-tight">
<div class="cursor-pointer" @click.stop="onSearch(record)">
{{
record.nickname ||
record.realName ||
record.name ||
'匿名'
}}
<span v-if="record.userId" class="text-gray-400">
(ID:{{ record.userId }})
</span>
</div>
<div
v-if="record.mobile || record.phone"
class="text-gray-500 text-xs"
>
{{ record.mobile || record.phone }}
</div>
</div>
</a-space>
</template>
<template v-if="column.key === 'statusInfo'">
<div class="py-1">
<a-space :size="6" wrap>
<!-- 支付状态 -->
<a-tag
v-if="record.payStatus == 1"
color="green"
@click.stop="updatePayStatus(record)"
class="cursor-pointer"
>已付款</a-tag
>
<a-tag
v-else-if="record.payStatus == 0 || record.payStatus == null"
@click.stop="updatePayStatus(record)"
class="cursor-pointer"
>未付款</a-tag
>
<a-tag
v-else-if="record.payStatus == 3"
color="orange"
@click.stop="updatePayStatus(record)"
class="cursor-pointer"
>占场中</a-tag
>
<!-- 发货状态 -->
<a-tag v-if="record.deliveryStatus == 10">未发货</a-tag>
<a-tag v-if="record.deliveryStatus == 20" color="green"
>已发货</a-tag
>
<a-tag v-if="record.deliveryStatus == 30" color="blue"
>部分发货</a-tag
>
<!-- 开票状态 -->
<!-- <a-tag v-if="record.isInvoice == 0">未开具</a-tag>-->
<a-tag v-if="record.isInvoice == 1" color="green">已开具</a-tag>
<a-tag v-if="record.isInvoice == 2" color="blue">不能开具</a-tag>
<!-- 订单状态 -->
<a-tag v-if="record.orderStatus === 0">未完成</a-tag>
<a-tag v-if="record.orderStatus === 1" color="green"
>已完成</a-tag
>
<a-tag v-if="record.orderStatus === 2">已关闭</a-tag>
<a-tag v-if="record.orderStatus === 3" color="red"
>关闭中</a-tag
>
<a-tag v-if="record.orderStatus === 4" color="red"
>退款申请中</a-tag
>
<a-tag v-if="record.orderStatus === 5" color="red"
>退款被拒绝</a-tag
>
<a-tag v-if="record.orderStatus === 6" color="orange"
>已退款</a-tag
>
<a-tag v-if="record.orderStatus === 7" color="pink"
>客户端申请退款</a-tag
>
</a-space>
</div>
</template> </template>
<template v-if="column.key === 'orderGoods'"> <template v-if="column.key === 'orderGoods'">
<template v-for="(item, index) in record.orderGoods" :key="index"> <template v-for="(item, index) in record.orderGoods" :key="index">
@@ -46,12 +133,6 @@
</div> </div>
</template> </template>
</template> </template>
<template v-if="column.key === 'phone'">
<div v-if="record.mobile" class="text-gray-400">{{
record.mobile
}}</div>
<div v-else class="text-gray-600">{{ record.phone }}</div>
</template>
<template v-if="column.key === 'payType'"> <template v-if="column.key === 'payType'">
<template v-for="item in getPayType()"> <template v-for="item in getPayType()">
<template v-if="record.payStatus == 1"> <template v-if="record.payStatus == 1">
@@ -64,22 +145,6 @@
</template> </template>
</template> </template>
</template> </template>
<template v-if="column.key === 'payStatus'">
<a-tag
v-if="record.payStatus"
color="green"
@click.stop="updatePayStatus(record)"
class="cursor-pointer"
>已付款
</a-tag>
<a-tag
v-if="!record.payStatus"
@click.stop="updatePayStatus(record)"
class="cursor-pointer"
>未付款
</a-tag>
<a-tag v-if="record.payStatus == 3">未付款,占场中</a-tag>
</template>
<template v-if="column.key === 'image'"> <template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" /> <a-image :src="record.image" :width="50" />
</template> </template>
@@ -87,53 +152,20 @@
<a-tag v-if="record.sex === 1"></a-tag> <a-tag v-if="record.sex === 1"></a-tag>
<a-tag v-if="record.sex === 2"></a-tag> <a-tag v-if="record.sex === 2"></a-tag>
</template> </template>
<template v-if="column.key === 'deliveryStatus'">
<a-tag v-if="record.deliveryStatus == 10">未发货</a-tag>
<a-tag v-if="record.deliveryStatus == 20" color="green"
>已发货</a-tag
>
<a-tag v-if="record.deliveryStatus == 30" color="blue"
>部分发货</a-tag
>
</template>
<template v-if="column.key === 'orderStatus'">
<a-tag v-if="record.orderStatus === 0">未完成</a-tag>
<a-tag v-if="record.orderStatus === 1" color="green">已完成</a-tag>
<a-tag v-if="record.orderStatus === 2">已关闭</a-tag>
<a-tag v-if="record.orderStatus === 3" color="red">关闭中</a-tag>
<a-tag v-if="record.orderStatus === 4" color="red"
>退款申请中</a-tag
>
<a-tag v-if="record.orderStatus === 5" color="red"
>退款被拒绝</a-tag
>
<a-tag v-if="record.orderStatus === 6" color="orange"
>退款成功</a-tag
>
<a-tag v-if="record.orderStatus === 7" color="pink"
>客户端申请退款</a-tag
>
</template>
<template v-if="column.key === 'isInvoice'">
<a-tag v-if="record.isInvoice == 0">未开具</a-tag>
<a-tag v-if="record.isInvoice == 1" color="green">已开具</a-tag>
<a-tag v-if="record.isInvoice == 2" color="blue">不能开具</a-tag>
</template>
<template v-if="column.key === 'status'"> <template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag> <a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag> <a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template> </template>
<template v-if="column.key === 'action'"> <template v-if="column.key === 'action'">
<a-space>
<!-- 查看详情 - 所有状态都可以查看 --> <!-- 查看详情 - 所有状态都可以查看 -->
<a @click.stop="openEdit(record)"> <EyeOutlined /> 详情 </a> <a @click.stop="openEdit(record)"> <EyeOutlined /> 详情 </a>
<!-- 未付款状态的操作 --> <!-- 未付款状态的操作 -->
<template v-if="!record.payStatus && record.orderStatus === 0"> <template v-if="!record.payStatus && record.orderStatus === 0">
<a-divider type="vertical" />
<a @click.stop="handleEditOrder(record)"> <a @click.stop="handleEditOrder(record)">
<EditOutlined /> 修改 <EditOutlined /> 修改
</a> </a>
<a-divider type="vertical" />
<a @click.stop="handleCancelOrder(record)"> <a @click.stop="handleCancelOrder(record)">
<span class="ele-text-warning"> <CloseOutlined /> 关闭 </span> <span class="ele-text-warning"> <CloseOutlined /> 关闭 </span>
</a> </a>
@@ -147,12 +179,10 @@
!isCancelledStatus(record.orderStatus) !isCancelledStatus(record.orderStatus)
" "
> >
<a-divider type="vertical" />
<a @click.stop="handleDelivery(record)" class="ele-text-primary"> <a @click.stop="handleDelivery(record)" class="ele-text-primary">
<SendOutlined /> 发货 <SendOutlined /> 发货
</a> </a>
<a-divider type="vertical" /> <a v-permission="'shop:shopOrder:refund'" @click.stop="handleApplyRefund(record)">
<a @click.stop="handleApplyRefund(record)">
<UndoOutlined /> 退款 <UndoOutlined /> 退款
</a> </a>
</template> </template>
@@ -165,15 +195,13 @@
record.orderStatus === 0 record.orderStatus === 0
" "
> >
<a-divider type="vertical" />
<a <a
@click.stop="handleConfirmReceive(record)" @click.stop="handleConfirmReceive(record)"
class="ele-text-primary" class="ele-text-primary"
> >
<CheckOutlined /> 确认收货 <CheckOutlined /> 确认收货
</a> </a>
<a-divider type="vertical" /> <a v-permission="'shop:shopOrder:refund'" @click.stop="handleApplyRefund(record)">
<a @click.stop="handleApplyRefund(record)">
<UndoOutlined /> 退款 <UndoOutlined /> 退款
</a> </a>
</template> </template>
@@ -183,14 +211,12 @@
<template <template
v-if="record.orderStatus === 4 || record.orderStatus === 7" v-if="record.orderStatus === 4 || record.orderStatus === 7"
> >
<a-divider type="vertical" />
<a <a
@click.stop="handleApproveRefund(record)" @click.stop="handleApproveRefund(record)"
class="ele-text-success" class="ele-text-success"
> >
<CheckCircleOutlined /> 同意退款 <CheckCircleOutlined /> 同意退款
</a> </a>
<a-divider type="vertical" />
<a <a
@click.stop="handleRejectRefund(record)" @click.stop="handleRejectRefund(record)"
class="ele-text-danger" class="ele-text-danger"
@@ -200,7 +226,6 @@
</template> </template>
<template v-if="record.orderStatus === 5"> <template v-if="record.orderStatus === 5">
<a-divider type="vertical" />
<a @click.stop="handleRetryRefund(record)"> <a @click.stop="handleRetryRefund(record)">
<RedoOutlined /> 重新处理 <RedoOutlined /> 重新处理
</a> </a>
@@ -209,15 +234,13 @@
<!-- 已完成状态的操作 --> <!-- 已完成状态的操作 -->
<template v-if="record.orderStatus === 1"> <template v-if="record.orderStatus === 1">
<a-divider type="vertical" /> <a v-permission="'shop:shopOrder:refund'" @click.stop="handleApplyRefund(record)">
<a @click.stop="handleApplyRefund(record)"> <UndoOutlined /> 退款
<UndoOutlined /> 申请退款
</a> </a>
</template> </template>
<!-- 删除操作 - 已完成、已关闭、退款成功的订单可以删除 --> <!-- 删除操作 - 已完成、已关闭、退款成功的订单可以删除 -->
<template v-if="canDeleteOrder(record)"> <template v-if="canDeleteOrder(record)">
<a-divider type="vertical" />
<a-popconfirm <a-popconfirm
title="确定要删除此订单吗?删除后无法恢复。" title="确定要删除此订单吗?删除后无法恢复。"
@confirm="remove(record)" @confirm="remove(record)"
@@ -227,6 +250,7 @@
</a> </a>
</a-popconfirm> </a-popconfirm>
</template> </template>
</a-space>
</template> </template>
</template> </template>
</ele-pro-table> </ele-pro-table>
@@ -275,7 +299,7 @@
repairOrder, repairOrder,
removeShopOrder, removeShopOrder,
removeBatchShopOrder, removeBatchShopOrder,
updateShopOrder updateShopOrder, refundShopOrder
} from '@/api/shop/shopOrder'; } from '@/api/shop/shopOrder';
import { updateUser } from '@/api/system/user'; import { updateUser } from '@/api/system/user';
import { getPayType } from '@/utils/shop'; import { getPayType } from '@/utils/shop';
@@ -297,7 +321,7 @@
// 加载状态 // 加载状态
const loading = ref(true); const loading = ref(true);
// 激活的标签 // 激活的标签
const activeKey = ref<string>('all'); const activeKey = ref<string>('undelivered');
// 表格数据源 // 表格数据源
const datasource: DatasourceFunction = ({ const datasource: DatasourceFunction = ({
page, page,
@@ -324,50 +348,37 @@
title: '订单编号', title: '订单编号',
dataIndex: 'orderNo', dataIndex: 'orderNo',
key: 'orderNo', key: 'orderNo',
align: 'center', align: 'center'
width: 200 },
{
title: '用户信息',
dataIndex: 'userId',
key: 'userInfo'
}, },
{ {
title: '商品信息', title: '商品信息',
dataIndex: 'orderGoods', dataIndex: 'orderGoods',
key: 'orderGoods', key: 'orderGoods'
width: 360
}, },
{ {
title: '实付金额', title: '实付金额',
dataIndex: 'payPrice', dataIndex: 'payPrice',
key: 'payPrice', key: 'payPrice',
align: 'center', align: 'center',
width: 120,
customRender: ({ text }) => '¥' + text customRender: ({ text }) => '¥' + text
}, },
// {
// title: '支付方式',
// dataIndex: 'payType',
// key: 'payType',
// align: 'center',
// width: 120,
// },
{ {
title: '支付方式', title: '状态',
dataIndex: 'payType',
key: 'payType',
align: 'center'
},
{
title: '支付状态',
dataIndex: 'payStatus',
key: 'payStatus',
align: 'center'
},
{
title: '发货状态',
dataIndex: 'deliveryStatus',
key: 'deliveryStatus',
align: 'center'
},
{
title: '开票状态',
dataIndex: 'isInvoice',
key: 'isInvoice',
align: 'center'
},
{
title: '订单状态',
dataIndex: 'orderStatus', dataIndex: 'orderStatus',
key: 'orderStatus', key: 'statusInfo',
align: 'center' align: 'center'
}, },
// { // {
@@ -390,15 +401,13 @@
dataIndex: 'createTime', dataIndex: 'createTime',
key: 'createTime', key: 'createTime',
align: 'center', align: 'center',
width: 180,
sorter: true, sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text) customRender: ({ text }) => toDateString(text)
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 280, width: 120,
fixed: 'right', fixed: 'right',
align: 'center', align: 'center',
hideInSetting: true hideInSetting: true
@@ -583,7 +592,7 @@
const now = new Date(); const now = new Date();
const refundTime = toDateString(now, 'yyyy-MM-dd HH:mm:ss'); const refundTime = toDateString(now, 'yyyy-MM-dd HH:mm:ss');
await updateShopOrder({ await refundShopOrder({
...record, ...record,
orderStatus: 6, // 退款成功 orderStatus: 6, // 退款成功
refundTime: refundTime refundTime: refundTime
@@ -647,7 +656,7 @@
const now = new Date(); const now = new Date();
const refundApplyTime = toDateString(now, 'yyyy-MM-dd HH:mm:ss'); const refundApplyTime = toDateString(now, 'yyyy-MM-dd HH:mm:ss');
await updateShopOrder({ await refundShopOrder({
...record, ...record,
orderStatus: 4, // 退款申请中 orderStatus: 4, // 退款申请中
refundApplyTime: refundApplyTime refundApplyTime: refundApplyTime
@@ -703,11 +712,11 @@
return { return {
// 行点击事件 // 行点击事件
onClick: () => { onClick: () => {
openEdit(record); // openEdit(record);
}, },
// 行双击事件 // 行双击事件
onDblclick: () => { onDblclick: () => {
// openEdit(record); openEdit(record);
} }
}; };
}; };

View File

@@ -0,0 +1,61 @@
<!-- 搜索表单 -->
<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-input-search
allow-clear
placeholder="网点名称"
style="width: 240px"
v-model:value="where.keywords"
@search="reload"
/>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import { watch } from 'vue';
import useSearch from "@/utils/use-search";
import {ShopStoreParam} from "@/api/shop/shopStore/model";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: ShopStoreParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
const reload = () => {
emit('search', where);
};
// 表单数据
const { where } = useSearch<ShopStoreParam>({
keywords: '',
userId: undefined
});
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,304 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:confirm-loading="loading"
: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="managerName">
<a-input
allow-clear
placeholder="请输入网点经理"
v-model:value="form.managerName"
/>
</a-form-item>
<a-form-item label="手机号码" name="phone">
<a-input
allow-clear
placeholder="请输入手机号码"
:maxlength="11"
v-model:value="form.phone"
/>
</a-form-item>
<a-form-item label="省市区" name="region">
<RegionsSelect
v-model:value="regions"
valueField="label"
placeholder="请选择省/市/区"
/>
</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="lngAndLat">
<a-space>
<a-input
allow-clear
placeholder="请输入经纬度"
style="width: 300px"
v-model:value="form.lngAndLat"
/>
<a-button type="primary" @click="openUrl(`https://lbs.qq.com/getPoint/`)">获取经纬度</a-button>
</a-space>
</a-form-item>
<!-- <a-form-item label="轮廓" name="points">-->
<!-- <div class="flex">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请选取电子围栏的轮廓"-->
<!-- v-model:value="form.points"-->
<!-- />-->
<!-- </div>-->
<!-- <template #extra>-->
<!-- <p class="py-2">-->
<!-- <a-->
<!-- class="text-blue-500"-->
<!-- href="https://lbs.qq.com/dev/console/cloud/placeCloud/datamanage?table_id=0q3W4MrK1-_6Xag7V1"-->
<!-- target="_blank"-->
<!-- >使用腾讯地图的工具绘制电子围栏</a-->
<!-- >-->
<!-- </p>-->
<!-- </template>-->
<!-- </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="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</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 { addShopStore, updateShopStore } from '@/api/shop/shopStore';
import { ShopStore } from '@/api/shop/shopStore/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';
import {openUrl} from "@/utils/common";
import RegionsSelect from '@/components/RegionsSelect/index.vue';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopStore | 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 regions = ref<string[]>();
// 用户信息
const form = reactive<ShopStore>({
id: undefined,
name: undefined,
address: undefined,
phone: undefined,
email: undefined,
managerName: undefined,
shopBanner: undefined,
province: undefined,
city: undefined,
region: undefined,
lngAndLat: undefined,
location: undefined,
district: undefined,
points: undefined,
userId: undefined,
isDelete: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: 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'
}
],
phone: [
{
required: true,
type: 'string',
message: '请填写手机号码',
trigger: 'blur'
}
],
region: [
{
required: true,
type: 'string',
message: '请选择省/市/区',
trigger: 'change'
}
],
address: [
{
required: true,
type: 'string',
message: '请填写网点地址',
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.shopBanner = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.shopBanner = '';
};
const { resetFields } = useForm(form, rules);
// 级联选择回填到表单字段,保持提交参数仍然是 province/city/region
watch(
regions,
(val) => {
form.province = val?.[0];
form.city = val?.[1];
form.region = val?.[2];
},
{ immediate: true }
);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateShopStore : addShopStore;
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.shopBanner){
images.value.push({
uid: uuid(),
url: props.data.shopBanner,
status: 'done'
})
}
regions.value =
form.province && form.city && form.region
? [form.province, form.city, form.region]
: undefined;
isUpdate.value = true;
} else {
isUpdate.value = false;
regions.value = undefined;
}
} else {
resetFields();
regions.value = undefined;
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,277 @@
<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 === 'name'">
<a-tag color="orange">{{ record.name }}</a-tag>
</template>
<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>
<!-- 编辑弹窗 -->
<ShopStoreEdit 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 {getPageTitle} from '@/utils/common';
import ShopStoreEdit from './components/shopStoreEdit.vue';
import { pageShopStore, removeShopStore, removeBatchShopStore } from '@/api/shop/shopStore';
import type { ShopStore, ShopStoreParam } from '@/api/shop/shopStore/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopStore[]>([]);
// 当前编辑数据
const current = ref<ShopStore | 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 pageShopStore({
...where,
...orders,
page,
limit
});
};
// 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([
{
title: '网点ID',
dataIndex: 'id',
key: 'id',
width: 90
},
{
title: '网点名称',
dataIndex: 'name',
key: 'name'
},
// {
// title: '手机号码',
// dataIndex: 'phone',
// key: 'phone',
// ellipsis: true
// },
{
title: '联系人',
dataIndex: 'managerName',
key: 'managerName'
},
{
title: '所在省份',
dataIndex: 'province',
key: 'province'
},
{
title: '所在城市',
dataIndex: 'city',
key: 'city'
},
{
title: '所在辖区',
dataIndex: 'region',
key: 'region'
},
{
title: '门店地址',
dataIndex: 'address',
key: 'address'
},
// {
// title: '经度',
// dataIndex: 'lng',
// key: 'lng',
// ellipsis: true
// },
// {
// title: '纬度',
// dataIndex: 'lat',
// key: 'lat',
// ellipsis: true
// },
// {
// title: '门店备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
{
title: '排序号',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
width: 90
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
align: 'center',
sorter: 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?: ShopStoreParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopStore) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopStore) => {
const hide = message.loading('请求中..', 0);
removeShopStore(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);
removeBatchShopStore(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: ShopStore) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopStore'
};
</script>
<style lang="less" scoped></style>

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,287 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:confirm-loading="loading"
:title="isUpdate ? '编辑电子围栏' : '添加电子围栏'"
:body-style="{ paddingBottom: '28px', maxHeight: '70vh', overflow: 'auto' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:disabled="loading"
: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-row :gutter="16">
<a-col :xs="24" :sm="24">
<a-form-item label="围栏类型" name="type">
<a-radio-group v-model:value="form.type">
<a-radio :value="0">圆形</a-radio>
<a-radio :value="1">方形/多边形</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="围栏范围" name="points" :extra="pointsHelp">
<a-textarea
:auto-size="{ minRows: 3, maxRows: 6 }"
allow-clear
:placeholder="pointsPlaceholder"
v-model:value="form.points"
/>
<p class="py-2">
<a
class="text-blue-500"
href="https://lbs.qq.com/dev/console/cloud/placeCloud/datamanage?table_id=0q3W4MrK1-_6Xag7V1"
target="_blank"
>使用腾讯地图的工具绘制电子围栏</a
>
</p>
</a-form-item>
<a-form-item label="定位描述" name="location">
<a-input
allow-clear
placeholder="如xx商圈/xx门店附近可选"
v-model:value="form.location"
/>
</a-form-item>
<a-form-item label="所属区域" name="district">
<a-input
allow-clear
placeholder="如:广东省/广州市/天河区(可选)"
v-model:value="form.district"
/>
</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="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-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addShopStoreFence, updateShopStoreFence } from '@/api/shop/shopStoreFence';
import { ShopStoreFence } from '@/api/shop/shopStoreFence/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { FormInstance } from 'ant-design-vue/es/form';
// 是否是修改
const isUpdate = ref(false);
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopStoreFence | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = true;
// 表单实例
const formRef = ref<FormInstance | null>(null);
const getDefaultForm = (): ShopStoreFence => ({
id: undefined,
name: '',
type: 0,
location: '',
longitude: '',
latitude: '',
district: '',
points: '',
sortNumber: 100,
comments: '',
status: 0,
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
const form = reactive<ShopStoreFence>(getDefaultForm());
/* 更新visible */
const updateVisible = (value: boolean) => {
// 保存中不允许关闭,避免重复提交/状态不一致
if (!value && loading.value) {
return;
}
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
type: 'string',
message: '请填写电子围栏名称',
trigger: 'blur'
}
],
type: [
{
required: true,
type: 'number',
message: '请选择围栏类型',
trigger: 'change'
}
],
longitude: [
{
required: true,
type: 'string',
message: '请填写经度',
trigger: 'blur'
}
],
latitude: [
{
required: true,
type: 'string',
message: '请填写纬度',
trigger: 'blur'
}
],
points: [
{
required: true,
type: 'string',
message: '请填写围栏范围',
trigger: 'blur'
}
]
});
const pointsPlaceholder = computed(() => {
return form.type === 0
? '圆形示例lng,lat,radius(米) 例如113.123456,23.123456,500'
: '多边形示例lng,lat;lng,lat;lng,lat 例如113.1,23.1;113.2,23.1;113.2,23.2;113.1,23.2';
});
const pointsHelp = computed(() => {
return form.type === 0
? '按 “经度,纬度,半径(米)” 填写'
: '按 “经度,纬度;经度,纬度;...” 填写,建议首尾闭合';
});
const resetForm = async () => {
assignObject(form, getDefaultForm());
await nextTick();
formRef.value?.clearValidate?.();
};
const fillForm = async (data: ShopStoreFence) => {
await resetForm();
assignObject(form, data);
await nextTick();
formRef.value?.clearValidate?.();
};
/* 保存编辑 */
const save = async () => {
if (!formRef.value || loading.value) {
return;
}
try {
await formRef.value.validate();
loading.value = true;
const formData: ShopStoreFence = {
...form,
name: form.name?.trim(),
location: form.location?.trim(),
longitude: form.longitude?.trim(),
latitude: form.latitude?.trim(),
district: form.district?.trim(),
points: form.points?.trim(),
comments: form.comments?.trim()
};
const saveOrUpdate = isUpdate.value ? updateShopStoreFence : addShopStoreFence;
const msg = await saveOrUpdate(formData);
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
} catch (e: any) {
loading.value = false;
if (e?.message) {
message.error(e.message);
}
}
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
void fillForm(props.data);
isUpdate.value = true;
} else {
void resetForm();
isUpdate.value = false;
}
} else {
void resetForm();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,246 @@
<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>
<!-- 编辑弹窗 -->
<ShopStoreFenceEdit v-model:visible="showEdit" :data="current" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed } 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 {getPageTitle} from '@/utils/common';
import ShopStoreFenceEdit from './components/shopStoreFenceEdit.vue';
import { pageShopStoreFence, removeShopStoreFence, removeBatchShopStoreFence } from '@/api/shop/shopStoreFence';
import type { ShopStoreFence, ShopStoreFenceParam } from '@/api/shop/shopStoreFence/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopStoreFence[]>([]);
// 当前编辑数据
const current = ref<ShopStoreFence | 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 pageShopStoreFence({
...where,
...orders,
page,
limit
});
};
// 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([
{
title: '围栏名称',
dataIndex: 'name',
key: 'name'
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
align: 'center',
customRender: ({ text }) => {
if (text === 0) {
return '圆形';
}
if (text === 1) {
return '方形/多边形';
}
return text;
}
},
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center'
},
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
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?: ShopStoreFenceParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopStoreFence) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopStoreFence) => {
const hide = message.loading('请求中..', 0);
removeShopStoreFence(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);
removeBatchShopStoreFence(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: ShopStoreFence) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopStoreFence'
};
</script>
<style lang="less" scoped></style>

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,478 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="1000"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:confirm-loading="loading"
:title="isUpdate ? '编辑配送员' : '添加配送员'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 5, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="选择用户" name="userId">
<!-- SelectUser 组件本身不支持 v-model只通过 @done 回传选择结果 key 强制刷新回显 -->
<SelectUser
:key="String(form.userId ?? '') + selectedUserText"
:value="selectedUserText"
:placeholder="`选择用户`"
@done="onChooseUser"
/>
</a-form-item>
<a-form-item label="所属门店" name="storeId">
<SelectShopStore
:key="String(form.storeId ?? '') + selectedStoreText"
:value="selectedStoreText"
:placeholder="`选择门店`"
@done="onChooseStore"
/>
</a-form-item>
<a-form-item label="配送点(小区)" name="dealerId">
<SelectCommunity
:key="String(form.dealerId ?? '') + selectedCommunityText"
:value="selectedCommunityText"
:placeholder="`选择小区`"
@done="onChooseCommunity"
/>
</a-form-item>
<a-form-item label="骑手编号" name="riderNo">
<a-input
allow-clear
placeholder="请输入骑手编号(可选)"
v-model:value="form.riderNo"
/>
</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="avatar">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入头像"-->
<!-- v-model:value="form.avatar"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="身份证号" name="idCardNo">
<a-input
allow-clear
placeholder="请输入身份证号"
:value="maskedIdCardNo"
/>
</a-form-item>
<a-form-item label="接单状态" name="workStatus">
<a-input
allow-clear
placeholder="请输入接单状态0休息/下线1在线2忙碌"
v-model:value="form.workStatus"
/>
</a-form-item>
<a-form-item label="自动派单" name="autoDispatchEnabled">
<a-input
allow-clear
placeholder="请输入是否开启自动派单1是0否"
v-model:value="form.autoDispatchEnabled"
/>
</a-form-item>
<a-form-item label="派单优先级" name="dispatchPriority">
<a-input
allow-clear
placeholder="请输入派单优先级(同小区多骑手时可用,值越大越优先)"
v-model:value="form.dispatchPriority"
/>
</a-form-item>
<a-form-item label="最大同时配送单数" name="maxOnhandOrders">
<a-input
allow-clear
placeholder="请输入最大同时配送单数0表示不限制"
v-model:value="form.maxOnhandOrders"
/>
</a-form-item>
<a-form-item label="是否计算工资" name="commissionCalcEnabled">
<a-input
allow-clear
placeholder="请输入是否计算工资(提成)1计算0不计算如三方配送点可设0"
v-model:value="form.commissionCalcEnabled"
/>
</a-form-item>
<a-form-item label="提成单价(元/桶)" name="waterBucketUnitFee">
<a-input
allow-clear
placeholder="请输入水每桶提成金额(元/桶)"
v-model:value="form.waterBucketUnitFee"
/>
</a-form-item>
<a-form-item label="其他商品提成方式" name="otherGoodsCommissionType">
<a-input
allow-clear
placeholder="请输入其他商品提成方式1按订单固定金额2按订单金额比例3按商品规则(另表)"
v-model:value="form.otherGoodsCommissionType"
/>
</a-form-item>
<a-form-item label="其他商品提成值" name="otherGoodsCommissionValue">
<a-input
allow-clear
placeholder="请输入其他商品提成值:固定金额(元)或比例(%)"
v-model:value="form.otherGoodsCommissionValue"
/>
</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="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="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="1">启用</a-radio>
<a-radio :value="0">禁用</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import {
addShopStoreRider,
updateShopStoreRider
} from '@/api/shop/shopStoreRider';
import { ShopStoreRider } from '@/api/shop/shopStoreRider/model';
import { getUser } from '@/api/system/user';
import { getShopCommunity } from '@/api/shop/shopCommunity';
import { getShopStore } from '@/api/shop/shopStore';
import SelectUser from '@/components/SelectUser/index.vue';
import SelectCommunity from '@/components/SelectCommunity/index.vue';
import SelectShopStore from '@/components/SelectShopStore/index.vue';
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';
import type { User } from '@/api/system/user/model';
import type { ShopCommunity } from '@/api/shop/shopCommunity/model';
import type { ShopStore } from '@/api/shop/shopStore/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopStoreRider | 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 selectedUserText = ref('');
const selectedCommunityText = ref('');
const selectedStoreText = ref('');
// 用户信息
const form = reactive<ShopStoreRider>({
id: undefined,
storeId: undefined,
dealerId: undefined,
riderNo: undefined,
realName: undefined,
mobile: undefined,
avatar: undefined,
idCardNo: undefined,
workStatus: undefined,
autoDispatchEnabled: undefined,
dispatchPriority: undefined,
maxOnhandOrders: undefined,
commissionCalcEnabled: undefined,
waterBucketUnitFee: undefined,
otherGoodsCommissionType: undefined,
otherGoodsCommissionValue: undefined,
userId: undefined,
isDelete: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
userId: [
{
validator: (_rule: unknown, value: number | undefined) => {
if (!value) {
return Promise.reject(new Error('请选择用户'));
}
return Promise.resolve();
},
trigger: 'change'
}
],
storeId: [
{
validator: (_rule: unknown, value: number | undefined) => {
if (!value) {
return Promise.reject(new Error('请选择门店'));
}
return Promise.resolve();
},
trigger: 'change'
}
]
// userId: [
// {
// required: true,
// type: 'string',
// message: '请选择用户',
// trigger: 'blur'
// }
// ],
// dealerId: [
// {
// required: true,
// type: 'string',
// message: '请选择配送点',
// trigger: 'blur'
// }
// ],
// realName: [
// {
// required: true,
// type: 'string',
// message: '请填写姓名',
// trigger: 'blur'
// }
// ],
});
const _chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.avatar = data.path;
};
const _onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.avatar = '';
};
const maskIdCard = (value?: string) => {
if (!value) return '';
const s = String(value).trim();
const startLen = Math.min(6, Math.max(0, s.length - 4));
const endLen = Math.min(4, s.length);
const starLen = Math.max(4, s.length - startLen - endLen);
if (s.length <= startLen + endLen) return s;
return `${s.slice(0, startLen)}${'*'.repeat(starLen)}${s.slice(-endLen)}`;
};
const maskedIdCardNo = computed(() => maskIdCard(form.idCardNo));
const onChooseUser = (user?: User) => {
if (!user) {
selectedUserText.value = '';
form.userId = undefined;
formRef.value?.validateFields(['userId']).catch(() => {});
return;
}
form.userId = user.userId;
form.realName = user.realName ?? user.nickname;
form.mobile = user.phone ?? user.mobile;
form.avatar = user.avatar ?? user.avatarUrl;
form.idCardNo = user.idCard ?? user.idcard;
const name = user.realName ?? user.nickname ?? '';
const phone = user.phone ?? user.mobile ?? '';
selectedUserText.value = phone ? `${name}${phone}` : name;
formRef.value?.validateFields(['userId']).catch(() => {});
};
const onChooseCommunity = (
community?: ShopCommunity & { index?: number }
) => {
if (!community) {
selectedCommunityText.value = '';
form.dealerId = undefined;
return;
}
form.dealerId = community.id;
selectedCommunityText.value = community.name ?? String(community.id ?? '');
};
const onChooseStore = (store?: ShopStore) => {
if (!store) {
selectedStoreText.value = '';
form.storeId = undefined;
formRef.value?.validateFields(['storeId']).catch(() => {});
return;
}
form.storeId = store.id;
selectedStoreText.value = store.name ?? String(store.id ?? '');
formRef.value?.validateFields(['storeId']).catch(() => {});
};
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
? updateShopStoreRider
: addShopStoreRider;
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 = [];
selectedUserText.value = '';
selectedCommunityText.value = '';
selectedStoreText.value = '';
if (props.data) {
assignObject(form, props.data);
if (props.data.avatar) {
images.value.push({
uid: uuid(),
url: props.data.avatar,
status: 'done'
});
}
isUpdate.value = true;
// 回显展示文本(避免组件内部不响应 props.value 更新)
if (form.userId) {
const uid = form.userId;
getUser(form.userId)
.then((user) => {
if (form.userId !== uid) return;
const name = user.realName ?? user.nickname ?? '';
const phone = user.phone ?? user.mobile ?? '';
selectedUserText.value = phone ? `${name}${phone}` : name;
})
.catch(() => {
if (form.userId !== uid) return;
selectedUserText.value = String(form.userId ?? '');
});
}
if (form.storeId) {
// 优先使用列表接口返回的 storeName 回显,避免额外请求
if ((props.data as any)?.storeName) {
selectedStoreText.value = String((props.data as any).storeName);
} else {
const sid = form.storeId;
getShopStore(form.storeId)
.then((store) => {
if (form.storeId !== sid) return;
selectedStoreText.value =
store.name ?? String(store.id ?? '');
})
.catch(() => {
if (form.storeId !== sid) return;
selectedStoreText.value = String(form.storeId ?? '');
});
}
}
if (form.dealerId) {
const dealerId = form.dealerId;
getShopCommunity(form.dealerId)
.then((community) => {
if (form.dealerId !== dealerId) return;
selectedCommunityText.value =
community.name ?? String(community.id ?? '');
})
.catch(() => {
if (form.dealerId !== dealerId) return;
selectedCommunityText.value = String(form.dealerId ?? '');
});
}
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,311 @@
<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 === 'storeName'">
<a-tag v-if="record.storeName" color="orange">{{ record.storeName }}</a-tag>
</template>
<template v-if="column.key === 'workStatus'">
<a-tag v-if="record.status === 2" color="red">忙碌</a-tag>
<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 === 'status'">
<a-tag v-if="record.status === 1" color="green">启用</a-tag>
<a-tag v-if="record.status === 0" 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>
<!-- 编辑弹窗 -->
<ShopStoreRiderEdit v-model:visible="showEdit" :data="current" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed } 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 {getPageTitle} from '@/utils/common';
import ShopStoreRiderEdit from './components/shopStoreRiderEdit.vue';
import { pageShopStoreRider, removeShopStoreRider, removeBatchShopStoreRider } from '@/api/shop/shopStoreRider';
import type { ShopStoreRider, ShopStoreRiderParam } from '@/api/shop/shopStoreRider/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopStoreRider[]>([]);
// 当前编辑数据
const current = ref<ShopStoreRider | 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 pageShopStoreRider({
...where,
...orders,
page,
limit
});
};
// 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
width: 90
},
{
title: '所属门店',
dataIndex: 'storeName',
key: 'storeName'
},
{
title: '配送员',
dataIndex: 'realName',
key: 'realName'
},
{
title: '手机号',
dataIndex: 'mobile',
key: 'mobile'
},
// {
// title: '头像',
// dataIndex: 'avatar',
// key: 'avatar'
// },
{
title: '身份证号',
dataIndex: 'idCardNo',
key: 'idCardNo'
},
{
title: '接单状态',
dataIndex: 'workStatus',
key: 'workStatus',
align: 'center'
},
{
title: '自动派单',
dataIndex: 'autoDispatchEnabled',
key: 'autoDispatchEnabled',
align: 'center'
},
{
title: '派单优先级',
dataIndex: 'dispatchPriority',
key: 'dispatchPriority',
width: 90,
align: 'center'
},
// {
// title: '最大同时配送单数0表示不限制',
// dataIndex: 'maxOnhandOrders',
// key: 'maxOnhandOrders',
// width: 120
// },
// {
// title: '是否计算工资',
// dataIndex: 'commissionCalcEnabled',
// key: 'commissionCalcEnabled',
// width: 90
// },
// {
// title: '水每桶提成金额(元/桶)',
// dataIndex: 'waterBucketUnitFee',
// key: 'waterBucketUnitFee'
// },
// {
// title: '商品提成方式',
// dataIndex: 'otherGoodsCommissionType',
// key: 'otherGoodsCommissionType'
// },
// {
// title: '其他商品提成值:固定金额(元)或比例(%)',
// dataIndex: 'otherGoodsCommissionValue',
// key: 'otherGoodsCommissionValue'
// },
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '排序号',
// dataIndex: 'sortNumber',
// key: 'sortNumber',
// width: 120
// },
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
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?: ShopStoreRiderParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopStoreRider) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopStoreRider) => {
const hide = message.loading('请求中..', 0);
removeShopStoreRider(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);
removeBatchShopStoreRider(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: ShopStoreRider) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopStoreRider'
};
</script>
<style lang="less" scoped></style>

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,219 @@
<!-- 编辑弹窗 -->
<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="storeId">
<a-input
allow-clear
placeholder="请选择门店"
v-model:value="form.storeId"
/>
</a-form-item>
<a-form-item label="选择用户" name="userId">
<SelectUser
:placeholder="`选择用户`"
v-model:value="form.userId"
@done="onToUser"
/>
</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="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="isDelete">
<a-input
allow-clear
placeholder="请输入是否删除"
v-model:value="form.isDelete"
/>
</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 { addShopStoreUser, updateShopStoreUser } from '@/api/shop/shopStoreUser';
import { ShopStoreUser } from '@/api/shop/shopStoreUser/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';
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?: ShopStoreUser | 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<ShopStoreUser>({
id: undefined,
storeId: undefined,
userId: undefined,
isDelete: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
shopStoreUserId: undefined,
shopStoreUserName: '',
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopStoreUserName: [
{
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 onToUser = (item: User) => {
form.userId = item.userId;
form.realName = item.realName;
// form.toUserId = item.userId;
// form.toUserName = item.nickname;
};
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 ? updateShopStoreUser : addShopStoreUser;
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,244 @@
<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>
<!-- 编辑弹窗 -->
<ShopStoreUserEdit v-model:visible="showEdit" :data="current" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed } 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 {getPageTitle} from '@/utils/common';
import ShopStoreUserEdit from './components/shopStoreUserEdit.vue';
import { pageShopStoreUser, removeShopStoreUser, removeBatchShopStoreUser } from '@/api/shop/shopStoreUser';
import type { ShopStoreUser, ShopStoreUserParam } from '@/api/shop/shopStoreUser/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopStoreUser[]>([]);
// 当前编辑数据
const current = ref<ShopStoreUser | 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 pageShopStoreUser({
...where,
...orders,
page,
limit
});
};
// 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
width: 120
},
{
title: '所属门店',
dataIndex: 'storeId',
key: 'storeId',
width: 120
},
{
title: '姓名',
dataIndex: 'name',
key: 'name',
width: 120
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone',
width: 120
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
ellipsis: true
},
{
title: '排序号',
dataIndex: 'sortNumber',
key: 'sortNumber',
width: 120
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
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?: ShopStoreUserParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopStoreUser) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopStoreUser) => {
const hide = message.loading('请求中..', 0);
removeShopStoreUser(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);
removeBatchShopStoreUser(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: ShopStoreUser) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopStoreUser'
};
</script>
<style lang="less" scoped></style>

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,237 @@
<!-- 编辑弹窗 -->
<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="type">
<a-input
allow-clear
placeholder="请输入类型 中心仓,区域仓,门店仓"
v-model:value="form.type"
/>
</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="realName">
<a-input
allow-clear
placeholder="请输入真实姓名"
v-model:value="form.realName"
/>
</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="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="region">
<a-input
allow-clear
placeholder="请输入所在辖区"
v-model:value="form.region"
/>
</a-form-item>
<a-form-item label="经纬度" name="lngAndLat">
<a-input
allow-clear
placeholder="请输入经纬度"
v-model:value="form.lngAndLat"
/>
</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="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</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 { addShopStoreWarehouse, updateShopStoreWarehouse } from '@/api/shop/shopStoreWarehouse';
import { ShopStoreWarehouse } from '@/api/shop/shopStoreWarehouse/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?: ShopStoreWarehouse | 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<ShopStoreWarehouse>({
id: undefined,
name: undefined,
code: undefined,
type: undefined,
address: undefined,
realName: undefined,
phone: undefined,
province: undefined,
city: undefined,
region: undefined,
lngAndLat: undefined,
userId: undefined,
isDelete: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopStoreWarehouseName: [
{
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 ? updateShopStoreWarehouse : addShopStoreWarehouse;
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,241 @@
<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>
<!-- 编辑弹窗 -->
<ShopStoreWarehouseEdit 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 {getPageTitle} from '@/utils/common';
import ShopStoreWarehouseEdit from './components/shopStoreWarehouseEdit.vue';
import { pageShopStoreWarehouse, removeShopStoreWarehouse, removeBatchShopStoreWarehouse } from '@/api/shop/shopStoreWarehouse';
import type { ShopStoreWarehouse, ShopStoreWarehouseParam } from '@/api/shop/shopStoreWarehouse/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopStoreWarehouse[]>([]);
// 当前编辑数据
const current = ref<ShopStoreWarehouse | 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 pageShopStoreWarehouse({
...where,
...orders,
page,
limit
});
};
// 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([
{
title: '仓库名称',
dataIndex: 'name',
key: 'name'
},
{
title: '仓库编号',
dataIndex: 'code',
key: 'code'
},
{
title: '类型',
dataIndex: 'type',
key: 'type'
},
{
title: '仓库地址',
dataIndex: 'address',
key: 'address',
},
{
title: '所在城市',
dataIndex: 'city',
key: 'city',
align: 'center'
},
{
title: '排序号',
dataIndex: 'sortNumber',
key: 'sortNumber',
width: 120,
align: 'center'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
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?: ShopStoreWarehouseParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopStoreWarehouse) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopStoreWarehouse) => {
const hide = message.loading('请求中..', 0);
removeShopStoreWarehouse(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);
removeBatchShopStoreWarehouse(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: ShopStoreWarehouse) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopStoreWarehouse'
};
</script>
<style lang="less" scoped></style>

View File

@@ -23,7 +23,7 @@
<div class="ele-text-center"> <div class="ele-text-center">
<span>只能上传xlsxlsx文件</span> <span>只能上传xlsxlsx文件</span>
<a <a
href="https://server.websoft.top/api/system/user/import/template" href="https://glt-server.websoft.top/api/system/user/import/template"
download="用户导入模板.xlsx" download="用户导入模板.xlsx"
> >
下载导入模板 下载导入模板