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
userId?: number;
// 分销商昵称
nickName?: string;
// 订单ID
orderId?: number;
// 订单编号

View File

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

View File

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

View File

@@ -94,7 +94,7 @@ export interface ShopGoods {
supplierMerchantId?: number;
supplierName?: string;
// 状态0未上架1上架
isShow?: number;
isShow?: boolean;
// 状态, 0上架 1待上架 2待审核 3审核不通过
status?: number;
// 备注
@@ -124,6 +124,21 @@ export interface ShopGoods {
canUseDate?: string;
ensureTag?: string;
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 {

View File

@@ -135,3 +135,18 @@ export async function shopOrderTotal(params?: ShopOrderParam) {
}
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;
// 头像
avatar?: string;
// 昵称(部分接口会返回)
nickname?: string;
// 兼容字段:部分接口可能返回 name
name?: string;
// 真实姓名
realName?: string;
// 手机号码
@@ -99,6 +103,8 @@ export interface ShopOrder {
expressId?: number;
// 快递公司名称
expressName?: string;
// 物流单号
expressNo?: 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: '发生时间',
dataIndex: 'occurrenceTime',
key: 'occurrenceTime',
dataIndex: 'releaseDate',
key: 'releaseDate',
width: 120
},
{

View File

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

View File

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

View File

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

View File

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

View File

@@ -169,10 +169,21 @@
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 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',
showSorterTooltip: false
},
{
title: '所属部门',
dataIndex: 'organizationName',
key: 'organizationName',
align: 'center'
},
// {
// title: '所属部门',
// dataIndex: 'organizationName',
// key: 'organizationName',
// align: 'center'
// },
{
title: '角色',
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>
<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="用户ID|订单编号"
style="width: 240px"
v-model:value="where.keywords"
@search="reload"
/>
<a-button type="dashed" @click="handleExport">导出xls</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';
import { ref, watch } from 'vue';
import { utils, writeFile } from 'xlsx';
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(
defineProps<{
@@ -24,15 +33,81 @@
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'search', where?: ShopDealerCapitalParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
const reload = () => {
emit('search', where);
};
// 表单数据
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(

View File

@@ -23,6 +23,9 @@
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'userId'">
<div>{{ record.nickName }}({{ record.userId }})</div>
</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>
@@ -57,7 +60,6 @@
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
@@ -111,33 +113,39 @@
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
key: 'index',
width: 48,
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',
key: 'userId',
align: 'center',
width: 100,
fixed: 'left'
},
{
title: '流动类型',
title: '收益类型',
dataIndex: 'flowType',
key: 'flowType',
align: 'center',
width: 120,
customRender: ({ text }) => {
const typeMap = {
10: { text: '佣金收入', color: 'success' },
20: { text: '提现支出', color: 'warning' },
30: { text: '转账支出', color: 'error' },
40: { text: '转账收入', color: 'processing' }
40: { text: '转账收入', color: 'processing' },
50: { text: '佣金解冻', color: 'processing' }
};
const type = typeMap[text] || { text: '未知', color: 'default' };
return {
@@ -152,43 +160,35 @@
dataIndex: 'money',
key: 'money',
align: 'center',
width: 120,
customRender: ({ text, record }) => {
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 {
type: 'span',
props: {
style: {
color: isIncome ? '#52c41a' : '#ff4d4f',
fontWeight: 'bold'
color: isIncome ? '#424242' : '#ff4d4f'
}
},
children: `${isIncome ? '+' : '-'}¥${amount}`
children: `${isIncome ? '' : '-'} ${amount}`
};
}
},
{
title: '关联订单',
dataIndex: 'orderNo',
key: 'orderNo',
align: 'center',
customRender: ({ text }) => text || '-'
},
{
title: '对方用户',
dataIndex: 'toUserId',
key: 'toUserId',
align: 'center',
width: 100,
customRender: ({ text }) => (text ? `ID: ${text}` : '-')
},
// {
// title: '对方用户',
// dataIndex: 'toUserId',
// key: 'toUserId',
// align: 'center',
// customRender: ({ text }) => (text ? `ID: ${text}` : '-')
// },
{
title: '描述',
dataIndex: 'describe',
key: 'describe',
dataIndex: 'comments',
key: 'comments',
align: 'left',
width: 200,
ellipsis: true,
customRender: ({ text }) => text || '-'
},
@@ -197,24 +197,16 @@
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
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
sorter: true
}
// {
// title: '操作',
// key: 'action',
// width: 180,
// fixed: 'right',
// align: 'center',
// hideInSetting: true
// }
]);
/* 搜索 */

View File

@@ -19,16 +19,6 @@
</p>
<p class="ant-upload-hint">将文件拖到此处或点击上传</p>
</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>
</ele-modal>
</template>
@@ -37,7 +27,7 @@
import { ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { importSdyDealerOrder } from '@/api/sdy/sdyDealerOrder';
import { importShopDealerOrder } from '@/api/shop/shopDealerOrder';
const emit = defineEmits<{
(e: 'done'): void;
@@ -68,7 +58,7 @@
return false;
}
loading.value = true;
importSdyDealerOrder(file)
importShopDealerOrder(file)
.then((msg) => {
loading.value = false;
message.success(msg);

View File

@@ -1,182 +1,147 @@
<template>
<div class="flex items-center gap-20">
<!-- 搜索表单 -->
<a-form
:model="where"
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>
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-input-search
allow-clear
placeholder="请输入关键词"
placeholder="客户名称|订单编号"
style="width: 240px"
v-model:value="where.keywords"
@search="handleSearch"
@search="reload"
/>
<!-- <a-button type="primary" html-type="submit" class="ele-btn-icon">-->
<!-- <template #icon>-->
<!-- <SearchOutlined/>-->
<!-- </template>-->
<!-- 搜索-->
<!-- </a-button>-->
<a-button @click="resetSearch"> 重置 </a-button>
<a-button type="dashed" @click="handleExport">导出xls</a-button>
</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>
<script lang="ts" setup>
import { ref } from 'vue';
import {
DollarOutlined,
UploadOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
import type { ShopDealerOrderParam } from '@/api/shop/shopDealerOrder/model';
import Import from './Import.vue';
import { utils, writeFile } from 'xlsx';
import { message } from 'ant-design-vue';
import { getTenantId } from '@/utils/domain';
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<{
// 选中的数据
selection?: any[];
// 选中的角色
selection?: [];
}>(),
{
selection: () => []
}
{}
);
const emit = defineEmits<{
(e: 'search', where?: ShopDealerOrderParam): void;
(e: 'batchSettle'): void;
(e: 'export'): void;
(e: 'importDone'): void;
(e: 'remove'): void;
}>();
// 是否显示导入弹窗
const showImport = ref(false);
const reload = () => {
emit('search', where);
};
// 搜索表单
const { where, resetFields } = useSearch<ShopDealerOrderParam>({
orderNo: '',
productName: '',
isInvalid: undefined,
isSettled: undefined
// 表单数据
const { where } = useSearch<ShopDealerOrderParam>({
keywords: '',
userId: undefined,
orderNo: undefined,
isSettled: 1, // 与列表页一致:只展示/导出已结算订单
page: 1,
limit: 5000
});
// 搜索
const handleSearch = () => {
const searchParams = { ...where };
// 清除空值
Object.keys(searchParams).forEach((key) => {
if (searchParams[key] === '' || searchParams[key] === undefined) {
delete searchParams[key];
}
const list = ref<ShopDealerOrder[]>([]);
const toMoney = (val: unknown) => {
const n = Number.parseFloat(String(val ?? '0'));
return Number.isFinite(n) ? n.toFixed(2) : '0.00';
};
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(() => {});
};
// 重置搜索
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;
};
void props;
</script>

View File

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

View File

@@ -7,28 +7,24 @@
:columns="columns"
:datasource="datasource"
:customRow="customRow"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@batchSettle="batchSettle"
@export="handleExport"
@remove="removeBatch"
@importDone="reload"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'title'">
<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 v-if="column.key === 'orderPrice'">
{{ record.orderPrice.toFixed(2) }}
{{ parseFloat(record.orderPrice).toFixed(2) }}
</template>
<template v-if="column.key === 'degreePrice'">
@@ -36,7 +32,7 @@
</template>
<template v-if="column.key === 'price'">
{{ record.price }}
{{ record.price || 0 }}
</template>
<template v-if="column.key === 'settledPrice'">
@@ -47,6 +43,30 @@
{{ record.payPrice.toFixed(2) }}
</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'">
<div class="dealer-info">
<div v-if="record.firstUserId" class="dealer-level">
@@ -76,8 +96,11 @@
</template>
<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 === 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 v-if="column.key === 'createTime'">
@@ -88,14 +111,30 @@
<a-tooltip title="结算时间">
<span class="text-purple-500">{{ record.settleTime }}</span>
</a-tooltip>
<a-tooltip title="解冻时间">
<span class="text-green-500">{{ record.unfreezeTime }}</span>
</a-tooltip>
</div>
</template>
<template v-if="column.key === 'action'">
<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" />
</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
v-if="record.isSettled === 0"
title="确定要删除吗?"
@@ -121,7 +160,10 @@
<script lang="ts" setup>
import { createVNode, ref } from '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 {
DatasourceFunction,
@@ -139,7 +181,9 @@
ShopDealerOrder,
ShopDealerOrderParam
} 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);
@@ -153,9 +197,6 @@
// 加载状态
const loading = ref(true);
// 当前搜索条件
const currentWhere = ref<ShopDealerOrderParam>({});
// 表格数据源
const datasource: DatasourceFunction = ({
page,
@@ -167,11 +208,8 @@
if (filters) {
where.status = filters.status;
}
// 保存当前搜索条件用于导出
currentWhere.value = { ...where };
// 未结算订单
where.isSettled = 0;
where.myOrder = 1;
// 已结算订单
where.isSettled = 1;
return pageShopDealerOrder({
...where,
...orders,
@@ -182,88 +220,77 @@
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '订单编号',
dataIndex: 'orderNo',
key: 'orderNo'
key: 'orderNo',
align: 'center',
width: 200
},
{
title: '客户名称',
title: '买家',
dataIndex: 'title',
key: 'title',
width: 220
key: 'title'
},
{
title: '结算电量',
title: '订单金额',
dataIndex: 'orderPrice',
key: 'orderPrice',
align: 'center'
},
{
title: '换算成度',
dataIndex: 'degreePrice',
key: 'degreePrice',
title: '一级佣金',
dataIndex: 'firstNickname',
key: 'firstNickname',
align: 'center'
},
{
title: '结算单价',
dataIndex: 'price',
key: 'price',
title: '二级佣金',
dataIndex: 'secondNickname',
key: 'secondNickname',
align: 'center'
},
{
title: '结算金额',
dataIndex: 'settledPrice',
key: 'settledPrice',
title: '一级门店分红',
dataIndex: 'firstDividendUserName',
key: 'firstDividendUserName',
align: 'center'
},
{
title: '税费',
dataIndex: 'rate',
key: 'rate',
title: '二级门店分红',
dataIndex: 'secondDividendUserName',
key: 'secondDividendUserName',
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: '结算状态',
dataIndex: 'isSettled',
key: 'isSettled',
align: 'center',
width: 100
align: 'center'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center'
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
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 });
};
/* 结算单个订单 */
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 = () => {
if (!selection.value.length) {
@@ -318,16 +376,10 @@
});
};
/* 导出数据 */
const handleExport = () => {
// 调用导出API传入当前搜索条件
exportSdyDealerOrder(currentWhere.value);
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerOrder) => {
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 :value="1">一级</a-radio>
<a-radio :value="2">二级</a-radio>
<a-radio :value="3">三级</a-radio>
<!-- <a-radio :value="3">三级</a-radio>-->
</a-radio-group>
<div class="setting-desc">设置分销商推荐层级关系</div>
</a-form-item>
@@ -235,14 +235,26 @@
import { PlusOutlined } from '@ant-design/icons-vue';
import { getPageTitle } from '@/utils/common';
import {
addShopDealerSetting,
updateShopDealerSetting,
getShopDealerSetting
listShopDealerSetting
} from '@/api/shop/shopDealerSetting';
import type { ShopDealerSetting } from '@/api/shop/shopDealerSetting/model';
// 当前激活的标签页
const activeTab = ref('basic');
// 保存状态
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({
@@ -283,6 +295,63 @@
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) => {
console.log('预览图片:', file);
@@ -291,37 +360,178 @@
/* 加载设置 */
const loadSettings = async () => {
try {
// 这里应该调用API获取设置数据
// const settings = await getShopDealerSetting();
// 然后将数据分配到各个设置对象中
console.log('加载设置数据');
const list = await listShopDealerSetting();
const nextMap: Record<string, ShopDealerSetting> = {};
const nextValues: Record<string, any> = {};
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) {
console.error('加载设置失败:', 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 () => {
saving.value = true;
try {
// 收集所有设置数据
const allSettings = {
basic: basicSettings,
commission: commissionSettings,
withdraw: withdrawSettings,
agreement: agreementSettings,
notification: notificationSettings,
page: pageSettings
const basicValues = {
is_open: getNumberFlag(basicSettings.enableDistribution),
level: basicSettings.distributionLevel,
self_buy: getNumberFlag(basicSettings.dealerSelfBuy)
};
console.log('保存设置:', allSettings);
const conditionValues = {
...(settingValuesMap.value.condition || {}),
become: commissionSettings.applyType
};
// 这里应该调用API保存设置
// await updateShopDealerSetting(allSettings);
const settlementValues = {
...(settingValuesMap.value.settlement || {}),
pay_type: [...withdrawSettings.withdrawMethods],
min_money: commissionSettings.minWithdrawAmount,
settle_days: commissionSettings.settlementType,
fee_rate: withdrawSettings.withdrawFeeRate,
audit: getNumberFlag(withdrawSettings.withdrawAudit)
};
// 模拟保存
await new Promise((resolve) => setTimeout(resolve, 1000));
const licenseValues = {
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('设置保存成功');
} catch (error) {

View File

@@ -1,221 +1,51 @@
<!-- 搜索表单 -->
<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="请输入申请人姓名"
<a-space :size="10" style="flex-wrap: wrap">
<a-input-search
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"
placeholder="姓名|电话"
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-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';
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?: any[];
// 选中的角色
selection?: [];
}>(),
{
selection: () => []
}
{}
);
const emit = defineEmits<{
(e: 'search', where?: ShopDealerApplyParam): void;
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'batchApprove'): void;
(e: 'export'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 搜索表单
const searchForm = reactive<any>({
realName: '',
mobile: '',
applyType: undefined,
applyStatus: undefined,
dateRange: undefined
const reload = () => {
emit('search', where);
};
// 表单数据
const { where } = useSearch<ShopDealerUserParam>({
keywords: '',
userId: undefined,
mobile: undefined,
realName: 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'
watch(
() => props.selection,
() => {}
);
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

@@ -5,12 +5,9 @@
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:confirm-loading="loading"
:title="isUpdate ? '编辑分销商用户' : '添加分销商用户'"
:body-style="{
paddingBottom: '28px',
maxHeight: '70vh',
overflowY: 'auto'
}"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
@@ -18,265 +15,189 @@
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
: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">信息</a-divider>
<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="用户ID"
name="userId"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<a-input-number
:min="1"
placeholder="请输入用户ID"
v-model:value="form.userId"
style="width: 100%"
:disabled="isUpdate"
<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="realName"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<a-input
allow-clear
placeholder="请输入真实姓名"
v-model:value="form.realName"
/>
<a-form-item label="类型" name="type">
<a-select v-model:value="form.type" 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-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item
label="手机号"
name="mobile"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<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="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-form-item label="真实姓名" name="realName">
<a-input
placeholder="系统自动生成"
v-model:value="form.qrcode"
style="width: calc(100% - 80px)"
:disabled="true"
allow-clear
:maxlength="20"
placeholder="请输入真实姓名"
v-model:value="form.realName"
/>
<a-button type="primary" @click="generateQrcode">生成</a-button>
</a-input-group>
</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="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 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="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>
</ele-modal>
</template>
<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 { assignObject, uuid } from 'ele-admin-pro';
import {
addShopDealerUser,
updateShopDealerUser
} from '@/api/shop/shopDealerUser';
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);
@@ -309,12 +230,16 @@
const form = reactive<ShopDealerUser>({
id: undefined,
userId: undefined,
avatar: undefined,
type: 0,
realName: undefined,
mobile: undefined,
payPassword: undefined,
payPassword: '',
money: undefined,
freezeMoney: undefined,
totalMoney: undefined,
rate: undefined,
price: undefined,
refereeId: undefined,
firstNum: undefined,
secondNum: undefined,
@@ -324,8 +249,6 @@
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
shopDealerUserId: undefined,
shopDealerUserName: '',
status: 0,
comments: '',
sortNumber: 100
@@ -336,58 +259,90 @@
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,
message: '请输入用户ID',
trigger: 'blur'
type: 'number',
message: '请选择类型',
trigger: 'change'
}
],
realName: [
{
required: true,
message: '请输入真实姓名',
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();
},
{
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'
}
]
});
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,
@@ -404,25 +359,6 @@
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 = () => {
if (!formRef.value) {
@@ -432,46 +368,34 @@
.validate()
.then(() => {
loading.value = true;
// 数据处理
const formData = {
...form
};
// 确保数值类型正确
if (formData.userId) formData.userId = Number(formData.userId);
if (formData.refereeId) formData.refereeId = Number(formData.refereeId);
if (formData.money !== undefined)
formData.money = Number(formData.money);
if (formData.freezeMoney !== undefined)
formData.freezeMoney = Number(formData.freezeMoney);
if (formData.totalMoney !== undefined)
formData.totalMoney = Number(formData.totalMoney);
if (formData.firstNum !== undefined)
formData.firstNum = Number(formData.firstNum);
if (formData.secondNum !== undefined)
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;
// 不在弹窗里编辑的字段不提交避免误更新如自增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 || '操作成功');
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
console.error('保存失败:', e);
message.error(e.message || '操作失败');
message.error(e.message);
});
})
.catch(() => {});
@@ -481,52 +405,39 @@
() => 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>
<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' }">
<ele-pro-table
ref="tableRef"
row-key="applyId"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
v-model:selection="selection"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@batchApprove="batchApprove"
@export="exportData"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'applyStatus'">
<a-tag v-if="record.applyStatus === 10" color="orange"
>待审核</a-tag
>
<a-tag v-if="record.applyStatus === 20" color="green">已通过</a-tag>
<a-tag v-if="record.applyStatus === 30" color="red">已驳回</a-tag>
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</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 @click="openEdit(record)" class="ele-text-primary">
<EditOutlined />
编辑
</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-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
v-if="record.applyStatus != 20"
title="确定要删除此申请记录吗?"
title="确定要删除此记录吗?"
@confirm="remove(record)"
placement="topRight"
>
<a class="ele-text-danger">
<DeleteOutlined />
删除
</a>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</template>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ShopDealerApplyEdit
v-model:visible="showEdit"
:data="current"
@done="reload"
/>
<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 } from 'vue';
import { createVNode, ref, computed } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
ExclamationCircleOutlined,
CheckOutlined,
CloseOutlined,
EditOutlined,
DeleteOutlined
} from '@ant-design/icons-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 ShopDealerApplyEdit from './components/shopDealerApplyEdit.vue';
import {
pageShopDealerApply,
removeShopDealerApply,
removeBatchShopDealerApply,
batchApproveShopDealerApply,
updateShopDealerApply
} from '@/api/shop/shopDealerApply';
import type {
ShopDealerApply,
ShopDealerApplyParam
} from '@/api/shop/shopDealerApply/model';
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<ShopDealerApply[]>([]);
const selection = ref<ShopDealerUser[]>([]);
// 当前编辑数据
const current = ref<ShopDealerApply | null>(null);
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,
@@ -124,9 +154,7 @@
if (filters) {
where.status = filters.status;
}
where.type = 4;
where.applyStatus = 20;
return pageShopDealerApply({
return pageShopDealerUser({
...where,
...orders,
page,
@@ -134,201 +162,140 @@
});
};
// 表格列配置
// 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'applyId',
key: 'applyId',
align: 'center',
width: 80,
fixed: 'left'
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
width: 90,
},
{
title: '申请人信息',
key: 'applicantInfo',
align: 'left',
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',
title: '类型',
dataIndex: 'type',
key: 'type',
align: 'center',
width: 120
},
{
title: '推荐人',
dataIndex: 'refereeId',
key: 'refereeId',
align: 'center',
width: 100,
customRender: ({ text }) => (text ? `ID: ${text}` : '')
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: '申请时间',
// dataIndex: 'applyTime',
// key: 'applyTime',
// align: 'center',
// width: 120,
// customRender: ({ text }) => text ? toDateString(new Date(text), 'yyyy-MM-dd HH:mm') : '-'
// title: '推荐人用户ID',
// dataIndex: 'refereeId',
// key: 'refereeId',
// width: 120
// },
// {
// title: '审核时间',
// dataIndex: 'auditTime',
// key: 'auditTime',
// align: 'center',
// customRender: ({ text }) => text ? toDateString(new Date(text), 'yyyy-MM-dd HH:mm') : '-'
// title: '成员数量(一级)',
// dataIndex: 'firstNum',
// key: 'firstNum',
// width: 120
// },
// {
// title: '成员数量(二级)',
// dataIndex: 'secondNum',
// key: 'secondNum',
// width: 120
// },
// {
// title: '成员数量(三级)',
// dataIndex: 'thirdNum',
// key: 'thirdNum',
// width: 120
// },
{
title: '驳回原因',
dataIndex: 'rejectReason',
key: 'rejectReason',
align: 'left',
ellipsis: true,
customRender: ({ text }) => text || '-'
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
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
width: 380,
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopDealerApplyParam) => {
const reload = (where?: ShopDealerUserParam) => {
selection.value = [];
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;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerApply) => {
if (!row.applyId) {
message.error('删除失败:缺少必要参数');
return;
}
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
const hide = message.loading('正在删除申请记录...', 0);
removeShopDealerApply(row.applyId)
/* 删除单个 */
const remove = (row: ShopDealerUser) => {
const hide = message.loading('请求中..', 0);
removeShopDealerUser(row.id)
.then((msg) => {
hide();
message.success(msg || '删除成功');
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message || '删除失败');
message.error(e.message);
});
};
@@ -338,99 +305,34 @@
message.error('请至少选择一条数据');
return;
}
const validIds = selection.value
.filter((d) => d.applyId)
.map((d) => d.applyId);
if (!validIds.length) {
message.error('选中的数据中没有有效的ID');
return;
}
Modal.confirm({
title: '批量删除确认',
content: `确定要删除选中的 ${validIds.length} 条申请记录吗?此操作不可恢复。`,
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
okText: '确认删除',
okType: 'danger',
cancelText: '取消',
onOk: () => {
const hide = message.loading(
`正在删除 ${validIds.length} 条记录...`,
0
);
removeBatchShopDealerApply(validIds)
const hide = message.loading('请求中..', 0);
removeBatchShopDealerUser(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg || `成功删除 ${validIds.length} 条记录`);
selection.value = [];
message.success(msg);
reload();
})
.catch((e) => {
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 = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopDealerApply) => {
const customRow = (record: ShopDealerUser) => {
return {
// 行点击事件
onClick: () => {
@@ -447,71 +349,8 @@
<script lang="ts">
export default {
name: 'ShopDealerApply'
name: 'ShopDealerUser'
};
</script>
<style lang="less" scoped>
.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>
<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,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 { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { importSdyDealerOrder } from '@/api/sdy/sdyDealerOrder';
import { importShopDealerOrder } from '@/api/shop/shopDealerOrder';
const emit = defineEmits<{
(e: 'done'): void;
@@ -68,7 +68,7 @@
return false;
}
loading.value = true;
importSdyDealerOrder(file)
importShopDealerOrder(file)
.then((msg) => {
loading.value = false;
message.success(msg);

View File

@@ -33,6 +33,7 @@
v-model:value="where.keywords"
@search="handleSearch"
/>
<a-button type="dashed" @click="handleExport">导出xls</a-button>
<a-button @click="resetSearch"> 重置 </a-button>
</a-space>
</a-form-item>
@@ -46,9 +47,16 @@
<script lang="ts" setup>
import { ref } from '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 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(
defineProps<{
@@ -73,9 +81,13 @@
// 搜索表单
const { where, resetFields } = useSearch<ShopDealerWithdrawParam>({
keywords: ''
keywords: '',
page: 1,
limit: 5000
});
const list = ref<ShopDealerWithdraw[]>([]);
// 搜索
const handleSearch = () => {
const searchParams = { ...where };
@@ -88,6 +100,102 @@
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 = () => {
resetFields();

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:width="700"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
@@ -14,7 +14,7 @@
ref="formRef"
:model="form"
: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="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
@@ -56,20 +56,6 @@
v-model:value="form.sortNumber"
/>
</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>
</ele-modal>
</template>

View File

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

View File

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

View File

@@ -24,17 +24,9 @@
>
<a-tabs type="card" v-model:active-key="active" @change="onChange">
<a-tab-pane tab="基本信息" key="base">
<a-form-item label="商品ID" name="goodsId">
{{ form.goodsId }}
</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="商品ID" name="goodsId">-->
<!-- {{ form.goodsId }}-->
<!-- </a-form-item>-->
<a-form-item label="所属栏目" name="categoryId">
<a-tree-select
allow-clear
@@ -49,6 +41,14 @@
@change="onCategoryId"
/>
</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-textarea
:rows="1"
@@ -177,13 +177,64 @@
</a-button>
</a-upload>
</a-form-item>
<a-form-item label="状态" name="isShow">
<a-radio-group v-model:value="form.isShow">
<a-radio :value="1">上架</a-radio>
<a-radio :value="0">下架</a-radio>
<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-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-form-item label="规格类型" name="specs">
<a-radio-group v-model:value="form.specs">
@@ -326,57 +377,6 @@
</div>
</a-form-item>
</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-form-item label="商品重量" name="goodsWeight">
<a-input
@@ -423,6 +423,15 @@
v-model:value="form.position"
/>
</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-switch
size="small"
@@ -439,26 +448,152 @@
:un-checked-value="0"
/>
</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
size="small"
v-model:checked="form.commissionRole"
v-model:checked="form.isOpenCommission"
: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>
<template v-if="form.isOpenCommission === 1">
<a-form-item label="分佣类型" name="commissionType">
<a-radio-group v-model:value="form.commissionType">
<a-radio :value="10">固定金额</a-radio>
<a-radio :value="20">百分比</a-radio>
</a-radio-group>
</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">
<a-form-item label="可用日期">
<a-select v-model:value="canUseDate" mode="multiple">
@@ -505,15 +640,6 @@
/>
</a-form-item>
</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-tabs>
</a-form>
@@ -988,6 +1114,15 @@ const form = reactive<ShopGoods>({
categoryName: undefined,
specs: 0,
commissionRole: 0,
// 分销佣金(新字段,后端保持 snake_case
isOpenCommission: 0,
commissionType: 10,
firstMoney: 0,
secondMoney: 0,
thirdMoney: 0,
firstDividend: 0,
secondDividend: 0,
deliveryMoney: 0,
position: undefined,
price: undefined,
originPrice: undefined,
@@ -1710,6 +1845,33 @@ const onPaste = (e) => {
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 = () => {
if (!formRef.value) {
@@ -1724,6 +1886,27 @@ const save = () => {
if (ensureTag.value.length) form.ensureTag = ensureTag.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.commissionRole === 1) {
for (let i = 0; i < form.goodsRoleCommission.length; i++) {
@@ -1750,6 +1933,14 @@ const save = () => {
if (isUpdate.value) {
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;
saveOrUpdate(formData)
.then((msg) => {
@@ -1875,6 +2066,17 @@ watch(
ensureTagItem.value = '';
if (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) {
images.value.push({
uid: uuid(),

View File

@@ -49,7 +49,7 @@
</a-space>
</template>
<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 === 2" color="purple">待审核</a-tag>
<a-tag v-if="record.status === 3" color="red">审核不通过</a-tag>

View File

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

View File

@@ -1,7 +1,7 @@
<!-- 用户编辑弹窗 -->
<template>
<a-drawer
:width="`65%`"
width="70%"
:visible="visible"
:confirm-loading="loading"
:maxable="maxAble"
@@ -11,110 +11,6 @@
:footer="null"
@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-descriptions :column="3">
<!-- 第一排-->
@@ -300,6 +196,12 @@
<a-tag v-if="form.isInvoice == 2" color="blue">不能开具</a-tag>
</a-descriptions-item>
<!-- 第六排-->
<a-descriptions-item
label="买家备注"
:labelStyle="{ width: '90px', color: '#808080' }"
>
{{ form.buyerRemarks }}
</a-descriptions-item>
<!-- <a-descriptions-item-->
<!-- label="结算状态"-->
<!-- :labelStyle="{ width: '90px', color: '#808080' }"-->
@@ -318,6 +220,32 @@
</a-descriptions>
</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">
<a-descriptions :column="2">
@@ -433,48 +361,22 @@
label="快递公司"
:labelStyle="{ width: '90px', color: '#808080' }"
>
{{ form.shopOrderDelivery.expressName || '未填写' }}
{{
form.expressName ||
form.shopOrderDelivery?.expressName ||
'未填写'
}}
</a-descriptions-item>
<a-descriptions-item
label="物流单号"
:labelStyle="{ width: '90px', color: '#808080' }"
>
{{ form.shopOrderDelivery.expressNo || '未填写' }}
{{ form.expressNo || form.shopOrderDelivery?.expressNo || '未填写' }}
</a-descriptions-item>
</a-descriptions>
</a-spin>
</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-spin :spinning="loading">
{{ form.merchantRemarks || '-' }}
@@ -653,6 +555,10 @@
deliveryTime: undefined,
// 无需发货备注
deliveryNote: undefined,
// 快递公司名称
expressName: undefined,
// 物流单号
expressNo: undefined,
// 优惠类型0无、1抵扣优惠券、2折扣优惠券、3、VIP月卡、4VIP年卡5VIP次卡、6VIP会员卡、7IC月卡、8IC年卡、9IC次卡、10IC会员卡、11免费订单、12VIP充值卡、13IC充值卡、14VIP季卡、15IC季卡
couponType: undefined,
// 优惠说明
@@ -736,10 +642,10 @@
},
{
title: '数量',
dataIndex: 'quantity',
dataIndex: 'totalNum',
align: 'center' as const,
customRender: ({ record }: { record: any }) => {
return record.quantity || 1;
return record.totalNum || 1;
}
},
{

View File

@@ -17,53 +17,53 @@
allow-clear
v-model:value="where.orderNo"
placeholder="订单编号"
style="width: 240px"
style="width: 260px"
@search="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-->
<!-- v-model:value="where.orderStatus"-->
<!-- v-model:value="where.type"-->
<!-- style="width: 150px"-->
<!-- placeholder="订单状态"-->
<!-- 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-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-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
:options="getPayType()"
v-model:value="where.payType"
v-model:value="where.orderStatus"
style="width: 150px"
placeholder="付款方式"
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-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
v-model:value="dateRange"
@@ -255,11 +255,13 @@
message.error(msg);
loading.value = false;
})
.finally(() => {});
.finally(() => {
});
};
watch(
() => props.selection,
() => {}
() => {
}
);
</script>

View File

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