This commit is contained in:
2026-09-29 15:15:49 +08:00
parent b98cd33203
commit b31d232020
134 changed files with 25479 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ArticleCollect, ArticleCollectParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageArticleCollect(params: ArticleCollectParam) {
const res = await request.get<ApiResult<PageResult<ArticleCollect>>>(
MODULES_API_URL + '/cms/article-collect/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listArticleCollect(params?: ArticleCollectParam) {
const res = await request.get<ApiResult<ArticleCollect[]>>(
MODULES_API_URL + '/cms/article-collect',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addArticleCollect(data: ArticleCollect) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/article-collect',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateArticleCollect(data: ArticleCollect) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/article-collect',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeArticleCollect(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/article-collect/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchArticleCollect(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/article-collect/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getArticleCollect(id: number) {
const res = await request.get<ApiResult<ArticleCollect>>(
MODULES_API_URL + '/cms/article-collect/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+31
View File
@@ -0,0 +1,31 @@
import type { PageParam } from '@/api';
/**
*
*/
export interface ArticleCollect {
//
id?: number;
//
articleId?: number;
//
userId?: number;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 搜索条件
*/
export interface ArticleCollectParam extends PageParam {
id?: number;
keywords?: string;
}
+106
View File
@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ArticleComment, ArticleCommentParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageArticleComment(params: ArticleCommentParam) {
const res = await request.get<ApiResult<PageResult<ArticleComment>>>(
MODULES_API_URL + '/cms/article-comment/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listArticleComment(params?: ArticleCommentParam) {
const res = await request.get<ApiResult<ArticleComment[]>>(
MODULES_API_URL + '/cms/article-comment',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addArticleComment(data: ArticleComment) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/article-comment',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateArticleComment(data: ArticleComment) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/article-comment',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeArticleComment(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/article-comment/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchArticleComment(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/article-comment/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getArticleComment(id: number) {
const res = await request.get<ApiResult<ArticleComment>>(
MODULES_API_URL + '/cms/article-comment/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+33
View File
@@ -0,0 +1,33 @@
import type { PageParam } from '@/api';
/**
*
*/
export interface ArticleComment {
//
id?: number;
//
articleId?: number;
//
userId?: number;
//
content?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 搜索条件
*/
export interface ArticleCommentParam extends PageParam {
id?: number;
keywords?: string;
}
+106
View File
@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { Coupon, CouponParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageCoupon(params: CouponParam) {
const res = await request.get<ApiResult<PageResult<Coupon>>>(
MODULES_API_URL + '/shop/coupon/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listCoupon(params?: CouponParam) {
const res = await request.get<ApiResult<Coupon[]>>(
MODULES_API_URL + '/shop/coupon',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addCoupon(data: Coupon) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/shop/coupon',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateCoupon(data: Coupon) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/shop/coupon',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeCoupon(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/coupon/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchCoupon(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/coupon/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getCoupon(id: number) {
const res = await request.get<ApiResult<Coupon>>(
MODULES_API_URL + '/shop/coupon/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+45
View File
@@ -0,0 +1,45 @@
import type { PageParam } from '@/api';
/**
*
*/
export interface Coupon {
//
id?: number;
//
title?: string;
// 0满减 1折扣
type?: number;
// 过期类型0领取计算 1固定时间
expireType?: number;
// 起始金额
startAmount?: string;
// 抵扣金额
reduceAmount?: string;
// 折扣比例
reduceRate?: string;
// 过期天数
expireDay?: number;
// 过期时间(固定)
expireEndDate?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 用户ID
userId?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 搜索条件
*/
export interface CouponParam extends PageParam {
id?: number;
keywords?: string;
}
+121
View File
@@ -0,0 +1,121 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { DealerWithdraw, DealerWithdrawParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import type { FileRecord } from '@/api/system/file/model';
/**
* 分页查询分销商提现明细表
*/
export async function pageDealerWithdraw(params: DealerWithdrawParam) {
const res = await request.get<ApiResult<PageResult<DealerWithdraw>>>(
MODULES_API_URL + '/shop/dealer-withdraw/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询分销商提现明细表列表
*/
export async function listDealerWithdraw(params?: DealerWithdrawParam) {
const res = await request.get<ApiResult<DealerWithdraw[]>>(
MODULES_API_URL + '/shop/dealer-withdraw',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加分销商提现明细表
*/
export async function addDealerWithdraw(data: DealerWithdraw) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/shop/dealer-withdraw',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改分销商提现明细表
*/
export async function updateDealerWithdraw(data: DealerWithdraw) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/shop/dealer-withdraw',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除分销商提现明细表
*/
export async function removeDealerWithdraw(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/dealer-withdraw/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除分销商提现明细表
*/
export async function removeBatchDealerWithdraw(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/dealer-withdraw/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询分销商提现明细表
*/
export async function getDealerWithdraw(id: number) {
const res = await request.get<ApiResult<DealerWithdraw>>(
MODULES_API_URL + '/shop/dealer-withdraw/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 导出分销商提现明细
*/
export async function exportDealerWithdraw(params?: DealerWithdrawParam) {
const res = await request.post<ApiResult<FileRecord>>(
MODULES_API_URL + '/shop/dealer-withdraw/export',
params
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
@@ -0,0 +1,54 @@
import type { PageParam } from '@/api';
import type { User } from '@/api/system/user/model';
/**
* 分销商提现明细表
*/
export interface DealerWithdraw {
// 主键ID
id?: number;
//
orderNo?: string;
// 分销商用户ID
userId?: number;
// 提现金额
money?: string;
// 打款方式 (10微信 20支付宝 30银行卡)
payType?: number;
// 支付宝姓名
alipayName?: string;
// 支付宝账号
alipayAccount?: string;
// 开户行名称
bankName?: string;
// 银行开户名
bankAccount?: string;
// 银行卡号
bankCard?: string;
// 申请状态 (10待审核 20审核通过 30驳回 40已打款)
applyStatus?: number;
// 审核时间
auditTime?: number;
// 驳回原因
rejectReason?: string;
// 来源客户端(APP、H5、小程序等)
platform?: string;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
// 用户
user?: User;
}
/**
* 分销商提现明细表搜索条件
*/
export interface DealerWithdrawParam extends PageParam {
id?: number;
keywords?: string;
userId?: number;
applyStatus?: number | number[];
}
+136
View File
@@ -0,0 +1,136 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { Merchant, MerchantParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询商户
*/
export async function pageMerchant(params: MerchantParam) {
const res = await request.get<ApiResult<PageResult<Merchant>>>(
MODULES_API_URL + '/shop/merchant/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询商户列表
*/
export async function listMerchant(params?: MerchantParam) {
const res = await request.get<ApiResult<Merchant[]>>(
MODULES_API_URL + '/shop/merchant',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询商户精简列表
*/
export async function listMerchantMini(params?: MerchantParam) {
const res = await request.get<ApiResult<Merchant[]>>(
MODULES_API_URL + '/shop/merchant/mini',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加商户
*/
export async function addMerchant(data: Merchant) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/shop/merchant',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改商户
*/
export async function updateMerchant(data: Merchant) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/shop/merchant',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除商户
*/
export async function removeMerchant(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/merchant/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除商户
*/
export async function removeBatchMerchant(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/merchant/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量新增商户
*/
export async function addBatchMerchant(data: Merchant[]) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/shop/merchant/batch',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询商户
*/
export async function getMerchant(id: number) {
const res = await request.get<ApiResult<Merchant>>(
MODULES_API_URL + '/shop/merchant/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+58
View File
@@ -0,0 +1,58 @@
import type { PageParam } from '@/api';
/**
* 商户
*/
export interface Merchant {
// ID
id?: number;
// 编码
code?: string;
areaName?: string;
chain?: string;
// 商户名称
name?: string;
// 商户图标
image?: string;
// 商户手机号
phone?: string;
// 商户姓名
realName?: string;
// 经纬度
lngAndLat?: string;
//
lng?: string;
//
lat?: string;
// 所在省份
province?: string;
// 所在城市
city?: string;
// 所在辖区
region?: string;
// 详细地址
address?: string;
// 备注
comments?: string;
// 所有人
userId?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 状态
status?: number;
// 排序号
sortNumber?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
}
/**
* 商户搜索条件
*/
export interface MerchantParam extends PageParam {
id?: number;
keywords?: string;
chain?: string;
}
+117
View File
@@ -0,0 +1,117 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { UserCoupon, UserCouponParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageUserCoupon(params: UserCouponParam) {
const res = await request.get<ApiResult<PageResult<UserCoupon>>>(
MODULES_API_URL + '/shop/user-coupon/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listUserCoupon(params?: UserCouponParam) {
const res = await request.get<ApiResult<UserCoupon[]>>(
MODULES_API_URL + '/shop/user-coupon',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addUserCoupon(data: UserCoupon) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/shop/user-coupon',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
export async function userCouponSend(data: UserCoupon) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/shop/user-coupon/send',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateUserCoupon(data: UserCoupon) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/shop/user-coupon',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeUserCoupon(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/user-coupon/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchUserCoupon(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/user-coupon/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getUserCoupon(id: number) {
const res = await request.get<ApiResult<UserCoupon>>(
MODULES_API_URL + '/shop/user-coupon/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+37
View File
@@ -0,0 +1,37 @@
import type { PageParam } from '@/api';
/**
*
*/
export interface UserCoupon {
//
id?: number;
//
couponId?: number;
//
orderId?: number;
//
used?: number;
// 使用时间
usedTime?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 用户ID
userId?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 搜索条件
*/
export interface UserCouponParam extends PageParam {
id?: number;
keywords?: string;
}
+106
View File
@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { UserParentList, UserParentListParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageUserParentList(params: UserParentListParam) {
const res = await request.get<ApiResult<PageResult<UserParentList>>>(
MODULES_API_URL + '/shop/user-parent-list/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listUserParentList(params?: UserParentListParam) {
const res = await request.get<ApiResult<UserParentList[]>>(
MODULES_API_URL + '/shop/user-parent-list',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addUserParentList(data: UserParentList) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/shop/user-parent-list',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateUserParentList(data: UserParentList) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/shop/user-parent-list',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeUserParentList(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/user-parent-list/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchUserParentList(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/user-parent-list/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getUserParentList(id: number) {
const res = await request.get<ApiResult<UserParentList>>(
MODULES_API_URL + '/shop/user-parent-list/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
@@ -0,0 +1,35 @@
import type { PageParam } from '@/api';
/**
*
*/
export interface UserParentList {
//
id?: number;
//
uid?: number;
//
parentId?: number;
// 管理员备注
remark?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 搜索条件
*/
export interface UserParentListParam extends PageParam {
id?: number;
keywords?: string;
}
+106
View File
@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { UserPointLog, UserPointLogParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageUserPointLog(params: UserPointLogParam) {
const res = await request.get<ApiResult<PageResult<UserPointLog>>>(
MODULES_API_URL + '/shop/user-point-log/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listUserPointLog(params?: UserPointLogParam) {
const res = await request.get<ApiResult<UserPointLog[]>>(
MODULES_API_URL + '/shop/user-point-log',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addUserPointLog(data: UserPointLog) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/shop/user-point-log',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateUserPointLog(data: UserPointLog) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/shop/user-point-log',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeUserPointLog(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/user-point-log/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchUserPointLog(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/user-point-log/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getUserPointLog(id: number) {
const res = await request.get<ApiResult<UserPointLog>>(
MODULES_API_URL + '/shop/user-point-log/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+42
View File
@@ -0,0 +1,42 @@
import type { PageParam } from '@/api';
/**
* 用户积分记录
*/
export interface UserPointLog {
// ID
id?: number;
// 用户ID
userId?: number;
// 积分说明
msg?: string;
// 管理员备注
remark?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 搜索条件
*/
export interface UserPointLogParam extends PageParam {
id?: number;
keywords?: string;
userId?: number;
msg?: string;
remark?: string;
status?: number | number[];
deleted?: number;
}
+106
View File
@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { SysArea, SysAreaParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageSysArea(params: SysAreaParam) {
const res = await request.get<ApiResult<PageResult<SysArea>>>(
MODULES_API_URL + '/sys/sys-area/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listSysArea(params?: SysAreaParam) {
const res = await request.get<ApiResult<SysArea[]>>(
MODULES_API_URL + '/sys/sys-area',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addSysArea(data: SysArea) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/sys/sys-area',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateSysArea(data: SysArea) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/sys/sys-area',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeSysArea(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/sys/sys-area/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchSysArea(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/sys/sys-area/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getSysArea(id: number) {
const res = await request.get<ApiResult<SysArea>>(
MODULES_API_URL + '/sys/sys-area/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+25
View File
@@ -0,0 +1,25 @@
import type { PageParam } from '@/api';
/**
*
*/
export interface SysArea {
//
id?: number;
//
name?: string;
//
fid?: number;
//
levelId?: number;
// 租户id
tenantId?: number;
}
/**
* 搜索条件
*/
export interface SysAreaParam extends PageParam {
id?: number;
keywords?: string;
}
+36
View File
@@ -0,0 +1,36 @@
import request from '@/utils/request';
import type { ApiResult } from '@/api';
import { MODULES_API_URL } from '@/config/setting';
export interface AddressInfo {
lng?: string | number;
lat?: string | number;
lon?: string | number;
longitude?: string | number;
latitude?: string | number;
province?: string;
prov?: string;
city?: string;
region?: string;
district?: string;
county?: string;
detail?: string;
addressDetail?: string;
address?: string;
}
/**
* 地址解析
*/
export async function getAddressInfo(address: string) {
const res = await request.post<ApiResult<any>>(
MODULES_API_URL + '/yxe/ai/getAddress',
{
address
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
@@ -0,0 +1,135 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type {
YxeChronicDiseaseCollection,
YxeChronicDiseaseCollectionParam
} from './model';
import { MODULES_API_URL } from '@/config/setting';
import type { FileRecord } from '@/api/system/file/model';
/**
* 分页查询慢病收集
*/
export async function pageYxeChronicDiseaseCollection(
params: YxeChronicDiseaseCollectionParam
) {
const res = await request.get<
ApiResult<PageResult<YxeChronicDiseaseCollection>>
>(MODULES_API_URL + '/yxe/yxe-chronic-disease-collection/page', {
params
});
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询慢病收集列表
*/
export async function listYxeChronicDiseaseCollection(
params?: YxeChronicDiseaseCollectionParam
) {
const res = await request.get<ApiResult<YxeChronicDiseaseCollection[]>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-collection',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加慢病收集
*/
export async function addYxeChronicDiseaseCollection(
data: YxeChronicDiseaseCollection
) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-collection',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改慢病收集
*/
export async function updateYxeChronicDiseaseCollection(
data: YxeChronicDiseaseCollection
) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-collection',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除慢病收集
*/
export async function removeYxeChronicDiseaseCollection(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-collection/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除慢病收集
*/
export async function removeBatchYxeChronicDiseaseCollection(
data: (number | undefined)[]
) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-collection/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询慢病收集
*/
export async function getYxeChronicDiseaseCollection(id: number) {
const res = await request.get<ApiResult<YxeChronicDiseaseCollection>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-collection/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 导出慢病收集
*/
export async function exportYxeChronicDiseaseCollection(
params?: YxeChronicDiseaseCollectionParam
) {
const res = await request.post<ApiResult<FileRecord>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-collection/export',
params
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
@@ -0,0 +1,81 @@
import type { PageParam } from '@/api';
/**
* 慢病收集
*/
export interface YxeChronicDiseaseCollection {
//
id?: number;
//
name?: string;
//
gender?: string;
//
phone?: string;
//
birthday?: string;
// 是否高血压
highBloodPressure?: number;
// 血压值(如:120/80)
highBloodPressureValue?: string;
// 收缩压
systolicPressure?: string;
// 舒张压
diastolicPressure?: string;
// 是否高血糖
highBloodSugar?: number;
// 空腹血糖
fastingBloodGlucose?: string;
// 餐后2小时血糖
postprandial2hBloodGlucose?: string;
// 糖化血红蛋白
hba1c?: string;
// 是否高血脂
hyperlipidemia?: number;
// 总胆固醇
totalCholesterol?: string;
// 甘油三酯
triglyceride?: string;
// 低密度脂蛋白
ldlC?: string;
// 高密度脂蛋白
hdlC?: string;
// 血红蛋白数值
hemoglobinValue?: string;
// 贫血程度
anemiaLevel?: string;
// 地贫类型
thalassemiaType?: string;
// 病史
medicalHistory?: string;
// 用药史
medicationHistory?: string;
// 用药追踪
tracking?: string;
//
userId?: number;
// 连锁名称
chainName?: string;
// 门店名称
merchantName?: string;
// 状态, 0待审核 1通过 2驳回
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 慢病收集搜索条件
*/
export interface YxeChronicDiseaseCollectionParam extends PageParam {
id?: number;
keywords?: string;
name?: string;
phone?: string;
}
@@ -0,0 +1,133 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type {
YxeChronicDiseaseProduct,
YxeChronicDiseaseProductParam
} from './model';
import { MODULES_API_URL } from '@/config/setting';
import type { FileRecord } from '@/api/system/file/model';
/**
* 分页查询慢病产品
*/
export async function pageYxeChronicDiseaseProduct(
params: YxeChronicDiseaseProductParam
) {
const res = await request.get<ApiResult<PageResult<YxeChronicDiseaseProduct>>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-product/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询慢病产品列表
*/
export async function listYxeChronicDiseaseProduct(
params?: YxeChronicDiseaseProductParam
) {
const res = await request.get<ApiResult<YxeChronicDiseaseProduct[]>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-product',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加慢病产品
*/
export async function addYxeChronicDiseaseProduct(
data: YxeChronicDiseaseProduct
) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-product',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 下载导入模板
*/
export async function downloadTemplateYxeChronicDiseaseProduct() {
const res = await request.get<ApiResult<FileRecord>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-product/import-template'
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改慢病产品
*/
export async function updateYxeChronicDiseaseProduct(
data: YxeChronicDiseaseProduct
) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-product',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除慢病产品
*/
export async function removeYxeChronicDiseaseProduct(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-product/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除慢病产品
*/
export async function removeBatchYxeChronicDiseaseProduct(
data: (number | undefined)[]
) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-product/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询慢病产品
*/
export async function getYxeChronicDiseaseProduct(id: number) {
const res = await request.get<ApiResult<YxeChronicDiseaseProduct>>(
MODULES_API_URL + '/yxe/yxe-chronic-disease-product/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
@@ -0,0 +1,40 @@
import type { PageParam } from '@/api';
/**
* 慢病产品
*/
export interface YxeChronicDiseaseProduct {
id?: number;
// 连锁
chain?: string;
// 产品名
productName?: string;
// 规格粒/片
specCount?: number;
// 说明书每天使用/次
dailyUsageCount?: number;
// 每次几粒/片
dosagePerTime?: number;
// 1盒服用几天
boxUsageDays?: number;
// 状态, 0禁用, 1启用
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 搜索条件
*/
export interface YxeChronicDiseaseProductParam extends PageParam {
id?: number;
chain?: string;
productName?: string;
status?: number;
}
+129
View File
@@ -0,0 +1,129 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { YxeGoods, YxeGoodsCoverZipResult, YxeGoodsParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageYxeGoods(params: YxeGoodsParam) {
const res = await request.get<ApiResult<PageResult<YxeGoods>>>(
MODULES_API_URL + '/yxe/yxe-goods/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listYxeGoods(params?: YxeGoodsParam) {
const res = await request.get<ApiResult<YxeGoods[]>>(
MODULES_API_URL + '/yxe/yxe-goods',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addYxeGoods(data: YxeGoods) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-goods',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 上传商品封面 zip
*/
export async function uploadYxeGoodsCoverZip(file: File) {
const formData = new FormData();
formData.append('file', file);
const res = await request.post<ApiResult<YxeGoodsCoverZipResult>>(
MODULES_API_URL + '/yxe/yxe-goods/cover/zip',
formData
);
if (res.data.code === 0) {
return (
res.data.data ?? {
totalFiles: 0,
updatedCount: 0,
skippedCount: 0,
notFoundTitles: []
}
);
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateYxeGoods(data: YxeGoods) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-goods',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeYxeGoods(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-goods/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchYxeGoods(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-goods/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getYxeGoods(id: number) {
const res = await request.get<ApiResult<YxeGoods>>(
MODULES_API_URL + '/yxe/yxe-goods/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+63
View File
@@ -0,0 +1,63 @@
import type { PageParam } from "@/api";
/**
*
*/
export interface YxeGoods {
//
id?: number;
// 分类
cate?: string;
chain?: string;
// 编号
code?: string;
// 品名
title?: string;
// 规格
spec?: string;
// 厂家
maker?: string;
// 厂家简称
makerAbb?: string;
//
cover?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
//
startTime?: string;
//
endTime?: string;
// 奖励金额
extra_income?: string;
// 额外奖励
extraIncome?: string;
//
extraStartTime?: string;
//
extraEndTime?: string;
}
export interface YxeGoodsCoverZipResult {
totalFiles: number;
updatedCount: number;
skippedCount: number;
notFoundTitles: string[];
}
/**
* 搜索条件
*/
export interface YxeGoodsParam extends PageParam {
id?: number;
keywords?: string;
}
+149
View File
@@ -0,0 +1,149 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type {
YxeProduct,
YxeProductCoverZipResult,
YxeProductParam
} from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageYxeProduct(params: YxeProductParam) {
const res = await request.get<ApiResult<PageResult<YxeProduct>>>(
MODULES_API_URL + '/yxe/yxe-product/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listYxeProduct(params?: YxeProductParam) {
const res = await request.get<ApiResult<YxeProduct[]>>(
MODULES_API_URL + '/yxe/yxe-product',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询分组列表
*/
export async function listYxeProductGroup(params?: YxeProductParam) {
const res = await request.get<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-product/group',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addYxeProduct(data: YxeProduct) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-product',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 上传商品封面 zip
*/
export async function uploadYxeProductCoverZip(file: File) {
const formData = new FormData();
formData.append('file', file);
const res = await request.post<ApiResult<YxeProductCoverZipResult>>(
MODULES_API_URL + '/yxe/yxe-product/cover/zip',
formData
);
if (res.data.code === 0) {
return (
res.data.data ?? {
totalFiles: 0,
updatedCount: 0,
skippedCount: 0,
notFoundTitles: []
}
);
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateYxeProduct(data: YxeProduct) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-product',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeYxeProduct(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-product/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchYxeProduct(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-product/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getYxeProduct(id: number) {
const res = await request.get<ApiResult<YxeProduct>>(
MODULES_API_URL + '/yxe/yxe-product/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+85
View File
@@ -0,0 +1,85 @@
import type { PageParam } from "@/api";
export interface YxeProductExtraIncomeItem {
// 额外奖励
extraIncome?: string;
// 额外奖励开始时间
extraStartTime?: string;
// 额外奖励结束时间
extraEndTime?: string;
// 兼容后端可能返回的字段
income?: string;
startTime?: string;
endTime?: string;
}
/**
*
*/
export interface YxeProduct {
//
id?: number;
merchantId?: number;
type?: number;
// 编号
code?: string;
// 品名
title?: string;
// 规格
spec?: string;
// 厂家
maker?: string;
cover?: string;
merchantCode?: string;
// 厂家简称
makerAbb?: string;
cate?: string;
//
income?: string;
taskIncome?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
// 所属连锁
chain?: string;
// 分享奖励活动开始时间
startTime?: string;
// 分享奖励活动结束时间
endTime?: string;
// 额外奖励
extraIncome?: string;
// 额外奖励开始时间
extraStartTime?: string;
// 额外奖励结束时间
extraEndTime?: string;
// 额外奖励列表
extraIncomeList?: YxeProductExtraIncomeItem[];
}
export interface YxeProductCoverZipResult {
totalFiles: number;
updatedCount: number;
skippedCount: number;
notFoundTitles: string[];
}
/**
* 搜索条件
*/
export interface YxeProductParam extends PageParam {
id?: number;
type?: number;
keywords?: string;
merchantId?: number;
chain?: string;
cate?: string;
}
+106
View File
@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { YxeProductExtraIncome, YxeProductExtraIncomeParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageYxeProductExtraIncome(params: YxeProductExtraIncomeParam) {
const res = await request.get<ApiResult<PageResult<YxeProductExtraIncome>>>(
MODULES_API_URL + '/yxe/yxe-product-extra-income/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listYxeProductExtraIncome(params?: YxeProductExtraIncomeParam) {
const res = await request.get<ApiResult<YxeProductExtraIncome[]>>(
MODULES_API_URL + '/yxe/yxe-product-extra-income',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addYxeProductExtraIncome(data: YxeProductExtraIncome) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-product-extra-income',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateYxeProductExtraIncome(data: YxeProductExtraIncome) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-product-extra-income',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeYxeProductExtraIncome(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-product-extra-income/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchYxeProductExtraIncome(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-product-extra-income/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getYxeProductExtraIncome(id: number) {
const res = await request.get<ApiResult<YxeProductExtraIncome>>(
MODULES_API_URL + '/yxe/yxe-product-extra-income/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
@@ -0,0 +1,37 @@
import type { PageParam } from '@/api';
/**
*
*/
export interface YxeProductExtraIncome {
//
id?: number;
//
productId?: number;
//
startTime?: string;
//
endTime?: string;
//
income?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 搜索条件
*/
export interface YxeProductExtraIncomeParam extends PageParam {
id?: number;
keywords?: string;
}
@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { YxeProductMerchantExtraIncome, YxeProductMerchantExtraIncomeParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageYxeProductMerchantExtraIncome(params: YxeProductMerchantExtraIncomeParam) {
const res = await request.get<ApiResult<PageResult<YxeProductMerchantExtraIncome>>>(
MODULES_API_URL + '/yxe/yxe-product-merchant-extra-income/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listYxeProductMerchantExtraIncome(params?: YxeProductMerchantExtraIncomeParam) {
const res = await request.get<ApiResult<YxeProductMerchantExtraIncome[]>>(
MODULES_API_URL + '/yxe/yxe-product-merchant-extra-income',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addYxeProductMerchantExtraIncome(data: YxeProductMerchantExtraIncome) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-product-merchant-extra-income',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateYxeProductMerchantExtraIncome(data: YxeProductMerchantExtraIncome) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-product-merchant-extra-income',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeYxeProductMerchantExtraIncome(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-product-merchant-extra-income/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchYxeProductMerchantExtraIncome(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-product-merchant-extra-income/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getYxeProductMerchantExtraIncome(id: number) {
const res = await request.get<ApiResult<YxeProductMerchantExtraIncome>>(
MODULES_API_URL + '/yxe/yxe-product-merchant-extra-income/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
@@ -0,0 +1,43 @@
import type { PageParam } from '@/api';
import { Merchant } from "@/api/shop/merchant/model";
/**
*
*/
export interface YxeProductMerchantExtraIncome {
//
id?: number;
//
productId?: number;
//
merchantId?: number;
//
startTime?: string;
//
endTime?: string;
//
income?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
merchant?: Merchant;
}
/**
* 搜索条件
*/
export interface YxeProductMerchantExtraIncomeParam extends PageParam {
id?: number;
productId?: number;
merchantId?: number;
keywords?: string;
}
+121
View File
@@ -0,0 +1,121 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { YxeReimburse, YxeReimburseParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import type { FileRecord } from '@/api/system/file/model';
/**
* 分页查询报销
*/
export async function pageYxeReimburse(params: YxeReimburseParam) {
const res = await request.get<ApiResult<PageResult<YxeReimburse>>>(
MODULES_API_URL + '/yxe/yxe-reimburse/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询报销列表
*/
export async function listYxeReimburse(params?: YxeReimburseParam) {
const res = await request.get<ApiResult<YxeReimburse[]>>(
MODULES_API_URL + '/yxe/yxe-reimburse',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加报销
*/
export async function addYxeReimburse(data: YxeReimburse) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-reimburse',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改报销
*/
export async function updateYxeReimburse(data: YxeReimburse) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-reimburse',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除报销
*/
export async function removeYxeReimburse(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-reimburse/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除报销
*/
export async function removeBatchYxeReimburse(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-reimburse/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询报销
*/
export async function getYxeReimburse(id: number) {
const res = await request.get<ApiResult<YxeReimburse>>(
MODULES_API_URL + '/yxe/yxe-reimburse/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 导出报销
*/
export async function exportYxeReimburse(params?: YxeReimburseParam) {
const res = await request.post<ApiResult<FileRecord>>(
MODULES_API_URL + '/yxe/yxe-reimburse/export',
params
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+66
View File
@@ -0,0 +1,66 @@
import type { PageParam } from '@/api';
import type { User } from '@/api/system/user/model';
/**
* 报销
*/
export interface YxeReimburse {
//
id?: number;
//
userId?: number;
//
startDate?: string;
//
endDate?: string;
// 交通费
transportCosts?: string;
// 住宿费
liveCosts?: string;
// 其他费用
otherCosts?: string;
// 凭证
images?: string;
// 出发省
fromProvince?: string;
// 出发市
fromCity?: string;
// 出发区
fromRegion?: string;
// 目的省
toProvince?: string;
// 目的市
toCity?: string;
// 目的区
toRegion?: string;
// 备注
remark?: string;
// 状态, 0待审核 1通过 2驳回
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
// 提交人
user?: User;
}
/**
* 报销搜索条件
*/
export interface YxeReimburseParam extends PageParam {
id?: number;
keywords?: string;
userId?: number;
status?: number | number[];
startDate?: string;
endDate?: string;
startDateStart?: string;
endDateEnd?: string;
fromAddress?: string;
toAddress?: string;
}
+97
View File
@@ -0,0 +1,97 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { YxeSelfReport, YxeSelfReportParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import type { FileRecord } from '@/api/system/file/model';
export async function pageYxeSelfReport(params: YxeSelfReportParam) {
const res = await request.get<ApiResult<PageResult<YxeSelfReport>>>(
MODULES_API_URL + '/yxe/yxe-self-report/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
export async function listYxeSelfReport(params?: YxeSelfReportParam) {
const res = await request.get<ApiResult<YxeSelfReport[]>>(
MODULES_API_URL + '/yxe/yxe-self-report',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
export async function addYxeSelfReport(data: YxeSelfReport) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-self-report',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
export async function updateYxeSelfReport(data: YxeSelfReport) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-self-report',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
export async function removeYxeSelfReport(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-self-report/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
export async function removeBatchYxeSelfReport(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-self-report/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
export async function getYxeSelfReport(id: number) {
const res = await request.get<ApiResult<YxeSelfReport>>(
MODULES_API_URL + '/yxe/yxe-self-report/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
export async function exportYxeSelfReport(params?: YxeSelfReportParam) {
const res = await request.post<ApiResult<FileRecord>>(
MODULES_API_URL + '/yxe/yxe-self-report/export',
params
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+60
View File
@@ -0,0 +1,60 @@
import type { PageParam } from '@/api';
import type { User } from '@/api/system/user/model';
export interface YxeSelfReport {
id?: number;
userId?: number;
merchantId?: number;
merchantName?: string;
chainId?: string;
chainName?: string;
date?: string;
activityTime?: string;
image?: string;
clockPhotos?: string;
boothPhotos?: string;
salesGroups?: string;
receiptPhoto?: string;
summary?: string;
rating?: string | number;
lat?: string;
lng?: string;
location?: string;
reward?: number | string;
sortNumber?: number;
comments?: string;
status?: number;
rejectReason?: string;
deleted?: number;
tenantId?: number;
createTime?: string;
updateTime?: string;
user?: User;
realName?: string;
userGroup?: string;
nickname?: string;
phone?: string;
merchant?: {
id?: number;
name?: string;
chain?: string;
province?: string;
city?: string;
region?: string;
address?: string;
lng?: string | number;
lat?: string | number;
};
}
export interface YxeSelfReportParam extends PageParam {
id?: number;
idList?: string;
keywords?: string;
realName?: string;
userGroup?: string;
chainName?: string;
merchantName?: string;
activityTime?: string;
status?: number | number[];
}
+106
View File
@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { YxeService, YxeServiceParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询客服
*/
export async function pageYxeService(params: YxeServiceParam) {
const res = await request.get<ApiResult<PageResult<YxeService>>>(
MODULES_API_URL + '/yxe/yxe-service/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询客服列表
*/
export async function listYxeService(params?: YxeServiceParam) {
const res = await request.get<ApiResult<YxeService[]>>(
MODULES_API_URL + '/yxe/yxe-service',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加客服
*/
export async function addYxeService(data: YxeService) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-service',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改客服
*/
export async function updateYxeService(data: YxeService) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-service',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除客服
*/
export async function removeYxeService(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-service/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除客服
*/
export async function removeBatchYxeService(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-service/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询客服
*/
export async function getYxeService(id: number) {
const res = await request.get<ApiResult<YxeService>>(
MODULES_API_URL + '/yxe/yxe-service/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+33
View File
@@ -0,0 +1,33 @@
import type { PageParam } from '@/api';
/**
* 客服
*/
export interface YxeService {
//
id?: number;
//
type?: string;
//
title?: string;
//
content?: string;
// 状态, 0上架, 1下架
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 客服搜索条件
*/
export interface YxeServiceParam extends PageParam {
id?: number;
keywords?: string;
}
+121
View File
@@ -0,0 +1,121 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { YxeShare, YxeShareParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import type { FileRecord } from '@/api/system/file/model';
/**
* 分页查询
*/
export async function pageYxeShare(params: YxeShareParam) {
const res = await request.get<ApiResult<PageResult<YxeShare>>>(
MODULES_API_URL + '/yxe/yxe-share/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listYxeShare(params?: YxeShareParam) {
const res = await request.get<ApiResult<YxeShare[]>>(
MODULES_API_URL + '/yxe/yxe-share',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addYxeShare(data: YxeShare) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-share',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateYxeShare(data: YxeShare) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-share',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeYxeShare(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-share/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchYxeShare(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-share/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getYxeShare(id: number) {
const res = await request.get<ApiResult<YxeShare>>(
MODULES_API_URL + '/yxe/yxe-share/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 导出
*/
export async function exportYxeShare(params?: YxeShareParam) {
const res = await request.post<ApiResult<FileRecord>>(
MODULES_API_URL + '/yxe/yxe-share/export',
params
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+66
View File
@@ -0,0 +1,66 @@
import type { PageParam } from '@/api';
import type { User } from '@/api/system/user/model';
import type { YxeShareDetail } from '@/api/yxe/yxeShareDetail/model';
/**
*
*/
export interface YxeShare {
//
id?: number;
//
userId?: number;
//
merchantId?: number;
//
merchantName?: string;
//
orderNo?: string;
//
amount?: string;
// 小票图片
image?: string;
// 溯源码
sourceCode?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
// 所属连锁
chain?: string;
// 提交人员
user?: User;
// 提交人姓名
realName?: string;
// 提交人昵称
nickname?: string;
// 提交人手机号
phone?: string;
merchant?: {
id?: number;
name?: string;
chain?: string;
};
// 详情列表
detailList?: YxeShareDetail[];
}
/**
* 搜索条件
*/
export interface YxeShareParam extends PageParam {
id?: number;
keywords?: string;
chain?: string;
merchantId?: number;
status?: number | number[];
userId?: number;
}
+106
View File
@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { YxeShareDetail, YxeShareDetailParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageYxeShareDetail(params: YxeShareDetailParam) {
const res = await request.get<ApiResult<PageResult<YxeShareDetail>>>(
MODULES_API_URL + '/yxe/yxe-share-detail/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listYxeShareDetail(params?: YxeShareDetailParam) {
const res = await request.get<ApiResult<YxeShareDetail[]>>(
MODULES_API_URL + '/yxe/yxe-share-detail',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addYxeShareDetail(data: YxeShareDetail) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-share-detail',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateYxeShareDetail(data: YxeShareDetail) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-share-detail',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeYxeShareDetail(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-share-detail/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchYxeShareDetail(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-share-detail/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getYxeShareDetail(id: number) {
const res = await request.get<ApiResult<YxeShareDetail>>(
MODULES_API_URL + '/yxe/yxe-share-detail/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+39
View File
@@ -0,0 +1,39 @@
import type { PageParam } from '@/api';
/**
*
*/
export interface YxeShareDetail {
//
id?: number;
//
shareId?: number;
//
goodsName?: string;
//
spec?: string;
//
num?: string;
// 溯源码
sourceCode?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 搜索条件
*/
export interface YxeShareDetailParam extends PageParam {
id?: number;
keywords?: string;
}
+117
View File
@@ -0,0 +1,117 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { YxeTask, YxeTaskParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageYxeTask(params: YxeTaskParam) {
const res = await request.get<ApiResult<PageResult<YxeTask>>>(
MODULES_API_URL + '/yxe/yxe-task/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listYxeTask(params?: YxeTaskParam) {
const res = await request.get<ApiResult<YxeTask[]>>(
MODULES_API_URL + '/yxe/yxe-task',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addYxeTask(data: YxeTask) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-task',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateYxeTask(data: YxeTask) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-task',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeYxeTask(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-task/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchYxeTask(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-task/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getYxeTask(id: number) {
const res = await request.get<ApiResult<YxeTask>>(
MODULES_API_URL + '/yxe/yxe-task/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
export async function exportYxeTask(params?: YxeTaskParam) {
const res = await request.post<ApiResult<any>>(
MODULES_API_URL + '/yxe/yxe-task/export',
params
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+123
View File
@@ -0,0 +1,123 @@
import type { PageParam } from '@/api';
import { User } from '@/api/system/user/model';
/**
*
*/
export interface YxeTask {
//
id?: number;
// 分类id
cateId?: number;
// 名称
title?: string;
// 需巡店数量
storeNum?: number;
// 金额
reward?: string;
extraReward?: string;
// 店铺id
merchantId?: number;
// 数量
num?: number;
// 详情
detail?: string;
policy?: string;
// 日期
date?: string;
taskStartTime?: string;
taskEndTime?: string;
province?: string;
city?: string;
// 用户ID
userId?: number;
// 指定人员ID
appointUserId?: number;
userGroup?: string;
// 指定人员手机号(提交字段)
phone?: string;
// 指定人员姓名
realName?: string;
// 指定人员手机号(前端表单字段)
userPhone?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0上架, 1下架
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 关联产品
productList?: Array<{
goodsId?: number;
productId?: number;
num?: number;
amount?: string;
income?: string;
reward?: string;
goods?: {
title?: string;
extra_income?: string;
extraIncome?: string;
};
product?: {
title?: string;
income?: string;
};
}>;
// 关联产品(接口字段拼写)
procutList?: Array<{
goodsId?: number;
productId?: number;
num?: number;
amount?: string;
income?: string;
reward?: string;
goods?: {
title?: string;
extra_income?: string;
extraIncome?: string;
};
product?: {
title?: string;
income?: string;
};
}>;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
chain?: string;
appointUser?: User;
merchant?: {
name?: string;
chain?: string;
};
// 指定人员
user?: {
realName?: string;
nickname?: string;
phone?: string;
};
}
/**
* 搜索条件
*/
export interface YxeTaskParam extends PageParam {
id?: number;
keywords?: string;
cateIdList?: string;
chain?: string;
merchantId?: number;
taskStartTime?: string;
taskStartTimeStart?: string;
taskStartTimeEnd?: string;
appointUserId?: number;
userGroup?: string;
userPhone?: string;
status?: number | number[];
}
+106
View File
@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { YxeTaskCate, YxeTaskCateParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageYxeTaskCate(params: YxeTaskCateParam) {
const res = await request.get<ApiResult<PageResult<YxeTaskCate>>>(
MODULES_API_URL + '/yxe/yxe-task-cate/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listYxeTaskCate(params?: YxeTaskCateParam) {
const res = await request.get<ApiResult<YxeTaskCate[]>>(
MODULES_API_URL + '/yxe/yxe-task-cate',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addYxeTaskCate(data: YxeTaskCate) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-task-cate',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateYxeTaskCate(data: YxeTaskCate) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-task-cate',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeYxeTaskCate(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-task-cate/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchYxeTaskCate(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-task-cate/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getYxeTaskCate(id: number) {
const res = await request.get<ApiResult<YxeTaskCate>>(
MODULES_API_URL + '/yxe/yxe-task-cate/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+35
View File
@@ -0,0 +1,35 @@
import type { PageParam } from '@/api';
/**
*
*/
export interface YxeTaskCate {
//
id?: number;
//
title?: string;
// 用户ID
userId?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0待审核 1通过 2驳回
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 搜索条件
*/
export interface YxeTaskCateParam extends PageParam {
id?: number;
keywords?: string;
}
+106
View File
@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { YxeTaskProduct, YxeTaskProductParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询
*/
export async function pageYxeTaskProduct(params: YxeTaskProductParam) {
const res = await request.get<ApiResult<PageResult<YxeTaskProduct>>>(
MODULES_API_URL + '/yxe/yxe-task-product/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询列表
*/
export async function listYxeTaskProduct(params?: YxeTaskProductParam) {
const res = await request.get<ApiResult<YxeTaskProduct[]>>(
MODULES_API_URL + '/yxe/yxe-task-product',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加
*/
export async function addYxeTaskProduct(data: YxeTaskProduct) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-task-product',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改
*/
export async function updateYxeTaskProduct(data: YxeTaskProduct) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-task-product',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除
*/
export async function removeYxeTaskProduct(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-task-product/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除
*/
export async function removeBatchYxeTaskProduct(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-task-product/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询
*/
export async function getYxeTaskProduct(id: number) {
const res = await request.get<ApiResult<YxeTaskProduct>>(
MODULES_API_URL + '/yxe/yxe-task-product/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+34
View File
@@ -0,0 +1,34 @@
import type { PageParam } from '@/api';
/**
*
*/
export interface YxeTaskProduct {
//
id?: number;
//
taskId?: number;
//
goodsId?: number;
num?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 搜索条件
*/
export interface YxeTaskProductParam extends PageParam {
id?: number;
keywords?: string;
}
+121
View File
@@ -0,0 +1,121 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { YxeUserTask, YxeUserTaskParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import type { FileRecord } from '@/api/system/file/model';
/**
* 分页查询用户任务
*/
export async function pageYxeUserTask(params: YxeUserTaskParam) {
const res = await request.get<ApiResult<PageResult<YxeUserTask>>>(
MODULES_API_URL + '/yxe/yxe-user-task/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询用户任务列表
*/
export async function listYxeUserTask(params?: YxeUserTaskParam) {
const res = await request.get<ApiResult<YxeUserTask[]>>(
MODULES_API_URL + '/yxe/yxe-user-task',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加用户任务
*/
export async function addYxeUserTask(data: YxeUserTask) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-user-task',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改用户任务
*/
export async function updateYxeUserTask(data: YxeUserTask) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-user-task',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除用户任务
*/
export async function removeYxeUserTask(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-user-task/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除用户任务
*/
export async function removeBatchYxeUserTask(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-user-task/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询用户任务
*/
export async function getYxeUserTask(id: number) {
const res = await request.get<ApiResult<YxeUserTask>>(
MODULES_API_URL + '/yxe/yxe-user-task/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 导出用户任务
*/
export async function exportYxeUserTask(params?: YxeUserTaskParam) {
const res = await request.post<ApiResult<FileRecord>>(
MODULES_API_URL + '/yxe/yxe-user-task/export',
params
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+90
View File
@@ -0,0 +1,90 @@
import type { PageParam } from '@/api';
import type { User } from '@/api/system/user/model';
/**
* 用户任务
*/
export interface YxeUserTask {
//
id?: number;
//
taskId?: number;
//
image?: string;
//
video?: string;
// 用户ID
userId?: number;
userGroup?: string;
//
lat?: string;
//
lng?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0待审核 1通过 2驳回
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
inClockTime?: string;
outClockTime?: string;
submitTime?: string;
summary?: string;
rating?: number | string;
clockPhotos?: string;
boothPhotos?: string;
salesGroups?: string;
receiptPhoto?: string;
income?: number | string;
user?: User;
task?: {
id?: number;
title?: string;
reward?: number | string;
income?: number | string;
extraReward?: number | string;
chain?: string;
merchantId?: number;
taskStartTime?: string;
taskEndTime?: string;
appointUserId?: number;
merchant?: {
id?: number;
name?: string;
chain?: string;
province?: string;
city?: string;
region?: string;
address?: string;
lngAndLat?: string;
lng?: string | number;
lat?: string | number;
};
appointUser?: User;
};
}
/**
* 用户任务搜索条件
*/
export interface YxeUserTaskParam extends PageParam {
id?: number;
idList?: string;
keywords?: string;
cateIdList?: string;
userId?: number;
userGroup?: string;
chain?: string;
merchantId?: number;
taskStartTimeStart?: string;
taskStartTimeEnd?: string;
status?: number | number[];
}
+121
View File
@@ -0,0 +1,121 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { YxeVisitList, YxeVisitListParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import type { FileRecord } from '@/api/system/file/model';
/**
* 分页查询访问列表
*/
export async function pageYxeVisitList(params: YxeVisitListParam) {
const res = await request.get<ApiResult<PageResult<YxeVisitList>>>(
MODULES_API_URL + '/yxe/yxe-visit-list/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询访问列表列表
*/
export async function listYxeVisitList(params?: YxeVisitListParam) {
const res = await request.get<ApiResult<YxeVisitList[]>>(
MODULES_API_URL + '/yxe/yxe-visit-list',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加访问列表
*/
export async function addYxeVisitList(data: YxeVisitList) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-visit-list',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改访问列表
*/
export async function updateYxeVisitList(data: YxeVisitList) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-visit-list',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除访问列表
*/
export async function removeYxeVisitList(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-visit-list/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除访问列表
*/
export async function removeBatchYxeVisitList(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/yxe/yxe-visit-list/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询访问列表
*/
export async function getYxeVisitList(id: number) {
const res = await request.get<ApiResult<YxeVisitList>>(
MODULES_API_URL + '/yxe/yxe-visit-list/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 导出访问列表
*/
export async function exportYxeVisitList(params?: YxeVisitListParam) {
const res = await request.post<ApiResult<FileRecord>>(
MODULES_API_URL + '/yxe/yxe-visit-list/export',
params
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+61
View File
@@ -0,0 +1,61 @@
import type { PageParam } from '@/api';
import type { User } from '@/api/system/user/model';
/**
* 访问列表
*/
export interface YxeVisitList {
//
id?: number;
//
userId?: number;
//
lat?: string;
//
lng?: string;
// 位置
location?: string;
// 拜访人员
name?: string;
// 沟通内容
content?: string;
// 小结
summary?: string;
//
photos?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0上架, 1下架
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
// 提交人
user?: User;
// 提交人姓名
realName?: string;
userGroup?: string;
// 提交人昵称
nickname?: string;
// 提交人手机号
phone?: string;
}
/**
* 访问列表搜索条件
*/
export interface YxeVisitListParam extends PageParam {
id?: number;
idList?: string;
keywords?: string;
realName?: string;
userGroup?: string;
status?: number | number[];
}
+12
View File
@@ -0,0 +1,12 @@
type ExportFile = {
url?: string;
downloadUrl?: string;
};
export function openExportFile(file?: ExportFile) {
const target = file?.downloadUrl || file?.url;
if (!target) {
throw new Error('未获取到导出文件地址');
}
window.open(target);
}
@@ -0,0 +1,165 @@
<!-- 编辑弹窗 -->
<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="articleId">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.articleId"
/>
</a-form-item>
<a-form-item label="" name="userId">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="状态, 0正常, 1冻结" 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-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>
<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 { addArticleCollect, updateArticleCollect } from '@/api/cms/articleCollect';
import { ArticleCollect } from '@/api/cms/articleCollect/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
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?: ArticleCollect | 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<ArticleCollect>({
id: undefined,
articleId: undefined,
userId: undefined,
status: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
});
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 ? updateArticleCollect : addArticleCollect;
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);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
@@ -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>
+245
View File
@@ -0,0 +1,245 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 1" color="green">显示</a-tag>
<a-tag v-if="record.status === 0" 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>
<!-- 编辑弹窗 -->
<ArticleCollectEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</div>
</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 ArticleCollectEdit from './components/articleCollectEdit.vue';
import { pageArticleCollect, removeArticleCollect, removeBatchArticleCollect } from '@/api/cms/articleCollect';
import type { ArticleCollect, ArticleCollectParam } from '@/api/cms/articleCollect/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ArticleCollect[]>([]);
// 当前编辑数据
const current = ref<ArticleCollect | 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 pageArticleCollect({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '',
dataIndex: 'articleId',
key: 'articleId',
align: 'center',
},
{
title: '',
dataIndex: 'userId',
key: 'userId',
align: 'center',
},
{
title: '状态, 0正常, 1冻结',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '是否删除, 0否, 1是',
dataIndex: 'deleted',
key: 'deleted',
align: 'center',
},
{
title: '注册时间',
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
}
]);
/* 搜索 */
const reload = (where?: ArticleCollectParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ArticleCollect) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ArticleCollect) => {
const hide = message.loading('请求中..', 0);
removeArticleCollect(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);
removeBatchArticleCollect(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: ArticleCollect) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ArticleCollect'
};
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,173 @@
<!-- 编辑弹窗 -->
<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="articleId">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.articleId"
/>
</a-form-item>
<a-form-item label="" name="userId">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="" name="content">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.content"
/>
</a-form-item>
<a-form-item label="状态, 0正常, 1冻结" 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-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>
<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 { addArticleComment, updateArticleComment } from '@/api/cms/articleComment';
import { ArticleComment } from '@/api/cms/articleComment/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
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?: ArticleComment | 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<ArticleComment>({
id: undefined,
articleId: undefined,
userId: undefined,
content: undefined,
status: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
});
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 ? updateArticleComment : addArticleComment;
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);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
@@ -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>
+251
View File
@@ -0,0 +1,251 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 1" color="green">显示</a-tag>
<a-tag v-if="record.status === 0" 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>
<!-- 编辑弹窗 -->
<ArticleCommentEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</div>
</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 ArticleCommentEdit from './components/articleCommentEdit.vue';
import { pageArticleComment, removeArticleComment, removeBatchArticleComment } from '@/api/cms/articleComment';
import type { ArticleComment, ArticleCommentParam } from '@/api/cms/articleComment/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ArticleComment[]>([]);
// 当前编辑数据
const current = ref<ArticleComment | 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 pageArticleComment({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '',
dataIndex: 'articleId',
key: 'articleId',
align: 'center',
},
{
title: '',
dataIndex: 'userId',
key: 'userId',
align: 'center',
},
{
title: '',
dataIndex: 'content',
key: 'content',
align: 'center',
},
{
title: '状态, 0正常, 1冻结',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '是否删除, 0否, 1是',
dataIndex: 'deleted',
key: 'deleted',
align: 'center',
},
{
title: '注册时间',
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
}
]);
/* 搜索 */
const reload = (where?: ArticleCommentParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ArticleComment) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ArticleComment) => {
const hide = message.loading('请求中..', 0);
removeArticleComment(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);
removeBatchArticleComment(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: ArticleComment) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ArticleComment'
};
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,238 @@
<!-- 编辑弹窗 -->
<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="orderNo">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.orderNo"
/>
</a-form-item>
<a-form-item label="分销商用户ID" name="userId">
<a-input
allow-clear
placeholder="请输入分销商用户ID"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="提现金额" name="money">
<a-input
allow-clear
placeholder="请输入提现金额"
v-model:value="form.money"
/>
</a-form-item>
<a-form-item label="打款方式 (10微信 20支付宝 30银行卡)" name="payType">
<a-input
allow-clear
placeholder="请输入打款方式 (10微信 20支付宝 30银行卡)"
v-model:value="form.payType"
/>
</a-form-item>
<a-form-item label="支付宝姓名" name="alipayName">
<a-input
allow-clear
placeholder="请输入支付宝姓名"
v-model:value="form.alipayName"
/>
</a-form-item>
<a-form-item label="支付宝账号" name="alipayAccount">
<a-input
allow-clear
placeholder="请输入支付宝账号"
v-model:value="form.alipayAccount"
/>
</a-form-item>
<a-form-item label="开户行名称" name="bankName">
<a-input
allow-clear
placeholder="请输入开户行名称"
v-model:value="form.bankName"
/>
</a-form-item>
<a-form-item label="银行开户名" name="bankAccount">
<a-input
allow-clear
placeholder="请输入银行开户名"
v-model:value="form.bankAccount"
/>
</a-form-item>
<a-form-item label="银行卡号" name="bankCard">
<a-input
allow-clear
placeholder="请输入银行卡号"
v-model:value="form.bankCard"
/>
</a-form-item>
<a-form-item label="申请状态 (10待审核 20审核通过 30驳回 40已打款)" name="applyStatus">
<a-input
allow-clear
placeholder="请输入申请状态 (10待审核 20审核通过 30驳回 40已打款)"
v-model:value="form.applyStatus"
/>
</a-form-item>
<a-form-item label="审核时间" name="auditTime">
<a-input
allow-clear
placeholder="请输入审核时间"
v-model:value="form.auditTime"
/>
</a-form-item>
<a-form-item label="驳回原因" name="rejectReason">
<a-input
allow-clear
placeholder="请输入驳回原因"
v-model:value="form.rejectReason"
/>
</a-form-item>
<a-form-item label="来源客户端(APP、H5、小程序等)" name="platform">
<a-input
allow-clear
placeholder="请输入来源客户端(APP、H5、小程序等)"
v-model:value="form.platform"
/>
</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 { addDealerWithdraw, updateDealerWithdraw } from '@/api/shop/dealerWithdraw';
import { DealerWithdraw } from '@/api/shop/dealerWithdraw/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
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?: DealerWithdraw | 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<DealerWithdraw>({
id: undefined,
orderNo: undefined,
userId: undefined,
money: undefined,
payType: undefined,
alipayName: undefined,
alipayAccount: undefined,
bankName: undefined,
bankAccount: undefined,
bankCard: undefined,
applyStatus: undefined,
auditTime: undefined,
rejectReason: undefined,
platform: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
});
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 ? updateDealerWithdraw : addDealerWithdraw;
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);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
@@ -0,0 +1,196 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="exportData">
<template #icon>
<DownloadOutlined />
</template>
<span>导出</span>
</a-button>
<a-select
v-model:value="where.userId"
show-search
allow-clear
:filter-option="false"
optionFilterProp="label"
:loading="userLoading"
:options="userOptions"
placeholder="请选择用户"
style="width: 220px"
@search="onSearchUser"
@focus="loadUserOptions()"
@change="search"
/>
<a-select
v-model:value="where.applyStatus"
allow-clear
placeholder="请选择申请状态"
style="width: 180px"
>
<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-option :value="3">已打款</a-select-option>
</a-select>
<a-button type="primary" @click="search">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { DownloadOutlined } from '@ant-design/icons-vue';
import { onBeforeUnmount, ref } from 'vue';
import { message } from 'ant-design-vue';
import type { SelectProps } from 'ant-design-vue';
import useSearch from '@/utils/use-search';
import { pageUsers } from '@/api/system/user';
import type { User as SystemUser } from '@/api/system/user/model';
import type { DealerWithdrawParam } from '@/api/shop/dealerWithdraw/model';
defineProps<{
selection?: [];
}>();
const emit = defineEmits<{
(e: 'search', where?: DealerWithdrawParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
(e: 'export'): void;
}>();
const { where, resetFields } = useSearch<DealerWithdrawParam>({
userId: undefined,
applyStatus: undefined
});
const userOptions = ref<SelectProps['options']>([]);
const userLoading = ref(false);
let userSearchTimer: ReturnType<typeof setTimeout> | null = null;
let userSearchSeed = 0;
const buildUserOption = (item: SystemUser) => {
if (item.userId == null) {
return null;
}
const phone = String(item.phone ?? '').trim();
const name = String(item.realName ?? item.nickname ?? '').trim();
const contact = phone || `ID:${item.userId}`;
return {
value: item.userId,
label: name ? `${name} (${contact})` : contact
};
};
const ensureCurrentUserOption = (userId?: number) => {
if (userId == null) {
return;
}
const exists = userOptions.value?.some((item) => item?.value === userId);
if (exists) {
return;
}
userOptions.value = [
{
value: userId,
label: `ID:${userId}`
},
...(userOptions.value || [])
];
};
const loadUserOptions = (keyword?: string) => {
const currentSeed = ++userSearchSeed;
const trimmed = String(keyword ?? '').trim();
const previousSelectedOption =
where.userId == null
? undefined
: (userOptions.value || []).find(
(item) => item?.value === where.userId
);
userLoading.value = true;
const requests = trimmed
? [
pageUsers({
page: 1,
limit: 20,
phone: trimmed
}),
pageUsers({
page: 1,
limit: 20,
realName: trimmed
})
]
: [
pageUsers({
page: 1,
limit: 20
})
];
Promise.all(requests)
.then((results) => {
if (currentSeed !== userSearchSeed) {
return;
}
const map = new Map<number, { value: number; label: string }>();
results.forEach((res) => {
(res?.list || []).forEach((item) => {
const option = buildUserOption(item as SystemUser);
if (option && !map.has(option.value)) {
map.set(option.value, option);
}
});
});
userOptions.value = Array.from(map.values());
if (
previousSelectedOption &&
where.userId != null &&
!map.has(where.userId)
) {
userOptions.value = [previousSelectedOption, ...userOptions.value];
}
ensureCurrentUserOption(where.userId);
})
.catch((e) => {
if (currentSeed !== userSearchSeed) {
return;
}
message.error(e.message ?? '查询用户失败');
})
.finally(() => {
if (currentSeed === userSearchSeed) {
userLoading.value = false;
}
});
};
const onSearchUser = (value: string) => {
if (userSearchTimer) {
clearTimeout(userSearchTimer);
}
userSearchTimer = setTimeout(() => {
loadUserOptions(value);
}, 300);
};
const search = () => {
emit('search', { ...where });
};
const reset = () => {
resetFields();
search();
};
const exportData = () => {
emit('export');
};
onBeforeUnmount(() => {
if (userSearchTimer) {
clearTimeout(userSearchTimer);
userSearchTimer = null;
}
});
</script>
+437
View File
@@ -0,0 +1,437 @@
<template>
<div class="page">
<div class="ele-body">
<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"
@export="exportData"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userId'">
<p>{{ record.user.nickname }}</p>
<p>{{ record.user.phone }}</p>
</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>
<template v-if="record.applyStatus === 0">
<a @click="approve(record)">通过</a>
<a-divider type="vertical" />
<a class="ele-text-danger" @click="openReject(record)"
>拒绝</a
>
</template>
<template v-else-if="record.applyStatus === 1">
<a @click="markPaid(record)">已打款</a>
</template>
<template v-else>
<span class="ele-text-secondary">--</span>
</template>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<DealerWithdrawEdit
v-model:visible="showEdit"
:data="current"
@done="reload"
/>
<!-- 拒绝原因弹窗 -->
<a-modal
v-model:visible="showReject"
:confirm-loading="rejectLoading"
:maskClosable="false"
title="拒绝原因"
@ok="submitReject"
@cancel="closeReject"
>
<a-textarea
v-model:value="rejectReason"
:rows="4"
:maxlength="200"
placeholder="请输入拒绝原因"
/>
</a-modal>
</div>
</div>
</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 DealerWithdrawEdit from './components/dealerWithdrawEdit.vue';
import {
pageDealerWithdraw,
removeBatchDealerWithdraw,
updateDealerWithdraw,
exportDealerWithdraw
} from '@/api/shop/dealerWithdraw';
import type {
DealerWithdraw,
DealerWithdrawParam
} from '@/api/shop/dealerWithdraw/model';
import { openExportFile } from '@/utils/export-file';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<DealerWithdraw[]>([]);
// 当前编辑数据
const current = ref<DealerWithdraw | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 是否显示拒绝原因弹窗
const showReject = ref(false);
// 拒绝原因
const rejectReason = ref('');
// 拒绝提交状态
const rejectLoading = ref(false);
// 当前操作数据
const actionCurrent = ref<DealerWithdraw | null>(null);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageDealerWithdraw({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90
},
// {
// title: '',
// dataIndex: 'orderNo',
// key: 'orderNo',
// align: 'center',
// },
{
title: '用户',
key: 'userId',
align: 'center'
},
{
title: '提现金额',
dataIndex: 'money',
key: 'money',
align: 'center'
},
{
title: '打款方式',
dataIndex: 'payType',
key: 'payType',
align: 'center',
customRender: ({ text }) => {
switch (text) {
case 10:
return '微信';
case 20:
return '支付宝';
case 30:
return '银行卡';
default:
return '未知';
}
}
},
{
title: '支付宝姓名',
dataIndex: 'alipayName',
key: 'alipayName',
align: 'center'
},
{
title: '支付宝账号',
dataIndex: 'alipayAccount',
key: 'alipayAccount',
align: 'center'
},
{
title: '开户行名称',
dataIndex: 'bankName',
key: 'bankName',
align: 'center'
},
{
title: '银行开户名',
dataIndex: 'bankAccount',
key: 'bankAccount',
align: 'center'
},
{
title: '银行卡号',
dataIndex: 'bankCard',
key: 'bankCard',
align: 'center'
},
{
title: '申请状态',
dataIndex: 'applyStatus',
key: 'applyStatus',
align: 'center',
customRender: ({ text }) => {
switch (text) {
case 0:
return '待审核';
case 1:
return '审核通过';
case 2:
return '驳回';
case 3:
return '已打款';
default:
return '未知';
}
}
},
{
title: '审核时间',
dataIndex: 'auditTime',
key: 'auditTime',
align: 'center'
},
{
title: '驳回原因',
dataIndex: 'rejectReason',
key: 'rejectReason',
align: 'center'
},
{
title: '提交时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: DealerWithdrawParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: DealerWithdraw) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchDealerWithdraw(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
const exportData = () => {
const hide = message.loading('导出中..', 0);
tableRef.value?.doRequest(({ where, orders, filters }) => {
exportDealerWithdraw({
...(where || {}),
...(orders || {}),
...(filters || {})
})
.then((file) => {
hide();
openExportFile(file);
message.success('导出成功');
})
.catch((e) => {
hide();
message.error(e.message);
});
});
};
/* 审核通过 */
const approve = (row: DealerWithdraw) => {
updateStatus(row, 1);
};
/* 打开拒绝弹窗 */
const openReject = (row: DealerWithdraw) => {
actionCurrent.value = row;
rejectReason.value = '';
showReject.value = true;
};
/* 关闭拒绝弹窗 */
const closeReject = () => {
showReject.value = false;
rejectReason.value = '';
actionCurrent.value = null;
};
/* 提交拒绝 */
const submitReject = () => {
const reason = rejectReason.value.trim();
if (!reason) {
message.error('请输入拒绝原因');
return;
}
if (!actionCurrent.value) {
message.error('数据异常,请重试');
return;
}
rejectLoading.value = true;
updateDealerWithdraw({
id: actionCurrent.value.id,
applyStatus: 2,
rejectReason: reason
})
.then((msg) => {
message.success(msg);
closeReject();
reload();
})
.catch((e) => {
message.error(e.message);
})
.finally(() => {
rejectLoading.value = false;
});
};
/* 标记已打款 */
const markPaid = (row: DealerWithdraw) => {
updateStatus(row, 3);
};
/* 更新申请状态 */
const updateStatus = (row: DealerWithdraw, status: number) => {
if (!row.id) {
message.error('数据异常,请重试');
return;
}
const hide = message.loading('请求中..', 0);
updateDealerWithdraw({
id: row.id,
applyStatus: status
})
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: DealerWithdraw) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'DealerWithdraw'
};
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,394 @@
<!-- 编辑弹窗 -->
<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="chain">
<a-input
allow-clear
placeholder="请输入所属连锁"
v-model:value="form.chain"
/>
</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="所属片区">
<a-select v-model:value="form.areaName">
<a-select-option v-for="item in areaDictList" :key="item.dictDataId" :value="item.dictDataCode">
{{ item.dictDataCode }}
</a-select-option>
</a-select>
</a-form-item>
<!-- <a-form-item-->
<!-- label="商户图标"-->
<!-- name="image">-->
<!-- <SelectFile-->
<!-- :placeholder="`请选择图片`"-->
<!-- :limit="1"-->
<!-- :data="images"-->
<!-- @done="chooseImage"-->
<!-- @del="onDeleteItem"-->
<!-- />-->
<!-- </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="realName">
<a-input
allow-clear
placeholder="请输入营运经理"
v-model:value="form.realName"
/>
</a-form-item>
<a-form-item label="省市区" name="province">
<a-cascader
v-model:value="selectedArea"
:options="areaList"
:fieldNames="{ label: 'name', value: 'name' }"
placeholder="请选择省市区"
@change="onAreaChange"
/>
</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">
<div style="display: flex; gap: 8px; width: 100%;">
<a-input
allow-clear
placeholder="请输入经纬度"
v-model:value="form.lngAndLat"
style="flex: 1;"
/>
<a-button
type="primary"
:loading="fetchingLngLat"
:disabled="!canFetchLngLat || fetchingLngLat"
@click="fetchLngLat"
>
获取经纬度
</a-button>
</div>
</a-form-item>
<a-form-item label="经度" name="lng">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.lng"
/>
</a-form-item>
<a-form-item label="纬度" name="lat">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.lat"
/>
</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 { ref, reactive, watch, computed } from "vue";
import { Form, message } from "ant-design-vue";
import { assignObject, uuid } from "ele-admin-pro";
import { addMerchant, updateMerchant } from "@/api/shop/merchant";
import { Merchant } from "@/api/shop/merchant/model";
import { useThemeStore } from "@/store/modules/theme";
import { storeToRefs } from "pinia";
import { FormInstance } from "ant-design-vue/es/form";
import { Area } from "@/api/system/area/model";
import { listArea } from "@/api/system/area";
import { DictData } from "@/api/system/dict-data/model";
import { listDictData } from "@/api/system/dict-data";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Merchant | null;
}>();
const emit = defineEmits<{
(e: "done"): void;
(e: "update:visible", visible: boolean): void;
}>();
const TIANDITU_TOKEN = "71f234f251d4cb1feeb3c293a0f21709";
// 提交状态
const loading = ref(false);
const fetchingLngLat = ref(false);
const selectedArea = ref<string[]>([]);
const areaList = ref<Area[]>([]);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
// 用户信息
const form = reactive<Merchant>({
id: undefined,
name: undefined,
code: undefined,
areaName: undefined,
chain: undefined,
image: undefined,
phone: undefined,
realName: undefined,
lngAndLat: undefined,
lng: undefined,
lat: undefined,
province: undefined,
city: undefined,
region: undefined,
address: undefined,
comments: undefined,
userId: undefined,
deleted: undefined,
status: undefined,
sortNumber: undefined,
tenantId: undefined,
createTime: undefined
});
const normalizeText = (value?: string) => (value ?? "").trim();
const getAreaList = async () => {
if (areaList.value.length) {
return;
}
try {
areaList.value = await listArea({ level: 3});
} catch (e) {
const err = e as Error;
message.error(err.message || "获取省市区失败");
}
};
const onAreaChange = (value?: string[]) => {
const [province, city, region] = value ?? [];
form.province = province;
form.city = city;
form.region = region;
};
const canFetchLngLat = computed(() => {
return Boolean(
normalizeText(form.province) &&
normalizeText(form.city) &&
normalizeText(form.region) &&
normalizeText(form.address)
);
});
const validateLngAndLat = async (_rule: unknown, value?: string) => {
if (!normalizeText(form.address) || normalizeText(value)) {
return Promise.resolve();
}
return Promise.reject("请输入经纬度");
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit("update:visible", value);
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
message: "请输入商户名称",
trigger: "blur"
}
],
code: [
{
required: true,
message: "请输入商户编码",
trigger: "blur"
}
],
chain: [
{
required: true,
message: "请输入所属连锁",
trigger: "blur"
}
],
province: [
{
required: true,
message: "请选择省市区",
trigger: "change"
}
],
lngAndLat: [
{
validator: validateLngAndLat,
trigger: "change"
}
],
lng: [
{
validator: validateLngAndLat,
trigger: "blur"
}
],
lat: [
{
validator: validateLngAndLat,
trigger: "change"
}
]
});
const { resetFields } = useForm(form, rules);
const fetchLngLat = async () => {
if (!canFetchLngLat.value) {
message.warning("请先选择省市区并填写详细地址");
return;
}
fetchingLngLat.value = true;
try {
const area = `${normalizeText(form.province)}${normalizeText(form.city)}${normalizeText(form.region)}`;
const payload = {
keyWord: normalizeText(form.address),
level: 18,
address: area
};
const url = `https://api.tianditu.gov.cn/geocoder?ds=${encodeURIComponent(
JSON.stringify(payload)
)}&tk=${TIANDITU_TOKEN}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error("获取经纬度失败,请稍后再试");
}
const data = await response.json();
const location = data?.location || data?.result?.location;
const lng = location?.lon ?? location?.lng ?? location?.longitude;
const lat = location?.lat ?? location?.latitude;
if (!lng || !lat) {
throw new Error(data?.msg || "未获取到经纬度");
}
form.lng = String(lng);
form.lat = String(lat);
form.lngAndLat = `${form.lng},${form.lat}`;
message.success("已获取经纬度");
} catch (e) {
const err = e as Error;
message.error(err.message || "获取经纬度失败");
} finally {
fetchingLngLat.value = false;
}
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateMerchant : addMerchant;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit("done");
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {
});
};
const areaDictList = ref<DictData[]>([]);
const getAreaDictList = async () => {
areaDictList.value = await listDictData({ dictId: 174 });
}
watch(
() => props.visible,
async (visible) => {
if (visible) {
await getAreaList();
await getAreaDictList();
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
const areaValue = [form.province, form.city, form.region].filter(Boolean);
selectedArea.value = areaValue.length ? (areaValue as string[]) : [];
} else {
resetFields();
selectedArea.value = [];
}
},
{ immediate: true }
);
</script>
@@ -0,0 +1,352 @@
<!-- 商户批量导入弹窗 -->
<template>
<ele-modal
:width="560"
:footer="null"
title="批量导入门店"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<a-upload-dragger
accept=".xls,.xlsx"
:show-upload-list="false"
:customRequest="doImport"
style="padding: 24px 0; margin-bottom: 12px"
>
<p class="ant-upload-drag-icon">
<cloud-upload-outlined />
</p>
<p class="ant-upload-hint">将 Excel 文件拖到此处,或点击上传</p>
</a-upload-dragger>
</a-spin>
<div class="ele-text-center" style="margin-top: 8px;">
<a
href="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/%E9%97%A8%E5%BA%97%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xlsx"
target="_blank"
rel="noopener noreferrer"
>
模板下载
</a>
</div>
<div v-if="progress.total" class="ele-text-center" style="margin-top: 8px;">
已处理 {{ progress.success + progress.fail }}/{{ progress.total }},
成功 {{ progress.success }},失败 {{ progress.fail }}
</div>
<div v-if="errorMessages.length" class="ele-text-danger" style="margin-top: 8px;">
<div>失败明细(展示最新 5 条):</div>
<div v-for="(item, index) in latestErrorMessages" :key="index">
{{ item }}
</div>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import { message } from "ant-design-vue";
import { CloudUploadOutlined } from "@ant-design/icons-vue";
import * as XLSX from "xlsx";
import { addMerchant } from "@/api/shop/merchant";
import type { Merchant } from "@/api/shop/merchant/model";
import { getAddressInfo } from "@/api/yxe/ai";
const emit = defineEmits<{
(e: "done"): void;
(e: "update:visible", visible: boolean): void;
}>();
defineProps<{
// 是否打开弹窗
visible: boolean;
}>();
const loading = ref(false);
const progress = ref({ total: 0, success: 0, fail: 0 });
const errorMessages = ref<string[]>([]);
const latestErrorMessages = computed(() =>
errorMessages.value.slice(-5).reverse()
);
const normalize = (value?: unknown) => String(value ?? "").trim();
const parseAddressInfo = (content?: string) => {
const raw = normalize(content).replace(/^\uFEFF/, "");
if (!raw) {
throw new Error("地址解析结果为空");
}
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
const body = fenced?.[1] ? fenced[1].trim() : raw;
const firstBrace = body.indexOf("{");
const lastBrace = body.lastIndexOf("}");
const jsonLike =
firstBrace >= 0 && lastBrace > firstBrace
? body.slice(firstBrace, lastBrace + 1)
: body;
const tryParse = (text: string) => {
const parsed = JSON.parse(text);
if (typeof parsed === "string") {
return JSON.parse(parsed);
}
return parsed;
};
const candidates = [
jsonLike,
jsonLike.replace(/,\s*([}\]])/g, "$1"),
jsonLike
.replace(/([{,]\s*)([A-Za-z_][A-Za-z0-9_]*)(\s*:)/g, '$1"$2"$3')
.replace(/'/g, '"')
.replace(/,\s*([}\]])/g, "$1")
];
for (const candidate of candidates) {
try {
return tryParse(candidate);
} catch {
continue;
}
}
throw new Error(`地址解析结果不是有效 JSON:${body}`);
};
const readRows = async (file: File) => {
const data = await file.arrayBuffer();
const workbook = XLSX.read(data, { type: "array" });
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
const rows = XLSX.utils.sheet_to_json<any[]>(sheet, {
header: 1,
defval: ""
}) as Array<string[]>;
if (!rows.length) {
return [];
}
const header = rows[0].map((item) => normalize(item).toLowerCase());
const headerIndex = (aliases: string[]) => {
const aliasSet = aliases.map((item) => item.toLowerCase());
return header.findIndex((item) => aliasSet.includes(item));
};
const chainIndex = headerIndex(["所属连锁", "连锁", "chain"]);
const codeIndex = headerIndex(["门店编码", "商户编号", "id", "code", "id(code)", "编号"]);
const nameIndex = headerIndex(["门店名称", "名称", "name", "门店名称(name)"]);
const areaNameIndex = headerIndex(["片区", "所属片区", "areaName", "area"]);
const phoneIndex = headerIndex(["联系方式", "联系电话", "手机", "phone", "联系方式(phone)"]);
const realNameIndex = headerIndex([
"营运经理",
"负责人",
"联系人",
"realName",
"营运经理(realName)"
]);
const provinceIndex = headerIndex(["省份", "省", "province", "省份(province)"]);
const cityIndex = headerIndex(["市级", "城市", "市", "city", "市级(city)"]);
const regionIndex = headerIndex([
"区/县级",
"区县",
"区",
"县",
"region",
"区/县级(region)"
]);
const addressIndex = headerIndex([
"门店具体地址",
"门店具体地址(addressfull)",
"门店具体地址(按营业执照填写)",
"门店具体地址 (按营业执照填写)",
"门店具体地址(按营业执照填写)",
"详细地址",
"地址",
"addressFull",
"门店具体地址(addressFull)"
]);
const hasHeader =
chainIndex >= 0 ||
codeIndex >= 0 ||
nameIndex >= 0 ||
areaNameIndex >= 0 ||
phoneIndex >= 0 ||
realNameIndex >= 0 ||
provinceIndex >= 0 ||
cityIndex >= 0 ||
regionIndex >= 0 ||
addressIndex >= 0;
const dataRows = hasHeader ? rows.slice(1) : rows;
const getByIndex = (row: string[], index: number, fallback: number) => {
const value = index >= 0 ? row[index] : row[fallback];
return normalize(value);
};
return dataRows
.map((row, i) => {
const chain = getByIndex(row, chainIndex, 0);
const code = getByIndex(row, codeIndex, 1);
const name = getByIndex(row, nameIndex, 2);
const areaName = getByIndex(row, areaNameIndex, 3);
const realName = getByIndex(row, realNameIndex, 4);
const phone = getByIndex(row, phoneIndex, 5);
const province = getByIndex(row, provinceIndex, 6);
const city = getByIndex(row, cityIndex, 7);
const region = getByIndex(row, regionIndex, 8);
const addressFull = getByIndex(row, addressIndex, 9);
return {
rowNumber: hasHeader ? i + 2 : i + 1,
chain,
code,
name,
areaName,
phone,
realName,
province,
city,
region,
addressFull
};
})
.filter(
(row) =>
row.chain ||
row.code ||
row.name ||
row.areaName ||
row.phone ||
row.realName ||
row.province ||
row.city ||
row.region ||
row.addressFull
);
};
const resolveAddressInfo = async (addressFull: string) => {
const res = await getAddressInfo(addressFull);
const info = parseAddressInfo(res?.output?.choices?.[0]?.message?.content);
const lng = info?.lng ?? info?.lon ?? info?.longitude;
const lat = info?.lat ?? info?.latitude;
const province = info?.province;
const city = info?.city;
const region = info?.region;
const detail = info?.detail;
if (!lng || !lat) {
throw new Error("未获取到经纬度");
}
return {
lng: String(lng),
lat: String(lat),
province: province,
city: city,
region: region,
detail: detail
};
};
const doImport = async ({ 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;
progress.value = { total: 0, success: 0, fail: 0 };
errorMessages.value = [];
try {
const rows = await readRows(file as File);
if (!rows.length) {
message.warning("未读取到可导入的数据");
return false;
}
progress.value.total = rows.length;
for (const row of rows) {
try {
if (
!row.name ||
!row.province ||
!row.city
) {
throw new Error("门店名称、省份、市级为空");
}
let lng: string | undefined;
let lat: string | undefined;
let province = row.province;
let city = row.city;
let region = row.region;
let detail = row.addressFull;
if (row.addressFull) {
const resolved = await resolveAddressInfo(row.addressFull);
lng = resolved.lng;
lat = resolved.lat;
province = resolved.province || province;
city = resolved.city || city;
region = resolved.region || region;
detail = resolved.detail || detail;
}
const merchantData: Merchant = {
chain: row.chain,
code: row.code || undefined,
name: row.name,
areaName: row.areaName,
phone: row.phone,
realName: row.realName,
lng,
lat,
lngAndLat: lng && lat ? `${lng},${lat}` : undefined,
province: province,
city: city,
region: region,
address: detail || undefined
};
await addMerchant(merchantData);
progress.value.success += 1;
} catch (e) {
progress.value.fail += 1;
const err = e as Error;
errorMessages.value.push(`第${row.rowNumber}行:${err.message || "导入失败"}`);
}
}
if (progress.value.fail > 0) {
message.warning(
`导入完成:成功${progress.value.success}条,失败${progress.value.fail}条`
);
} else {
message.success(`导入完成:成功${progress.value.success}条`);
}
if (progress.value.success > 0) {
emit("done");
}
updateVisible(false);
} catch (e) {
const err = e as Error;
message.error(err.message || "导入失败");
} finally {
loading.value = false;
}
return false;
};
const updateVisible = (value: boolean) => {
emit("update:visible", value);
};
</script>
@@ -0,0 +1,94 @@
<!-- 搜索表单 -->
<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-button type="primary" @click="openImport">
<template #icon>
<UploadOutlined />
</template>
批量导入
</a-button>
<a-popconfirm title="确定要批量删除选择的记录吗?" @confirm="removeBatch">
<a-button type="primary" danger :disabled="!props.selection?.length">
<template #icon>
<DeleteOutlined />
</template>
批量删除
</a-button>
</a-popconfirm>
<a-input-search
v-model:value="where.keywords"
allow-clear
placeholder="名称筛选"
@pressEnter="search"
@search="search"
/>
<a-input-search
v-model:value="where.chain"
allow-clear
placeholder="连锁筛选"
@pressEnter="search"
@search="search"
/>
<a-button type="primary" @click="search">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</template>
<script lang="ts" setup>
import {
DeleteOutlined,
PlusOutlined,
UploadOutlined
} from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import type { MerchantParam } from '@/api/shop/merchant/model';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: MerchantParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
(e: 'import'): void;
}>();
const { where, resetFields } = useSearch<MerchantParam>({
keywords: undefined,
chain: undefined
});
// 新增
const add = () => {
emit('add');
};
const search = () => {
emit('search', where);
};
const reset = () => {
resetFields();
search();
};
const openImport = () => {
emit('import');
};
const removeBatch = () => {
emit('remove');
};
</script>
+264
View File
@@ -0,0 +1,264 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
:pageSizeOptions="['10', '20', '50', '100', '200', '500', '1000']"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
@import="openImport"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'province'">
<span>{{ record.province }}{{ record.city }}{{ record.region }}</span>
</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>
<!-- 编辑弹窗 -->
<MerchantEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 批量导入弹窗 -->
<MerchantImport v-model:visible="showImport" @done="reload" />
</div>
</div>
</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 MerchantEdit from './components/merchantEdit.vue';
import MerchantImport from './components/merchantImport.vue';
import { pageMerchant, removeMerchant, removeBatchMerchant } from '@/api/shop/merchant';
import type { Merchant, MerchantParam } from '@/api/shop/merchant/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<Merchant[]>([]);
// 当前编辑数据
const current = ref<Merchant | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 是否显示批量导入弹窗
const showImport = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageMerchant({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '编码',
dataIndex: 'code',
align: 'center',
},
{
title: '所属连锁',
dataIndex: 'chain',
align: 'center',
},
{
title: '所属片区',
dataIndex: 'areaName',
align: 'center',
},
{
title: '商户名称',
dataIndex: 'name',
key: 'name',
align: 'center',
},
{
title: '联系方式',
dataIndex: 'phone',
key: 'phone',
align: 'center',
},
{
title: '营运经理',
dataIndex: 'realName',
key: 'realName',
align: 'center',
},
{
title: '所在区域',
key: 'province',
align: 'center',
},
{
title: '详细地址',
dataIndex: 'address',
key: 'address',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: MerchantParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Merchant) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 打开批量导入弹窗 */
const openImport = () => {
showImport.value = true;
};
/* 删除单个 */
const remove = (row: Merchant) => {
const hide = message.loading('请求中..', 0);
removeMerchant(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;
}
const hide = message.loading('请求中..', 0);
removeBatchMerchant(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: Merchant) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'Merchant'
};
</script>
<style lang="less" scoped></style>
@@ -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>
@@ -0,0 +1,185 @@
<!-- 编辑弹窗 -->
<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="uid">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.uid"
/>
</a-form-item>
<a-form-item label="" name="parentId">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.parentId"
/>
</a-form-item>
<a-form-item label="管理员备注" name="remark">
<a-input
allow-clear
placeholder="请输入管理员备注"
v-model:value="form.remark"
/>
</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="是否删除, 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>
<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 { addUserParentList, updateUserParentList } from '@/api/shop/userParentList';
import { UserParentList } from '@/api/shop/userParentList/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
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?: UserParentList | 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<UserParentList>({
id: undefined,
uid: undefined,
parentId: undefined,
remark: undefined,
sortNumber: undefined,
comments: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
});
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 ? updateUserParentList : addUserParentList;
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);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
+257
View File
@@ -0,0 +1,257 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 1" color="green">显示</a-tag>
<a-tag v-if="record.status === 0" 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>
<!-- 编辑弹窗 -->
<UserParentListEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</div>
</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 UserParentListEdit from './components/userParentListEdit.vue';
import { pageUserParentList, removeUserParentList, removeBatchUserParentList } from '@/api/shop/userParentList';
import type { UserParentList, UserParentListParam } from '@/api/shop/userParentList/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<UserParentList[]>([]);
// 当前编辑数据
const current = ref<UserParentList | 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 pageUserParentList({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '',
dataIndex: 'uid',
key: 'uid',
align: 'center',
},
{
title: '',
dataIndex: 'parentId',
key: 'parentId',
align: 'center',
},
{
title: '管理员备注',
dataIndex: 'remark',
key: 'remark',
align: 'center',
},
{
title: '排序(数字越小越靠前)',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
align: 'center',
},
{
title: '是否删除, 0否, 1是',
dataIndex: 'deleted',
key: 'deleted',
align: 'center',
},
{
title: '注册时间',
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
}
]);
/* 搜索 */
const reload = (where?: UserParentListParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: UserParentList) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: UserParentList) => {
const hide = message.loading('请求中..', 0);
removeUserParentList(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);
removeBatchUserParentList(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: UserParentList) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'UserParentList'
};
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,232 @@
<!-- 搜索表单 -->
<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-select
v-model:value="where.userId"
show-search
allow-clear
:filter-option="false"
optionFilterProp="label"
:loading="userLoading"
:options="userOptions"
placeholder="请选择用户"
style="width: 220px"
@search="onSearchUser"
@focus="loadUserOptions()"
@change="search"
/>
<a-input
v-model:value.trim="where.msg"
allow-clear
placeholder="请输入积分说明"
style="width: 220px"
@pressEnter="search"
/>
<a-select
v-model:value="where.status"
allow-clear
placeholder="请选择状态"
style="width: 160px"
>
<a-select-option :value="0">正常</a-select-option>
<a-select-option :value="1">冻结</a-select-option>
</a-select>
<a-range-picker
v-model:value="dateRange"
value-format="YYYY-MM-DD"
:placeholder="['请选择开始日期', '请选择结束日期']"
style="width: 260px"
/>
<a-button type="primary" @click="search">查询</a-button>
<a-button @click="reset">重置</a-button>
<a-popconfirm title="确定要批量删除选择的记录吗?" @confirm="removeBatch">
<a-button type="primary" danger :disabled="!props.selection?.length">
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
</a-popconfirm>
</a-space>
</template>
<script lang="ts" setup>
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
import { onBeforeUnmount, ref } from 'vue';
import { message } from 'ant-design-vue';
import type { SelectProps } from 'ant-design-vue';
import useSearch from '@/utils/use-search';
import { pageUsers } from '@/api/system/user';
import type { User as SystemUser } from '@/api/system/user/model';
import type { UserPointLogParam } from '@/api/shop/userPointLog/model';
const props = defineProps<{
selection?: [];
}>();
const emit = defineEmits<{
(e: 'search', where?: UserPointLogParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
const { where, resetFields } = useSearch<UserPointLogParam>({
userId: undefined,
msg: undefined,
status: undefined,
createTimeStart: undefined,
createTimeEnd: undefined
});
const dateRange = ref<[string, string] | []>([]);
const userOptions = ref<SelectProps['options']>([]);
const userLoading = ref(false);
let userSearchTimer: ReturnType<typeof setTimeout> | null = null;
let userSearchSeed = 0;
const buildUserOption = (item: SystemUser) => {
if (item.userId == null) {
return null;
}
const phone = String(item.phone ?? '').trim();
const name = String(item.realName ?? item.nickname ?? '').trim();
const contact = phone || `ID:${item.userId}`;
return {
value: item.userId,
label: name ? `${name} (${contact})` : contact
};
};
const ensureCurrentUserOption = (userId?: number) => {
if (userId == null) {
return;
}
const exists = userOptions.value?.some((item) => item?.value === userId);
if (exists) {
return;
}
userOptions.value = [
{
value: userId,
label: `ID:${userId}`
},
...(userOptions.value || [])
];
};
const loadUserOptions = (keyword?: string) => {
const currentSeed = ++userSearchSeed;
const trimmed = String(keyword ?? '').trim();
const previousSelectedOption =
where.userId == null
? undefined
: (userOptions.value || []).find(
(item) => item?.value === where.userId
);
userLoading.value = true;
const requests = trimmed
? [
pageUsers({
page: 1,
limit: 20,
phone: trimmed
}),
pageUsers({
page: 1,
limit: 20,
realName: trimmed
})
]
: [
pageUsers({
page: 1,
limit: 20
})
];
Promise.all(requests)
.then((results) => {
if (currentSeed !== userSearchSeed) {
return;
}
const map = new Map<number, { value: number; label: string }>();
results.forEach((res) => {
(res?.list || []).forEach((item) => {
const option = buildUserOption(item as SystemUser);
if (option && !map.has(option.value)) {
map.set(option.value, option);
}
});
});
userOptions.value = Array.from(map.values());
if (
previousSelectedOption &&
where.userId != null &&
!map.has(where.userId)
) {
userOptions.value = [previousSelectedOption, ...userOptions.value];
}
ensureCurrentUserOption(where.userId);
})
.catch((e) => {
if (currentSeed !== userSearchSeed) {
return;
}
message.error(e.message ?? '查询用户失败');
})
.finally(() => {
if (currentSeed === userSearchSeed) {
userLoading.value = false;
}
});
};
const onSearchUser = (value: string) => {
if (userSearchTimer) {
clearTimeout(userSearchTimer);
}
userSearchTimer = setTimeout(() => {
loadUserOptions(value);
}, 300);
};
const syncCreateTimeRange = () => {
const [startDate, endDate] = Array.isArray(dateRange.value)
? dateRange.value
: [];
where.createTimeStart = startDate ? `${startDate} 00:00:00` : undefined;
where.createTimeEnd = endDate ? `${endDate} 23:59:59` : undefined;
};
const add = () => {
emit('add');
};
const search = () => {
syncCreateTimeRange();
emit('search', { ...where });
};
const reset = () => {
resetFields();
dateRange.value = [];
search();
};
const removeBatch = () => {
emit('remove');
};
onBeforeUnmount(() => {
if (userSearchTimer) {
clearTimeout(userSearchTimer);
userSearchTimer = null;
}
});
</script>
@@ -0,0 +1,190 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑积分记录' : '添加积分记录'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="用户ID" name="userId">
<a-input-number
:min="1"
class="ele-fluid"
placeholder="请输入用户ID"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="积分说明" name="msg">
<a-input
allow-clear
placeholder="请输入积分说明"
v-model:value="form.msg"
/>
</a-form-item>
<a-form-item label="管理员备注" name="remark">
<a-textarea
:rows="3"
:maxlength="200"
placeholder="请输入管理员备注"
v-model:value="form.remark"
/>
</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="3"
: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 { reactive, ref, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addUserPointLog, updateUserPointLog } from '@/api/shop/userPointLog';
import type { UserPointLog } from '@/api/shop/userPointLog/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import type { 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?: UserPointLog | 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<UserPointLog>({
id: undefined,
userId: undefined,
msg: undefined,
remark: undefined,
sortNumber: undefined,
comments: undefined,
status: 0,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const rules = reactive({
userId: [
{
required: true,
type: 'number',
message: '请输入用户ID'
}
],
msg: [
{
required: true,
message: '请输入积分说明'
}
],
status: [
{
required: true,
type: 'number',
message: '请选择状态'
}
]
});
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
? updateUserPointLog
: addUserPointLog;
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);
isUpdate.value = true;
} else {
isUpdate.value = false;
form.status = 0;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
+259
View File
@@ -0,0 +1,259 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
v-model:selection="selection"
: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 === 'userId'">
<span>{{ record.userId ?? '-' }}</span>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">正常</a-tag>
<a-tag v-else-if="record.status === 1" color="red">冻结</a-tag>
<span v-else>-</span>
</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>
<UserPointLogEdit
v-model:visible="showEdit"
:data="current"
@done="reload"
/>
</div>
</div>
</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 UserPointLogEdit from './components/userPointLogEdit.vue';
import {
pageUserPointLog,
removeUserPointLog,
removeBatchUserPointLog
} from '@/api/shop/userPointLog';
import type {
UserPointLog,
UserPointLogParam
} from '@/api/shop/userPointLog/model';
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
const selection = ref<UserPointLog[]>([]);
const current = ref<UserPointLog | null>(null);
const showEdit = ref(false);
const showMove = ref(false);
const loading = ref(true);
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters && filters.status !== void 0) {
where.status = filters.status;
}
return pageUserPointLog({
...where,
...orders,
page,
limit
});
};
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90
},
{
title: '用户ID',
key: 'userId',
align: 'center',
width: 120
},
{
title: '积分说明',
dataIndex: 'msg',
key: 'msg',
align: 'center',
ellipsis: true
},
{
title: '管理员备注',
dataIndex: 'remark',
key: 'remark',
align: 'center',
ellipsis: true
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
align: 'center',
ellipsis: true
},
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
width: 100
},
{
title: '状态',
key: 'status',
align: 'center',
width: 100
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '修改时间',
dataIndex: 'updateTime',
key: 'updateTime',
align: 'center',
ellipsis: true,
customRender: ({ text }) =>
text ? toDateString(text, 'yyyy-MM-dd HH:mm:ss') : '-'
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
const reload = (where?: UserPointLogParam) => {
selection.value = [];
tableRef?.value?.reload({ where });
};
const openEdit = (row?: UserPointLog) => {
current.value = row ?? null;
showEdit.value = true;
};
const openMove = () => {
showMove.value = true;
};
const remove = (row: UserPointLog) => {
const hide = message.loading('请求中..', 0);
removeUserPointLog(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);
removeBatchUserPointLog(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: UserPointLog) => {
return {
onClick: () => {
return record;
},
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'UserPointLog'
};
</script>
<style lang="less" scoped></style>
@@ -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>
@@ -0,0 +1,149 @@
<!-- 编辑弹窗 -->
<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="fid">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.fid"
/>
</a-form-item>
<a-form-item label="" name="levelId">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.levelId"
/>
</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 { addSysArea, updateSysArea } from '@/api/sys/sysArea';
import { SysArea } from '@/api/sys/sysArea/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
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?: SysArea | 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<SysArea>({
id: undefined,
name: undefined,
fid: undefined,
levelId: undefined,
tenantId: undefined,
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
});
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 ? updateSysArea : addSysArea;
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);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
+224
View File
@@ -0,0 +1,224 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 1" color="green">显示</a-tag>
<a-tag v-if="record.status === 0" 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>
<!-- 编辑弹窗 -->
<SysAreaEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</div>
</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 SysAreaEdit from './components/sysAreaEdit.vue';
import { pageSysArea, removeSysArea, removeBatchSysArea } from '@/api/sys/sysArea';
import type { SysArea, SysAreaParam } from '@/api/sys/sysArea/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<SysArea[]>([]);
// 当前编辑数据
const current = ref<SysArea | 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 pageSysArea({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '',
dataIndex: 'name',
key: 'name',
align: 'center',
},
{
title: '',
dataIndex: 'fid',
key: 'fid',
align: 'center',
},
{
title: '',
dataIndex: 'levelId',
key: 'levelId',
align: 'center',
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: SysAreaParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: SysArea) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: SysArea) => {
const hide = message.loading('请求中..', 0);
removeSysArea(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);
removeBatchSysArea(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: SysArea) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'SysArea'
};
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,289 @@
<template>
<ele-modal
:width="760"
:visible="props.visible"
:maskClosable="false"
:confirm-loading="saving"
title="金币充值"
:body-style="{ paddingBottom: '10px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 5, sm: 6, xs: 24 } : { flex: '100px' }"
:wrapper-col="
styleResponsive ? { md: 18, sm: 17, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="用户">
<div class="user-brief">
<span class="user-name">{{ userName || "未选择用户" }}</span>
<span v-if="props.data?.userId" class="user-id">ID: {{ props.data?.userId }}</span>
</div>
</a-form-item>
<a-form-item label="增减方式" name="type">
<a-radio-group v-model:value="form.type">
<a-radio value="+">增加</a-radio>
<a-radio value="-">扣减</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="金币数量" name="num">
<a-input-number
class="ele-fluid"
:min="1"
:precision="0"
placeholder="请输入金币数量"
v-model:value="form.num"
/>
</a-form-item>
<a-form-item label="备注" name="desc">
<a-input
allow-clear
placeholder="请输入备注"
v-model:value="form.desc"
/>
</a-form-item>
</a-form>
<a-divider orientation="left">金币记录</a-divider>
<a-table
size="small"
:loading="logLoading"
:columns="logColumns"
:data-source="logs"
:pagination="pagination"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
<a-tag :color="record.type === '+' ? 'green' : 'red'">
{{ record.type === "+" ? "增加" : "减少" }}
</a-tag>
</template>
<template v-else-if="column.key === 'num'">
<span :class="record.type === '+' ? 'text-add' : 'text-minus'">
{{ record.type === "+" ? "+" : "-" }}{{ record.num }}
</span>
</template>
<template v-else-if="column.key === 'createTime'">
{{ record.createTime ? toDateString(record.createTime, "yyyy-MM-dd HH:mm") : "-" }}
</template>
</template>
</a-table>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, reactive, ref, watch } from "vue";
import { message } from "ant-design-vue/es";
import type { FormInstance, Rule } from "ant-design-vue/es/form";
import type { TablePaginationConfig } from "ant-design-vue/es/table/interface";
import { toDateString } from "ele-admin-pro";
import { storeToRefs } from "pinia";
import { useThemeStore } from "@/store/modules/theme";
import { addGxCoinLog, pageGxCoinLog } from "@/api/gx/gxCoinLog";
import type { GxCoinLog, GxCoinLogParam } from "@/api/gx/gxCoinLog/model";
import type { User } from "@/api/system/user/model";
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
visible: boolean;
data?: User | null;
}>();
const emit = defineEmits<{
(e: "done"): void;
(e: "update:visible", visible: boolean): void;
}>();
const formRef = ref<FormInstance | null>(null);
const saving = ref(false);
const logLoading = ref(false);
const logs = ref<GxCoinLog[]>([]);
const pagination = reactive<TablePaginationConfig>({
current: 1,
pageSize: 10,
total: 0,
showTotal: (total) => `共 ${total} 条`
});
const form = reactive<{
type: string;
num?: number;
desc?: string;
}>({
type: "+",
num: undefined,
desc: ""
});
const logColumns = [
{
title: "时间",
dataIndex: "createTime",
key: "createTime",
width: 180
},
{
title: "类型",
dataIndex: "type",
key: "type",
width: 100
},
{
title: "金币",
dataIndex: "num",
key: "num",
width: 120
},
{
title: "描述",
dataIndex: "desc",
key: "desc",
ellipsis: true
}
];
const rules = reactive<Record<string, Rule[]>>({
type: [{ required: true, message: "请选择增减方式", trigger: "change" }],
num: [
{ required: true, message: "请输入金币数量", trigger: "blur" },
{
validator: (_rule, value) => {
if (value && value > 0) {
return Promise.resolve();
}
return Promise.reject("金币数量需大于0");
},
trigger: "blur"
}
]
});
const userName = computed(() => {
const target = props.data;
if (!target) {
return "";
}
return target.realName || target.nickname || target.username || "";
});
const updateVisible = (value: boolean) => {
emit("update:visible", value);
};
const resetForm = () => {
form.type = "+";
form.num = undefined;
form.desc = "";
formRef.value?.clearValidate();
};
const loadLogs = (resetPage = false) => {
if (resetPage) {
pagination.current = 1;
}
const userId = props.data?.userId;
if (!userId) {
logs.value = [];
pagination.total = 0;
return;
}
logLoading.value = true;
const params: GxCoinLogParam = {
page: pagination.current || 1,
limit: pagination.pageSize || 10,
userId
};
pageGxCoinLog(params)
.then((res) => {
logs.value = res.list || [];
pagination.total = res.count || 0;
})
.catch((e: Error) => {
message.error(e.message);
})
.finally(() => {
logLoading.value = false;
});
};
const handleTableChange = (pag: TablePaginationConfig) => {
pagination.current = pag.current || 1;
pagination.pageSize = pag.pageSize || 10;
loadLogs();
};
const save = () => {
if (!formRef.value) {
return;
}
formRef.value.validate().then(() => {
const userId = props.data?.userId;
if (!userId) {
message.error("未找到用户信息");
return;
}
saving.value = true;
const payload: GxCoinLog = {
userId,
type: form.type,
num: form.num,
desc: form.desc,
status: 1,
isAdmin: true
};
addGxCoinLog(payload)
.then((msg) => {
message.success(msg);
emit("done");
loadLogs(true);
resetForm();
})
.catch((e: Error) => {
message.error(e.message);
})
.finally(() => {
saving.value = false;
});
});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
resetForm();
loadLogs(true);
}
}
);
</script>
<style scoped>
.user-brief {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.user-id {
color: #999;
font-weight: 400;
}
.text-add {
color: #52c41a;
}
.text-minus {
color: #f5222d;
}
</style>
@@ -0,0 +1,62 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="exportData">
<template #icon>
<DownloadOutlined />
</template>
<span>导出</span>
</a-button>
<a-input
v-model:value="where.name"
allow-clear
placeholder="请输入姓名"
@pressEnter="reload"
/>
<a-input
v-model:value="where.phone"
allow-clear
placeholder="请输入手机号"
@pressEnter="reload"
/>
<a-button type="primary" @click="reload">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { DownloadOutlined } from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import type { YxeChronicDiseaseCollectionParam } from '@/api/yxe/yxeChronicDiseaseCollection/model';
defineProps<{
// 选中的角色
selection?: [];
}>();
const emit = defineEmits<{
(e: 'search', where?: YxeChronicDiseaseCollectionParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
(e: 'export'): void;
}>();
const { where, resetFields } = useSearch<YxeChronicDiseaseCollectionParam>({
name: '',
phone: ''
});
const reload = () => {
emit('search', where);
};
const reset = () => {
resetFields();
reload();
};
const exportData = () => {
emit('export');
};
</script>
@@ -0,0 +1,335 @@
<template>
<ele-modal
title="详情"
:width="980"
:footer="null"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="detailLoading">
<a-space direction="vertical" class="ele-fluid">
<a-descriptions bordered :column="{ xs: 1, sm: 2 }">
<a-descriptions-item label="ID">
{{ formatText(detailData?.id) }}
</a-descriptions-item>
<a-descriptions-item label="用户ID">
{{ formatText(detailData?.userId) }}
</a-descriptions-item>
<a-descriptions-item label="姓名">
{{ formatText(detailData?.name) }}
</a-descriptions-item>
<a-descriptions-item label="性别">
{{ formatText(detailData?.gender) }}
</a-descriptions-item>
<a-descriptions-item label="手机号">
{{ formatText(detailData?.phone) }}
</a-descriptions-item>
<a-descriptions-item label="出生年月日">
{{ formatText(detailData?.birthday) }}
</a-descriptions-item>
<a-descriptions-item label="连锁">
{{ formatText(detailData?.chainName) }}
</a-descriptions-item>
<a-descriptions-item label="门店">
{{ formatText(detailData?.merchantName) }}
</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag v-if="detailData?.status === 0">待审核</a-tag>
<a-tag v-else-if="detailData?.status === 1" color="green">
通过
</a-tag>
<a-tag v-else-if="detailData?.status === 2" color="red">
驳回
</a-tag>
<span v-else class="ele-text-secondary">-</span>
</a-descriptions-item>
<a-descriptions-item label="添加时间">
{{ formatDateTime(detailData?.createTime) }}
</a-descriptions-item>
<a-descriptions-item label="修改时间">
{{ formatDateTime(detailData?.updateTime) }}
</a-descriptions-item>
<a-descriptions-item label="血红蛋白数值">
{{ formatText(detailData?.hemoglobinValue) }}
</a-descriptions-item>
<a-descriptions-item label="贫血程度">
{{ formatText(detailData?.anemiaLevel) }}
</a-descriptions-item>
<a-descriptions-item label="地贫类型">
{{ formatText(detailData?.thalassemiaType) }}
</a-descriptions-item>
<a-descriptions-item label="是否高血压">
<a-tag v-if="isYes(detailData?.highBloodPressure)" color="green">
是
</a-tag>
<a-tag v-else color="red">否</a-tag>
</a-descriptions-item>
<a-descriptions-item label="血压值">
{{ formatText(detailData?.highBloodPressureValue) }}
</a-descriptions-item>
<a-descriptions-item label="收缩压">
{{ formatText(detailData?.systolicPressure) }}
</a-descriptions-item>
<a-descriptions-item label="舒张压">
{{ formatText(detailData?.diastolicPressure) }}
</a-descriptions-item>
<a-descriptions-item label="是否高血糖">
<a-tag v-if="isYes(detailData?.highBloodSugar)" color="green">
是
</a-tag>
<a-tag v-else color="red">否</a-tag>
</a-descriptions-item>
<a-descriptions-item label="空腹血糖">
{{ formatText(detailData?.fastingBloodGlucose) }}
</a-descriptions-item>
<a-descriptions-item label="餐后2小时血糖">
{{ formatText(detailData?.postprandial2hBloodGlucose) }}
</a-descriptions-item>
<a-descriptions-item label="糖化血红蛋白(HbA1c)">
{{ formatText(detailData?.hba1c) }}
</a-descriptions-item>
<a-descriptions-item label="是否高血脂">
<a-tag v-if="isYes(detailData?.hyperlipidemia)" color="green">
是
</a-tag>
<a-tag v-else color="red">否</a-tag>
</a-descriptions-item>
<a-descriptions-item label="总胆固醇(TC)">
{{ formatText(detailData?.totalCholesterol) }}
</a-descriptions-item>
<a-descriptions-item label="甘油三酯(TG)">
{{ formatText(detailData?.triglyceride) }}
</a-descriptions-item>
<a-descriptions-item label="低密度脂蛋白(LDL-C)">
{{ formatText(detailData?.ldlC) }}
</a-descriptions-item>
<a-descriptions-item label="高密度脂蛋白(HDL-C)">
{{ formatText(detailData?.hdlC) }}
</a-descriptions-item>
<a-descriptions-item label="病史" :span="2">
<div class="detail-text">
{{ formatText(detailData?.medicalHistory) }}
</div>
</a-descriptions-item>
<a-descriptions-item label="用药史" :span="2">
<div class="detail-text">
{{ formatText(detailData?.medicationHistory) }}
</div>
</a-descriptions-item>
</a-descriptions>
<a-card size="small" title="用药追踪">
<a-table
v-if="trackingRows.length"
:columns="trackingColumns"
:data-source="trackingRows"
:pagination="false"
:scroll="{ x: 960 }"
size="small"
row-key="rowKey"
/>
<div v-else-if="trackingRawText" class="detail-text">
{{ trackingRawText }}
</div>
<span v-else class="ele-text-secondary">-</span>
</a-card>
</a-space>
</a-spin>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import { toDateString } from 'ele-admin-pro';
import { getYxeChronicDiseaseCollection } from '@/api/yxe/yxeChronicDiseaseCollection';
import type { YxeChronicDiseaseCollection } from '@/api/yxe/yxeChronicDiseaseCollection/model';
type TrackingItem = {
productId?: number;
productName?: string;
goodsCode?: string;
dailyUsageCount?: number | string;
dosagePerTime?: number | string;
boxUsageDays?: number | string;
purchaseTime?: string;
boxCount?: number | string;
estimatedFinishTime?: string;
};
type TrackingRow = TrackingItem & {
rowKey: string;
};
const props = defineProps<{
visible: boolean;
data?: YxeChronicDiseaseCollection | null;
}>();
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
}>();
const detailLoading = ref(false);
const detailData = ref<YxeChronicDiseaseCollection | null>(null);
let detailRequestSeed = 0;
const trackingColumns = [
{
title: '商品ID',
dataIndex: 'productId',
key: 'productId',
align: 'center'
},
{
title: '商品名称',
dataIndex: 'productName',
key: 'productName',
align: 'center',
ellipsis: true
},
{
title: '商品编码',
dataIndex: 'goodsCode',
key: 'goodsCode',
align: 'center'
},
{
title: '每日次数',
dataIndex: 'dailyUsageCount',
key: 'dailyUsageCount',
align: 'center'
},
{
title: '每次剂量',
dataIndex: 'dosagePerTime',
key: 'dosagePerTime',
align: 'center'
},
{
title: '单盒可用天数',
dataIndex: 'boxUsageDays',
key: 'boxUsageDays',
align: 'center'
},
{
title: '购买时间',
dataIndex: 'purchaseTime',
key: 'purchaseTime',
align: 'center'
},
{
title: '购买盒数',
dataIndex: 'boxCount',
key: 'boxCount',
align: 'center'
},
{
title: '预计用完时间',
dataIndex: 'estimatedFinishTime',
key: 'estimatedFinishTime',
align: 'center'
}
];
const formatText = (value: unknown) => {
if (value === null || value === undefined) {
return '-';
}
const text = String(value).trim();
return text ? text : '-';
};
const formatDateTime = (value?: string) => {
return value ? toDateString(value, 'yyyy-MM-dd HH:mm:ss') : '-';
};
const isYes = (value: unknown) => value === 1 || value === '1';
const parseTrackingList = (value?: string) => {
const raw = String(value ?? '').trim();
if (!raw) {
return [] as TrackingRow[];
}
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) {
return [] as TrackingRow[];
}
return parsed.map((item, index) => ({
...(item as TrackingItem),
rowKey: `${item?.productId ?? 'tracking'}-${index}`
}));
} catch (error) {
return [] as TrackingRow[];
}
};
const trackingRows = computed(() =>
parseTrackingList(detailData.value?.tracking)
);
const trackingRawText = computed(() => {
const raw = String(detailData.value?.tracking ?? '').trim();
if (!raw) {
return '';
}
return trackingRows.value.length ? '' : raw;
});
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const resetDetailState = () => {
detailRequestSeed += 1;
detailLoading.value = false;
detailData.value = null;
};
const loadDetail = async (row?: YxeChronicDiseaseCollection | null) => {
const currentRow = row ?? null;
detailData.value = currentRow;
if (!currentRow?.id) {
detailLoading.value = false;
return;
}
const requestSeed = ++detailRequestSeed;
detailLoading.value = true;
try {
const data = await getYxeChronicDiseaseCollection(currentRow.id);
if (requestSeed !== detailRequestSeed) {
return;
}
detailData.value = data;
} catch (e: any) {
if (requestSeed !== detailRequestSeed) {
return;
}
message.error(e.message);
detailData.value = currentRow;
} finally {
if (requestSeed === detailRequestSeed) {
detailLoading.value = false;
}
}
};
watch(
() => [props.visible, props.data?.id] as const,
([visible]) => {
if (!visible) {
resetDetailState();
return;
}
loadDetail(props.data);
},
{ immediate: true }
);
</script>
<style lang="less" scoped>
.detail-text {
white-space: pre-line;
word-break: break-word;
}
</style>
@@ -0,0 +1,229 @@
<!-- 编辑弹窗 -->
<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="gender">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.gender"
/>
</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="birthday">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.birthday"
/>
</a-form-item>
<a-form-item label="是否高血压" name="highBloodPressure">
<a-input
allow-clear
placeholder="请输入是否高血压"
v-model:value="form.highBloodPressure"
/>
</a-form-item>
<a-form-item label="是否高血糖" name="highBloodSugar">
<a-input
allow-clear
placeholder="请输入是否高血糖"
v-model:value="form.highBloodSugar"
/>
</a-form-item>
<a-form-item label="是否高血脂" name="hyperlipidemia">
<a-input
allow-clear
placeholder="请输入是否高血脂"
v-model:value="form.hyperlipidemia"
/>
</a-form-item>
<a-form-item label="病史" name="medicalHistory">
<a-input
allow-clear
placeholder="请输入病史"
v-model:value="form.medicalHistory"
/>
</a-form-item>
<a-form-item label="用药史" name="medicationHistory">
<a-input
allow-clear
placeholder="请输入用药史"
v-model:value="form.medicationHistory"
/>
</a-form-item>
<a-form-item label="" name="userId">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="状态, 0待审核 1通过 2驳回" 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-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>
<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 { addYxeChronicDiseaseCollection, updateYxeChronicDiseaseCollection } from '@/api/yxe/yxeChronicDiseaseCollection';
import { YxeChronicDiseaseCollection } from '@/api/yxe/yxeChronicDiseaseCollection/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
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?: YxeChronicDiseaseCollection | 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<YxeChronicDiseaseCollection>({
id: undefined,
name: undefined,
gender: undefined,
phone: undefined,
birthday: undefined,
highBloodPressure: undefined,
highBloodSugar: undefined,
hyperlipidemia: undefined,
medicalHistory: undefined,
medicationHistory: undefined,
userId: undefined,
status: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
});
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 ? updateYxeChronicDiseaseCollection : addYxeChronicDiseaseCollection;
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);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
@@ -0,0 +1,280 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
:scroll="{ x: 960 }"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
@export="exportData"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 1" color="green">显示</a-tag>
<a-tag v-if="record.status === 0" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openDetail(record)">详情</a>
<a-divider type="vertical" />
<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>
<!-- 编辑弹窗 -->
<YxeChronicDiseaseCollectionEdit
v-model:visible="showEdit"
:data="current"
@done="reload"
/>
<YxeChronicDiseaseCollectionDetail
v-model:visible="showDetail"
:data="detailCurrent"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import YxeChronicDiseaseCollectionEdit from './components/yxeChronicDiseaseCollectionEdit.vue';
import YxeChronicDiseaseCollectionDetail from './components/yxeChronicDiseaseCollectionDetail.vue';
import {
pageYxeChronicDiseaseCollection,
removeYxeChronicDiseaseCollection,
removeBatchYxeChronicDiseaseCollection,
exportYxeChronicDiseaseCollection
} from '@/api/yxe/yxeChronicDiseaseCollection';
import type {
YxeChronicDiseaseCollection,
YxeChronicDiseaseCollectionParam
} from '@/api/yxe/yxeChronicDiseaseCollection/model';
import { openExportFile } from '@/utils/export-file';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<YxeChronicDiseaseCollection[]>([]);
// 当前编辑数据
const current = ref<YxeChronicDiseaseCollection | null>(null);
// 当前详情数据
const detailCurrent = ref<YxeChronicDiseaseCollection | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示详情弹窗
const showDetail = 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 pageYxeChronicDiseaseCollection({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '姓名',
dataIndex: 'name',
key: 'name',
align: 'center'
},
{
title: '性别',
dataIndex: 'gender',
key: 'gender',
align: 'center'
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone',
align: 'center'
},
{
title: '连锁',
dataIndex: 'chainName',
key: 'chainName',
align: 'center'
},
{
title: '门店',
dataIndex: 'merchantName',
key: 'merchantName',
align: 'center'
},
{
title: '操作',
key: 'action',
align: 'center',
width: 180,
fixed: 'right'
}
]);
/* 搜索 */
const reload = (where?: YxeChronicDiseaseCollectionParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: YxeChronicDiseaseCollection) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开详情弹窗 */
const openDetail = (row: YxeChronicDiseaseCollection) => {
detailCurrent.value = row;
showDetail.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: YxeChronicDiseaseCollection) => {
const hide = message.loading('请求中..', 0);
removeYxeChronicDiseaseCollection(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);
removeBatchYxeChronicDiseaseCollection(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
const exportData = () => {
const hide = message.loading('导出中..', 0);
tableRef.value?.doRequest(({ where, orders, filters }) => {
exportYxeChronicDiseaseCollection({
...(where || {}),
...(orders || {}),
...(filters || {})
})
.then((file) => {
hide();
openExportFile(file);
message.success('导出成功');
})
.catch((e) => {
hide();
message.error(e.message);
});
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: YxeChronicDiseaseCollection) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openDetail(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'YxeChronicDiseaseCollection'
};
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,119 @@
<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-button @click="onImport">批量导入</a-button>
<a-button danger :disabled="!selection?.length" @click="remove">
批量删除
</a-button>
<a-select
v-model:value="where.chain"
show-search
allow-clear
optionFilterProp="label"
:options="chainOptions"
:loading="chainLoading"
placeholder="请选择连锁"
style="width: 180px"
/>
<a-input
v-model:value="where.productName"
allow-clear
placeholder="请输入产品名"
@pressEnter="reload"
/>
<a-button type="primary" @click="reload">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import { message, type SelectProps } from 'ant-design-vue';
import { onMounted, ref } from 'vue';
import useSearch from '@/utils/use-search';
import type { YxeChronicDiseaseProductParam } from '@/api/yxe/yxeChronicDiseaseProduct/model';
import { listDictData } from '@/api/system/dict-data';
import type { DictData } from '@/api/system/dict-data/model';
defineProps<{
selection?: [];
}>();
const emit = defineEmits<{
(e: 'search', where?: YxeChronicDiseaseProductParam): void;
(e: 'add'): void;
(e: 'import'): void;
(e: 'remove'): void;
}>();
const { where, resetFields } = useSearch<YxeChronicDiseaseProductParam>({
chain: '',
productName: ''
});
const chainOptions = ref<SelectProps['options']>([]);
const chainLoading = ref(false);
type ChainOption = {
value: string;
label: string;
};
const normalizeText = (value?: unknown) => String(value ?? '').trim();
const buildChainOption = (item: DictData): ChainOption | null => {
const code = normalizeText(item.dictDataCode);
const name = normalizeText(item.dictDataName);
const value = code || name;
if (!value) {
return null;
}
return {
value,
label: name || code
};
};
const loadChainOptions = async () => {
chainLoading.value = true;
try {
const list = await listDictData({ dictId: 175 });
chainOptions.value = (list || [])
.map((item: DictData) => buildChainOption(item))
.filter((item): item is ChainOption => !!item);
} catch (e) {
message.error((e as Error).message);
} finally {
chainLoading.value = false;
}
};
const reload = () => {
emit('search', where);
};
const reset = () => {
resetFields();
reload();
};
const add = () => {
emit('add');
};
const onImport = () => {
emit('import');
};
const remove = () => {
emit('remove');
};
onMounted(() => {
loadChainOptions();
});
</script>
@@ -0,0 +1,302 @@
<template>
<ele-modal
:width="720"
:visible="visible"
:confirm-loading="loading"
:maskClosable="false"
:title="isUpdate ? '编辑慢病产品' : '添加慢病产品'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 7, sm: 6, xs: 24 } : { flex: '160px' }"
:wrapper-col="
styleResponsive ? { md: 16, sm: 18, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="连锁" name="chain">
<a-select
v-model:value="form.chain"
show-search
allow-clear
optionFilterProp="label"
:options="chainOptions"
:loading="chainLoading"
placeholder="请选择连锁"
/>
</a-form-item>
<a-form-item label="产品名" name="productName">
<a-input
v-model:value="form.productName"
allow-clear
placeholder="请输入产品名"
/>
</a-form-item>
<a-form-item label="规格粒/片" name="specCount">
<a-input-number
v-model:value="form.specCount"
:min="0"
:precision="2"
:step="1"
class="ele-fluid"
placeholder="请输入规格粒/片"
/>
</a-form-item>
<a-form-item label="说明书每天使用/次" name="dailyUsageCount">
<a-input-number
v-model:value="form.dailyUsageCount"
:min="0"
:precision="2"
:step="1"
class="ele-fluid"
placeholder="请输入说明书每天使用次数"
/>
</a-form-item>
<a-form-item label="每次几粒/片" name="dosagePerTime">
<a-input-number
v-model:value="form.dosagePerTime"
:min="0"
:precision="2"
:step="0.5"
class="ele-fluid"
placeholder="请输入每次使用粒/片数"
/>
</a-form-item>
<a-form-item label="1盒服用几天">
<a-input
:value="boxUsageDaysText"
readonly
placeholder="根据规格和用量自动计算"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import { Form, message, type SelectProps } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import type { FormInstance } from 'ant-design-vue/es/form';
import { assignObject } from 'ele-admin-pro';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import {
addYxeChronicDiseaseProduct,
updateYxeChronicDiseaseProduct
} from '@/api/yxe/yxeChronicDiseaseProduct';
import type { YxeChronicDiseaseProduct } from '@/api/yxe/yxeChronicDiseaseProduct/model';
import { listDictData } from '@/api/system/dict-data';
import type { DictData } from '@/api/system/dict-data/model';
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const useForm = Form.useForm;
const formRef = ref<FormInstance | null>(null);
const isUpdate = ref(false);
const loading = ref(false);
const chainOptions = ref<SelectProps['options']>([]);
const chainLoading = ref(false);
const props = defineProps<{
visible: boolean;
data?: YxeChronicDiseaseProduct | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const form = reactive<YxeChronicDiseaseProduct>({
id: undefined,
chain: undefined,
productName: undefined,
specCount: undefined,
dailyUsageCount: undefined,
dosagePerTime: undefined,
boxUsageDays: undefined,
status: 1,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
const rules = reactive<Record<string, Rule[]>>({
productName: [
{ required: true, type: 'string', message: '请输入产品名', trigger: 'blur' }
],
chain: [
{ required: true, type: 'string', message: '请输入连锁', trigger: 'blur' }
],
specCount: [
{ required: true, type: 'number', message: '请输入规格粒/片', trigger: 'blur' }
],
dailyUsageCount: [
{
required: true,
type: 'number',
message: '请输入说明书每天使用/次',
trigger: 'blur'
}
],
dosagePerTime: [
{
required: true,
type: 'number',
message: '请输入每次几粒/片',
trigger: 'blur'
}
]
});
const { resetFields } = useForm(form, rules);
type ChainOption = {
value: string;
label: string;
code?: string;
name?: string;
};
const normalizeText = (value?: unknown) => String(value ?? '').trim();
const buildChainOption = (item: DictData): ChainOption | null => {
const code = normalizeText(item.dictDataCode);
const name = normalizeText(item.dictDataName);
const value = code || name;
if (!value) {
return null;
}
return {
value,
label: name || code,
code: code || undefined,
name: name || undefined
};
};
const ensureCurrentChainOption = (value?: string) => {
const target = normalizeText(value);
if (!target) {
return;
}
const exists = (chainOptions.value || []).some((item) => {
return [item?.value, item?.label]
.map((optionValue) => normalizeText(optionValue))
.filter(Boolean)
.includes(target);
});
if (exists) {
return;
}
chainOptions.value = [
...(chainOptions.value || []),
{
value: target,
label: target
}
];
};
const loadChainOptions = async () => {
chainLoading.value = true;
try {
const list = await listDictData({ dictId: 175 });
chainOptions.value = (list || [])
.map((item: DictData) => buildChainOption(item))
.filter((item): item is ChainOption => !!item);
ensureCurrentChainOption(form.chain);
} catch (e) {
message.error((e as Error).message);
} finally {
chainLoading.value = false;
}
};
const calculateBoxUsageDays = () => {
const specCount = Number(form.specCount);
const dailyUsageCount = Number(form.dailyUsageCount);
const dosagePerTime = Number(form.dosagePerTime);
const denominator = dailyUsageCount * dosagePerTime;
if (
!Number.isFinite(specCount) ||
!Number.isFinite(dailyUsageCount) ||
!Number.isFinite(dosagePerTime) ||
specCount <= 0 ||
denominator <= 0
) {
form.boxUsageDays = undefined;
return;
}
form.boxUsageDays = Number((specCount / denominator).toFixed(2));
};
const boxUsageDaysText = computed(() => {
return form.boxUsageDays != null ? `${form.boxUsageDays} 天` : '';
});
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const save = () => {
if (!formRef.value) {
return;
}
calculateBoxUsageDays();
formRef.value
.validate()
.then(() => {
loading.value = true;
const request = isUpdate.value
? updateYxeChronicDiseaseProduct
: addYxeChronicDiseaseProduct;
request({ ...form })
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => [form.specCount, form.dailyUsageCount, form.dosagePerTime],
() => {
calculateBoxUsageDays();
}
);
watch(
() => props.visible,
(visible) => {
if (visible) {
loadChainOptions();
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
ensureCurrentChainOption(props.data.chain);
} else {
isUpdate.value = false;
form.status = 1;
}
calculateBoxUsageDays();
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
@@ -0,0 +1,252 @@
<template>
<ele-modal
:width="560"
:footer="null"
title="批量导入慢病产品"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<a-upload-dragger
accept=".xls,.xlsx"
:show-upload-list="false"
:customRequest="doImport"
style="padding: 24px 0; margin-bottom: 12px"
>
<p class="ant-upload-drag-icon">
<cloud-upload-outlined />
</p>
<p class="ant-upload-hint">将 Excel 文件拖到此处,或点击上传</p>
</a-upload-dragger>
</a-spin>
<div class="ele-text-center">
<a href="" @click.prevent="downloadTemplate">模板下载</a>
</div>
<div class="ele-text-secondary ele-text-center" style="margin-top: 4px">
导入字段顺序:连锁、产品名、规格粒/片、说明书每天使用/次、每次几粒/片
</div>
<div v-if="progress.total" class="ele-text-center" style="margin-top: 8px">
已处理 {{ progress.success + progress.fail }}/{{ progress.total }}, 成功
{{ progress.success }},失败 {{ progress.fail }}
</div>
<div
v-if="errorMessages.length"
class="ele-text-danger"
style="margin-top: 8px"
>
<div>失败明细(最多展示 5 条):</div>
<div v-for="(item, index) in errorMessages.slice(0, 5)" :key="index">
{{ item }}
</div>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import * as XLSX from 'xlsx';
import {
addYxeChronicDiseaseProduct,
downloadTemplateYxeChronicDiseaseProduct
} from '@/api/yxe/yxeChronicDiseaseProduct';
import type { YxeChronicDiseaseProduct } from '@/api/yxe/yxeChronicDiseaseProduct/model';
import { openExportFile } from '@/utils/export-file';
type ImportRow = {
rowNumber: number;
chain: string;
productName: string;
specCount: string;
dailyUsageCount: string;
dosagePerTime: string;
};
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
visible: boolean;
}>();
const loading = ref(false);
const progress = ref({ total: 0, success: 0, fail: 0 });
const errorMessages = ref<string[]>([]);
const normalize = (value?: unknown) => String(value ?? '').trim();
const normalizeKey = (value?: unknown) => normalize(value).toLowerCase();
const readRows = async (file: File): Promise<ImportRow[]> => {
const data = await file.arrayBuffer();
const workbook = XLSX.read(data, { type: 'array', cellDates: true });
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
const rows = XLSX.utils.sheet_to_json<any[]>(sheet, {
header: 1,
defval: '',
raw: false
}) as Array<unknown[]>;
if (!rows.length) {
return [];
}
const header = rows[0].map((item) => normalizeKey(item));
const headerIndex = (aliases: string[]) => {
const aliasSet = aliases.map((item) => item.toLowerCase());
return header.findIndex((item) => aliasSet.includes(item));
};
const chainIndex = headerIndex(['连锁', '所属连锁', 'chain']);
const productNameIndex = headerIndex(['产品名', '品名', '产品名称', 'productname']);
const specCountIndex = headerIndex(['规格粒/片', '规格', 'speccount']);
const dailyUsageCountIndex = headerIndex([
'说明书每天使用/次',
'每天使用/次',
'dailyusagecount'
]);
const dosagePerTimeIndex = headerIndex([
'每次几粒/片',
'每次用量',
'dosagepertime'
]);
const hasHeader =
chainIndex >= 0 ||
productNameIndex >= 0 ||
specCountIndex >= 0 ||
dailyUsageCountIndex >= 0 ||
dosagePerTimeIndex >= 0;
const getByIndex = (row: unknown[], index: number, fallback: number) => {
const value = index >= 0 ? row[index] : row[fallback];
return normalize(value);
};
return (hasHeader ? rows.slice(1) : rows)
.map((row, index) => ({
rowNumber: hasHeader ? index + 2 : index + 1,
chain: getByIndex(row, chainIndex, 0),
productName: getByIndex(row, productNameIndex, 1),
specCount: getByIndex(row, specCountIndex, 2),
dailyUsageCount: getByIndex(row, dailyUsageCountIndex, 3),
dosagePerTime: getByIndex(row, dosagePerTimeIndex, 4)
}))
.filter(
(row) =>
row.chain ||
row.productName ||
row.specCount ||
row.dailyUsageCount ||
row.dosagePerTime
);
};
const parseNumber = (value: string, fieldName: string, rowNumber: number) => {
const num = Number(value);
if (!Number.isFinite(num) || num <= 0) {
throw new Error(`第${rowNumber}行:${fieldName}必须是大于0的数字`);
}
return num;
};
const buildProductData = (row: ImportRow): YxeChronicDiseaseProduct => {
if (!row.chain) {
throw new Error(`第${row.rowNumber}行:连锁不能为空`);
}
if (!row.productName) {
throw new Error(`第${row.rowNumber}行:产品名不能为空`);
}
return {
chain: row.chain,
productName: row.productName,
specCount: parseNumber(row.specCount, '规格粒/片', row.rowNumber),
dailyUsageCount: parseNumber(
row.dailyUsageCount,
'说明书每天使用/次',
row.rowNumber
),
dosagePerTime: parseNumber(row.dosagePerTime, '每次几粒/片', row.rowNumber),
status: 1
};
};
const doImport = async ({ 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;
progress.value = { total: 0, success: 0, fail: 0 };
errorMessages.value = [];
try {
const rows = await readRows(file as File);
if (!rows.length) {
message.warning('未读取到可导入的数据');
return false;
}
progress.value.total = rows.length;
for (const row of rows) {
try {
await addYxeChronicDiseaseProduct(buildProductData(row));
progress.value.success += 1;
} catch (e) {
progress.value.fail += 1;
const err = e as Error;
errorMessages.value.push(err.message || `第${row.rowNumber}行:导入失败`);
}
}
if (progress.value.fail > 0) {
message.warning(
`导入完成:成功${progress.value.success}条,失败${progress.value.fail}条`
);
} else {
message.success(`导入完成:成功${progress.value.success}条`);
updateVisible(false);
}
if (progress.value.success > 0) {
emit('done');
}
} catch (e) {
const err = e as Error;
message.error(err.message || '导入失败');
} finally {
loading.value = false;
}
return false;
};
const downloadTemplate = async () => {
try {
const file = await downloadTemplateYxeChronicDiseaseProduct();
openExportFile(file);
} catch (e) {
const err = e as Error;
message.error(err.message || '模板下载失败');
}
};
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>
@@ -0,0 +1,218 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
:selection="selection"
@search="reload"
@add="openEdit"
@import="openImport"
@remove="removeBatch"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'boxUsageDays'">
{{ record.boxUsageDays ?? '-' }}
</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>
<YxeChronicDiseaseProductEdit
v-model:visible="showEdit"
:data="current"
@done="reload"
/>
<YxeChronicDiseaseProductImport
v-model:visible="showImport"
@done="reload"
/>
</div>
</div>
</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 {
ColumnItem,
DatasourceFunction
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import YxeChronicDiseaseProductEdit from './components/yxeChronicDiseaseProductEdit.vue';
import YxeChronicDiseaseProductImport from './components/yxeChronicDiseaseProductImport.vue';
import {
pageYxeChronicDiseaseProduct,
removeBatchYxeChronicDiseaseProduct,
removeYxeChronicDiseaseProduct
} from '@/api/yxe/yxeChronicDiseaseProduct';
import type {
YxeChronicDiseaseProduct,
YxeChronicDiseaseProductParam
} from '@/api/yxe/yxeChronicDiseaseProduct/model';
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
const selection = ref<YxeChronicDiseaseProduct[]>([]);
const current = ref<YxeChronicDiseaseProduct | null>(null);
const showEdit = ref(false);
const showImport = ref(false);
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
return pageYxeChronicDiseaseProduct({
...where,
...orders,
page,
limit
});
};
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 90,
align: 'center'
},
{
title: '连锁',
dataIndex: 'chain',
key: 'chain',
align: 'center'
},
{
title: '产品名',
dataIndex: 'productName',
key: 'productName',
align: 'center'
},
{
title: '规格粒/片',
dataIndex: 'specCount',
key: 'specCount',
align: 'center'
},
{
title: '说明书每天使用/次',
dataIndex: 'dailyUsageCount',
key: 'dailyUsageCount',
align: 'center'
},
{
title: '每次几粒/片',
dataIndex: 'dosagePerTime',
key: 'dosagePerTime',
align: 'center'
},
{
title: '1盒服用几天',
dataIndex: 'boxUsageDays',
key: 'boxUsageDays',
align: 'center'
},
{
title: '添加时间',
dataIndex: 'createTime',
key: 'createTime',
sorter: true,
align: 'center',
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
const reload = (where?: YxeChronicDiseaseProductParam) => {
selection.value = [];
tableRef.value?.reload({ where });
};
const openEdit = (row?: YxeChronicDiseaseProduct) => {
current.value = row ?? null;
showEdit.value = true;
};
const openImport = () => {
showImport.value = true;
};
const remove = (row: YxeChronicDiseaseProduct) => {
const hide = message.loading('请求中..', 0);
removeYxeChronicDiseaseProduct(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);
removeBatchYxeChronicDiseaseProduct(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
const customRow = (record: YxeChronicDiseaseProduct) => {
return {
onDblclick: () => {
openEdit(record);
}
};
};
</script>
@@ -0,0 +1,119 @@
<!-- 搜索表单 -->
<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-button type="primary" @click="openImport">
<template #icon>
<UploadOutlined />
</template>
<span>导入</span>
</a-button>
<a-upload
accept=".zip"
:show-upload-list="false"
:before-upload="beforeUploadCoverZip"
>
<a-button type="primary">
<template #icon>
<UploadOutlined />
</template>
<span>上传封面压缩包</span>
</a-button>
</a-upload>
<a-popconfirm
title="确定要删除选中的记录吗?"
:disabled="!props.selection?.length"
@confirm="removeBatch"
>
<a-button
danger
type="primary"
class="ele-btn-icon"
:disabled="!props.selection?.length"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
</a-popconfirm>
<a-input-search
allow-clear
v-model:value="where.keywords"
placeholder="请输入编号/品名/厂家关键词"
style="width: 260px"
@search="reload"
@pressEnter="reload"
/>
<a-button @click="reset">重置</a-button>
</a-space>
</template>
<script lang="ts" setup>
import {
DeleteOutlined,
PlusOutlined,
UploadOutlined
} from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import type { YxeGoodsParam } from '@/api/yxe/yxeGoods/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: YxeGoodsParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
(e: 'import'): void;
(e: 'coverZip', file: File): void;
}>();
const { where, resetFields } = useSearch<YxeGoodsParam>({
keywords: ''
});
// 新增
const add = () => {
emit('add');
};
const removeBatch = () => {
emit('remove');
};
const openImport = () => {
emit('import');
};
const reload = () => {
emit('search', { ...where });
};
const beforeUploadCoverZip = (file: File) => {
emit('coverZip', file);
return false;
};
const reset = () => {
resetFields();
reload();
};
watch(
() => props.selection,
() => {}
);
</script>
@@ -0,0 +1,295 @@
<!-- 编辑弹窗 -->
<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">
<a-form-item label="分类" name="cate">
<a-input
allow-clear
placeholder="请输入分类"
v-model:value="form.cate"
/>
</a-form-item>
<a-form-item label="所属连锁" name="chain">
<a-select
show-search
optionFilterProp="label"
allow-clear
:loading="chainLoading"
:options="chainOptions"
placeholder="请选择所属连锁"
v-model:value="form.chain"
@change="onChangeChain"
/>
</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="title">
<a-input
allow-clear
placeholder="请输入品名"
v-model:value="form.title"
/>
</a-form-item>
<a-form-item label="规格" name="spec">
<a-input
allow-clear
placeholder="请输入规格"
v-model:value="form.spec"
/>
</a-form-item>
<a-form-item label="厂家" name="maker">
<a-input
allow-clear
placeholder="请输入厂家"
v-model:value="form.maker"
/>
</a-form-item>
<a-form-item label="厂家简称" name="makerAbb">
<a-input
allow-clear
placeholder="请输入厂家简称"
v-model:value="form.makerAbb"
/>
</a-form-item>
<a-form-item label="图片" name="cover">
<a-space>
<a-image
v-if="form.cover"
:src="form.cover"
style="width: 100px; height: 100px; object-fit: cover"
/>
<UploadFile v-model:value="form.cover" />
</a-space>
</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, type SelectProps } from "ant-design-vue";
import { assignObject, uuid } from "ele-admin-pro";
import { addYxeGoods, updateYxeGoods } from "@/api/yxe/yxeGoods";
import { YxeGoods } from "@/api/yxe/yxeGoods/model";
import { useThemeStore } from "@/store/modules/theme";
import { storeToRefs } from "pinia";
import { FormInstance } from "ant-design-vue/es/form";
import { FileRecord } from "@/api/system/file/model";
import type { DictData } from "@/api/system/dict-data/model";
import { listDictData } from "@/api/system/dict-data";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: YxeGoods | 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 chainOptions = ref<SelectProps["options"]>([]);
const chainLoading = ref(false);
// 用户信息
const form = reactive<YxeGoods>({
id: undefined,
cate: undefined,
chain: undefined,
code: undefined,
title: undefined,
spec: undefined,
maker: undefined,
makerAbb: undefined,
cover: undefined,
sortNumber: undefined,
status: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
startTime: undefined,
endTime: undefined,
extra_income: undefined,
extraIncome: undefined,
extraStartTime: undefined,
extraEndTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit("update:visible", value);
};
// 表单验证规则
const rules = reactive({});
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form,
extra_income: form.extra_income || form.extraIncome,
extraIncome: form.extra_income || form.extraIncome
};
const saveOrUpdate = isUpdate.value ? updateYxeGoods : addYxeGoods;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit("done");
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {
});
};
type ChainOption = {
label: string;
value: string;
code?: string;
name?: string;
};
const normalizeText = (value?: unknown) => String(value ?? '').trim();
const buildChainOption = (item: DictData): ChainOption | null => {
const code = normalizeText(item.dictDataCode);
const name = normalizeText(item.dictDataName);
const value = code || name;
if (!value) {
return null;
}
return {
value,
label: name || code,
code: code || undefined,
name: name || undefined
};
};
const ensureCurrentChainOption = (value?: string) => {
const target = normalizeText(value);
if (!target) {
return;
}
const exists = (chainOptions.value || []).some((item) => {
const option = item as ChainOption;
return [option.value, option.code, option.name]
.map((optionValue) => normalizeText(optionValue))
.filter(Boolean)
.includes(target);
});
if (exists) {
return;
}
chainOptions.value = [
...(chainOptions.value || []),
{
value: target,
label: target,
code: target,
name: target
}
];
};
const resolveChainValue = (value?: string) => {
const target = normalizeText(value);
if (!target) {
return undefined;
}
const matched = (chainOptions.value || []).find((item) => {
const option = item as ChainOption;
return [option.value, option.code, option.name]
.map((optionValue) => normalizeText(optionValue))
.filter(Boolean)
.includes(target);
}) as ChainOption | undefined;
return matched?.value ?? target;
};
const loadChainOptions = async () => {
chainLoading.value = true;
try {
const list = await listDictData({ dictId: 175 });
chainOptions.value = (list || [])
.map((item: DictData) => buildChainOption(item))
.filter((item): item is ChainOption => !!item);
ensureCurrentChainOption(form.chain);
form.chain = resolveChainValue(form.chain);
} catch (e) {
message.error((e as Error).message);
} finally {
chainLoading.value = false;
}
};
watch(
() => props.visible,
async (visible) => {
if (visible) {
await loadChainOptions();
if (props.data) {
assignObject(form, props.data);
form.extra_income = form.extra_income || form.extraIncome;
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
@@ -0,0 +1,249 @@
<!-- 商品导入弹窗 -->
<template>
<ele-modal
:width="560"
:footer="null"
title="批量导入商品"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<a-upload-dragger
accept=".xls,.xlsx"
:show-upload-list="false"
:customRequest="doImport"
style="padding: 24px 0; margin-bottom: 12px"
>
<p class="ant-upload-drag-icon">
<cloud-upload-outlined />
</p>
<p class="ant-upload-hint">将 Excel 文件拖到此处,或点击上传</p>
</a-upload-dragger>
</a-spin>
<div class="ele-text-center">
<a href="" @click.prevent="downloadTemplate">模板下载</a>
</div>
<div class="ele-text-secondary ele-text-center" style="margin-top: 4px">
导入字段顺序:厂家简称、产品编码、分类、连锁、品名、规格、厂家、奖励金额
</div>
<div v-if="progress.total" class="ele-text-center" style="margin-top: 8px">
已处理 {{ progress.success + progress.fail }}/{{ progress.total }}, 成功
{{ progress.success }},失败 {{ progress.fail }}
</div>
<div
v-if="errorMessages.length"
class="ele-text-danger"
style="margin-top: 8px"
>
<div>失败明细(最多展示 5 条):</div>
<div v-for="(item, index) in errorMessages.slice(0, 5)" :key="index">
{{ item }}
</div>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import * as XLSX from 'xlsx';
import { addYxeGoods } from '@/api/yxe/yxeGoods';
import type { YxeGoods } from '@/api/yxe/yxeGoods/model';
type ImportRow = {
rowNumber: number;
makerAbb: string;
code: string;
cate: string;
chain: string;
title: string;
spec: string;
maker: string;
extra_income: string;
};
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
visible: boolean;
}>();
const loading = ref(false);
const progress = ref({ total: 0, success: 0, fail: 0 });
const errorMessages = ref<string[]>([]);
const normalize = (value?: unknown) => String(value ?? '').trim();
const normalizeKey = (value?: unknown) => normalize(value).toLowerCase();
const readRows = async (file: File): Promise<ImportRow[]> => {
const data = await file.arrayBuffer();
const workbook = XLSX.read(data, { type: 'array', cellDates: true });
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
const rows = XLSX.utils.sheet_to_json<any[]>(sheet, {
header: 1,
defval: '',
raw: false
}) as Array<unknown[]>;
if (!rows.length) {
return [];
}
const header = rows[0].map((item) => normalizeKey(item));
const headerIndex = (aliases: string[]) => {
const aliasSet = aliases.map((item) => item.toLowerCase());
return header.findIndex((item) => aliasSet.includes(item));
};
const makerAbbIndex = headerIndex(['厂家简称', '简称', 'makerabb']);
const codeIndex = headerIndex([
'产品编码',
'商品编码',
'商品编号',
'编号',
'code'
]);
const cateIndex = headerIndex(['分类', '分类名称', 'cate', 'category']);
const chainIndex = headerIndex(['连锁', '所属连锁', 'chain']);
const titleIndex = headerIndex(['品名', '商品名称', '名称', 'title']);
const specIndex = headerIndex(['规格', 'spec']);
const makerIndex = headerIndex(['厂家', '生产厂家', 'maker']);
const extraIncomeIndex = headerIndex([
'奖励金额',
'额外奖励',
'extra_income',
'extraincome'
]);
const hasHeader =
makerAbbIndex >= 0 ||
codeIndex >= 0 ||
cateIndex >= 0 ||
chainIndex >= 0 ||
titleIndex >= 0 ||
specIndex >= 0 ||
makerIndex >= 0 ||
extraIncomeIndex >= 0;
const getByIndex = (row: unknown[], index: number, fallback: number) => {
const value = index >= 0 ? row[index] : row[fallback];
return normalize(value);
};
return (hasHeader ? rows.slice(1) : rows)
.map((row, index) => ({
rowNumber: hasHeader ? index + 2 : index + 1,
makerAbb: getByIndex(row, makerAbbIndex, 0),
code: getByIndex(row, codeIndex, 1),
cate: getByIndex(row, cateIndex, 2),
chain: getByIndex(row, chainIndex, 3),
title: getByIndex(row, titleIndex, 4),
spec: getByIndex(row, specIndex, 5),
maker: getByIndex(row, makerIndex, 6),
extra_income: getByIndex(row, extraIncomeIndex, 7)
}))
.filter(
(row) =>
row.makerAbb ||
row.code ||
row.cate ||
row.chain ||
row.title ||
row.spec ||
row.maker ||
row.extra_income
);
};
const doImport = async ({ 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;
progress.value = { total: 0, success: 0, fail: 0 };
errorMessages.value = [];
try {
const rows = await readRows(file as File);
if (!rows.length) {
message.warning('未读取到可导入的数据');
return false;
}
progress.value.total = rows.length;
for (const row of rows) {
try {
const data: YxeGoods = {
makerAbb: row.makerAbb,
code: row.code,
cate: row.cate,
chain: row.chain,
title: row.title,
spec: row.spec,
maker: row.maker,
extra_income: row.extra_income || undefined,
extraIncome: row.extra_income || undefined
};
await addYxeGoods(data);
progress.value.success += 1;
} catch (e) {
progress.value.fail += 1;
const err = e as Error;
errorMessages.value.push(
`第${row.rowNumber}行:${err.message || '导入失败'}`
);
}
}
if (progress.value.fail > 0) {
message.warning(
`导入完成:成功${progress.value.success}条,失败${progress.value.fail}条`
);
} else {
message.success(`导入完成:成功${progress.value.success}条`);
updateVisible(false);
}
if (progress.value.success > 0) {
emit('done');
}
} catch (e) {
const err = e as Error;
message.error(err.message || '导入失败');
} finally {
loading.value = false;
}
return false;
};
const downloadTemplate = () => {
const workbook = XLSX.utils.book_new();
const sheet = XLSX.utils.aoa_to_sheet([
['厂家简称', '产品编码', '分类', '连锁', '品名', '规格', '厂家', '奖励金额']
]);
XLSX.utils.book_append_sheet(workbook, sheet, '商品导入模板');
XLSX.writeFile(workbook, '药销儿商品导入模板.xlsx');
};
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>
+303
View File
@@ -0,0 +1,303 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
: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"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
@import="openImport"
@coverZip="handleCoverZipUpload"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'cover'">
<a-image
:src="record.cover"
v-if="record.cover"
style="width: 50px; height: 50px; object-fit: cover"
/>
</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>
<!-- 编辑弹窗 -->
<YxeGoodsEdit v-model:visible="showEdit" :data="current" @done="reload" />
<YxeGoodsImport v-model:visible="showImport" @done="reload" />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-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 YxeGoodsEdit from './components/yxeGoodsEdit.vue';
import YxeGoodsImport from './components/yxeGoodsImport.vue';
import {
pageYxeGoods,
removeYxeGoods,
removeBatchYxeGoods,
uploadYxeGoodsCoverZip
} from '@/api/yxe/yxeGoods';
import type { YxeGoods, YxeGoodsParam } from '@/api/yxe/yxeGoods/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<YxeGoods[]>([]);
// 当前编辑数据
const current = ref<YxeGoods | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示导入弹窗
const showImport = 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 pageYxeGoods({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90
},
{
title: '分类',
dataIndex: 'cate',
key: 'cate',
align: 'center'
},
{
title: '连锁',
dataIndex: 'chain',
align: 'center'
},
{
title: '编号',
dataIndex: 'code',
key: 'code',
align: 'center'
},
{
title: '品名',
dataIndex: 'title',
key: 'title',
align: 'center'
},
{
title: '规格',
dataIndex: 'spec',
key: 'spec',
align: 'center'
},
{
title: '厂家',
dataIndex: 'maker',
key: 'maker',
align: 'center'
},
{
title: '厂家简称',
dataIndex: 'makerAbb',
key: 'makerAbb',
align: 'center'
},
{
title: '封面',
dataIndex: 'cover',
key: 'cover',
align: 'center'
},
{
title: '添加时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: YxeGoodsParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: YxeGoods) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开导入弹窗 */
const openImport = () => {
showImport.value = true;
};
const handleCoverZipUpload = (file: File) => {
const fileName = file.name?.toLowerCase?.() ?? '';
if (!fileName.endsWith('.zip')) {
message.error('只能选择 zip 压缩包');
return false;
}
const hide = message.loading('封面压缩包处理中..', 0);
uploadYxeGoodsCoverZip(file)
.then((result) => {
hide();
message.success(
`封面压缩包处理完成,识别${result.totalFiles ?? 0}个文件,更新${
result.updatedCount ?? 0
}条记录,跳过${result.skippedCount ?? 0}个文件`
);
if (result.notFoundTitles?.length) {
message.warning(
`有 ${result.notFoundTitles.length} 个商品标题未匹配`
);
}
reload();
})
.catch((e) => {
hide();
message.error(e.message || '封面压缩包处理失败');
});
return false;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: YxeGoods) => {
const hide = message.loading('请求中..', 0);
removeYxeGoods(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;
}
const hide = message.loading('请求中..', 0);
removeBatchYxeGoods(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: YxeGoods) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'YxeGoods'
};
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,158 @@
<!-- 搜索表单 -->
<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-button type="primary" @click="openImport">
<template #icon>
<UploadOutlined />
</template>
<span>导入</span>
</a-button>
<a-upload
accept=".zip"
:show-upload-list="false"
:before-upload="beforeUploadCoverZip"
>
<a-button type="primary">
<template #icon>
<UploadOutlined />
</template>
<span>上传封面压缩包</span>
</a-button>
</a-upload>
<a-input-search
v-model:value="where.keywords"
allow-clear
placeholder="请输入名称"
@pressEnter="reload"
@search="reload"
/>
<a-input-search
v-model:value="where.chain"
allow-clear
placeholder="请输入连锁"
@pressEnter="reload"
@search="reload"
/>
<!-- <a-select-->
<!-- v-model:value="where.merchantId"-->
<!-- allow-clear-->
<!-- show-search-->
<!-- optionFilterProp="label"-->
<!-- :loading="merchantLoading"-->
<!-- :options="merchantOptions"-->
<!-- placeholder="请选择门店"-->
<!-- style="min-width: 300px"-->
<!-- @change="reload"-->
<!-- />-->
<a-input-search
v-model:value="where.cate"
allow-clear
placeholder="请输入分类"
@pressEnter="reload"
@search="reload"
/>
<a-button type="primary" @click="reload">查询</a-button>
<a-button @click="reset">重置</a-button>
<a-popconfirm title="确定要批量删除选择的记录吗?" @confirm="removeBatch">
<a-button type="primary" danger :disabled="!selection?.length">
<template #icon>
<DeleteOutlined />
</template>
批量删除
</a-button>
</a-popconfirm>
</a-space>
</template>
<script lang="ts" setup>
import {
DeleteOutlined,
PlusOutlined,
UploadOutlined
} from '@ant-design/icons-vue';
import { onMounted, ref } from 'vue';
import { message } from 'ant-design-vue';
import useSearch from '@/utils/use-search';
import { listMerchant } from '@/api/shop/merchant';
import type { Merchant } from '@/api/shop/merchant/model';
import type { SelectProps } from 'ant-design-vue';
import type { YxeProductParam } from '@/api/yxe/yxeProduct/model';
defineProps<{
// 选中的角色
selection?: [];
}>();
const emit = defineEmits<{
(e: 'search', where?: YxeProductParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
(e: 'import'): void;
(e: 'coverZip', file: File): void;
}>();
const { where, resetFields } = useSearch<YxeProductParam>({
keywords: '',
merchantId: undefined,
chain: undefined,
cate: undefined
});
const merchantOptions = ref<SelectProps['options']>([]);
const merchantLoading = ref(false);
// 新增
const add = () => {
emit('add');
};
const openImport = () => {
emit('import');
};
const beforeUploadCoverZip = (file: File) => {
emit('coverZip', file);
return false;
};
const removeBatch = () => {
emit('remove');
};
const reload = () => {
emit('search', where);
};
const reset = () => {
resetFields();
reload();
};
const loadMerchantOptions = () => {
merchantLoading.value = true;
listMerchant()
.then((list) => {
merchantOptions.value = (list || []).map((item: Merchant) => ({
value: item.id,
label: item.name ?? `${item.id ?? ''}`
}));
})
.catch((e) => {
message.error(e.message);
})
.finally(() => {
merchantLoading.value = false;
});
};
onMounted(() => {
loadMerchantOptions();
});
</script>
@@ -0,0 +1,725 @@
<!-- 编辑弹窗 -->
<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">
<a-form-item label="所属连锁" name="chain">
<a-select
show-search
optionFilterProp="label"
allow-clear
:loading="chainLoading"
:options="chainOptions"
placeholder="请选择所属连锁"
v-model:value="form.chain"
@change="onChangeChain"
/>
</a-form-item>
<!-- <a-form-item label="所属门店" name="merchantId">-->
<!-- <a-select-->
<!-- show-search-->
<!-- optionFilterProp="label"-->
<!-- allow-clear-->
<!-- :disabled="!form.chain"-->
<!-- :loading="merchantLoading"-->
<!-- :options="merchantOptions"-->
<!-- :placeholder="form.chain ? '请选择门店' : '请先选择所属连锁'"-->
<!-- v-model:value="form.merchantId"-->
<!-- @change="onChangeMerchant"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="商品">
<a-select
show-search
optionFilterProp="label"
allow-clear
:loading="goodsLoading"
:options="goodsOptions"
placeholder="请选择商品"
v-model:value="selectedGoodsId"
@change="onChangeGoods"
/>
</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="cover">
<a-space>
<a-image
v-if="form.cover"
:src="form.cover"
style="width: 100px; height: 100px; object-fit: cover"
/>
<UploadFile v-model:value="form.cover" />
</a-space>
</a-form-item>
<a-form-item label="品名" name="title">
<a-input
allow-clear
placeholder="请输入品名"
v-model:value="form.title"
/>
</a-form-item>
<a-form-item label="规格" name="spec">
<a-input
allow-clear
placeholder="请输入规格"
v-model:value="form.spec"
/>
</a-form-item>
<a-form-item label="分类" name="cate">
<a-input
allow-clear
placeholder="请输入分类"
v-model:value="form.cate"
/>
</a-form-item>
<a-form-item label="厂家" name="maker">
<a-input
allow-clear
placeholder="请输入厂家"
v-model:value="form.maker"
/>
</a-form-item>
<a-form-item label="厂家简称" name="makerAbb">
<a-input
allow-clear
placeholder="请输入厂家简称"
v-model:value="form.makerAbb"
/>
</a-form-item>
<a-form-item label="任务奖励" name="taskIncome" v-if="type === 0">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.taskIncome"
/>
</a-form-item>
<template v-if="type === 1">
<a-form-item label="晒单政策" name="income">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.income"
/>
</a-form-item>
<a-form-item label="分享有效期" name="startTime">
<a-range-picker
class="ele-fluid"
value-format="YYYY-MM-DD HH:mm"
:show-time="{ format: 'HH:mm' }"
:placeholder="['请选择开始时间', '请选择结束时间']"
v-model:value="shareTimeRange"
/>
</a-form-item>
<a-form-item label="额外奖励活动" name="extraIncomeList">
<a-space direction="vertical" class="ele-fluid" size="small">
<div
v-if="!extraIncomeRows.length"
class="ele-text-secondary extra-income-empty"
>
暂无额外奖励,请点击下方添加
</div>
<div
v-for="(item, index) in extraIncomeRows"
:key="item.rowId"
class="extra-income-row"
>
<a-input
allow-clear
placeholder="请输入额外奖励"
class="extra-income-input"
v-model:value="item.income"
/>
<a-range-picker
value-format="YYYY-MM-DD HH:mm:ss"
:show-time="{ format: 'HH:mm:ss' }"
:placeholder="['请选择开始时间', '请选择结束时间']"
class="extra-income-range"
:value="getExtraTimeRange(item)"
@update:value="(value) => updateExtraTimeRange(item, value)"
/>
<a-button
type="text"
danger
@click="removeExtraIncomeRow(index)"
:disabled="extraIncomeRows.length <= 1"
>
删除
</a-button>
</div>
<a-button type="dashed" block @click="addExtraIncomeRow()">
添加一行
</a-button>
</a-space>
</a-form-item>
</template>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref, reactive, watch } from 'vue';
import { Form, message, type SelectProps } from 'ant-design-vue';
import { assignObject, toDateString } from 'ele-admin-pro';
import { addYxeProduct, updateYxeProduct } from '@/api/yxe/yxeProduct';
import { YxeProduct } from '@/api/yxe/yxeProduct/model';
import { FormInstance } from 'ant-design-vue/es/form';
import { listMerchantMini } from '@/api/shop/merchant';
import type { Merchant } from '@/api/shop/merchant/model';
import { listYxeGoods } from '@/api/yxe/yxeGoods';
import type { YxeGoods } from '@/api/yxe/yxeGoods/model';
import { YxeProductExtraIncome } from '@/api/yxe/yxeProductExtraIncome/model';
import { listDictData } from '@/api/system/dict-data';
import type { DictData } from '@/api/system/dict-data/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
type: number;
// 修改回显的数据
data?: YxeProduct | 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 chainOptions = ref<SelectProps['options']>([]);
const chainLoading = ref(false);
const goodsOptions = ref<SelectProps['options']>([]);
const goodsLoading = ref(false);
const goodsList = ref<YxeGoods[]>([]);
const selectedGoodsId = ref<number>();
// 用户信息
const form = reactive<YxeProduct>({
id: undefined,
merchantId: undefined,
type: undefined,
chain: undefined,
code: undefined,
title: undefined,
cover: undefined,
spec: undefined,
maker: undefined,
cate: undefined,
makerAbb: undefined,
income: undefined,
taskIncome: undefined,
sortNumber: undefined,
status: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
startTime: undefined,
endTime: undefined,
extraIncome: undefined,
extraStartTime: undefined,
extraEndTime: undefined
});
const extraIncomeRows = ref<YxeProductExtraIncome[]>([]);
const createExtraIncomeRow = (
row?: Partial<YxeProductExtraIncome>
): YxeProductExtraIncome => ({
income: row?.income,
startTime: row?.startTime,
endTime: row?.endTime
});
const addExtraIncomeRow = (row?: Partial<YxeProductExtraIncome>) => {
extraIncomeRows.value.push(createExtraIncomeRow(row));
};
const removeExtraIncomeRow = (index: number) => {
extraIncomeRows.value.splice(index, 1);
if (!extraIncomeRows.value.length) {
addExtraIncomeRow();
}
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
taskIncome: [{ required: true, message: '请输入任务奖励' }]
});
const { resetFields } = useForm(form, rules);
const shareTimeRange = computed({
get: () => {
const start = formatMinuteValue(form.startTime);
const end = formatMinuteValue(form.endTime);
if (!start && !end) {
return undefined;
}
return start && end ? [start, end] : undefined;
},
set: (value?: string[]) => {
form.startTime = value?.[0];
form.endTime = value?.[1];
}
});
const formatMinuteValue = (value?: string) => {
if (!value) {
return undefined;
}
return toDateString(value, 'yyyy-MM-dd HH:mm');
};
const getExtraTimeRange = (row: YxeProductExtraIncome) => {
if (!row.startTime && !row.startTime) {
return undefined;
}
return [row.startTime, row.endTime];
};
const updateExtraTimeRange = (
row: YxeProductExtraIncome,
value?: string[]
) => {
row.startTime = value?.[0];
row.endTime = value?.[1];
};
const initExtraIncomeRows = (data?: YxeProduct | null) => {
const rows =
data?.extraIncomeList?.map((item) =>
createExtraIncomeRow({
income: item.income ?? item.income,
startTime: item.startTime ?? item.startTime,
endTime: item.endTime ?? item.endTime
})
) ?? [];
const filteredRows = rows.filter(
(item) => item.income || item.startTime || item.endTime
);
if (filteredRows.length) {
extraIncomeRows.value = filteredRows;
return;
}
if (data?.extraIncome || data?.extraStartTime || data?.extraEndTime) {
extraIncomeRows.value = [
createExtraIncomeRow({
income: data.income,
endTime: data.endTime,
startTime: data.startTime
})
];
return;
}
extraIncomeRows.value = [createExtraIncomeRow()];
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form,
type: props.type,
extraIncomeList: extraIncomeRows.value
.filter((item) => item.income || item.startTime || item.endTime)
.map((item) => ({
income: item.income,
startTime: item.startTime,
endTime: item.endTime
}))
};
delete formData.extraIncome;
delete formData.extraStartTime;
delete formData.extraEndTime;
const saveOrUpdate = isUpdate.value ? updateYxeProduct : addYxeProduct;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
const merchantOptions = ref<SelectProps['options']>([]);
const merchantLoading = ref(false);
const merchantList = ref<Merchant[]>([]);
const merchantLoaded = ref(false);
const merchantLoadedChain = ref('');
type ChainOption = {
label: string;
value: string;
code?: string;
name?: string;
};
type YxeProductEditData = YxeProduct & {
merchant?: Merchant | null;
startDate?: string;
endDate?: string;
extraStartDate?: string;
extraEndDate?: string;
};
const normalizeText = (value?: unknown) => String(value ?? '').trim();
const buildChainOption = (item: DictData): ChainOption | null => {
const code = normalizeText(item.dictDataCode);
const name = normalizeText(item.dictDataName);
const value = code || name;
if (!value) {
return null;
}
return {
value,
label: name || code,
code: code || undefined,
name: name || undefined
};
};
const ensureCurrentChainOption = (value?: string) => {
const target = normalizeText(value);
if (!target) {
return;
}
const exists = (chainOptions.value || []).some((item) => {
const option = item as ChainOption;
return [option.value, option.code, option.name]
.map((optionValue) => normalizeText(optionValue))
.filter(Boolean)
.includes(target);
});
if (exists) {
return;
}
chainOptions.value = [
...(chainOptions.value || []),
{
value: target,
label: target,
code: target,
name: target
}
];
};
const resolveChainValue = (value?: string) => {
const target = normalizeText(value);
if (!target) {
return undefined;
}
const matched = (chainOptions.value || []).find((item) => {
const option = item as ChainOption;
return [option.value, option.code, option.name]
.map((optionValue) => normalizeText(optionValue))
.filter(Boolean)
.includes(target);
}) as ChainOption | undefined;
return matched?.value ?? target;
};
const loadChainOptions = async () => {
chainLoading.value = true;
try {
const list = await listDictData({ dictId: 175 });
chainOptions.value = (list || [])
.map((item: DictData) => buildChainOption(item))
.filter((item): item is ChainOption => !!item);
ensureCurrentChainOption(form.chain);
form.chain = resolveChainValue(form.chain);
} catch (e) {
message.error((e as Error).message);
} finally {
chainLoading.value = false;
}
};
const getChainCandidates = (value?: string) => {
const target = normalizeText(value);
if (!target) {
return [];
}
const option = (chainOptions.value || []).find(
(item) => normalizeText(item?.value) === target
) as ChainOption | undefined;
return Array.from(
new Set(
[
target,
normalizeText(option?.code),
normalizeText(option?.name)
].filter(Boolean)
)
);
};
const ensureMerchantListLoaded = async (chainValue?: string) => {
const target = normalizeText(chainValue);
if (!target) {
merchantList.value = [];
merchantLoaded.value = false;
merchantLoadedChain.value = '';
return;
}
if (merchantLoaded.value && merchantLoadedChain.value === target) {
return;
}
merchantLoading.value = true;
try {
merchantList.value = await listMerchantMini({
chain: target
});
merchantLoaded.value = true;
merchantLoadedChain.value = target;
} catch (e) {
message.error((e as Error).message);
} finally {
merchantLoading.value = false;
}
};
const loadMerchantOptions = async (chainValue?: string) => {
const target = normalizeText(chainValue);
if (!target) {
merchantList.value = [];
merchantLoaded.value = false;
merchantLoadedChain.value = '';
merchantOptions.value = [];
return;
}
await ensureMerchantListLoaded(target);
const candidates = getChainCandidates(target);
merchantOptions.value = merchantList.value
.filter((item: Merchant) => {
const merchantChain = normalizeText(item.chain);
return !!merchantChain && candidates.includes(merchantChain);
})
.map((item: Merchant) => ({
value: item.id as number,
label: item.name ?? `${item.id ?? ''}`,
chain: item.chain,
code: item.code
}));
};
const syncChainByMerchantId = async (merchantId?: number) => {
if (merchantId == null) {
merchantOptions.value = [];
return;
}
if (!normalizeText(form.chain)) {
return;
}
await ensureMerchantListLoaded(form.chain);
const merchant = merchantList.value.find((item) => item.id === merchantId);
if (!merchant) {
return;
}
ensureCurrentChainOption(merchant.chain);
form.chain = resolveChainValue(merchant.chain);
await loadMerchantOptions(form.chain);
form.merchantCode = merchant.code;
};
const resolveMerchant = (data?: YxeProductEditData | null) => {
const merchantName = normalizeText(data?.merchant?.name);
const merchantCode = normalizeText(
form.merchantCode || data?.merchant?.code
);
const merchantId = form.merchantId ?? data?.merchant?.id;
return merchantList.value.find((item) => {
if (merchantId != null && item.id === merchantId) {
return true;
}
if (merchantCode && normalizeText(item.code) === merchantCode) {
return true;
}
return !!merchantName && normalizeText(item.name) === merchantName;
});
};
const syncMerchantSelection = async (data?: YxeProductEditData | null) => {
let merchant = data?.merchant ?? null;
if (!merchant && !normalizeText(form.chain)) {
return;
}
await ensureMerchantListLoaded(form.chain);
merchant = merchant || resolveMerchant(data);
if (!merchant) {
return;
}
if (!form.chain) {
ensureCurrentChainOption(merchant.chain);
form.chain = resolveChainValue(merchant.chain);
await loadMerchantOptions(form.chain);
}
form.merchantId = merchant.id;
form.merchantCode = merchant.code;
};
const loadGoodsOptions = () => {
goodsLoading.value = true;
listYxeGoods()
.then((list) => {
goodsList.value = list || [];
goodsOptions.value = goodsList.value.map((item: YxeGoods) => ({
value: item.id as number,
label:
`${item.code ?? ''} ${item.title ?? ''}`.trim() ||
`${item.id ?? ''}`
}));
syncSelectedGoods();
})
.catch((e) => {
message.error(e.message);
})
.finally(() => {
goodsLoading.value = false;
});
};
const syncSelectedGoods = () => {
const matched = goodsList.value.find(
(item) =>
normalizeText(item.code) &&
normalizeText(item.code) === normalizeText(form.code)
);
selectedGoodsId.value = matched?.id;
};
const onChangeChain = async (value?: string) => {
form.merchantId = undefined;
form.merchantCode = undefined;
await loadMerchantOptions(value);
};
const onChangeMerchant = (merchantId?: number) => {
const merchant = merchantList.value.find((item) => item.id === merchantId);
form.merchantCode = merchant?.code;
};
const onChangeGoods = (goodsId?: number) => {
if (goodsId == null) {
return;
}
const goods = goodsList.value.find((item) => item.id === goodsId);
if (!goods) {
return;
}
form.code = goods.code;
form.cate = goods.cate;
form.title = goods.title;
form.spec = goods.spec;
form.maker = goods.maker;
form.makerAbb = goods.makerAbb;
form.cover = goods.cover;
};
watch(
() => props.visible,
async (visible) => {
if (visible) {
loadGoodsOptions();
if (props.data) {
const currentData = { ...props.data } as YxeProductEditData;
currentData.startTime =
currentData.startTime || currentData.startDate;
currentData.endTime = currentData.endTime || currentData.endDate;
currentData.extraStartTime =
currentData.extraStartTime || currentData.extraStartDate;
currentData.extraEndTime =
currentData.extraEndTime || currentData.extraEndDate;
assignObject(form, currentData);
await loadChainOptions();
ensureCurrentChainOption(form.chain);
form.chain = resolveChainValue(form.chain);
if (form.chain) {
await loadMerchantOptions(form.chain);
await syncMerchantSelection(currentData);
onChangeMerchant(form.merchantId);
} else if (currentData.merchant || form.merchantCode) {
await syncMerchantSelection(currentData);
} else if (form.merchantId != null) {
await syncChainByMerchantId(form.merchantId);
} else {
await loadMerchantOptions(form.chain);
}
initExtraIncomeRows(currentData);
syncSelectedGoods();
isUpdate.value = true;
} else {
await loadChainOptions();
initExtraIncomeRows();
merchantOptions.value = [];
selectedGoodsId.value = undefined;
isUpdate.value = false;
}
} else {
resetFields();
extraIncomeRows.value = [createExtraIncomeRow()];
merchantOptions.value = [];
selectedGoodsId.value = undefined;
}
},
{ immediate: true }
);
</script>
<style scoped>
.extra-income-empty {
padding: 4px 0;
}
.extra-income-row {
display: flex;
gap: 8px;
align-items: center;
}
.extra-income-input {
width: 180px;
}
.extra-income-range {
flex: 1;
}
</style>
@@ -0,0 +1,452 @@
<!-- 商品导入弹窗 -->
<template>
<ele-modal
:width="560"
:footer="null"
:title="modalTitle"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<a-upload-dragger
accept=".xls,.xlsx"
:show-upload-list="false"
:customRequest="doImport"
style="padding: 24px 0; margin-bottom: 12px"
>
<p class="ant-upload-drag-icon">
<cloud-upload-outlined />
</p>
<p class="ant-upload-hint">将 Excel 文件拖到此处,或点击上传</p>
</a-upload-dragger>
</a-spin>
<div class="ele-text-center">
<a
v-if="isTaskType"
:href="taskTemplateUrl"
target="_blank"
rel="noopener noreferrer"
>
模板下载
</a>
<a v-else href="" @click.prevent="downloadTemplate">模板下载</a>
</div>
<div class="ele-text-secondary ele-text-center" style="margin-top: 4px">
导入字段顺序:{{ fieldOrderText }}
</div>
<div v-if="progress.total" class="ele-text-center" style="margin-top: 8px">
已处理 {{ progress.success + progress.fail }}/{{ progress.total }}, 成功
{{ progress.success }},失败 {{ progress.fail }}
</div>
<div
v-if="errorMessages.length"
class="ele-text-danger"
style="margin-top: 8px"
>
<div>失败明细(最多展示 5 条):</div>
<div v-for="(item, index) in errorMessages.slice(0, 5)" :key="index">
{{ item }}
</div>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { message } from 'ant-design-vue';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import * as XLSX from 'xlsx';
import { addYxeProduct } from '@/api/yxe/yxeProduct';
import type { YxeProduct } from '@/api/yxe/yxeProduct/model';
type ImportRow = {
rowNumber: number;
chain: string;
merchantName: string;
code: string;
category: string;
title: string;
spec: string;
maker: string;
makerAbb: string;
income: string;
taskIncome: string;
startTime: string;
endTime: string;
extraIncome: string;
extraStartTime: string;
extraEndTime: string;
};
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 是否打开弹窗
visible: boolean;
// 商品类型: 0任务商品, 1分享商品
type: number;
}>();
const loading = ref(false);
const progress = ref({ total: 0, success: 0, fail: 0 });
const errorMessages = ref<string[]>([]);
const isTaskType = computed(() => props.type === 0);
const taskTemplateUrl =
'https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/%E4%BB%BB%E5%8A%A1%E8%8D%AF%E5%93%81%E7%9B%AE%E5%BD%95%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xlsx';
const modalTitle = computed(() =>
isTaskType.value ? '批量导入任务商品' : '批量导入分享商品'
);
const fieldOrderText = computed(() =>
isTaskType.value
? '所属连锁、门店名称、厂家简称、产品编码、分类、品名、规格、厂家、任务奖励(taskIncome)'
: '所属连锁、门店名称、厂家简称、产品编码、分类、品名、规格、厂家、分享奖励、分享奖励活动开始时间、分享奖励活动结束时间、额外奖励、额外奖励开始时间、额外奖励结束时间'
);
const normalize = (value?: unknown) => String(value ?? '').trim();
const normalizeKey = (value?: unknown) => normalize(value).toLowerCase();
const pad = (value: number) => String(value).padStart(2, '0');
const formatDateValue = (value: unknown) => {
if (value instanceof Date) {
const hasTime =
value.getHours() > 0 ||
value.getMinutes() > 0 ||
value.getSeconds() > 0;
const datePart = `${value.getFullYear()}-${pad(
value.getMonth() + 1
)}-${pad(value.getDate())}`;
if (!hasTime) {
return datePart;
}
return `${datePart} ${pad(value.getHours())}:${pad(
value.getMinutes()
)}:${pad(value.getSeconds())}`;
}
if (typeof value === 'number' && Number.isFinite(value)) {
const parsed = XLSX.SSF.parse_date_code(value);
if (parsed) {
const hasTime = parsed.H > 0 || parsed.M > 0 || parsed.S > 0;
const datePart = `${parsed.y}-${pad(parsed.m)}-${pad(parsed.d)}`;
if (!hasTime) {
return datePart;
}
return `${datePart} ${pad(parsed.H)}:${pad(parsed.M)}:${pad(parsed.S)}`;
}
}
return normalize(value);
};
const readRows = async (file: File): Promise<ImportRow[]> => {
const data = await file.arrayBuffer();
const workbook = XLSX.read(data, { type: 'array', cellDates: true });
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
const rows = XLSX.utils.sheet_to_json<any[]>(sheet, {
header: 1,
defval: '',
raw: false
}) as Array<string[]>;
if (!rows.length) {
return [];
}
const header = rows[0].map((item) => normalizeKey(item));
const headerIndex = (aliases: string[]) => {
const aliasSet = aliases.map((item) => item.toLowerCase());
return header.findIndex((item) => aliasSet.includes(item));
};
const chainIndex = headerIndex(['所属连锁', '连锁', 'chain']);
const merchantNameIndex = headerIndex([
'门店名称',
'门店编号',
'门店编码',
'merchantname',
'merchantcode'
]);
const makerAbbIndex = headerIndex(['厂家简称', '简称', 'makerabb']);
const codeIndex = headerIndex(['产品编码', '商品编号', '编号', 'code']);
const categoryIndex = headerIndex([
'分类',
'分类名称',
'category',
'catename'
]);
const titleIndex = headerIndex(['品名', '名称', 'title']);
const specIndex = headerIndex(['规格', 'spec']);
const makerIndex = headerIndex(['厂家', 'maker']);
const taskIncomeIndex = headerIndex([
'任务奖励',
'任务奖励(taskincome)',
'任务奖励(taskincome)',
'taskincome',
'奖励金额',
'奖金金额'
]);
const incomeIndex = headerIndex(['分享奖励', '晒单政策', 'income']);
const startTimeIndex = headerIndex([
'分享奖励活动开始时间',
'分享奖励开始时间',
'活动开始时间',
'starttime',
'startdate'
]);
const endTimeIndex = headerIndex([
'分享奖励活动结束时间',
'分享奖励结束时间',
'活动结束时间',
'endtime',
'enddate'
]);
const extraIncomeIndex = headerIndex([
'额外奖励',
'额外加提',
'extraincome'
]);
const extraStartTimeIndex = headerIndex([
'额外奖励开始时间',
'额外加提开始时间',
'extrastarttime',
'extrastartdate'
]);
const extraEndTimeIndex = headerIndex([
'额外奖励结束时间',
'额外加提结束时间',
'extraendtime',
'extraenddate'
]);
const hasHeader = isTaskType.value
? chainIndex >= 0 ||
merchantNameIndex >= 0 ||
makerAbbIndex >= 0 ||
codeIndex >= 0 ||
categoryIndex >= 0 ||
titleIndex >= 0 ||
specIndex >= 0 ||
makerIndex >= 0 ||
taskIncomeIndex >= 0
: chainIndex >= 0 ||
merchantNameIndex >= 0 ||
makerAbbIndex >= 0 ||
codeIndex >= 0 ||
categoryIndex >= 0 ||
titleIndex >= 0 ||
specIndex >= 0 ||
makerIndex >= 0 ||
incomeIndex >= 0 ||
startTimeIndex >= 0 ||
endTimeIndex >= 0 ||
extraIncomeIndex >= 0 ||
extraStartTimeIndex >= 0 ||
extraEndTimeIndex >= 0;
const dataRows = hasHeader ? rows.slice(1) : rows;
const getByIndex = (row: unknown[], index: number, fallback: number) => {
const value = index >= 0 ? row[index] : row[fallback];
return normalize(value);
};
return dataRows
.map((row, i) => {
const chain = getByIndex(row, chainIndex, 0);
const merchantName = getByIndex(row, merchantNameIndex, 1);
const makerAbb = getByIndex(row, makerAbbIndex, 2);
const code = getByIndex(row, codeIndex, 3);
const category = getByIndex(row, categoryIndex, 4);
const title = getByIndex(row, titleIndex, 5);
const spec = getByIndex(row, specIndex, 6);
const maker = getByIndex(row, makerIndex, 7);
const taskIncome = getByIndex(row, taskIncomeIndex, 8);
const income = getByIndex(row, incomeIndex, 8);
const startTime = formatDateValue(
startTimeIndex >= 0 ? row[startTimeIndex] : row[9]
);
const endTime = formatDateValue(
endTimeIndex >= 0 ? row[endTimeIndex] : row[10]
);
const extraIncome = getByIndex(row, extraIncomeIndex, 11);
const extraStartTime = formatDateValue(
extraStartTimeIndex >= 0 ? row[extraStartTimeIndex] : row[12]
);
const extraEndTime = formatDateValue(
extraEndTimeIndex >= 0 ? row[extraEndTimeIndex] : row[13]
);
return {
rowNumber: hasHeader ? i + 2 : i + 1,
chain,
category,
code,
title,
spec,
maker,
makerAbb,
income,
taskIncome,
merchantName,
startTime,
endTime,
extraIncome,
extraStartTime,
extraEndTime
};
})
.filter(
(row) =>
row.chain ||
row.category ||
row.code ||
row.title ||
row.spec ||
row.maker ||
row.makerAbb ||
row.taskIncome ||
row.income ||
row.merchantName ||
row.startTime ||
row.endTime ||
row.extraIncome ||
row.extraStartTime ||
row.extraEndTime
);
};
const doImport = async ({ 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;
progress.value = { total: 0, success: 0, fail: 0 };
errorMessages.value = [];
try {
const rows = await readRows(file as File);
if (!rows.length) {
message.warning('未读取到可导入的数据');
return false;
}
progress.value.total = rows.length;
for (const row of rows) {
try {
const data: YxeProduct = {
type: props.type,
chain: row.chain || undefined,
code: row.code || undefined,
cate: row.category || undefined,
title: row.title || undefined,
spec: row.spec || undefined,
maker: row.maker || undefined,
makerAbb: row.makerAbb || undefined,
merchantCode: row.merchantName || undefined
};
if (isTaskType.value) {
data.taskIncome = row.taskIncome || undefined;
} else {
data.income = row.income || undefined;
data.startTime = row.startTime || undefined;
data.endTime = row.endTime || undefined;
data.extraIncomeList =
row.extraIncome || row.extraStartTime || row.extraEndTime
? [
{
income: row.extraIncome,
startTime: row.extraStartTime,
endTime: row.extraEndTime
}
]
: [];
}
await addYxeProduct(data);
progress.value.success += 1;
} catch (e) {
progress.value.fail += 1;
const err = e as Error;
errorMessages.value.push(
`第${row.rowNumber}行:${err.message || '导入失败'}`
);
}
}
if (progress.value.fail > 0) {
message.warning(
`导入完成:成功${progress.value.success}条,失败${progress.value.fail}条`
);
} else {
message.success(`导入完成:成功${progress.value.success}条`);
updateVisible(false);
}
if (progress.value.success > 0) {
emit('done');
}
} catch (e) {
const err = e as Error;
message.error(err.message || '导入失败');
} finally {
loading.value = false;
}
return false;
};
const downloadTemplate = () => {
const workbook = XLSX.utils.book_new();
const headers = isTaskType.value
? [
'所属连锁',
'门店名称',
'厂家简称',
'产品编码',
'分类',
'品名',
'规格',
'厂家',
'任务奖励(taskIncome)'
]
: [
'所属连锁',
'门店名称',
'厂家简称',
'产品编码',
'分类',
'品名',
'规格',
'厂家',
'分享奖励',
'分享奖励活动开始时间',
'分享奖励活动结束时间',
'额外奖励',
'额外奖励开始时间',
'额外奖励结束时间'
];
const sheet = XLSX.utils.aoa_to_sheet([headers]);
XLSX.utils.book_append_sheet(
workbook,
sheet,
isTaskType.value ? '任务商品导入模板' : '分享商品导入模板'
);
XLSX.writeFile(
workbook,
isTaskType.value
? '药销儿任务商品导入模板.xlsx'
: '药销儿分享商品导入模板.xlsx'
);
};
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>
@@ -0,0 +1,206 @@
<template>
<ele-modal
:width="640"
:visible="visible"
:maskClosable="false"
:confirm-loading="loading"
:title="isUpdate ? '编辑门店政策' : '新增门店政策'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ flex: '88px' }"
:wrapper-col="{ flex: '1' }"
>
<a-form-item label="门店" name="merchantId">
<a-select
show-search
allow-clear
optionFilterProp="label"
:loading="merchantLoading"
:options="merchantOptions"
:placeholder="product?.chain ? '请选择门店' : '当前产品未配置连锁'"
:disabled="!product?.chain"
v-model:value="form.merchantId"
/>
</a-form-item>
<a-form-item label="金额" name="income">
<a-input-number
class="ele-fluid"
:min="0"
:step="0.01"
:precision="2"
string-mode
placeholder="请输入金额"
v-model:value="form.income"
/>
</a-form-item>
<a-form-item label="有效期" name="timeRange">
<a-range-picker
class="ele-fluid"
value-format="YYYY-MM-DD HH:mm"
:show-time="{ format: 'HH:mm' }"
:placeholder="['请选择开始时间', '请选择结束时间']"
v-model:value="form.timeRange"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { reactive, ref, watch } from 'vue';
import { toDateString } from 'ele-admin-pro';
import { Form, message, type Rule, type SelectProps } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue/es/form';
import {
addYxeProductMerchantExtraIncome,
updateYxeProductMerchantExtraIncome
} from '@/api/yxe/yxeProductMerchantExtraIncome';
import type { YxeProductMerchantExtraIncome } from '@/api/yxe/yxeProductMerchantExtraIncome/model';
import type { YxeProduct } from '@/api/yxe/yxeProduct/model';
type PolicyForm = YxeProductMerchantExtraIncome & {
timeRange?: string[];
};
const props = defineProps<{
visible: boolean;
product?: YxeProduct | null;
data?: YxeProductMerchantExtraIncome | null;
merchantOptions?: SelectProps['options'];
merchantLoading?: boolean;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const isUpdate = ref(false);
const loading = ref(false);
const formRef = ref<FormInstance | null>(null);
const useForm = Form.useForm;
const createDefaultForm = (): PolicyForm => ({
id: undefined,
productId: undefined,
merchantId: undefined,
startTime: undefined,
endTime: undefined,
income: undefined,
sortNumber: 0,
status: 0,
deleted: 0,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
timeRange: undefined
});
const form = reactive<PolicyForm>(createDefaultForm());
const validateAmount: Rule['validator'] = async (_rule, value) => {
if (value == null || value === '') {
return Promise.reject('请输入金额');
}
if (!/^\d+(\.\d{1,2})?$/.test(String(value))) {
return Promise.reject('金额最多支持两位小数');
}
return Promise.resolve();
};
const rules = reactive({
merchantId: [{ required: true, message: '请选择门店', trigger: 'change' }],
income: [{ required: true, validator: validateAmount, trigger: 'change' }],
timeRange: [
{
required: true,
type: 'array',
message: '请选择有效期',
trigger: 'change'
}
]
});
const { resetFields } = useForm(form, rules);
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const initForm = (data?: YxeProductMerchantExtraIncome | null) => {
const defaults = createDefaultForm();
const timeRange =
data?.startTime || data?.endTime
? [
formatMinuteValue(data?.startTime),
formatMinuteValue(data?.endTime)
].filter(Boolean)
: undefined;
Object.assign(form, defaults, data ?? {}, {
productId: props.product?.id,
timeRange: timeRange?.length === 2 ? (timeRange as string[]) : undefined
});
isUpdate.value = !!data?.id;
};
const formatMinuteValue = (value?: string) => {
if (!value) {
return undefined;
}
return toDateString(value, 'yyyy-MM-dd HH:mm');
};
const save = async () => {
if (!props.product?.id) {
message.error('当前产品不存在,无法保存门店政策');
return;
}
if (!formRef.value) {
return;
}
try {
await formRef.value.validate();
loading.value = true;
const { timeRange, ...rest } = { ...form };
const payload: YxeProductMerchantExtraIncome = {
...rest,
productId: props.product.id,
startTime: timeRange?.[0],
endTime: timeRange?.[1],
status: rest.status ?? 0,
deleted: rest.deleted ?? 0
};
const request = isUpdate.value
? updateYxeProductMerchantExtraIncome
: addYxeProductMerchantExtraIncome;
const msg = await request(payload);
message.success(msg);
updateVisible(false);
emit('done');
} catch (e) {
if ((e as Error)?.message) {
message.error((e as Error).message);
}
} finally {
loading.value = false;
}
};
watch(
() => props.visible,
(visible) => {
if (visible) {
initForm(props.data);
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
@@ -0,0 +1,261 @@
<template>
<ele-modal
:visible="visible"
:width="1000"
:maskClosable="false"
:footer="null"
:title="modalTitle"
@update:visible="updateVisible"
>
<a-alert
v-if="!product?.chain"
type="warning"
show-icon
message="当前产品未配置所属连锁,无法新增门店政策"
style="margin-bottom: 16px"
/>
<div class="toolbar">
<a-space wrap>
<a-button
type="primary"
@click="openEdit()"
:disabled="!canCreatePolicy"
>
新增
</a-button>
<span class="ele-text-secondary">
所属连锁:{{ product?.chain || "-" }}
</span>
</a-space>
</div>
<a-table
size="middle"
:loading="loading"
:columns="columns"
:data-source="list"
:row-key="rowKey"
:pagination="false"
:scroll="{ x: 760 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'merchantName'">
{{ getMerchantName(record) }}
</template>
<template v-else-if="column.key === 'income'">
{{ record.income || "-" }}
</template>
<template v-else-if="column.key === 'effectivePeriod'">
{{ formatEffectivePeriod(record) }}
</template>
<template v-else-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>
</a-table>
<YxeProductMerchantPolicyEdit
v-model:visible="showEdit"
:product="product"
:data="current"
:merchant-options="merchantOptions"
:merchant-loading="merchantLoading"
@done="loadData"
/>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { message, type SelectProps } from 'ant-design-vue';
import { toDateString } from 'ele-admin-pro';
import { listMerchantMini } from '@/api/shop/merchant';
import type { Merchant } from '@/api/shop/merchant/model';
import {
listYxeProductMerchantExtraIncome,
removeYxeProductMerchantExtraIncome
} from '@/api/yxe/yxeProductMerchantExtraIncome';
import type { YxeProductMerchantExtraIncome } from '@/api/yxe/yxeProductMerchantExtraIncome/model';
import type { YxeProduct } from '@/api/yxe/yxeProduct/model';
import YxeProductMerchantPolicyEdit from './yxeProductMerchantPolicyEdit.vue';
type MerchantOption = NonNullable<SelectProps['options']>[number];
const props = defineProps<{
visible: boolean;
product?: YxeProduct | null;
}>();
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void;
}>();
const loading = ref(false);
const merchantLoading = ref(false);
const list = ref<YxeProductMerchantExtraIncome[]>([]);
const merchantOptions = ref<SelectProps['options']>([]);
const current = ref<YxeProductMerchantExtraIncome | null>(null);
const showEdit = ref(false);
const columns = [
{
title: '门店名称',
dataIndex: ['merchant', 'name'],
key: 'merchantName',
align: 'center'
},
{
title: '金额',
dataIndex: 'income',
key: 'income',
align: 'center',
width: 140
},
{
title: '有效期',
key: 'effectivePeriod',
align: 'center',
width: 360
},
{
title: '操作',
key: 'action',
align: 'center',
width: 160
}
];
const modalTitle = computed(() => {
const title = String(props.product?.title ?? '').trim();
return title ? `${title} - 门店政策配置` : '门店政策配置';
});
const canCreatePolicy = computed(
() => !!props.product?.id && !!String(props.product?.chain ?? '').trim()
);
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const rowKey = (record: YxeProductMerchantExtraIncome) =>
record.id ?? `${record.merchantId ?? ''}-${record.startTime ?? ''}`;
const getMerchantName = (record: YxeProductMerchantExtraIncome) => {
if (record.merchant?.name) {
return record.merchant.name;
}
const matched = (merchantOptions.value || []).find(
(item) => item?.value === record.merchantId
) as MerchantOption | undefined;
return matched?.label ?? '-';
};
const formatEffectivePeriod = (record: YxeProductMerchantExtraIncome) => {
const start = record.startTime
? toDateString(record.startTime, 'yyyy-MM-dd HH:mm')
: '';
const end = record.endTime
? toDateString(record.endTime, 'yyyy-MM-dd HH:mm')
: '';
if (!start && !end) {
return '-';
}
return [start, end].filter(Boolean).join(' ~ ');
};
const loadMerchantOptions = async () => {
const chain = String(props.product?.chain ?? '').trim();
if (!chain) {
merchantOptions.value = [];
return;
}
merchantLoading.value = true;
try {
const merchants = await listMerchantMini({ chain });
merchantOptions.value = (merchants || []).map((item: Merchant) => ({
value: item.id as number,
label: item.name ?? `${item.id ?? ''}`
}));
} catch (e) {
message.error((e as Error).message || '获取门店列表失败');
} finally {
merchantLoading.value = false;
}
};
const loadList = async () => {
if (!props.product?.id) {
list.value = [];
return;
}
loading.value = true;
try {
list.value = await listYxeProductMerchantExtraIncome({
productId: props.product.id
});
} catch (e) {
message.error((e as Error).message || '获取门店政策失败');
} finally {
loading.value = false;
}
};
const loadData = async () => {
await Promise.all([loadMerchantOptions(), loadList()]);
};
const openEdit = (row?: YxeProductMerchantExtraIncome) => {
if (!row && !canCreatePolicy.value) {
message.warning('当前产品未配置所属连锁,无法新增门店政策');
return;
}
current.value = row ?? null;
showEdit.value = true;
};
const remove = async (row: YxeProductMerchantExtraIncome) => {
const hide = message.loading('请求中..', 0);
try {
const msg = await removeYxeProductMerchantExtraIncome(row.id);
hide();
message.success(msg);
await loadList();
} catch (e) {
hide();
message.error((e as Error).message);
}
};
watch(
() => [props.visible, props.product?.id, props.product?.chain],
([visible]) => {
if (visible) {
loadData();
} else {
list.value = [];
merchantOptions.value = [];
current.value = null;
showEdit.value = false;
}
},
{ immediate: true }
);
</script>
<style scoped>
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
</style>
+456
View File
@@ -0,0 +1,456 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="type === 0 ? columns0 : columns1"
:datasource="datasource"
:customRow="customRow"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
:pageSizeOptions="['10', '20', '50', '100', '200', '500', '1000']"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
@import="openImport"
@coverZip="handleCoverZipUpload"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'cover'">
<a-image
:src="record.cover"
v-if="record.cover"
style="width: 50px; height: 50px; object-fit: cover"
/>
</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 === 'shareDate'">
<span v-if="record.startTime || record.endTime">
{{ formatDateRange(record.startTime, record.endTime) }}
</span>
<span v-else>-</span>
</template>
<template v-if="column.key === 'extraIncome'">
<div v-if="getExtraIncomeList(record).length">
<div
v-for="(item, index) in getExtraIncomeList(record)"
:key="index"
>
{{ item.startTime }}~{{ item.endTime }}, {{ item.income }}元
</div>
</div>
<span v-else>-</span>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<template v-if="type === 1">
<a-divider type="vertical" />
<a @click="openMerchantPolicy(record)">门店政策配置</a>
</template>
<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>
<!-- 编辑弹窗 -->
<YxeProductEdit
v-model:visible="showEdit"
:type="type"
:data="current"
@done="reload"
/>
<YxeProductImport v-model:visible="showImport" :type="type" @done="reload" />
<YxeProductMerchantPolicyModal
v-model:visible="showMerchantPolicy"
:product="currentPolicyProduct"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { message } from "ant-design-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 YxeProductEdit from "./components/yxeProductEdit.vue";
import YxeProductImport from "./components/yxeProductImport.vue";
import YxeProductMerchantPolicyModal from "./components/yxeProductMerchantPolicyModal.vue";
import {
pageYxeProduct,
removeYxeProduct,
removeBatchYxeProduct,
uploadYxeProductCoverZip
} from "@/api/yxe/yxeProduct";
import type {
YxeProduct,
YxeProductParam,
YxeProductExtraIncomeItem
} from "@/api/yxe/yxeProduct/model";
import { useRoute } from "vue-router";
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<YxeProduct[]>([]);
// 当前编辑数据
const current = ref<YxeProduct | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示导入弹窗
const showImport = ref(false);
// 是否显示门店政策配置弹窗
const showMerchantPolicy = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 当前配置门店政策的产品
const currentPolicyProduct = ref<YxeProduct | null>(null);
const route = useRoute();
const type = parseInt(route.query.type as string) || 0;
const getExtraIncomeList = (
record: YxeProduct
): YxeProductExtraIncomeItem[] => {
if (record.extraIncomeList?.length) {
return record.extraIncomeList;
}
return [];
};
const formatDateRange = (startTime?: string, endTime?: string) => {
const start = startTime ? toDateString(startTime, "yyyy-MM-dd HH:mm") : "";
const end = endTime ? toDateString(endTime, "yyyy-MM-dd HH:mm") : "";
return [start, end].filter(Boolean).join(" ~ ");
};
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
where.type = type || 0;
return pageYxeProduct({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns0 = ref<ColumnItem[]>([
{
title: "编号",
dataIndex: "code",
align: "center"
},
{
title: "所属连锁",
dataIndex: "chain",
align: "center"
},
// {
// title: "门店",
// dataIndex: ["merchant", "name"],
// align: "center"
// },
{
title: "品名",
dataIndex: "title",
key: "title",
align: "center"
},
{
title: "分类",
dataIndex: "cate",
key: "cate",
align: "center"
},
{
title: "图片",
key: "cover",
align: "center"
},
{
title: "规格",
dataIndex: "spec",
key: "spec",
align: "center"
},
{
title: "厂家",
dataIndex: "maker",
key: "maker",
align: "center"
},
{
title: "厂家简称",
dataIndex: "makerAbb",
key: "makerAbb",
align: "center"
},
{
title: "奖金金额",
dataIndex: "taskIncome",
align: "center",
},
{
title: "添加时间",
dataIndex: "createTime",
key: "createTime",
align: "center",
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, "yyyy-MM-dd HH:mm:ss")
},
{
title: "操作",
key: "action",
width: 280,
fixed: "right",
align: "center",
hideInSetting: true
}
]);
const columns1 = ref<ColumnItem[]>([
{
title: "编号",
dataIndex: "code",
align: "center"
},
{
title: "所属连锁",
dataIndex: "chain",
align: "center"
},
// {
// title: "门店",
// dataIndex: ["merchant", "name"],
// align: "center"
// },
{
title: "品名",
dataIndex: "title",
key: "title",
align: "center"
},
{
title: "分类",
dataIndex: "cate",
key: "cate",
align: "center"
},
{
title: "图片",
key: "cover",
align: "center"
},
{
title: "规格",
dataIndex: "spec",
key: "spec",
align: "center"
},
{
title: "厂家",
dataIndex: "maker",
key: "maker",
align: "center"
},
{
title: "厂家简称",
dataIndex: "makerAbb",
key: "makerAbb",
align: "center"
},
{
title: "晒单政策",
dataIndex: "income",
key: "income",
align: "center"
},
{
title: "分享有效期",
key: "shareDate",
align: "center"
},
{
title: "额外加提",
key: "extraIncome",
align: "center",
width: 250
},
{
title: "添加时间",
dataIndex: "createTime",
key: "createTime",
align: "center",
customRender: ({ text }) => toDateString(text, "yyyy-MM-dd HH:mm:ss")
},
{
title: "操作",
key: "action",
width: 220,
fixed: "right",
align: "center",
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: YxeProductParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: YxeProduct) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 打开导入弹窗 */
const openImport = () => {
showImport.value = true;
};
/* 打开门店政策配置弹窗 */
const openMerchantPolicy = (row: YxeProduct) => {
currentPolicyProduct.value = row;
showMerchantPolicy.value = true;
};
const handleCoverZipUpload = (file: File) => {
const fileName = file.name?.toLowerCase?.() ?? "";
if (!fileName.endsWith(".zip")) {
message.error("只能选择 zip 压缩包");
return false;
}
const hide = message.loading("封面压缩包处理中..", 0);
uploadYxeProductCoverZip(file)
.then((result) => {
hide();
message.success(
`封面压缩包处理完成,识别${result.totalFiles ?? 0}个文件,更新${
result.updatedCount ?? 0
}条记录,跳过${result.skippedCount ?? 0}个文件`
);
if (result.notFoundTitles?.length) {
message.warning(
`有 ${result.notFoundTitles.length} 个商品标题未匹配`
);
}
reload();
})
.catch((e) => {
hide();
message.error(e.message || "封面压缩包处理失败");
});
return false;
};
/* 删除单个 */
const remove = (row: YxeProduct) => {
const hide = message.loading("请求中..", 0);
removeYxeProduct(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;
}
const hide = message.loading("请求中..", 0);
removeBatchYxeProduct(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: YxeProduct) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: "YxeProduct"
};
</script>
<style lang="less" scoped></style>
@@ -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>
@@ -0,0 +1,191 @@
<!-- 编辑弹窗 -->
<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="productId">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.productId"
/>
</a-form-item>
<a-form-item label="" name="startTime">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.startTime"
/>
</a-form-item>
<a-form-item label="" name="endTime">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.endTime"
/>
</a-form-item>
<a-form-item label="" name="income">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.income"
/>
</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="状态, 0正常, 1冻结" 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-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>
<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 { addYxeProductExtraIncome, updateYxeProductExtraIncome } from '@/api/yxe/yxeProductExtraIncome';
import { YxeProductExtraIncome } from '@/api/yxe/yxeProductExtraIncome/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
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?: YxeProductExtraIncome | 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<YxeProductExtraIncome>({
id: undefined,
productId: undefined,
startTime: undefined,
endTime: undefined,
income: undefined,
sortNumber: undefined,
status: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
});
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 ? updateYxeProductExtraIncome : addYxeProductExtraIncome;
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);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
@@ -0,0 +1,263 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 1" color="green">显示</a-tag>
<a-tag v-if="record.status === 0" 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>
<!-- 编辑弹窗 -->
<YxeProductExtraIncomeEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</div>
</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 YxeProductExtraIncomeEdit from './components/yxeProductExtraIncomeEdit.vue';
import { pageYxeProductExtraIncome, removeYxeProductExtraIncome, removeBatchYxeProductExtraIncome } from '@/api/yxe/yxeProductExtraIncome';
import type { YxeProductExtraIncome, YxeProductExtraIncomeParam } from '@/api/yxe/yxeProductExtraIncome/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<YxeProductExtraIncome[]>([]);
// 当前编辑数据
const current = ref<YxeProductExtraIncome | 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 pageYxeProductExtraIncome({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '',
dataIndex: 'productId',
key: 'productId',
align: 'center',
},
{
title: '',
dataIndex: 'startTime',
key: 'startTime',
align: 'center',
},
{
title: '',
dataIndex: 'endTime',
key: 'endTime',
align: 'center',
},
{
title: '',
dataIndex: 'income',
key: 'income',
align: 'center',
},
{
title: '排序(数字越小越靠前)',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
},
{
title: '状态, 0正常, 1冻结',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '是否删除, 0否, 1是',
dataIndex: 'deleted',
key: 'deleted',
align: 'center',
},
{
title: '注册时间',
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
}
]);
/* 搜索 */
const reload = (where?: YxeProductExtraIncomeParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: YxeProductExtraIncome) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: YxeProductExtraIncome) => {
const hide = message.loading('请求中..', 0);
removeYxeProductExtraIncome(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);
removeBatchYxeProductExtraIncome(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: YxeProductExtraIncome) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'YxeProductExtraIncome'
};
</script>
<style lang="less" scoped></style>
@@ -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>
@@ -0,0 +1,199 @@
<!-- 编辑弹窗 -->
<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="productId">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.productId"
/>
</a-form-item>
<a-form-item label="" name="merchantId">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.merchantId"
/>
</a-form-item>
<a-form-item label="" name="startTime">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.startTime"
/>
</a-form-item>
<a-form-item label="" name="endTime">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.endTime"
/>
</a-form-item>
<a-form-item label="" name="income">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.income"
/>
</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="状态, 0正常, 1冻结" 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-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>
<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 { addYxeProductMerchantExtraIncome, updateYxeProductMerchantExtraIncome } from '@/api/yxe/yxeProductMerchantExtraIncome';
import { YxeProductMerchantExtraIncome } from '@/api/yxe/yxeProductMerchantExtraIncome/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
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?: YxeProductMerchantExtraIncome | 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<YxeProductMerchantExtraIncome>({
id: undefined,
productId: undefined,
merchantId: undefined,
startTime: undefined,
endTime: undefined,
income: undefined,
sortNumber: undefined,
status: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
});
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 ? updateYxeProductMerchantExtraIncome : addYxeProductMerchantExtraIncome;
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);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
@@ -0,0 +1,269 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 1" color="green">显示</a-tag>
<a-tag v-if="record.status === 0" 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>
<!-- 编辑弹窗 -->
<YxeProductMerchantExtraIncomeEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</div>
</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 YxeProductMerchantExtraIncomeEdit from './components/yxeProductMerchantExtraIncomeEdit.vue';
import { pageYxeProductMerchantExtraIncome, removeYxeProductMerchantExtraIncome, removeBatchYxeProductMerchantExtraIncome } from '@/api/yxe/yxeProductMerchantExtraIncome';
import type { YxeProductMerchantExtraIncome, YxeProductMerchantExtraIncomeParam } from '@/api/yxe/yxeProductMerchantExtraIncome/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<YxeProductMerchantExtraIncome[]>([]);
// 当前编辑数据
const current = ref<YxeProductMerchantExtraIncome | 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 pageYxeProductMerchantExtraIncome({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '',
dataIndex: 'productId',
key: 'productId',
align: 'center',
},
{
title: '',
dataIndex: 'merchantId',
key: 'merchantId',
align: 'center',
},
{
title: '',
dataIndex: 'startTime',
key: 'startTime',
align: 'center',
},
{
title: '',
dataIndex: 'endTime',
key: 'endTime',
align: 'center',
},
{
title: '',
dataIndex: 'income',
key: 'income',
align: 'center',
},
{
title: '排序(数字越小越靠前)',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
},
{
title: '状态, 0正常, 1冻结',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '是否删除, 0否, 1是',
dataIndex: 'deleted',
key: 'deleted',
align: 'center',
},
{
title: '注册时间',
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
}
]);
/* 搜索 */
const reload = (where?: YxeProductMerchantExtraIncomeParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: YxeProductMerchantExtraIncome) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: YxeProductMerchantExtraIncome) => {
const hide = message.loading('请求中..', 0);
removeYxeProductMerchantExtraIncome(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);
removeBatchYxeProductMerchantExtraIncome(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: YxeProductMerchantExtraIncome) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'YxeProductMerchantExtraIncome'
};
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,231 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="exportData">
<template #icon>
<DownloadOutlined />
</template>
<span>导出</span>
</a-button>
<a-select
v-model:value="where.userId"
show-search
allow-clear
:filter-option="false"
optionFilterProp="label"
:loading="userLoading"
:options="userOptions"
placeholder="请选择用户"
style="width: 220px"
@search="onSearchUser"
@focus="loadUserOptions()"
@change="search"
/>
<a-range-picker
v-model:value="dateRange"
value-format="YYYY-MM-DD"
:placeholder="['请选择开始日期', '请选择结束日期']"
style="width: 260px"
/>
<a-input
v-model:value.trim="where.fromAddress"
allow-clear
placeholder="请输入出发点"
style="width: 200px"
@pressEnter="search"
/>
<a-input
v-model:value.trim="where.toAddress"
allow-clear
placeholder="请输入目的地"
style="width: 200px"
@pressEnter="search"
/>
<a-select
v-model:value="where.status"
allow-clear
placeholder="请选择状态"
style="width: 160px"
>
<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-button type="primary" @click="search">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { DownloadOutlined } from '@ant-design/icons-vue';
import { onBeforeUnmount, ref } from 'vue';
import { message } from 'ant-design-vue';
import type { SelectProps } from 'ant-design-vue';
import useSearch from '@/utils/use-search';
import { pageUsers } from '@/api/system/user';
import type { User as SystemUser } from '@/api/system/user/model';
import type { YxeReimburseParam } from '@/api/yxe/yxeReimburse/model';
defineProps<{
selection?: [];
}>();
const emit = defineEmits<{
(e: 'search', where?: YxeReimburseParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
(e: 'export'): void;
}>();
const { where, resetFields } = useSearch<YxeReimburseParam>({
userId: undefined,
status: undefined,
startDateStart: undefined,
endDateEnd: undefined,
fromAddress: undefined,
toAddress: undefined
});
const dateRange = ref<[string, string] | []>([]);
const userOptions = ref<SelectProps['options']>([]);
const userLoading = ref(false);
let userSearchTimer: ReturnType<typeof setTimeout> | null = null;
let userSearchSeed = 0;
const buildUserOption = (item: SystemUser) => {
if (item.userId == null) {
return null;
}
const phone = String(item.phone ?? '').trim();
const name = String(item.realName ?? item.nickname ?? '').trim();
const contact = phone || `ID:${item.userId}`;
return {
value: item.userId,
label: name ? `${name} (${contact})` : contact
};
};
const ensureCurrentUserOption = (userId?: number) => {
if (userId == null) {
return;
}
const exists = userOptions.value?.some((item) => item?.value === userId);
if (exists) {
return;
}
userOptions.value = [
{
value: userId,
label: `ID:${userId}`
},
...(userOptions.value || [])
];
};
const loadUserOptions = (keyword?: string) => {
const currentSeed = ++userSearchSeed;
const trimmed = String(keyword ?? '').trim();
const previousSelectedOption =
where.userId == null
? undefined
: (userOptions.value || []).find(
(item) => item?.value === where.userId
);
userLoading.value = true;
const requests = trimmed
? [
pageUsers({
page: 1,
limit: 20,
phone: trimmed
}),
pageUsers({
page: 1,
limit: 20,
realName: trimmed
})
]
: [
pageUsers({
page: 1,
limit: 20
})
];
Promise.all(requests)
.then((results) => {
if (currentSeed !== userSearchSeed) {
return;
}
const map = new Map<number, { value: number; label: string }>();
results.forEach((res) => {
(res?.list || []).forEach((item) => {
const option = buildUserOption(item as SystemUser);
if (option && !map.has(option.value)) {
map.set(option.value, option);
}
});
});
userOptions.value = Array.from(map.values());
if (
previousSelectedOption &&
where.userId != null &&
!map.has(where.userId)
) {
userOptions.value = [previousSelectedOption, ...userOptions.value];
}
ensureCurrentUserOption(where.userId);
})
.catch((e) => {
if (currentSeed !== userSearchSeed) {
return;
}
message.error(e.message ?? '查询用户失败');
})
.finally(() => {
if (currentSeed === userSearchSeed) {
userLoading.value = false;
}
});
};
const onSearchUser = (value: string) => {
if (userSearchTimer) {
clearTimeout(userSearchTimer);
}
userSearchTimer = setTimeout(() => {
loadUserOptions(value);
}, 300);
};
const syncDateRange = () => {
const [startDate, endDate] = Array.isArray(dateRange.value)
? dateRange.value
: [];
where.startDateStart = startDate || undefined;
where.endDateEnd = endDate || undefined;
};
const search = () => {
syncDateRange();
emit('search', { ...where });
};
const reset = () => {
resetFields();
dateRange.value = [];
search();
};
const exportData = () => {
syncDateRange();
emit('export');
};
onBeforeUnmount(() => {
if (userSearchTimer) {
clearTimeout(userSearchTimer);
userSearchTimer = null;
}
});
</script>

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