@@ -65,6 +65,7 @@ const DraggableTab: React.FC<TabsProps & { onItemsChange?: (items: any[]) => voi | |||
return ( | |||
<Tabs | |||
style={{ position: 'relative' }} | |||
renderTabBar={(tabBarProps, DefaultTabBar) => ( | |||
<DndContext sensors={[sensor]} onDragEnd={onDragEnd} modifiers={[restrictToHorizontalAxis]}> | |||
<SortableContext items={items.map((i) => i.key)} strategy={horizontalListSortingStrategy}> | |||
@@ -80,7 +81,7 @@ const DraggableTab: React.FC<TabsProps & { onItemsChange?: (items: any[]) => voi | |||
)} | |||
{...props} | |||
items={items} | |||
tabBarStyle={{ marginBottom: 8 }} | |||
tabBarStyle={{ marginBottom: 8, position: 'sticky', top: 0, zIndex: 997}} | |||
className='tab-layout' | |||
/> | |||
); | |||
@@ -18,7 +18,6 @@ const Content: FC<any> = ({ children }) => { | |||
style={{ | |||
borderRadius: '8px', | |||
marginLeft: collapsed ? 100 : defaultSetting.slideWidth, | |||
minHeight: 'calc(100vh - 60px)', | |||
transition: "all 200ms cubic-bezier(0.4, 0, 0.6, 1) 0ms", | |||
width: `calc(100vw - ${isPC ? collapsed ? 100 : defaultSetting.slideWidth : 32}px)` | |||
}} | |||
@@ -67,7 +67,9 @@ const BasicLayout: React.FC = () => { | |||
item.path = [...item.parentPaths, item.path].join('') | |||
} | |||
item.component = item.component ? (item.path.endsWith('/index.tsx') ? item.path : `${item.path}/index.tsx`) : item.component | |||
if(item.component) { | |||
item.component = `${item.component.startsWith('/')?'':'/'}${item.component}${item.component.endsWith('.tsx')?'':'.tsx'}` | |||
} | |||
routes.push(item) | |||
if(item.children && item.children.length > 0) { | |||
const parentPaths= item.path.replace('/index.tsx', '').split("/").filter(it => it!== '').map(it => `/${it}`) | |||
@@ -73,8 +73,17 @@ const TabsLayout: React.FC = () => { | |||
<div | |||
key={tab.key} | |||
className='px-[16px]' | |||
style={{ | |||
height: 'calc(100vh - 107px)' | |||
}} | |||
> | |||
{tab.children} | |||
<div style={{ | |||
height: '100%', | |||
overflow: 'auto' | |||
}}> | |||
{tab.children} | |||
</div> | |||
</div> | |||
), | |||
closable: tabs.length > 1, // 剩最后一个就不能删除了 | |||
@@ -0,0 +1,144 @@ | |||
/** | |||
* CategoryVO,管理后台 - 类目 Response VO | |||
*/ | |||
export interface CategoryVO { | |||
/** | |||
* 类目名称,最大长度不能超过125 | |||
*/ | |||
categoryName: string; | |||
/** | |||
* 类目英文名称,最大长度255 | |||
*/ | |||
categoryNameEn: string; | |||
/** | |||
* 创建时间 | |||
*/ | |||
createTime: Date; | |||
/** | |||
* 主键 | |||
*/ | |||
id: number; | |||
/** | |||
* 是否叶子节点,(1: 是, 2:不是) | |||
*/ | |||
isLeaf?: string; | |||
/** | |||
* 目录级别 | |||
*/ | |||
level?: string; | |||
/** | |||
* 父类目id | |||
*/ | |||
parentId?: number; | |||
/** | |||
* 备注,最大长度255 | |||
*/ | |||
remark?: string; | |||
} | |||
/** | |||
* CategoryTreeVO,管理后台 - 类目 Response VO | |||
*/ | |||
export interface CategoryTreeVO { | |||
attrVO?: CategoryAdditionalAttrVO; | |||
/** | |||
* 类目名称,最大长度不能超过125 | |||
*/ | |||
categoryName: string; | |||
/** | |||
* 类目英文名称,最大长度255 | |||
*/ | |||
categoryNameEn: string; | |||
/** | |||
* 子节点 | |||
*/ | |||
childrens?: CategoryTreeVO[]; | |||
/** | |||
* 创建时间 | |||
*/ | |||
createTime?: Date; | |||
/** | |||
* 主键 | |||
*/ | |||
id: number; | |||
/** | |||
* 是否叶子节点,(1: 是, 2:不是) | |||
*/ | |||
isLeaf?: string; | |||
/** | |||
* 目录级别 | |||
*/ | |||
level?: string; | |||
/** | |||
* 父类目id | |||
*/ | |||
parentId?: number; | |||
/** | |||
* 备注,最大长度255 | |||
*/ | |||
remark?: string; | |||
} | |||
/** | |||
* CategoryAdditionalAttrVO,管理后台 - 类目附加属性 Response VO | |||
*/ | |||
export interface CategoryAdditionalAttrVO { | |||
/** | |||
* 类目Id | |||
*/ | |||
categoryId: number; | |||
/** | |||
* 创建时间 | |||
*/ | |||
createTime: Date; | |||
/** | |||
* 海关编码,最大长度100 | |||
*/ | |||
customsName?: string; | |||
/** | |||
* 申报名称,最大长度125 | |||
*/ | |||
declareName?: string; | |||
/** | |||
* 申报名称_英文,最大长度125 | |||
*/ | |||
declareNameEn?: string; | |||
/** | |||
* 申报价格 | |||
*/ | |||
declarePrice?: number; | |||
/** | |||
* 申报重量 | |||
*/ | |||
declareWeight?: number; | |||
/** | |||
* 主键 | |||
*/ | |||
id: number; | |||
/** | |||
* 是否上传尺码表(1:是, 2:否) | |||
*/ | |||
isSizeTable: string; | |||
/** | |||
* 包材id | |||
*/ | |||
packingId: number; | |||
} | |||
export interface CategoryPageReqVO extends PageParam { | |||
/** | |||
* 类目名称 | |||
*/ | |||
categoryName?: string; | |||
/** | |||
* 类目英文名称 | |||
*/ | |||
categoryNameEn?: string; | |||
/** | |||
* 创建时间 | |||
*/ | |||
createTime?: string[]; | |||
} |
@@ -0,0 +1,58 @@ | |||
//GoodsAttrVO - 主属性 Response VO | |||
export interface GoodsAttrVO { | |||
//属性名称_中文,最大长度50 | |||
attrNameCn?: string; | |||
//属性名称_英文,最大长度50 | |||
attrNameEn: string; | |||
//状态 (1: 正常使用, 2: 停止使用) | |||
attrStatus: string; | |||
createTime?: Date; | |||
id?: number; | |||
} | |||
//GoodsAttrValVO - 主属性值 Response VO | |||
export interface GoodsAttrValVO { | |||
attrId: number; | |||
createTime: Date; | |||
id: number; | |||
//属性值中文名称,最大长度100 | |||
valNameCn?: string; | |||
//属性值英文名称,最大长度100 | |||
valNameEn: string; | |||
} | |||
export interface GoodsAttrPageReqVO extends PageParam { | |||
//属性名称_中文 | |||
attrNameCn?: string; | |||
//属性名称_英文 | |||
attrNameEn?: string; | |||
//状态 (1: 正常使用, 2: 停止使用) | |||
attrStatus?: string; | |||
//创建时间 | |||
createTime?: string[]; | |||
} | |||
export interface AttrAndAttrValByParam { | |||
/** | |||
* 属性Id | |||
*/ | |||
attrId: number; | |||
/** | |||
* 属性名称(中/英) | |||
*/ | |||
attrName: string; | |||
} | |||
export interface AttrValNameEnVerifyParam { | |||
/** | |||
* 属性Id | |||
*/ | |||
attrId: number; | |||
/** | |||
* 属性值英文名称 | |||
*/ | |||
valNameEn: string; | |||
} |
@@ -0,0 +1,18 @@ | |||
//GoodsClassifyVO - 商品分类 | |||
export interface GoodsClassifyVO { | |||
//分类名称,分类名称,最大长度125 | |||
classifyName: string; | |||
createTime: Date; | |||
id: number; | |||
//是否是默认(1: 是, 2: 否) | |||
isDefault: string; | |||
} | |||
export interface GoodsClassifyPageReqVO extends PageParam { | |||
//分类名称 | |||
classifyName?: string; | |||
//创建时间 | |||
createTime?: string[]; | |||
} |
@@ -13,6 +13,12 @@ export * from './error-code.data.ts' | |||
export * from './data-source.data.ts' | |||
export * from './redis.data.ts' | |||
export * from './api-log.data.ts' | |||
export * from './oauth2.data.ts' | |||
export * from './platform.data.ts' | |||
export * from './category.data.ts' | |||
export * from './material-classify.data.ts' | |||
export * from './goods-classify.data.ts' | |||
export * from './goods-attr.data.ts' | |||
export interface ResponseDTO<T>{ | |||
code: number; | |||
@@ -0,0 +1,15 @@ | |||
export interface MaterialClassifyVO {\ | |||
id: number; | |||
classifyName: string; | |||
createTime: Date; | |||
isLeaf?: string; | |||
level?: string; | |||
parentId?: number; | |||
childrens?: MaterialClassifyVO[]; | |||
} | |||
export interface MaterialClassifyPageReqVO extends PageParam { | |||
classifyName?: string; | |||
createTime?: string[]; | |||
parentId?: number; | |||
} |
@@ -0,0 +1,42 @@ | |||
export interface OAuth2TokenVO { | |||
id: number | |||
accessToken: string | |||
refreshToken: string | |||
userId: number | |||
userType: number | |||
clientId: string | |||
createTime: Date | |||
expiresTime: Date | |||
} | |||
export interface OAuth2TokenPageReqVO extends PageParam { | |||
userId?: number | |||
userType?: number | |||
clientId?: string | |||
} | |||
export interface OAuth2ClientVO { | |||
id: number | |||
clientId: string | |||
secret: string | |||
name: string | |||
logo: string | |||
description: string | |||
status: number | |||
accessTokenValiditySeconds: number | |||
refreshTokenValiditySeconds: number | |||
redirectUris: string[] | |||
autoApprove: boolean | |||
authorizedGrantTypes: string[] | |||
scopes: string[] | |||
authorities: string[] | |||
resourceIds: string[] | |||
additionalInformation: string | |||
isAdditionalInformationJson: boolean | |||
createTime: Date | |||
} | |||
export interface OAuth2ClientPageReqVO extends PageParam { | |||
name?: string | |||
status?: number | |||
} |
@@ -1,32 +1,256 @@ | |||
/** | |||
* SHProductVO 虾皮商品 | |||
*/ | |||
export interface SHProductVO { | |||
/** | |||
* 品牌id(类目属性冗余过来,这个必填) | |||
*/ | |||
brandId: number; | |||
/** | |||
* 类目id | |||
*/ | |||
catePubId: string; | |||
/** | |||
* 类目名称 | |||
*/ | |||
catePubName: string; | |||
/** | |||
* 项目状况,可以是新的或二手的 | |||
*/ | |||
conditionVal?: string; | |||
/** | |||
* 创建时间 | |||
*/ | |||
createTime: Date; | |||
/** | |||
* 保证发货订单的天数 | |||
*/ | |||
daysToShip?: number; | |||
/** | |||
* 是否删除 0:未删除 1:删除 | |||
*/ | |||
deleted: boolean; | |||
/** | |||
* 描述-需要确认大小 | |||
*/ | |||
description: string; | |||
/** | |||
* description_type(正常,扩展)--- 不要了,白名单卖家字段关联,那个先不管 | |||
*/ | |||
descriptionType?: string; | |||
/** | |||
* 虾皮折扣活动ID | |||
*/ | |||
discountId?: number; | |||
/** | |||
* (仅适用于 BR 本地卖家)全球贸易项目代码。如果已上传,请通过get_item_base_info api 检查项目的gtin_code | |||
*/ | |||
gtinCode?: string; | |||
/** | |||
* 自增id | |||
*/ | |||
id: number; | |||
/** | |||
* 图片 | |||
*/ | |||
imgUrl: string; | |||
/** | |||
* 产品备货数量 | |||
*/ | |||
inventory?: number; | |||
/** | |||
* 商品是否为预购商品(0.否 1.是) | |||
*/ | |||
isPreOrder: number; | |||
/** | |||
* 是否已刊登到店铺1: 是, 2: 否 3.仍需手动重新刊登 | |||
*/ | |||
isPublish: string; | |||
/** | |||
* 此字段仅适用于印度尼西亚和马来西亚的本地卖家。使用此字段可确定产品是否为危险产品。0 表示非危险品,1 表示危险品。有关更多信息,请访问市场相应的卖家教育中心。) | |||
*/ | |||
itemDangerous?: string; | |||
/** | |||
* 产品id | |||
*/ | |||
itemId?: number; | |||
/** | |||
* 产品名称 | |||
*/ | |||
itemName: string; | |||
/** | |||
* 产品状态(UNLIST:下架状态; NORMAL:上架状态) | |||
*/ | |||
itemStatus?: string; | |||
/** | |||
* 产品价格 | |||
*/ | |||
originalPrice?: number; | |||
/** | |||
* 单价折扣 | |||
*/ | |||
priceDiscount?: number; | |||
/** | |||
* 刊登失败原因 | |||
*/ | |||
publishErrInfo?: string; | |||
/** | |||
* 刊登时间 | |||
*/ | |||
publishTime?: Date; | |||
/** | |||
* 用于错误跟踪的 API 请求的标识符 | |||
*/ | |||
requestId?: string; | |||
/** | |||
* 店铺id | |||
*/ | |||
shopId: number; | |||
/** | |||
* 尺码ID | |||
*/ | |||
sizeId?: number; | |||
/** | |||
* 产品spu编码 | |||
*/ | |||
spuCode: string; | |||
/** | |||
* 成品spuCode | |||
*/ | |||
tbSpuCodes: string[]; | |||
/** | |||
* 模版Id | |||
*/ | |||
templateId: number; | |||
/** | |||
* 模版名称 | |||
*/ | |||
templateName: string; | |||
} | |||
export interface SHProductPageReqVO extends PageParam { | |||
/** | |||
* 品牌id(类目属性冗余过来,这个必填) | |||
*/ | |||
brandId?: number; | |||
/** | |||
* 类目id | |||
*/ | |||
catePubId?: string; | |||
/** | |||
* 项目状况,可以是新的或二手的 | |||
*/ | |||
conditionVal?: string; | |||
/** | |||
* 创建时间 | |||
*/ | |||
createTime?: string[]; | |||
/** | |||
* 保证发货订单的天数 | |||
*/ | |||
daysToShip?: number; | |||
/** | |||
* 描述-需要确认大小 | |||
*/ | |||
description?: string; | |||
/** | |||
* description_type(正常,扩展)--- 不要了,白名单卖家字段关联,那个先不管 | |||
*/ | |||
descriptionType?: string; | |||
/** | |||
* 虾皮折扣活动ID | |||
*/ | |||
discountId?: number; | |||
/** | |||
* (仅适用于 BR 本地卖家)全球贸易项目代码。如果已上传,请通过get_item_base_info api 检查项目的gtin_code | |||
*/ | |||
gtinCode?: string; | |||
/** | |||
* 产品备货数量 | |||
*/ | |||
inventory?: number; | |||
/** | |||
* 商品是否为预购商品(0.否 1.是) | |||
*/ | |||
isPreOrder?: number; | |||
/** | |||
* 是否已刊登到店铺1: 是, 2: 否 3.仍需手动重新刊登 | |||
*/ | |||
isPublish?: string; | |||
/** | |||
* 此字段仅适用于印度尼西亚和马来西亚的本地卖家。使用此字段可确定产品是否为危险产品。0 表示非危险品,1 表示危险品。有关更多信息,请访问市场相应的卖家教育中心。) | |||
*/ | |||
itemDangerous?: string; | |||
/** | |||
* 产品id | |||
*/ | |||
itemId?: number; | |||
/** | |||
* 产品名称 | |||
*/ | |||
itemName?: string; | |||
/** | |||
* 产品状态(UNLIST:下架状态; NORMAL:上架状态) | |||
*/ | |||
itemStatus?: string; | |||
/** | |||
* 产品价格 | |||
*/ | |||
originalPrice?: number; | |||
/** | |||
* 单价折扣 | |||
*/ | |||
priceDiscount?: number; | |||
/** | |||
* 刊登失败原因 | |||
*/ | |||
publishErrInfo?: string; | |||
/** | |||
* 刊登时间 | |||
*/ | |||
publishTime?: string[]; | |||
/** | |||
* 用于错误跟踪的 API 请求的标识符 | |||
*/ | |||
requestId?: string; | |||
/** | |||
* 店铺id | |||
*/ | |||
shopId?: number; | |||
/** | |||
* 尺码ID | |||
*/ | |||
sizeId?: number; | |||
/** | |||
* 产品spu编码 | |||
*/ | |||
spuCode?: string; | |||
/** | |||
* 模版Id | |||
*/ | |||
templateId?: number; | |||
} | |||
export interface UpdateSHProductNameReqVO { | |||
id: number; | |||
spuCode: string; | |||
gpts: ProductKeywordGpt[]; | |||
} | |||
export interface ProductKeywordGpt { | |||
dynamicTableName: string; | |||
id: number; | |||
idList: string[], | |||
keyword: string; | |||
keywordType: number; | |||
lang: string; | |||
spuCode: string; | |||
tableName: string; | |||
} | |||
export interface ShpopeeProductVO { | |||
catePubId: string; | |||
catePubName: string; | |||
export interface UpdateSHProductPriceReqVO { | |||
id: number; | |||
imgUrl: string; | |||
isDelete: number; | |||
isPublish: number; | |||
itemId: number; | |||
itemName: string; | |||
originalPrice: number; | |||
publishErrInfo: string; | |||
publishTime: string; | |||
requestId: string; | |||
spuCode: string; | |||
tbProductKeywordGpts: ProductKeywordGpt[]; | |||
tbSpuCodes: string[], | |||
templateId: number; | |||
templateName: string; | |||
} | |||
export interface UpdateSHProductDescReqVO { | |||
id: number; | |||
description: string; | |||
} |
@@ -0,0 +1,82 @@ | |||
/** | |||
* ShopCreateReqDTO,管理后台 - 平台店铺创建 Request DTO | |||
*/ | |||
export interface PlatformShop { | |||
/** | |||
* 授权过期时间 | |||
*/ | |||
expiresTime?: number; | |||
/** | |||
* 调用参数_动态_空值 | |||
*/ | |||
paramDynamicJson?: string; | |||
/** | |||
* 调用参数_静态_空值 | |||
*/ | |||
paramStaticJson?: string; | |||
/** | |||
* 密码(AES加密 CBC模式) | |||
*/ | |||
password?: string; | |||
/** | |||
* 平台ID | |||
*/ | |||
platformId: number; | |||
/** | |||
* 平台店铺id | |||
*/ | |||
platformShopId?: string; | |||
shopConfig: PlatformShopConfig; | |||
/** | |||
* 店铺名称 | |||
*/ | |||
shopName: string; | |||
/** | |||
* 状态: 1:开启,2,停用 | |||
*/ | |||
shopStatus: number; | |||
/** | |||
* 授权状态,1:成功, 2:失败,0:未授权 | |||
*/ | |||
tokenFlag: number; | |||
/** | |||
* 用户账号 | |||
*/ | |||
userName?: string; | |||
} | |||
/** | |||
* ShopConfigCreateReqDTO,管理后台 - 平台店铺配置创建 Request DTO | |||
*/ | |||
export interface PlatformShopConfig { | |||
/** | |||
* 每天最大刊登量 | |||
*/ | |||
dayMaxPublishNumber?: number; | |||
/** | |||
* ioss税号(欧盟税号) | |||
*/ | |||
iossNo?: string; | |||
/** | |||
* 语言 | |||
*/ | |||
language?: string; | |||
/** | |||
* 店铺_id | |||
*/ | |||
shopId: number; | |||
/** | |||
* vat税号 | |||
*/ | |||
vatNum?: string; | |||
} | |||
export interface PlatformShopPageReqVO extends PageParam { | |||
shopName?: string, | |||
shopStatus?: number, | |||
tokenFlag?: number, | |||
platformId?: number, | |||
shopManagerId?: number, | |||
shopStaffId?: number | |||
} |
@@ -1,28 +1,356 @@ | |||
//数据字典 | |||
export interface DataDictVO{ | |||
//模板字典 | |||
export interface TemplateDictVO{ | |||
description: string; | |||
id: number; | |||
name: string; | |||
createTime?: number; | |||
} | |||
//数据字典详情 | |||
export interface DataDictDetailVO { | |||
//模板字典详情 | |||
export interface TemplateDictDetailVO { | |||
dictId: number; | |||
dictSort: number; | |||
id: number; | |||
label: string; | |||
value: string; | |||
createTime: number; | |||
} | |||
export interface ShopeeTemplateVO { | |||
/** | |||
* TemplateInfoVO,模板数据 | |||
*/ | |||
export interface TemplateInfoVO { | |||
/** | |||
* 背景ids | |||
*/ | |||
backgroupIds?: number[]; | |||
/** | |||
* 类别 | |||
*/ | |||
catePubId: string; | |||
/** | |||
* 每天最大刊登量 | |||
*/ | |||
dayMaxPublishNumber?: number; | |||
/** | |||
* 模版id | |||
*/ | |||
id: number; | |||
categoryName: string; | |||
createName: string; | |||
createTime: string; | |||
/** | |||
* 图片信息 | |||
*/ | |||
imageInfo: TemplateImgVO[]; | |||
/** | |||
* 物流渠道 | |||
*/ | |||
logistics: TemplateLogisticsVO[]; | |||
/** | |||
* 素材组ids | |||
*/ | |||
materialClassifyIds?: number[]; | |||
/** | |||
* 店铺最大上架数量 | |||
*/ | |||
maxPrintCount: number; | |||
/** | |||
* 产品价格 | |||
*/ | |||
originalPrice: number; | |||
/** | |||
* 平台编码 | |||
*/ | |||
platformCode: string; | |||
remark: string; | |||
shopName: string; | |||
/** | |||
* 单价折扣 | |||
*/ | |||
priceDiscount?: number; | |||
/** | |||
* 变种比例 | |||
*/ | |||
proportion?: number; | |||
/** | |||
* 模板备注 | |||
*/ | |||
remark?: string; | |||
/** | |||
* 店铺id | |||
*/ | |||
shopId: number; | |||
/** | |||
* 类目属性 | |||
*/ | |||
templateAttrs: TemplateAttrInfoVO[]; | |||
templateDimension: TemplateDimensionVO; | |||
/** | |||
* 模板名称编码 | |||
*/ | |||
templateName: string; | |||
templateProduct: TemplateProductVO; | |||
templateSkuSpace?: TemplateSkuSpaceVO; | |||
/** | |||
* 图片信息 | |||
*/ | |||
videoInfo: TemplateVideoVO[]; | |||
} | |||
/** | |||
* TemplateImgVO,模板图片信息 | |||
*/ | |||
export interface TemplateImgVO { | |||
/** | |||
* 图片id | |||
*/ | |||
id?: number; | |||
/** | |||
* 图片Url信息 | |||
*/ | |||
url: string; | |||
} | |||
/** | |||
* TemplateLogisticsVO,模板物流通道 | |||
*/ | |||
export interface TemplateLogisticsVO { | |||
/** | |||
* 是否为此项目启用通道(0.否 1.是) | |||
*/ | |||
enabled: number; | |||
/** | |||
* 是否为买家支付运费(0.否 1.是) | |||
*/ | |||
isFree?: number; | |||
/** | |||
* 频道的 ID | |||
*/ | |||
logisticId: number; | |||
/** | |||
* 运费 | |||
*/ | |||
shippingFee?: number; | |||
/** | |||
* 大小 ID | |||
*/ | |||
sizeId?: number; | |||
} | |||
/** | |||
* TemplateAttrInfoVO,模板属性信息封装VO | |||
*/ | |||
export interface TemplateAttrInfoVO { | |||
templateAttr: TemplateAttrVO; | |||
/** | |||
* 模板属性值信息 | |||
*/ | |||
templateAttrVals: TemplateAttrValVO[]; | |||
} | |||
/** | |||
* TemplateAttrVO,模板属性信息 | |||
*/ | |||
export interface TemplateAttrVO { | |||
/** | |||
* 属性ID | |||
*/ | |||
attrId: number; | |||
/** | |||
* 属性名称 | |||
*/ | |||
attrName: string; | |||
} | |||
/** | |||
* TemplateAttrValVO,模板属性值信息 | |||
*/ | |||
export interface TemplateAttrValVO { | |||
/** | |||
* 属性值ID | |||
*/ | |||
attrValId: number; | |||
/** | |||
* 属性值名称 | |||
*/ | |||
attrValName: string; | |||
/** | |||
* 价值单位(仅限定量属性) | |||
*/ | |||
valueUnit?: string; | |||
} | |||
/** | |||
* TemplateDimensionVO,模板包装尺寸 | |||
*/ | |||
export interface TemplateDimensionVO { | |||
/** | |||
* 包装高度,单位为厘米 | |||
*/ | |||
packageHeight: number; | |||
/** | |||
* 包装长度,单位为厘米 | |||
*/ | |||
packageLength: number; | |||
/** | |||
* 包装宽度,单位为厘米 | |||
*/ | |||
packageWidth: number; | |||
/** | |||
* 重量,单位为千克 | |||
*/ | |||
weight: number; | |||
} | |||
/** | |||
* TemplateProductVO,模板产品信息 | |||
*/ | |||
export interface TemplateProductVO { | |||
/** | |||
* 品牌ID | |||
*/ | |||
brandId?: number; | |||
/** | |||
* 品牌名称 | |||
*/ | |||
brandName?: string; | |||
/** | |||
* 项目的状况,可以是二手的或新的 | |||
*/ | |||
conditionVal?: string; | |||
/** | |||
* 保证发货数量 | |||
*/ | |||
daysToShip?: number; | |||
/** | |||
* 描述数据字典ID | |||
*/ | |||
description: string; | |||
/** | |||
* 产品Id | |||
*/ | |||
id: number; | |||
/** | |||
* 商品是否为预购商品(0.否 1.是) | |||
*/ | |||
isPreOrder: number; | |||
/** | |||
* 产品名称数据字典ID | |||
*/ | |||
itemName: string; | |||
/** | |||
* 产品状态 | |||
*/ | |||
itemStatus: string; | |||
} | |||
/** | |||
* TemplateSkuSpaceVO,模板sku | |||
*/ | |||
export interface TemplateSkuSpaceVO { | |||
/** | |||
* 销售数量 | |||
*/ | |||
inventory?: number; | |||
/** | |||
* 价格 | |||
*/ | |||
originalPrice?: number; | |||
/** | |||
* 尺码 | |||
*/ | |||
sizeId?: number; | |||
/** | |||
* 批发价格 | |||
*/ | |||
templateWholesale?: TemplateWholesaleVO[]; | |||
} | |||
/** | |||
* TemplateWholesaleVO,模板批发价格 | |||
*/ | |||
export interface TemplateWholesaleVO { | |||
/** | |||
* 此层的最大计数 | |||
*/ | |||
maxCount: number; | |||
/** | |||
* 此层的最小计数 | |||
*/ | |||
minCount: number; | |||
/** | |||
* 价格 | |||
*/ | |||
unitPrice: number; | |||
} | |||
/** | |||
* TemplateVideoVO,模板视频信息 | |||
*/ | |||
export interface TemplateVideoVO { | |||
/** | |||
* 视频id | |||
*/ | |||
id?: number; | |||
/** | |||
* 视频信息 | |||
*/ | |||
url: string; | |||
} | |||
//模板分页请求参数 | |||
export interface TemplateInfoPageReqVO extends PageParam { | |||
/** | |||
* 背景id(a,b,c) | |||
*/ | |||
backgroupIds?: string; | |||
/** | |||
* 类目id | |||
*/ | |||
catePubId?: string; | |||
/** | |||
* 创建时间 | |||
*/ | |||
createTime?: string[]; | |||
/** | |||
* 素材id(a,b,c) | |||
*/ | |||
materialClassifyIds?: string; | |||
/** | |||
* 最大上刊数量 | |||
*/ | |||
maxPrintCount?: number; | |||
/** | |||
* 平台编码 | |||
*/ | |||
platformCode?: string; | |||
/** | |||
* 变种比例 | |||
*/ | |||
proportion?: number; | |||
/** | |||
* 备注 | |||
*/ | |||
remark?: string; | |||
/** | |||
* 店铺id | |||
*/ | |||
shopId?: number; | |||
/** | |||
* 模板名称 | |||
*/ | |||
templateName?: string; | |||
} | |||
export interface TemplateDictPageReqVO extends PageParam { | |||
createTime?: string[]; | |||
description?: string; | |||
name?: string; | |||
} | |||
export interface TemplateDictDetaiPageReqVO extends PageParam { | |||
createTime?: string[]; | |||
dictId?: number; | |||
dictSort?: number; | |||
label?: string; | |||
value?: string; | |||
} |
@@ -1,18 +1,17 @@ | |||
import React, { useEffect, useState } from 'react'; | |||
import { Space, Table, Form, Button, Card, Input, TreeSelect, Image } from 'antd'; | |||
import type { ColumnsType } from 'antd/es/table'; | |||
import { t } from '@/utils/i18n'; | |||
import React, { useState } from 'react'; | |||
import { useNavigate } from 'react-router-dom'; | |||
import { | |||
ExclamationCircleFilled, | |||
PlusOutlined, | |||
CarryOutOutlined, | |||
SearchOutlined, | |||
UndoOutlined | |||
} from '@ant-design/icons'; | |||
import { antdUtils } from '@/utils/antd'; | |||
import { ShpopeeProductVO } from '@/models'; | |||
import mockData from '../../../../../mock/shopeeProduct.json' | |||
import { useSetState } from 'ahooks'; | |||
import { SHProductVO, SHProductPageReqVO } from '@/models'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import shopeeProductService from '@/request/service/shopee-product'; | |||
const treeData = [ | |||
{ | |||
@@ -55,35 +54,30 @@ const treeData = [ | |||
export default () => { | |||
const showDeleteConfirm = (item: ShpopeeProductVO) => { | |||
antdUtils.modal?.confirm({ | |||
title: `确认删除编码为: ${item.spuCode} 的产品吗?`, | |||
icon: <ExclamationCircleFilled />, | |||
content: `请注意删除以后不可恢复!`, | |||
okText: '删除', | |||
okType: 'danger', | |||
cancelText: '取消', | |||
onOk() { | |||
return new Promise((resolve, reject) => { | |||
setTimeout(() => { | |||
antdUtils.message?.open({ | |||
type: 'success', | |||
content: '删除成功', | |||
}); | |||
resolve(null) | |||
}, 1000); | |||
//TODO: batch update title、price、desc | |||
}).catch(() => antdUtils.message?.open({ | |||
type: 'error', | |||
content: '操作失败', | |||
})); | |||
}, | |||
onCancel() { | |||
}, | |||
}); | |||
}; | |||
const [dataSource, setDataSource] = useState<SHProductVO[]>([]); | |||
const [searchFrom] = Form.useForm(); | |||
const [searchState, setSearchState] = useSetState<SHProductPageReqVO>({}); | |||
const [total, setTotal] = useState(0); | |||
const { runAsync: getPageApi } = useRequest(shopeeProductService.pageApi, { manual: true }); | |||
const { runAsync: batchUpdateNameApi } = useRequest(shopeeProductService.batchUpdateNameApi, { manual: true }); | |||
const { runAsync: batchUpdateDescApi } = useRequest(shopeeProductService.batchUpdateDescApi, { manual: true }); | |||
const { runAsync: batchUpdatePriceApi } = useRequest(shopeeProductService.batchUpdatePriceApi, { manual: true }); | |||
const columns: ColumnsType<ShpopeeProductVO> = [ | |||
const load = async () => { | |||
searchFrom.setFieldValue("pageSize", searchState.pageSize); | |||
searchFrom.setFieldValue("pageNo", searchState.pageNo); | |||
//TODO: merge search params | |||
const [error, { data }] = await getPageApi(searchFrom.getFieldsValue()); | |||
if (!error) { | |||
setDataSource(data.list); | |||
setTotal(data.total); | |||
} | |||
} | |||
const columns: ColumnsType<SHProductVO> = [ | |||
{ | |||
title: '缩略图', | |||
dataIndex: 'imgUrl', | |||
@@ -127,30 +121,17 @@ export default () => { | |||
title: '刊登时间', | |||
key: 'publishTime', | |||
dataIndex: 'publishTime' | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
key: 'action', | |||
render: (_, record) => ( | |||
<Space size="middle"> | |||
<a onClick={() => { | |||
}}>编辑</a> | |||
<a onClick={() => { | |||
showDeleteConfirm(record) | |||
}}>删除</a> | |||
</Space> | |||
), | |||
}, | |||
} | |||
]; | |||
const [searchFrom] = Form.useForm(); | |||
const navigate = useNavigate(); | |||
const [treeLine, setTreeLine] = useState(true); | |||
const [showLeafIcon, setShowLeafIcon] = useState(false); | |||
const [showIcon, setShowIcon] = useState<boolean>(false); | |||
useEffect(() => { | |||
load(); | |||
}, [searchFrom, searchState]); | |||
return ( | |||
<div> | |||
<div> | |||
@@ -182,8 +163,17 @@ export default () => { | |||
</div> | |||
</Card> | |||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||
<Table rowKey="id" scroll={{ x: true }} columns={columns} dataSource={mockData as ShpopeeProductVO[]} className='bg-transparent' | |||
pagination={{ position: ['bottomRight'] }} | |||
<Table rowKey="id" | |||
scroll={{ x: true }} | |||
columns={columns} | |||
dataSource={dataSource} | |||
className='bg-transparent' | |||
pagination={{ | |||
position: ['bottomRight'], | |||
current: searchState.pageNo, | |||
pageSize: searchState.pageSize, | |||
total | |||
}} | |||
/> | |||
</Card> | |||
</div> | |||
@@ -0,0 +1,205 @@ | |||
import React, { useState, useEffect, useRef } from 'react'; | |||
import { Tree, Space, Input } from 'antd'; | |||
import type { InputRef } from 'antd'; | |||
import type { DataNode, TreeProps } from 'antd/es/tree'; | |||
import { t } from '@/utils/i18n'; | |||
import { DownOutlined, EditOutlined, DeleteOutlined, PlusOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import { antdUtils } from '@/utils/antd'; | |||
import { MaterialClassifyVO } from '@/models'; | |||
import classifyService from '@/request/service/material-classify'; | |||
import { Key } from 'antd/lib/table/interface'; | |||
interface TreeDataNode extends DataNode { | |||
parentId? : Key; | |||
isNewNode: boolean | |||
} | |||
const convert2DataNode = (source: MaterialClassifyVO) => { | |||
const node: TreeDataNode = { | |||
title: source.classifyName, | |||
key: source.id, | |||
parentId: source.parentId??-1, | |||
isNewNode: false | |||
}; | |||
if (source.childrens && source.childrens.length > 0) { | |||
node.children = source.childrens.map(it => convert2DataNode(it)) | |||
} | |||
return node; | |||
} | |||
export default () => { | |||
const { runAsync: getTreeApi } = useRequest(classifyService.getMaterialClassifyTreeApi, { manual: true }); | |||
const { runAsync: deleteApi } = useRequest(classifyService.deleteMaterialClassifyApi, { manual: true }); | |||
const { runAsync: updateApi } = useRequest(classifyService.updateMaterialClassifyApi, { manual: true }); | |||
const { runAsync: createApi } = useRequest(classifyService.createMaterialClassifyApi, { manual: true }); | |||
const [dataSource, setDataSource] = useState<TreeDataNode[]>([]); | |||
const [selectedItem, setSelectedItem] = useState<Key | null>(null); | |||
const [hoveredItem, setHoveredItem] = useState<Key | null>(null); | |||
const [editItem, setEditItem] = useState<TreeDataNode | null>(null); | |||
const [expandedKeys, setExpandedKeys] = useState<Key[]>([]); | |||
const inputRef = useRef<InputRef>(null); | |||
const [inputStatus, setInputStatus] = useState<"" | "error" | "warning">(""); | |||
const load = async () => { | |||
const [error, { data }] = await getTreeApi(); | |||
if (!error) { | |||
setDataSource([{ | |||
title: '全部素材', | |||
key: -1, | |||
isNewNode: false, | |||
children: data.map(it => convert2DataNode(it)) | |||
}]); | |||
} | |||
}; | |||
const onSelect: TreeProps['onSelect'] = (selectedKeys) => { | |||
if (selectedKeys.length > 0) { | |||
setSelectedItem(selectedKeys[0]); | |||
} else { | |||
setSelectedItem(null); | |||
} | |||
}; | |||
const onHover = (node: TreeDataNode) => { | |||
setHoveredItem(node.key); | |||
} | |||
useEffect(() => { | |||
load(); | |||
}, []); | |||
const onEdit = (node: TreeDataNode) => { | |||
// 编辑节点title | |||
onQuitEdit(); | |||
setEditItem(node); | |||
}; | |||
const onAdd = (node: TreeDataNode) => { | |||
// 新增子节点 | |||
if (!node.children) { | |||
node.children = []; | |||
} | |||
const subNode: TreeDataNode = { | |||
title: '新增分类', | |||
key: new Date().getTime(), | |||
parentId: node.key, | |||
isNewNode: true | |||
}; | |||
node.children?.push(subNode); | |||
setDataSource([...dataSource]); | |||
if (expandedKeys.indexOf(node.key) < 0) { | |||
setExpandedKeys([...expandedKeys, node.key]) | |||
} | |||
onQuitEdit(); | |||
setEditItem(subNode); | |||
}; | |||
const filterNode: (nodes: TreeDataNode[], item: TreeDataNode) => TreeDataNode[] = (nodes: TreeDataNode[], item: TreeDataNode) => { | |||
return nodes.filter(child => child.key !== item.key) | |||
.map(child => { | |||
if (child.children && child.children.length > 0) { | |||
return { ...child, children: filterNode(child.children as TreeDataNode[] || [], item) } | |||
} else { | |||
return { ...child } | |||
} | |||
}); | |||
} | |||
const onDelete = async (node: TreeDataNode) => { | |||
// 删除子节点 | |||
if(!node.isNewNode) { | |||
await deleteApi(node.key) | |||
} | |||
const newTreeData = filterNode(dataSource, node) | |||
setDataSource(newTreeData); | |||
}; | |||
const onSaveEditValue = async (node: TreeDataNode) => { | |||
const newValue = inputRef.current?.input?.value; | |||
const api = node.isNewNode ? createApi : updateApi; | |||
const param = node.isNewNode? { | |||
parentId: node.parentId??-1 < 0 ? 0 : node.parentId, | |||
classifyName: inputRef.current?.input?.value | |||
}: { | |||
id: node.key as number, | |||
classifyName: inputRef.current?.input?.value | |||
} | |||
const [error, { code, msg }] = await api(param); | |||
if(!error && code === 0) { | |||
node.title = newValue; | |||
setEditItem(null); | |||
} else { | |||
antdUtils.message?.error(msg); | |||
setInputStatus('error'); | |||
} | |||
} | |||
const onQuitEdit = () => { | |||
if(editItem != null && editItem.isNewNode) { | |||
onDelete(editItem); | |||
} | |||
setEditItem(null); | |||
} | |||
const shouldShowActions = (key: Key) => { | |||
return key === selectedItem || key === hoveredItem; | |||
}; | |||
const renderEditItem = (node: TreeDataNode) => { | |||
return ( | |||
<Space> | |||
<Input size='small' status={inputStatus} ref={inputRef} defaultValue={node.title as string} /> | |||
<CheckOutlined onClick={() => onSaveEditValue(node)} /> | |||
<CloseOutlined onClick={onQuitEdit} /> | |||
</Space> | |||
) | |||
} | |||
const renderItem = (node: TreeDataNode) => { | |||
return (<div className='flex' | |||
onMouseEnter={() => { onHover(node) }} | |||
onMouseLeave={() => { setHoveredItem(null) }}> | |||
{ | |||
editItem?.key === node.key ? (renderEditItem(node)) : node.title as string | |||
} | |||
{ | |||
shouldShowActions(node.key) && editItem?.key !== node.key ? (<div className='ml-4'> | |||
<Space> | |||
<PlusOutlined onClick={() => onAdd(node)} /> | |||
{ | |||
node.key !== -1 ? (<> | |||
<EditOutlined onClick={() => onEdit(node)} /> | |||
<DeleteOutlined onClick={() => onDelete(node)} /> | |||
</>) : null | |||
} | |||
</Space> | |||
</div>) : null | |||
} | |||
</div>) | |||
} | |||
const onExpand: TreeProps['onExpand'] = (expandedKeys) => { | |||
console.log(expandedKeys) | |||
setExpandedKeys(expandedKeys) | |||
} | |||
return ( | |||
<> | |||
<div> | |||
<Tree | |||
showLine | |||
switcherIcon={<DownOutlined />} | |||
onSelect={onSelect} | |||
treeData={dataSource} | |||
onExpand={onExpand} | |||
expandedKeys={expandedKeys} | |||
autoExpandParent={true} | |||
titleRender={(node) => renderItem(node)} | |||
> | |||
</Tree> | |||
</div> | |||
</> | |||
); | |||
}; |
@@ -7,6 +7,7 @@ import { t } from '@/utils/i18n'; | |||
import { PlusOutlined, ExclamationCircleFilled, DeleteOutlined, SearchOutlined } from '@ant-design/icons'; | |||
import { antdUtils } from '@/utils/antd'; | |||
import mData from '../../../../../mock/findMaterialPage.json' | |||
import MaterialClassifyView from './classify'; | |||
const { Search } = Input; | |||
@@ -311,6 +312,7 @@ const TablePage: React.FC = () => { | |||
<> | |||
<div className='flex flex-row'> | |||
<Card className='basis-1/4 w-[100px] mb-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||
<MaterialClassifyView/> | |||
</Card> | |||
<Card className='basis-3/4 mb-[10px] ml-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]' bodyStyle={{ | |||
paddingTop: 0, | |||
@@ -1,18 +1,18 @@ | |||
import React, { useEffect, useMemo, useState } from 'react' | |||
import { CloseOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'; | |||
import { Drawer, Form, Input, Card, Space, Button, Upload, Popconfirm, Modal, FormListOperation, FormListFieldData } from 'antd' | |||
import { Drawer, Form, Input, Card, Space, Button, Upload, Popconfirm, Modal, FormListOperation, FormListFieldData, Tooltip } from 'antd' | |||
import type { RcFile, UploadProps } from 'antd/es/upload'; | |||
import type { UploadFile } from 'antd/es/upload/interface'; | |||
import './editor.css'; | |||
const layout = { | |||
labelCol: { span: 4, }, | |||
wrapperCol: { span: 16 }, | |||
bordered: false, | |||
labelCol: { span: 5, }, | |||
wrapperCol: { span: 15 }, | |||
}; | |||
interface AttributeValue { | |||
id: number; | |||
attrId: number; | |||
id?: number; | |||
attrId?: number; | |||
valName: string; | |||
imgId?: number; | |||
imgUrl?: string; | |||
@@ -29,7 +29,6 @@ export interface SampleAttribute { | |||
interface CreateSampleAttrProps { | |||
visible: boolean; | |||
onCancel: (flag?: boolean) => void; | |||
curRecord?: SampleAttribute[] | null; | |||
onSave: () => void; | |||
editData?: SampleAttribute[] | null; | |||
} | |||
@@ -45,13 +44,15 @@ const getBase64 = (file: RcFile): Promise<string> => | |||
const SampleAttrEditor: React.FC<CreateSampleAttrProps> = (props) => { | |||
const { visible, onCancel, curRecord, onSave, editData } = props; | |||
const { visible, onCancel, onSave, editData } = props; | |||
const [saveLoading, setSaveLoading] = useState(false); | |||
const [previewOpen, setPreviewOpen] = useState(false); | |||
const [previewImage, setPreviewImage] = useState(''); | |||
const [previewTitle, setPreviewTitle] = useState(''); | |||
const [form] = Form.useForm(); | |||
const isEdit = !!editData; | |||
useEffect(() => { | |||
if (visible) { | |||
setInitValue(); | |||
@@ -61,6 +62,9 @@ const SampleAttrEditor: React.FC<CreateSampleAttrProps> = (props) => { | |||
}, [visible]); | |||
async function setInitValue() { | |||
if (editData) { | |||
form.setFieldsValue({ attrData: editData }); | |||
} | |||
} | |||
const save = async (values: any) => { | |||
@@ -83,60 +87,80 @@ const SampleAttrEditor: React.FC<CreateSampleAttrProps> = (props) => { | |||
}; | |||
const uploadButton = ( | |||
<div> | |||
<div className='flex'> | |||
<PlusOutlined /> | |||
<div style={{ marginTop: 8 }}>上传</div> | |||
<div style={{ marginLeft: 8 }}>上传</div> | |||
</div> | |||
); | |||
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { | |||
// setFileList(newFileList); | |||
} | |||
const readerAttrValueEditForm = (subFields: FormListFieldData[], {remove, add}: FormListOperation, isContainImg: boolean) => { | |||
return (<div style={{ display: 'flex', flexDirection: 'column', rowGap: 16 }}> | |||
{subFields.map((subField) => ( | |||
<Space key={subField.key}> | |||
<Form.Item noStyle name={[subField.name, 'valName']}> | |||
<Input size='middle'/> | |||
</Form.Item> | |||
{isContainImg && <Form.Item noStyle shouldUpdate name={[subField.name, 'imgUrl']}> | |||
<Upload | |||
action="https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188" | |||
listType="picture-card" | |||
onPreview={handlePreview} | |||
onChange={handleChange} | |||
showUploadList={false} | |||
maxCount={1} | |||
> | |||
{uploadButton} | |||
</Upload> | |||
</Form.Item> | |||
} | |||
<Popconfirm | |||
title="删除属性值" | |||
description="确认是否删除当前属性值?" | |||
onConfirm={() => { remove(subField.name); }} | |||
okText="确认" | |||
cancelText="取消" | |||
> | |||
<CloseOutlined/> | |||
</Popconfirm> | |||
</Space> | |||
))} | |||
<Button type="dashed" size='middle' onClick={() => add()} block> | |||
+ 添加自定义值 | |||
</Button> | |||
</div>) | |||
const readerAttrValueEditForm = (subFields: FormListFieldData[], attrVals: AttributeValue[], { remove, add }: FormListOperation, isContainImg: boolean) => { | |||
return ( | |||
<div style={{ display: 'flex', flexDirection: 'column', rowGap: 16 }}> | |||
{subFields.map((subField) => { | |||
const item = attrVals[subField.key]; | |||
const uploadFile: UploadFile | null = item && item.imgUrl && item.imgId ? { | |||
uid: `${item.imgId}`, | |||
name: '', | |||
status: 'done', | |||
url: item.imgUrl | |||
} : null | |||
const fileList: UploadFile[] = []; | |||
if (uploadFile) { | |||
fileList.push(uploadFile) | |||
} | |||
return ( | |||
<Form.Item {...subField} style={{marginBottom: 0}}> | |||
<Space key={subField.key}> | |||
<Form.Item noStyle name={[subField.name, 'valName']}> | |||
<Input size='middle' /> | |||
</Form.Item> | |||
{isContainImg && <Form.Item noStyle shouldUpdate name={[subField.name, 'imgUrl']}> | |||
<Upload className='attr-image' | |||
action="https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188" | |||
listType="picture-card" | |||
onPreview={handlePreview} | |||
onChange={handleChange} | |||
maxCount={1} | |||
defaultFileList={[...fileList]} | |||
> | |||
{fileList.length > 0 ? null : uploadButton} | |||
</Upload> | |||
</Form.Item> | |||
} | |||
<Popconfirm | |||
title="删除属性值" | |||
description="确认是否删除当前属性值?" | |||
onConfirm={() => { remove(subField.name); }} | |||
okText="确认" | |||
cancelText="取消" | |||
> | |||
<Tooltip title="删除属性值"> | |||
<DeleteOutlined className='delete-icon'/> | |||
</Tooltip> | |||
</Popconfirm> | |||
</Space> | |||
</Form.Item> | |||
) | |||
})} | |||
<Button type="dashed" size='middle' onClick={() => add({ valName: '', imgUrl: '', imgId: 0 })} block> | |||
+ 添加自定义值 | |||
</Button> | |||
</div>) | |||
} | |||
const renderAttrEditForm = (fields: FormListFieldData[], {remove, add}: FormListOperation, dataSource: SampleAttribute[]) => { | |||
return ( | |||
<div style={{ display: 'flex', rowGap: 16, flexDirection: 'column' }}> | |||
{fields.map((field) => { | |||
const data = dataSource[field.key] | |||
return ( | |||
<Card size="small" title={field.name} key={field.key} | |||
extra={ | |||
const renderAttrEditForm = (fields: FormListFieldData[], { remove, add }: FormListOperation, dataSource: SampleAttribute[]) => { | |||
return ( | |||
<div style={{ display: 'flex', rowGap: 16, flexDirection: 'column' }}> | |||
{fields.map((field) => { | |||
const data = dataSource[field.key]; | |||
return ( | |||
<Form.Item {...field} style={{marginBottom: 4}}> | |||
<Card size="small" title={field.name} key={field.key} | |||
style={{ width: 500 }} | |||
extra={ | |||
<Popconfirm | |||
title="删除属性" | |||
description="确认是否删除当前属性?" | |||
@@ -144,62 +168,71 @@ const SampleAttrEditor: React.FC<CreateSampleAttrProps> = (props) => { | |||
okText="确认" | |||
cancelText="取消" | |||
> | |||
<DeleteOutlined style={{ fontSize: '16px' }} className='hover:(bg-[rgb(94,53,177)]'/> | |||
<Tooltip title="删除属性"> | |||
<CloseOutlined style={{ fontSize: '16px' }} className='delete-icon' /> | |||
</Tooltip> | |||
</Popconfirm> | |||
} | |||
} | |||
> | |||
<Form.Item label="属性名称" name={[field.name, 'attrName']}> | |||
<Input size='middle'/> | |||
<Form.Item label="属性名称" name={[field.name, 'attrName']} labelCol={{ span: 4 }}> | |||
<Input size='middle' /> | |||
</Form.Item> | |||
<Form.Item label="属性值"> | |||
<Form.Item label="属性值" labelCol={{ span: 4 }}> | |||
<Form.List name={[field.name, 'attrVals']}> | |||
{(subFields, subOpt) => readerAttrValueEditForm(subFields, subOpt, data.isContainImg === 1)} | |||
{(subFields, subOpt) => { | |||
return readerAttrValueEditForm(subFields, data.attrVals, subOpt, data.isContainImg === 1) | |||
}} | |||
</Form.List> | |||
</Form.Item> | |||
</Card> | |||
)})} | |||
<Button type="dashed" size='middle' onClick={() => add()} block> | |||
+ 添加属性 | |||
</Button> | |||
</div> | |||
) | |||
</Form.Item> | |||
) | |||
})} | |||
<Button type="dashed" size='middle' onClick={() => add({ | |||
"prototypeId": 88, | |||
"attrName": "", | |||
"isContainImg": 1, | |||
"attrVals": [ | |||
] | |||
},)} block> | |||
+ 添加属性 | |||
</Button> | |||
</div> | |||
) | |||
} | |||
return ( | |||
<> | |||
<Drawer | |||
open={visible} | |||
title="新建" | |||
title={isEdit ? "编辑" : "新建"} | |||
width={640} | |||
onClose={() => { onCancel() }} | |||
extra={ | |||
<Space> | |||
<Button type="primary" size='middle' onClick={() => {save}}> | |||
<Button type="primary" size='middle' onClick={() => { save }}> | |||
保存 | |||
</Button> | |||
</Space> | |||
} | |||
destroyOnClose | |||
> | |||
<Form | |||
form={form} | |||
{...layout} | |||
initialValues={{ editData }} | |||
onFinish={save} | |||
labelCol={{ flex: '0 0 100px' }} | |||
wrapperCol={{ span: 16 }} | |||
> | |||
<Form.List name="editData"> | |||
{(fields, operation) => { | |||
return renderAttrEditForm(fields, operation, editData!!) | |||
}} | |||
</Form.List> | |||
</Form> | |||
</Drawer> | |||
<Form | |||
form={form} | |||
{...layout} | |||
onFinish={save} | |||
labelCol={{ flex: '0 0 100px' }} | |||
wrapperCol={{ span: 16 }} | |||
> | |||
<Form.List name="attrData"> | |||
{(fields, operation) => renderAttrEditForm(fields, operation, form.getFieldsValue()['attrData'])} | |||
</Form.List> | |||
</Form> | |||
</Drawer> | |||
<Modal open={previewOpen} title={previewTitle} footer={null} onCancel={handleCancel}> | |||
<img alt="example" style={{ width: '100%' }} src={previewImage} /> | |||
</Modal> | |||
@@ -0,0 +1,45 @@ | |||
.attr-image { | |||
height: 50px; | |||
} | |||
:where(.attr-image).ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload.ant-upload-select, | |||
:where(.attr-image).ant-upload-wrapper.ant-upload-picture-circle-wrapper .ant-upload.ant-upload-select { | |||
height: 50px; | |||
margin-inline-end: 8px; | |||
margin-bottom: 0; | |||
text-align: center; | |||
vertical-align: top; | |||
background-color: rgba(0, 0, 0, 0.02); | |||
border: 1px dashed #d9d9d9; | |||
border-radius: 4px; | |||
cursor: pointer; | |||
transition: border-color 0.3s; | |||
} | |||
:where(.attr-image).ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item-container, | |||
:where(.attr-image).ant-upload-wrapper.ant-upload-picture-circle-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item-container, | |||
:where(.attr-image).ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload-list.ant-upload-list-picture-circle .ant-upload-list-item-container, | |||
:where(.attr-image).ant-upload-wrapper.ant-upload-picture-circle-wrapper .ant-upload-list.ant-upload-list-picture-circle .ant-upload-list-item-container { | |||
display: inline-block; | |||
width: 50px; | |||
height: 50px; | |||
margin-block: 0 8px; | |||
margin-inline: 0 8px; | |||
margin: 0 2px; | |||
vertical-align: top; | |||
} | |||
:where(.attr-image).ant-upload-wrapper .ant-upload-list.ant-upload-list-picture .ant-upload-list-item, | |||
:where(.attr-image).ant-upload-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item, | |||
:where(.attr-image).ant-upload-wrapper .ant-upload-list.ant-upload-list-picture-circle .ant-upload-list-item { | |||
position: relative; | |||
padding: 1px; | |||
border: 1px solid #d9d9d9; | |||
border-radius: 2px; | |||
} | |||
.delete-icon:hover { | |||
color: #646cffaa; | |||
} |
@@ -1,17 +1,26 @@ | |||
:where(.css-dev-only-do-not-override-11asvft).ant-card .ant-card-body { | |||
.workbench {} | |||
:where(.workbench).ant-card .ant-card-body { | |||
padding: 0; | |||
border-radius: 0 0 8px 8px; | |||
} | |||
:where(.css-dev-only-do-not-override-phnixs).ant-card .ant-card-body { | |||
:where(.workbench).ant-card .ant-card-body { | |||
padding: 0; | |||
border-radius: 0 0 8px 8px; | |||
} | |||
:where(.css-dev-only-do-not-override-phnixs).ant-list-lg .ant-list-item { | |||
:where(.workbench).ant-list-lg .ant-list-item { | |||
padding: 16px 8px; | |||
} | |||
:where(.css-dev-only-do-not-override-11asvft).ant-list-lg .ant-list-item { | |||
:where(.workbench).ant-list-lg .ant-list-item { | |||
padding: 16px 8px; | |||
} | |||
:where(.workbench).ant-list .ant-spin-container { | |||
position: relative; | |||
transition: opacity 0.3s; | |||
margin-top: 10px; | |||
} |
@@ -7,7 +7,7 @@ import 'react-advanced-cropper/dist/style.css'; | |||
import { ImageStencil } from "./components/ImageStencil"; | |||
import { DeleteOutlined, EditOutlined, UploadOutlined } from '@ant-design/icons'; | |||
import cn from 'classnames'; | |||
import './index.css' | |||
import './index.css'; | |||
import property from '../../../../../../mock/propertyById.json' | |||
interface SampleProperty { | |||
@@ -113,8 +113,8 @@ export default () => { | |||
return ( | |||
<div className='flex'> | |||
<Card className='flex-none mb-[10px] w-[270px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||
<div style={{ padding: '20px' }}> | |||
<Card className='flex-none mb-[10px] w-[270px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px] workbench'> | |||
<div style={{ padding: '20px 20px 0 20px' }}> | |||
<Anchor | |||
direction="horizontal" | |||
items={[ | |||
@@ -133,21 +133,21 @@ export default () => { | |||
</div> | |||
<div > | |||
<div > | |||
<div id="part-1" > | |||
<div id="part-1"> | |||
<List dataSource={sample.prototypeImgs.filter(it => it.imgType === 2)} | |||
grid={{ | |||
gutter: 2, | |||
column: 2 | |||
}} | |||
header={( | |||
<div className='flex justify-center'> | |||
<div className='flex justify-center' > | |||
<Upload {...props}> | |||
<Button icon={<UploadOutlined />}>点击上传主图</Button> | |||
</Upload> | |||
</div> | |||
)} | |||
renderItem={(item) => (renderListItem(item))} | |||
className='workbench' | |||
/> | |||
</div> | |||
<div id="part-2"> | |||
@@ -165,13 +165,14 @@ export default () => { | |||
</div> | |||
)} | |||
renderItem={(item) => (renderListItem(item))} | |||
className='workbench' | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
</Card> | |||
<Card className='flex-auto mx-[10px] mb-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||
<Card className='flex-auto mx-[10px] mb-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px] workbench'> | |||
<div className='flex flex-col justify-center'> | |||
<div className='flex justify-center text-3xl p-5 font-extrabold'>工作台</div> | |||
<div className='flex justify-center'> | |||
@@ -201,7 +202,7 @@ export default () => { | |||
</div> | |||
</Card> | |||
<Card className='flex-none mb-[10px] w-[270px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||
<Card className='flex-none mb-[10px] w-[270px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px] workbench'> | |||
<div className='flex justify-center pt-2'> | |||
<Radio.Group options={options} | |||
onChange={onStyleChanged} | |||
@@ -384,7 +384,6 @@ const TablePage: React.FC = () => { | |||
onSave={saveAttributeHandle} | |||
onCancel={cancelHandle} | |||
visible={attrEditorVisible} | |||
curRecord={attrData} | |||
editData={attrData} /> | |||
<MaskPictureEditor | |||
@@ -0,0 +1,202 @@ | |||
import React, { useState, useEffect, useRef } from 'react'; | |||
import { useSetState } from 'ahooks'; | |||
import { Space, Table, Button, Divider, Card, Input } from 'antd'; | |||
import type { InputRef } from 'antd'; | |||
import type { ColumnsType, ColumnType, TableProps } from 'antd/es/table'; | |||
import { t } from '@/utils/i18n'; | |||
import { PlusOutlined, ExclamationCircleFilled, SearchOutlined } from '@ant-design/icons'; | |||
import { antdUtils } from '@/utils/antd'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import { TemplateDictVO, TemplateDictPageReqVO } from '@/models'; | |||
import DictEditor from './dict-editor'; | |||
import templateDictService from '@/request/service/template-dict'; | |||
export default (props: { | |||
onSelectItem: (item: TemplateDictVO) => void; | |||
}) => { | |||
const { onSelectItem } = props; | |||
const [dataDicts, setDataDicts] = useState<TemplateDictVO[]>(); | |||
const [searchState, setSearchState] = useSetState<TemplateDictPageReqVO>({ | |||
pageNo: 1, | |||
pageSize: 10 | |||
}); | |||
const [total, setTotal] = useState(0) | |||
const searchInput = useRef<InputRef>(null); | |||
const [onSearching, setOnSearching] = useState(false); | |||
const [editorVisable, seEditorVisable] = useState<boolean>(false); | |||
const [editData, seEditData] = useState<TemplateDictVO>(); | |||
//获取字典分页数据 | |||
const { runAsync: getDictPageApi } = useRequest(templateDictService.getDictPageApi, { manual: true }); | |||
//删除字典数据 | |||
const { runAsync: deleteDictApi } = useRequest(templateDictService.deleteDictApi, { manual: true }); | |||
const load = async () => { | |||
setOnSearching(true); | |||
const [error, { data }] = await getDictPageApi(searchState); | |||
setOnSearching(false); | |||
if (!error) { | |||
setDataDicts(data.list); | |||
setTotal(data.total); | |||
} | |||
} | |||
const showDeleteConfirm = (item: TemplateDictVO) => { | |||
antdUtils.modal?.confirm({ | |||
title: `确认删除名称为: ${item.name} 的字典吗?`, | |||
icon: <ExclamationCircleFilled />, | |||
content: '请注意删除以后不可恢复!', | |||
okText: '删除', | |||
okType: 'danger', | |||
cancelText: '取消', | |||
onOk: async () => { | |||
const [error, { code, msg }] = await deleteDictApi(item.id); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }) | |||
} else { | |||
antdUtils.message?.open({ type: 'success', content: '删除成功' }) | |||
} | |||
await load(); | |||
}, | |||
onCancel() { | |||
}, | |||
}); | |||
}; | |||
const getColumnSearchProps = (placeholder: string): ColumnType<TemplateDictVO> => ({ | |||
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => ( | |||
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}> | |||
<Input.Search | |||
ref={searchInput} | |||
placeholder={placeholder} | |||
value={selectedKeys[0]} | |||
onChange={(e) => { | |||
setSelectedKeys(e.target.value && e.target.value !== '' ? [e.target.value] : []) | |||
}} | |||
onSearch={(value) => { | |||
if (value === '' && clearFilters) { | |||
clearFilters!!() | |||
} | |||
confirm(); | |||
}} | |||
onPressEnter={() => confirm()} | |||
allowClear | |||
style={{ marginBottom: 8, display: 'block' }} | |||
enterButton="搜索" | |||
size="middle" | |||
loading={onSearching} | |||
/> | |||
</div> | |||
), | |||
filterIcon: (filtered: boolean) => ( | |||
<SearchOutlined style={{ color: filtered ? 'primaryColor' : undefined }} /> | |||
), | |||
onFilterDropdownOpenChange: (visible) => { | |||
if (visible) { | |||
setTimeout(() => searchInput.current?.select(), 100); | |||
} | |||
}, | |||
}); | |||
const columns: ColumnsType<TemplateDictVO> = [ | |||
{ | |||
title: '名称', | |||
dataIndex: 'name', | |||
key: 'name', | |||
align: 'center', | |||
...getColumnSearchProps('请输入名称'), | |||
}, | |||
{ | |||
title: '描述', | |||
key: 'description', | |||
dataIndex: 'description', | |||
align: 'center', | |||
// ...getColumnSearchProps('请输入描述'), | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
key: 'action', | |||
align: 'center', | |||
render: (_, record) => ( | |||
<Space size="middle" split={( | |||
<Divider type='vertical' /> | |||
)}> | |||
<a | |||
onClick={() => { | |||
seEditData(record) | |||
seEditorVisable(true); | |||
}}> | |||
编辑 | |||
</a> | |||
<a | |||
onClick={() => { | |||
showDeleteConfirm(record) | |||
}}> | |||
删除 | |||
</a> | |||
</Space> | |||
), | |||
width: 150, | |||
}, | |||
]; | |||
useEffect(() => { | |||
load(); | |||
}, [searchState]); | |||
const onChange: TableProps<TemplateDictVO>['onChange'] = (pagination, filters, sorter, extra) => { | |||
const state: TemplateDictPageReqVO = { | |||
name: filters.name ? filters.name[0] as string : undefined, | |||
description: filters.description ? filters.description[0] as string : undefined, | |||
pageNo: pagination.current, | |||
pageSize: pagination.pageSize | |||
} | |||
setOnSearching(true); | |||
setSearchState(state); | |||
}; | |||
return ( | |||
<> | |||
<div> | |||
<Card className='mb-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]' bodyStyle={{ | |||
paddingTop: 0, | |||
paddingBottom: 0 | |||
}}> | |||
<div className="py-[8px] flex flex-row-reverse"> | |||
<Button className="ml-5" type='primary' size='middle' icon={<PlusOutlined />} | |||
onClick={() => { | |||
seEditData(undefined); | |||
seEditorVisable(true); | |||
}}> 新增 </Button> | |||
</div> | |||
<Table rowKey="id" scroll={{ x: true }} columns={columns} dataSource={dataDicts} className='bg-transparent' | |||
onRow={(record) => { | |||
return { | |||
onClick: () => { | |||
onSelectItem(record); | |||
} | |||
}; | |||
}} | |||
onChange={onChange} | |||
pagination={{ | |||
current: searchState.pageNo, | |||
pageSize: searchState.pageSize, | |||
total, | |||
position: ['bottomRight'] | |||
}} | |||
/> | |||
</Card> | |||
</div> | |||
<DictEditor | |||
onSave={() => { | |||
load(); | |||
seEditorVisable(false); | |||
}} | |||
onCancel={() => { seEditorVisable(false) }} | |||
visible={editorVisable} | |||
data={editData} | |||
/> | |||
</> | |||
); | |||
}; |
@@ -1,8 +1,8 @@ | |||
import React, { useEffect, useState } from 'react' | |||
import { Form, Input, InputNumber, Modal } from 'antd'; | |||
import dictService from '@/request/service/template-dict'; | |||
import dictService from '@/request/service/template-dict-detail'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import type { DataDictDetailVO } from '@/models' | |||
import type { TemplateDictDetailVO } from '@/models' | |||
import { antdUtils } from '@/utils/antd'; | |||
const layout = { | |||
@@ -13,14 +13,15 @@ const layout = { | |||
export default (props: { | |||
visible: boolean; | |||
onCancel: (flag?: boolean) => void; | |||
onSave: (role: DataDictDetailVO) => void; | |||
data?: DataDictDetailVO | null; | |||
onSave: (role: TemplateDictDetailVO) => void; | |||
dictId?: number; | |||
data?: TemplateDictDetailVO | null; | |||
}) => { | |||
const { visible, onCancel, onSave, data } = props; | |||
const { visible, onCancel, onSave, dictId, data } = props; | |||
const { runAsync: updateApi } = useRequest(dictService.updateDictDetailApi, { manual: true }); | |||
const { runAsync: createApi } = useRequest(dictService.deleteDictDetailApi, { manual: true }); | |||
const { runAsync: updateApi } = useRequest(dictService.updateApi, { manual: true }); | |||
const { runAsync: createApi } = useRequest(dictService.createApi, { manual: true }); | |||
const isEdit = !!data; | |||
@@ -41,7 +42,7 @@ export default (props: { | |||
const save = async () => { | |||
setSaveLoading(true); | |||
const fieldValues = form.getFieldsValue(); | |||
const newValue = isEdit ? { ...data, ...fieldValues } : fieldValues; | |||
const newValue = isEdit ? { ...data, ...fieldValues } : {...fieldValues, dictId}; | |||
const [error, { msg, code }] = isEdit ? await updateApi(newValue) : await createApi(newValue); | |||
if (!error && code === 0) { | |||
onSave(newValue); | |||
@@ -58,7 +59,7 @@ export default (props: { | |||
<> | |||
<Modal | |||
open={visible} | |||
title="新建" | |||
title={isEdit ? "编辑" : "新建"} | |||
width={640} | |||
onOk={save} | |||
onCancel={() => onCancel()} | |||
@@ -0,0 +1,178 @@ | |||
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; | |||
import { Space, Table, Button, Divider, Card } from 'antd'; | |||
import type { ColumnsType, TableProps } from 'antd/es/table'; | |||
import { useSetState } from 'ahooks'; | |||
import { t } from '@/utils/i18n'; | |||
import { PlusOutlined, ExclamationCircleFilled } from '@ant-design/icons'; | |||
import { antdUtils } from '@/utils/antd'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import { TemplateDictVO, TemplateDictDetailVO, TemplateDictDetaiPageReqVO } from '@/models'; | |||
import DictDetailEditor from './dict-detail-editor'; | |||
import templateDictDetailService from '@/request/service/template-dict-detail'; | |||
export interface DictDetailRef { | |||
updateDictData: (dictData: TemplateDictVO) => void; | |||
} | |||
const defaultPage = { | |||
pageNo: 1, | |||
pageSize: 10 | |||
}; | |||
export default forwardRef((props, ref) => { | |||
const [dict, setDict] = useState<TemplateDictVO>(); | |||
const [dataSource, setDataSource] = useState<TemplateDictDetailVO[]>(); | |||
const [searchState, setSearchState] = useSetState<TemplateDictDetaiPageReqVO>(defaultPage); | |||
const [total, setTotal] = useState(0); | |||
const [detailEditorVisable, seEdtailEditorVisable] = useState<boolean>(false); | |||
const [editDetailData, seEditDetailData] = useState<TemplateDictDetailVO | null>(); | |||
const { runAsync: getPageApi } = useRequest(templateDictDetailService.getPageApi, { manual: true }); | |||
const { runAsync: deleteApi } = useRequest(templateDictDetailService.deleteApi, { manual: true }); | |||
useImperativeHandle(ref, () => ({ | |||
updateDictData: (dictData: TemplateDictVO) => { | |||
setDict(dictData); | |||
setSearchState({...defaultPage, dictId: dictData.id}); | |||
}, | |||
})); | |||
const load = async () => { | |||
const [error, { data }] = await getPageApi(searchState); | |||
if (!error) { | |||
setDataSource(data.list); | |||
setTotal(data.total); | |||
} | |||
} | |||
useEffect(() => { | |||
if (dict) { | |||
load(); | |||
} | |||
}, [searchState]); | |||
const showDeleteConfirm = (item: TemplateDictDetailVO) => { | |||
antdUtils.modal?.confirm({ | |||
title: `确认删除标签为: ${item.label} 的字典吗?`, | |||
icon: <ExclamationCircleFilled />, | |||
content: '请注意删除以后不可恢复!', | |||
okText: '删除', | |||
okType: 'danger', | |||
cancelText: '取消', | |||
onOk: async () => { | |||
const [error, { code, msg }] = await deleteApi(item.id); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }) | |||
} else { | |||
antdUtils.message?.open({ type: 'success', content: '删除成功' }) | |||
} | |||
await load(); | |||
}, | |||
onCancel() { | |||
}, | |||
}); | |||
}; | |||
const columns: ColumnsType<TemplateDictDetailVO> = [ | |||
{ | |||
title: '字典标签', | |||
dataIndex: 'label', | |||
key: 'label', | |||
align: 'center', | |||
width: 150, | |||
}, | |||
{ | |||
title: '字典值', | |||
key: 'value', | |||
dataIndex: 'value', | |||
align: 'center', | |||
}, | |||
{ | |||
title: '排序', | |||
key: 'dictSort', | |||
dataIndex: 'dictSort', | |||
width: 100, | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
key: 'action', | |||
render: (_, record) => ( | |||
<Space size="middle" split={( | |||
<Divider type='vertical' /> | |||
)}> | |||
<a | |||
onClick={() => { | |||
seEditDetailData(record); | |||
seEdtailEditorVisable(true); | |||
}}> | |||
编辑 | |||
</a> | |||
<a | |||
onClick={() => { | |||
showDeleteConfirm(record) | |||
}}> | |||
删除 | |||
</a> | |||
</Space> | |||
), | |||
width: 150, | |||
}, | |||
]; | |||
const onChange: TableProps<TemplateDictDetailVO>['onChange'] = (pagination, filters, sorter, extra) => { | |||
const state: TemplateDictDetaiPageReqVO = { | |||
pageNo: pagination.current, | |||
pageSize: pagination.pageSize, | |||
dictId: dict?.id | |||
} | |||
setSearchState(state); | |||
}; | |||
return ( | |||
<> | |||
<div> | |||
<Card className='mb-[10px] ml-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]' bodyStyle={{ | |||
paddingTop: 0, | |||
paddingBottom: 0 | |||
}}> | |||
<div className="py-[8px] flex justify-between w-full"> | |||
<div className='py-[5px]'> | |||
{dict ? <span className='text-center text-lg font-semibold'>所属字典: {dict?.name}</span> : null} | |||
</div> | |||
<Button className="ml-5" type='primary' size='middle' | |||
icon={<PlusOutlined />} | |||
onClick={() => { | |||
seEditDetailData(null); | |||
seEdtailEditorVisable(true); | |||
}} | |||
disabled={!dict}> 新增 </Button> | |||
</div> | |||
<Table rowKey="id" scroll={{ x: true }} columns={columns} dataSource={dataSource} className='bg-transparent' | |||
onChange={onChange} | |||
pagination={{ | |||
current: searchState.pageNo, | |||
pageSize: searchState.pageSize, | |||
total, | |||
position: ['bottomRight'] | |||
}} | |||
/> | |||
</Card> | |||
</div> | |||
<DictDetailEditor | |||
onSave={() => { | |||
load(); | |||
seEdtailEditorVisable(false); | |||
}} | |||
onCancel={() => { seEdtailEditorVisable(false) }} | |||
visible={detailEditorVisable} | |||
data={editDetailData} | |||
dictId={dict?.id} | |||
/> | |||
</> | |||
); | |||
}); |
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react' | |||
import { Form, Input, Modal } from 'antd'; | |||
import dictService from '@/request/service/template-dict'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import type { DataDictVO } from '@/models' | |||
import type { TemplateDictVO } from '@/models' | |||
import { antdUtils } from '@/utils/antd'; | |||
const layout = { | |||
@@ -13,8 +13,8 @@ const layout = { | |||
export default (props: { | |||
visible: boolean; | |||
onCancel: (flag?: boolean) => void; | |||
onSave: (role: DataDictVO) => void; | |||
data?: DataDictVO | null; | |||
onSave: (role: TemplateDictVO) => void; | |||
data?: TemplateDictVO | null; | |||
}) => { | |||
const { visible, onCancel, onSave, data } = props; | |||
@@ -58,7 +58,7 @@ export default (props: { | |||
<> | |||
<Modal | |||
open={visible} | |||
title="新建" | |||
title={isEdit ? "编辑" : "新建"} | |||
width={640} | |||
onOk={save} | |||
onCancel={() => onCancel()} | |||
@@ -1,300 +1,26 @@ | |||
import React, { useState, useRef } from 'react'; | |||
import { Space, Table, Button, Image, Divider, Card, Input } from 'antd'; | |||
import type { InputRef } from 'antd'; | |||
import type { ColumnType, ColumnsType } from 'antd/es/table'; | |||
import type { FilterConfirmProps, TableRowSelection } from 'antd/es/table/interface'; | |||
import { t } from '@/utils/i18n'; | |||
import { PlusOutlined, ExclamationCircleFilled, DeleteOutlined, SearchOutlined } from '@ant-design/icons'; | |||
import { antdUtils } from '@/utils/antd'; | |||
import { DataDictVO, DataDictDetailVO } from '@/models'; | |||
import DictEditor from './dict-editor'; | |||
import DictDetailEditor from './dict-detail-editor'; | |||
const dataDicts: DataDictVO[] = [ | |||
{ | |||
"id": 43, | |||
"name": "爆款词", | |||
"description": "爆款词" | |||
}, | |||
{ | |||
"id": 42, | |||
"name": "T-T", | |||
"description": "t-shirt" | |||
}, | |||
{ | |||
"id": 41, | |||
"name": "T-shirt Background", | |||
"description": "T桖背景" | |||
} | |||
] | |||
const dictDetailsA: DataDictDetailVO[] = [ | |||
{ | |||
"id": 114, | |||
"dictId": 43, | |||
"label": "tshirt for women", | |||
"value": "tshirt for women", | |||
"dictSort": 1 | |||
}, | |||
{ | |||
"id": 113, | |||
"dictId": 43, | |||
"label": "oversize t shirt black", | |||
"value": "oversize t shirt black", | |||
"dictSort": 1 | |||
}, | |||
{ | |||
"id": 112, | |||
"dictId": 43, | |||
"label": "oversize t shirt", | |||
"value": "oversize t shirt", | |||
"dictSort": 1 | |||
} | |||
] | |||
const dictDetailsB: DataDictDetailVO[] = [ | |||
{ | |||
"id": 107, | |||
"dictId": 42, | |||
"label": "three", | |||
"value": "three", | |||
"dictSort": 3 | |||
}, | |||
{ | |||
"id": 106, | |||
"dictId": 42, | |||
"label": "two", | |||
"value": "two", | |||
"dictSort": 2 | |||
}, | |||
{ | |||
"id": 105, | |||
"dictId": 42, | |||
"label": "one", | |||
"value": "one", | |||
"dictSort": 1 | |||
} | |||
] | |||
const dictDetailsC: DataDictDetailVO[] = [ | |||
{ | |||
"id": 111, | |||
"dictId": 41, | |||
"label": "Soothing hot springs", | |||
"value": "Hot spring photos, hot spring water, beautiful scenery, no one, film quality, 8K, meticulous", | |||
"dictSort": 5 | |||
}, | |||
{ | |||
"id": 110, | |||
"dictId": 41, | |||
"label": "Great Eiffel Tower", | |||
"value": "Keywords Paris Tower, photo, high quality, 8K,", | |||
"dictSort": 4 | |||
}, | |||
{ | |||
"id": 109, | |||
"dictId": 41, | |||
"label": "Iconic Eiffel Tower", | |||
"value": "Keywords Paris Tower, photo, high quality, 8K,", | |||
"dictSort": 3 | |||
}, | |||
{ | |||
"id": 108, | |||
"dictId": 41, | |||
"label": "Pleasant Tunisia", | |||
"value": "Venice, photos, high quality, 8K, water, details, film quality, rendering, beautiful scenery.", | |||
"dictSort": 2 | |||
}, | |||
{ | |||
"id": 104, | |||
"dictId": 41, | |||
"label": "Trendy Tunisia", | |||
"value": "Venice, photos, high quality, 8K, water, details, film quality, rendering, beautiful scenery.", | |||
"dictSort": 1 | |||
} | |||
] | |||
import React, { useRef } from 'react'; | |||
import { TemplateDictVO } from '@/models'; | |||
import DictDetail from './dict-detail'; | |||
import DictData from './dict-data'; | |||
import type { DictDetailRef } from './dict-detail' | |||
export default () => { | |||
const [dictDetail, setDictDetail] = useState<DataDictDetailVO[]>(); | |||
const [editorVisable, seEditorVisable] = useState<boolean>(false); | |||
const [editData, seEditData] = useState<DataDictVO>(); | |||
const [detailEditorVisable, seEdtailEditorVisable] = useState<boolean>(false); | |||
const [editDetailData, seEditDetailData] = useState<DataDictDetailVO>(); | |||
const showDeleteConfirm = (item: DataDictVO) => { | |||
antdUtils.modal?.confirm({ | |||
title: `确认删除名称为: ${item.name} 的字典吗?`, | |||
icon: <ExclamationCircleFilled />, | |||
content: '请注意删除以后不可恢复!', | |||
okText: '删除', | |||
okType: 'danger', | |||
cancelText: '取消', | |||
onOk() { | |||
return new Promise((resolve, reject) => { | |||
setTimeout(() => { | |||
antdUtils.message?.open({ | |||
type: 'success', | |||
content: '删除成功', | |||
}); | |||
resolve(null) | |||
}, 1000); | |||
}).catch(() => antdUtils.message?.open({ | |||
type: 'error', | |||
content: '操作失败', | |||
})); | |||
}, | |||
onCancel() { | |||
}, | |||
}); | |||
}; | |||
const columns: ColumnsType<DataDictDetailVO> = [ | |||
{ | |||
title: '字典标签', | |||
dataIndex: 'label', | |||
key: 'label', | |||
align: 'center', | |||
width: 150, | |||
}, | |||
{ | |||
title: '字典值', | |||
key: 'value', | |||
dataIndex: 'value', | |||
align: 'center', | |||
}, | |||
{ | |||
title: '排序', | |||
key: 'dictSort', | |||
dataIndex: 'dictSort', | |||
width: 100, | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
key: 'action', | |||
render: (_, record) => ( | |||
<Space size="middle" split={( | |||
<Divider type='vertical' /> | |||
)}> | |||
<a | |||
onClick={() => { | |||
seEditDetailData(record) | |||
seEdtailEditorVisable(true); | |||
}}> | |||
编辑 | |||
</a> | |||
<a | |||
onClick={() => { | |||
}}> | |||
删除 | |||
</a> | |||
</Space> | |||
), | |||
width: 150, | |||
}, | |||
]; | |||
const updateDactDetail = (data: DataDictVO) => { | |||
if (data.id == 43) { | |||
setDictDetail(dictDetailsA) | |||
} else if (data.id == 42) { | |||
setDictDetail(dictDetailsB) | |||
} else { | |||
setDictDetail(dictDetailsC) | |||
} | |||
} | |||
const dictColumns: ColumnsType<DataDictVO> = [ | |||
{ | |||
title: '名称', | |||
dataIndex: 'name', | |||
key: 'name', | |||
align: 'center', | |||
}, | |||
{ | |||
title: '描述', | |||
key: 'description', | |||
dataIndex: 'description', | |||
align: 'center', | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
key: 'action', | |||
align: 'center', | |||
render: (_, record) => ( | |||
<Space size="middle" split={( | |||
<Divider type='vertical' /> | |||
)}> | |||
<a | |||
onClick={() => { | |||
seEditData(record) | |||
seEditorVisable(true); | |||
}}> | |||
编辑 | |||
</a> | |||
<a | |||
onClick={() => { | |||
showDeleteConfirm(record) | |||
}}> | |||
删除 | |||
</a> | |||
</Space> | |||
), | |||
width: 150, | |||
}, | |||
]; | |||
const detailRef = useRef<DictDetailRef>() | |||
return ( | |||
<> | |||
<div className='flex flex-row'> | |||
<Card className='basis-2/5 w-[100px] mb-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||
<Table rowKey="id" scroll={{ x: true }} columns={dictColumns} dataSource={dataDicts} className='bg-transparent' | |||
onRow={(record) => { | |||
return { | |||
onClick: () => { | |||
updateDactDetail(record) | |||
} | |||
}; | |||
}} | |||
pagination={{ position: ['bottomRight'] }} | |||
/> | |||
</Card> | |||
<Card className='basis-3/5 mb-[10px] ml-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]' bodyStyle={{ | |||
paddingTop: 0, | |||
paddingBottom: 0 | |||
}}> | |||
<div className="py-[8px] flex flex-row-reverse"> | |||
<Button className="ml-5" type='primary' size='middle' icon={<PlusOutlined />}> 新增 </Button> | |||
</div> | |||
<Table rowKey="id" scroll={{ x: true }} columns={columns} dataSource={dictDetail} className='bg-transparent' | |||
pagination={{ position: ['bottomRight'] }} | |||
/> | |||
</Card> | |||
<div className='basis-2/5 '> | |||
<DictData | |||
onSelectItem={(item: TemplateDictVO) => { | |||
detailRef.current?.updateDictData(item); | |||
}} /> | |||
</div> | |||
<div className='basis-3/5 '> | |||
<DictDetail ref={detailRef} /> | |||
</div> | |||
</div> | |||
<DictEditor | |||
onSave={() => { | |||
}} | |||
onCancel={() => { seEditorVisable(false) }} | |||
visible={editorVisable} | |||
data={editData} | |||
/> | |||
<DictDetailEditor | |||
onSave={() => { | |||
}} | |||
onCancel={() => { seEdtailEditorVisable(false) }} | |||
visible={detailEditorVisable} | |||
data={editDetailData} | |||
/> | |||
</> | |||
); | |||
}; |
@@ -1,7 +1,7 @@ | |||
import React, { useState, useEffect } from 'react'; | |||
import { Space, Table, Form, Button, Card, Input, TreeSelect } from 'antd'; | |||
import type { ColumnsType } from 'antd/es/table'; | |||
import { t } from '@/utils/i18n'; | |||
import React, { useState } from 'react'; | |||
import { useNavigate } from 'react-router-dom'; | |||
import { | |||
ExclamationCircleFilled, | |||
@@ -11,7 +11,10 @@ import { | |||
UndoOutlined | |||
} from '@ant-design/icons'; | |||
import { antdUtils } from '@/utils/antd'; | |||
import { ShopeeTemplateVO } from '@/models'; | |||
import { useSetState } from 'ahooks'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import platformShopService from '@/request/service/platform-template'; | |||
import { TemplateInfoVO, TemplateInfoPageReqVO } from '@/models'; | |||
const treeData = [ | |||
{ | |||
@@ -53,42 +56,38 @@ const treeData = [ | |||
]; | |||
export default () => { | |||
const data: ShopeeTemplateVO[] = [ | |||
{ | |||
"id": 78, | |||
"platformCode": "Shopee", | |||
"shopName": "Trista", | |||
"categoryName": "T-Shirts", | |||
"templateName": "T-SHIRT", | |||
"remark": "1", | |||
"createName": "admin", | |||
"createTime": "2023-06-15 14:45:15" | |||
}, | |||
{ | |||
"id": 77, | |||
"platformCode": "Shopee", | |||
"shopName": "Trista", | |||
"categoryName": "T-Shirts", | |||
"templateName": "T-SHIRT", | |||
"remark": "1", | |||
"createName": "admin", | |||
"createTime": "2023-06-15 14:44:46" | |||
}, | |||
{ | |||
"id": 76, | |||
"platformCode": "Shopee", | |||
"shopName": "Trista", | |||
"categoryName": "T-Shirts", | |||
"templateName": "T-SHIRT", | |||
"remark": "", | |||
"createName": "admin", | |||
"createTime": "2023-06-15 14:42:24" | |||
const [dataSource, setDataSource] = useState<TemplateInfoVO[]>([]); | |||
const [total, setTotal] = useState(0); | |||
const [searchFrom] = Form.useForm(); | |||
const [searchState, setSearchState] = useSetState<TemplateInfoPageReqVO>({ | |||
pageNo: 1, | |||
pageSize: 10 | |||
}); | |||
const [treeLine, setTreeLine] = useState(true); | |||
const [showLeafIcon, setShowLeafIcon] = useState(false); | |||
const [showIcon, setShowIcon] = useState<boolean>(false); | |||
//获取分页数据 | |||
const { runAsync: getPageApi } = useRequest(platformShopService.pageApi, { manual: true }); | |||
//删除数据 | |||
const { runAsync: deleteApi } = useRequest(platformShopService.deleteApi, { manual: true }); | |||
//加载数据 | |||
const load = async () => { | |||
const [error, { code, data, msg }] = await getPageApi(searchState); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }) | |||
} else { | |||
setDataSource(data.list); | |||
setTotal(data.total); | |||
} | |||
]; | |||
}; | |||
const showDeleteConfirm = (item: ShopeeTemplateVO) => { | |||
const showDeleteConfirm = (item: TemplateInfoVO) => { | |||
antdUtils.modal?.confirm({ | |||
title: `确认删除标题为: ${item.shopName} 的模板吗?`, | |||
title: `确认删除标题为: ${item.templateName} 的模板吗?`, | |||
icon: <ExclamationCircleFilled />, | |||
content: `请注意删除以后不可恢复!`, | |||
okText: '删除', | |||
@@ -114,7 +113,7 @@ export default () => { | |||
}); | |||
}; | |||
const columns: ColumnsType<ShopeeTemplateVO> = [ | |||
const columns: ColumnsType<TemplateInfoVO> = [ | |||
{ | |||
title: '店铺', | |||
dataIndex: 'shopName', | |||
@@ -162,13 +161,9 @@ export default () => { | |||
}, | |||
]; | |||
const [searchFrom] = Form.useForm(); | |||
const navigate = useNavigate(); | |||
const [treeLine, setTreeLine] = useState(true); | |||
const [showLeafIcon, setShowLeafIcon] = useState(false); | |||
const [showIcon, setShowIcon] = useState<boolean>(false); | |||
useEffect(() => { | |||
load(); | |||
}, []) | |||
return ( | |||
<div> | |||
@@ -204,8 +199,18 @@ export default () => { | |||
</div> | |||
</Card> | |||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||
<Table rowKey="id" scroll={{ x: true }} columns={columns} dataSource={data} className='bg-transparent' | |||
pagination={{ position: ['bottomRight'] }} | |||
<Table | |||
rowKey="id" | |||
scroll={{ x: true }} | |||
columns={columns} | |||
dataSource={dataSource} | |||
className='bg-transparent' | |||
pagination={{ | |||
position: ['bottomRight'], | |||
current: searchState.pageNo, | |||
pageSize: searchState.pageSize, | |||
total | |||
}} | |||
/> | |||
</Card> | |||
</div> | |||
@@ -0,0 +1,408 @@ | |||
import React, { useState, useEffect, useRef, useContext, MutableRefObject, forwardRef, useImperativeHandle } from 'react'; | |||
import { Space, Table, Button, Input, Badge, Divider, Form, Popconfirm, Modal, Radio } from 'antd'; | |||
import type { TableColumnsType } from 'antd'; | |||
import type { InputRef } from 'antd'; | |||
import type { FormInstance } from 'antd/es/form'; | |||
import type { ColumnType, TableProps } from 'antd/es/table'; | |||
import { t } from '@/utils/i18n'; | |||
import { PlusOutlined, ExclamationCircleFilled, SearchOutlined, UndoOutlined } from '@ant-design/icons'; | |||
import type { GoodsClassifyPageReqVO, GoodsClassifyVO } from '@/models' | |||
import { antdUtils } from '@/utils/antd'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import goodsClassifyService from '@/request/service/goods-classify'; | |||
import { formatDate } from '@/utils/formatTime'; | |||
import { useSetState } from 'ahooks'; | |||
const CreateClassify = (props: { | |||
visible: boolean; | |||
onCancel: (flag?: boolean) => void; | |||
onSave: (role: GoodsClassifyVO) => void; | |||
}) => { | |||
const { visible, onCancel, onSave } = props; | |||
const { runAsync: createApi } = useRequest(goodsClassifyService.createGoodsClassifyApi, { manual: true }); | |||
const [saveLoading, setSaveLoading] = useState(false); | |||
const [form] = Form.useForm(); | |||
useEffect(() => { | |||
if (visible) { | |||
form.setFieldValue("isDefault", 1); | |||
} else { | |||
form.resetFields(); | |||
} | |||
}, [visible]); | |||
const save = async () => { | |||
setSaveLoading(true); | |||
const fieldValues = form.getFieldsValue(); | |||
const [error, { msg, code }] = await createApi(fieldValues); | |||
if (!error && code === 0) { | |||
onSave(fieldValues); | |||
} else { | |||
antdUtils.message?.open({ | |||
type: 'error', | |||
content: msg ?? '操作失败', | |||
}); | |||
} | |||
setSaveLoading(false); | |||
} | |||
return ( | |||
<> | |||
<Modal | |||
open={visible} | |||
title={"新建"} | |||
width={640} | |||
onOk={save} | |||
onCancel={() => onCancel()} | |||
confirmLoading={saveLoading} | |||
destroyOnClose | |||
> | |||
<Form | |||
form={form} | |||
// layout={{ | |||
// labelCol: { span: 4, }, | |||
// wrapperCol: { span: 16 } | |||
// }} | |||
onFinish={save} | |||
labelCol={{ flex: '0 0 100px' }} | |||
wrapperCol={{ span: 16 }} | |||
> | |||
<Form.Item name="classifyName" label="分类名称:" | |||
rules={[ | |||
{ | |||
required: true, | |||
message: '请输分类名称', | |||
}, | |||
]} | |||
> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="isDefault" label="是否默认:" | |||
rules={[ | |||
{ | |||
required: true, | |||
message: '请输选择', | |||
}, | |||
]} | |||
> | |||
<Radio.Group options={[ | |||
{ value: 1, label: "是" }, | |||
{ value: 2, label: "否" } | |||
]} optionType="default"> | |||
</Radio.Group> | |||
</Form.Item> | |||
</Form> | |||
</Modal> | |||
</> | |||
) | |||
}; | |||
const EditableContext = React.createContext<FormInstance<any> | null>(null); | |||
interface EditableRowProps { | |||
index: number; | |||
} | |||
const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => { | |||
const [form] = Form.useForm(); | |||
return ( | |||
<Form form={form} component={false}> | |||
<EditableContext.Provider value={form}> | |||
<tr {...props} /> | |||
</EditableContext.Provider> | |||
</Form> | |||
); | |||
}; | |||
interface EditableCellProps { | |||
title: React.ReactNode; | |||
editable: boolean; | |||
children: React.ReactNode; | |||
dataIndex: keyof GoodsClassifyVO; | |||
record: GoodsClassifyVO; | |||
handleSave: (record: GoodsClassifyVO) => void; | |||
onEditing: (record: GoodsClassifyVO) => void; | |||
} | |||
const EditableCell: React.FC<EditableCellProps> = ({ | |||
title, | |||
editable, | |||
children, | |||
dataIndex, | |||
record, | |||
handleSave, | |||
...restProps | |||
}) => { | |||
const [editing, setEditing] = useState(false); | |||
const inputRef = useRef<InputRef>(null); | |||
const form = useContext(EditableContext)!; | |||
useEffect(() => { | |||
if (editing) { | |||
inputRef.current!.focus(); | |||
} | |||
}, [editing]); | |||
const toggleEdit = () => { | |||
setEditing(!editing); | |||
form.setFieldsValue({ [dataIndex]: record[dataIndex] }); | |||
}; | |||
const save = async () => { | |||
try { | |||
const values = await form.validateFields(); | |||
toggleEdit(); | |||
handleSave({ ...record, ...values }); | |||
} catch (errInfo) { | |||
console.log('Save failed:', errInfo); | |||
} | |||
}; | |||
let childNode = children; | |||
if (editable) { | |||
childNode = editing ? ( | |||
<Form.Item | |||
style={{ margin: 0 }} | |||
name={dataIndex} | |||
rules={[ | |||
{ | |||
required: true, | |||
message: `${title} is required.`, | |||
}, | |||
]} | |||
> | |||
<Input size="middle" ref={inputRef} onPressEnter={save} onBlur={save} /> | |||
</Form.Item> | |||
) : ( | |||
<div className="editable-cell-value-wrap" style={{ paddingRight: 24 }} onClick={toggleEdit}> | |||
{children} | |||
</div> | |||
); | |||
} | |||
return <td {...restProps}>{childNode}</td>; | |||
}; | |||
type EditableTableProps = Parameters<typeof Table>[0]; | |||
type ColumnTypes = Exclude<EditableTableProps['columns'], undefined>; | |||
export default forwardRef((props, ref) => { | |||
//TODO: pagination | |||
const [visible, setVisible] = useState<boolean>(false); | |||
const [dataSource, setDataSource] = useState<GoodsClassifyVO[]>([]); | |||
const [searchFrom] = Form.useForm(); | |||
const [searchState, setSearchState] = useSetState<GoodsClassifyPageReqVO>({ | |||
pageNo: 1, | |||
pageSize: 10 | |||
}); | |||
const [total, setTotal] = useState(0) | |||
const searchInput = useRef<InputRef>(null); | |||
const [onSearching, setOnSearching] = useState(false); | |||
const { runAsync: getPageApi } = useRequest(goodsClassifyService.getGoodsClassifyPageApi, { manual: true }); | |||
const { runAsync: updateApi } = useRequest(goodsClassifyService.updateGoodsClassifyApi, { manual: true }); | |||
const { runAsync: verifyApi } = useRequest(goodsClassifyService.classifyNameVerifyUnique, { manual: true }); | |||
const { runAsync: deleteApi } = useRequest(goodsClassifyService.deleteGoodsClassifyApi, { manual: true }); | |||
const load = async () => { | |||
const [error, { data }] = await getPageApi(searchFrom.getFieldsValue()); | |||
if (!error) { | |||
setDataSource(data.list); | |||
setTotal(data.total); | |||
} | |||
}; | |||
const deleteItem = async (data: GoodsClassifyVO) => { | |||
const [error, { code, msg }] = await deleteApi(data.id); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }); | |||
} else { | |||
antdUtils.message?.open({ type: 'success', content: '删除成功' }); | |||
} | |||
await load(); | |||
}; | |||
const getColumnSearchProps = (placeholder: string): ColumnType<GoodsClassifyVO> => ({ | |||
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => ( | |||
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}> | |||
<Input.Search | |||
ref={searchInput} | |||
placeholder={placeholder} | |||
value={selectedKeys[0]} | |||
onChange={(e) => { | |||
setSelectedKeys(e.target.value && e.target.value !== '' ? [e.target.value] : []) | |||
}} | |||
onSearch={(value) => { | |||
if (value === '' && clearFilters) { | |||
clearFilters!!() | |||
} | |||
confirm(); | |||
}} | |||
onPressEnter={() => confirm()} | |||
allowClear | |||
style={{ marginBottom: 8, display: 'block' }} | |||
enterButton="搜索" | |||
size="middle" | |||
loading={onSearching} | |||
/> | |||
</div> | |||
), | |||
filterIcon: (filtered: boolean) => ( | |||
<SearchOutlined style={{ color: filtered ? 'primaryColor' : undefined }} /> | |||
), | |||
onFilterDropdownOpenChange: (visible) => { | |||
if (visible) { | |||
setTimeout(() => searchInput.current?.select(), 100); | |||
} | |||
}, | |||
}); | |||
const defaultColumns: (ColumnTypes[number] & { editable?: boolean; dataIndex: string })[] = [ | |||
{ | |||
title: '类目名称', | |||
dataIndex: 'classifyName', | |||
key: 'classifyName', | |||
align: 'left', | |||
width: '30%', | |||
editable: true, | |||
}, | |||
{ | |||
title: '是否默认属性', | |||
key: 'isDefault', | |||
dataIndex: 'isDefault', | |||
width: 120, | |||
align: 'center', | |||
render: (value: number) => { | |||
return (value === 1 ? <Badge status="success" text="是" /> : <Badge status="error" text="否" />) | |||
} | |||
}, | |||
{ | |||
title: '创建时间', | |||
dataIndex: 'createTime', | |||
key: 'createTime', | |||
align: 'center', | |||
render: (value: number) => { | |||
return formatDate(new Date(value), "YYYY-mm-dd HH:MM:SS") | |||
} | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
dataIndex: 'operation', | |||
key: 'action', | |||
render: (_, record, index) => | |||
dataSource.length >= 1 ? ( | |||
<Popconfirm title="确认要将该分类删除吗?" onConfirm={() => deleteItem(dataSource[index])}> | |||
<a>删除</a> | |||
</Popconfirm> | |||
) : null, | |||
}, | |||
]; | |||
const columns = defaultColumns.map((col) => { | |||
if (!col.editable) { | |||
return col; | |||
} | |||
return { | |||
...col, | |||
...getColumnSearchProps("请输入类目名称"), | |||
onCell: (record: GoodsClassifyVO) => ({ | |||
record, | |||
editable: col.editable, | |||
dataIndex: col.dataIndex, | |||
title: col.title, | |||
handleSave, | |||
}), | |||
}; | |||
}); | |||
const handleSave = async (row: GoodsClassifyVO) => { | |||
const [error, { code, msg }] = await updateApi(row); | |||
if (!error && code === 0) { | |||
const newData = [...dataSource]; | |||
const index = newData.findIndex((item) => row.id === item.id); | |||
const item = newData[index]; | |||
newData.splice(index, 1, { | |||
...item, | |||
...row, | |||
}); | |||
setDataSource(newData); | |||
} else { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '更新失败' }) | |||
} | |||
}; | |||
const components = { | |||
body: { | |||
row: EditableRow, | |||
cell: EditableCell, | |||
}, | |||
}; | |||
useEffect(() => { | |||
load(); | |||
}, []); | |||
const onReset = () => { | |||
searchFrom.resetFields() | |||
load(); | |||
} | |||
return ( | |||
<> | |||
<div> | |||
<div className='flex justify-between content-center mb-2'> | |||
<div className='flex justify-normal items-center'> | |||
<Form layout='inline' form={searchFrom}> | |||
<Form.Item name="classifyName" label="分类名称"> | |||
<Input className='w-[150px]' placeholder='请输入名称' allowClear /> | |||
</Form.Item> | |||
</Form> | |||
<Space.Compact className="ml-5"> | |||
<Button type='primary' size='large' icon={<SearchOutlined />} onClick={load}> 搜索 </Button> | |||
<Button type='primary' size='large' icon={<UndoOutlined />} onClick={onReset}> 重置 </Button> | |||
</Space.Compact> | |||
</div> | |||
<Button type='primary' size='large' icon={<PlusOutlined />} onClick={() => { | |||
setVisible(true); | |||
}}> 新增分类 </Button> | |||
</div> | |||
<Table rowKey="id" | |||
scroll={{ x: true }} | |||
columns={columns as ColumnTypes} | |||
dataSource={dataSource} | |||
components={components} | |||
rowClassName={() => 'editable-row'} | |||
onChange={(pagination) => { | |||
}} | |||
pagination={{ | |||
position: ['bottomRight'], | |||
current: searchState.pageNo, | |||
pageSize: searchState.pageSize, | |||
total | |||
}} /> | |||
</div> | |||
<CreateClassify | |||
visible={visible} | |||
onCancel={() => { | |||
setVisible(false); | |||
}} | |||
onSave={(values: GoodsClassifyVO) => { | |||
console.log(values) | |||
setVisible(false); | |||
load(); | |||
}} | |||
/> | |||
</> | |||
); | |||
}); | |||
@@ -0,0 +1,415 @@ | |||
import React, { useState, useEffect, useRef, useContext, forwardRef, useImperativeHandle } from 'react'; | |||
import { Space, Table, Button, Input, Modal, Radio, Form, Popconfirm, Badge } from 'antd'; | |||
import type { TableColumnsType } from 'antd'; | |||
import type { InputRef } from 'antd'; | |||
import type { FormInstance } from 'antd/es/form'; | |||
import type { ColumnType, TableProps } from 'antd/es/table'; | |||
import { t } from '@/utils/i18n'; | |||
import { PlusOutlined, ExclamationCircleFilled, SearchOutlined } from '@ant-design/icons'; | |||
import type { GoodsAttrVO, GoodsAttrPageReqVO } from '@/models' | |||
import { antdUtils } from '@/utils/antd'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import goodsAttrService from '@/request/service/goods-attr'; | |||
import { formatDate } from '@/utils/formatTime'; | |||
import { useSetState } from 'ahooks'; | |||
const AttrEditor = (props: { | |||
visible: boolean; | |||
onCancel: (flag?: boolean) => void; | |||
onSave: (role: GoodsAttrVO) => void; | |||
}) => { | |||
const { visible, onCancel, onSave } = props; | |||
const { runAsync: createApi } = useRequest(goodsAttrService.createGoodsAttrApi, { manual: true }); | |||
const [saveLoading, setSaveLoading] = useState(false); | |||
const [form] = Form.useForm(); | |||
useEffect(() => { | |||
if (visible) { | |||
form.setFieldValue("attrStatus", 1); | |||
} else { | |||
form.resetFields(); | |||
} | |||
}, [visible]); | |||
const save = async () => { | |||
await form.validateFields(); | |||
setSaveLoading(true); | |||
const fieldValues = form.getFieldsValue(); | |||
const [error, { msg, code }] = await createApi(fieldValues); | |||
if (!error && code === 0) { | |||
onSave(fieldValues); | |||
} else { | |||
antdUtils.message?.open({ | |||
type: 'error', | |||
content: msg ?? '操作失败', | |||
}); | |||
} | |||
setSaveLoading(false); | |||
} | |||
return ( | |||
<> | |||
<Modal | |||
open={visible} | |||
title={"新建"} | |||
width={640} | |||
onOk={save} | |||
onCancel={() => onCancel()} | |||
confirmLoading={saveLoading} | |||
destroyOnClose | |||
> | |||
<Form | |||
form={form} | |||
onFinish={save} | |||
labelCol={{ flex: '0 0 120px' }} | |||
wrapperCol={{ span: 16 }} | |||
> | |||
<Form.Item name="attrNameCn" label="属性名称(中文):" | |||
rules={[ | |||
{ | |||
required: true, | |||
message: '请输属性中文名称', | |||
}, | |||
]} | |||
> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="attrNameEn" label="属性名称(英文):" | |||
rules={[ | |||
{ | |||
required: true, | |||
message: '请输属性英文名称', | |||
}, | |||
]} | |||
> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="attrStatus" label="可用状态:" | |||
rules={[ | |||
{ | |||
required: true, | |||
message: '请输选择', | |||
}, | |||
]} | |||
> | |||
<Radio.Group options={[ | |||
{ value: 1, label: "正常使用" }, | |||
{ value: 2, label: "停止使用" } | |||
]} optionType="default"> | |||
</Radio.Group> | |||
</Form.Item> | |||
</Form> | |||
</Modal> | |||
</> | |||
) | |||
}; | |||
const EditableContext = React.createContext<FormInstance<any> | null>(null); | |||
interface EditableRowProps { | |||
index: number; | |||
} | |||
const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => { | |||
const [form] = Form.useForm(); | |||
return ( | |||
<Form form={form} component={false}> | |||
<EditableContext.Provider value={form}> | |||
<tr {...props} /> | |||
</EditableContext.Provider> | |||
</Form> | |||
); | |||
}; | |||
interface EditableCellProps { | |||
title: React.ReactNode; | |||
editable: boolean; | |||
children: React.ReactNode; | |||
dataIndex: keyof GoodsAttrVO; | |||
record: GoodsAttrVO; | |||
handleSave: (record: GoodsAttrVO) => void; | |||
} | |||
const EditableCell: React.FC<EditableCellProps> = ({ | |||
title, | |||
editable, | |||
children, | |||
dataIndex, | |||
record, | |||
handleSave, | |||
...restProps | |||
}) => { | |||
const [editing, setEditing] = useState(false); | |||
const inputRef = useRef<InputRef>(null); | |||
const form = useContext(EditableContext)!; | |||
useEffect(() => { | |||
if (editing) { | |||
inputRef.current!.focus(); | |||
} | |||
}, [editing]); | |||
const toggleEdit = () => { | |||
setEditing(!editing); | |||
form.setFieldsValue({ [dataIndex]: record[dataIndex] }); | |||
}; | |||
const save = async () => { | |||
try { | |||
const values = await form.validateFields(); | |||
toggleEdit(); | |||
handleSave({ ...record, ...values }); | |||
} catch (errInfo) { | |||
console.log('Save failed:', errInfo); | |||
} | |||
}; | |||
let childNode = children; | |||
if (editable) { | |||
childNode = editing ? ( | |||
<Form.Item | |||
style={{ margin: 0 }} | |||
name={dataIndex} | |||
rules={[ | |||
{ | |||
required: true, | |||
message: `${title} is required.`, | |||
}, | |||
]} | |||
> | |||
<Input size="middle" ref={inputRef} onPressEnter={save} onBlur={save} /> | |||
</Form.Item> | |||
) : ( | |||
<div className="editable-cell-value-wrap" style={{ paddingRight: 24 }} onClick={toggleEdit}> | |||
{children} | |||
</div> | |||
); | |||
} | |||
return <td {...restProps}>{childNode}</td>; | |||
}; | |||
type EditableTableProps = Parameters<typeof Table>[0]; | |||
type ColumnTypes = Exclude<EditableTableProps['columns'], undefined>; | |||
export default forwardRef((props, ref) => { | |||
useImperativeHandle(ref, () => { | |||
addItem | |||
}); | |||
const [visible, setVisible] = useState<boolean>(false); | |||
const [dataSource, setDataSource] = useState<GoodsAttrVO[]>([]); | |||
const [searchFrom] = Form.useForm(); | |||
const [searchState, setSearchState] = useSetState<GoodsAttrPageReqVO>({ | |||
pageNo: 1, | |||
pageSize: 10 | |||
}); | |||
const [total, setTotal] = useState(0) | |||
const searchInput = useRef<InputRef>(null); | |||
const [onSearching, setOnSearching] = useState(false); | |||
const { runAsync: getPageApi } = useRequest(goodsAttrService.getGoodsAttrPageApi, { manual: true }); | |||
const { runAsync: updateApi } = useRequest(goodsAttrService.updateGoodsAttrApi, { manual: true }); | |||
const { runAsync: verifyApi } = useRequest(goodsAttrService.attrNameEnVerifyUnique, { manual: true }); | |||
const { runAsync: deleteApi } = useRequest(goodsAttrService.deleteGoodsAttrApi, { manual: true }); | |||
const load = async () => { | |||
const [error, { data }] = await getPageApi(searchState); | |||
if (!error) { | |||
setDataSource(data.list); | |||
setTotal(data.total); | |||
} | |||
}; | |||
const deleteItem = async (data: GoodsAttrVO) => { | |||
const [error, { code, msg }] = await deleteApi(data.id); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }) | |||
} else { | |||
antdUtils.message?.open({ type: 'success', content: '删除成功' }) | |||
} | |||
await load(); | |||
}; | |||
const addItem = () => { | |||
console.log("on call add Item") | |||
} | |||
const getColumnSearchProps = (placeholder: string): ColumnType<GoodsAttrVO> => ({ | |||
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => ( | |||
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}> | |||
<Input.Search | |||
ref={searchInput} | |||
placeholder={placeholder} | |||
value={selectedKeys[0]} | |||
onChange={(e) => { | |||
setSelectedKeys(e.target.value && e.target.value !== '' ? [e.target.value] : []) | |||
}} | |||
onSearch={(value) => { | |||
if (value === '' && clearFilters) { | |||
clearFilters!!() | |||
} | |||
confirm(); | |||
}} | |||
onPressEnter={() => confirm()} | |||
allowClear | |||
style={{ marginBottom: 8, display: 'block' }} | |||
enterButton="搜索" | |||
size="middle" | |||
loading={onSearching} | |||
/> | |||
</div> | |||
), | |||
filterIcon: (filtered: boolean) => ( | |||
<SearchOutlined style={{ color: filtered ? 'primaryColor' : undefined }} /> | |||
), | |||
onFilterDropdownOpenChange: (visible) => { | |||
if (visible) { | |||
setTimeout(() => searchInput.current?.select(), 100); | |||
} | |||
}, | |||
}); | |||
const defaultColumns: (ColumnTypes[number] & { editable?: boolean; dataIndex: string })[] = [ | |||
{ | |||
title: '颜色中文名称', | |||
dataIndex: 'attrNameCn', | |||
key: 'attrNameCn', | |||
align: 'left', | |||
width: '30%', | |||
editable: true, | |||
}, | |||
{ | |||
title: '颜色英文名称', | |||
dataIndex: 'attrNameEn', | |||
key: 'attrNameEn', | |||
align: 'left', | |||
width: '30%', | |||
editable: true, | |||
}, | |||
{ | |||
title: '状态', | |||
key: 'attrStatus', | |||
dataIndex: 'attrStatus', | |||
width: 100, | |||
align: 'center', | |||
render: (value: number) => { | |||
return (value === 1 ? <Badge status="success" text="已开启" /> : <Badge status="error" text="已关闭" />) | |||
} | |||
}, | |||
{ | |||
title: '创建时间', | |||
dataIndex: 'createTime', | |||
key: 'createTime', | |||
align: 'center', | |||
render: (value: number) => { | |||
return formatDate(new Date(value), "YYYY-mm-dd HH:MM:SS") | |||
} | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
dataIndex: 'operation', | |||
key: 'action', | |||
render: (_, record, index) => | |||
dataSource.length >= 1 ? ( | |||
<Popconfirm title="确认要将该属性删除吗?" onConfirm={() => deleteItem(dataSource[index])}> | |||
<a>删除</a> | |||
</Popconfirm> | |||
) : null, | |||
}, | |||
]; | |||
const columns = defaultColumns.map((col) => { | |||
if (!col.editable) { | |||
return col; | |||
} | |||
return { | |||
...col, | |||
...getColumnSearchProps("请输入颜色名称"), | |||
onCell: (record: GoodsAttrVO) => ({ | |||
record, | |||
editable: col.editable, | |||
dataIndex: col.dataIndex, | |||
title: col.title, | |||
handleSave, | |||
}), | |||
}; | |||
}); | |||
const handleSave = async (row: GoodsAttrVO) => { | |||
const [error, { code, msg }] = await updateApi(row); | |||
if (!error && code === 0) { | |||
const newData = [...dataSource]; | |||
const index = newData.findIndex((item) => row.id === item.id); | |||
const item = newData[index]; | |||
newData.splice(index, 1, { | |||
...item, | |||
...row, | |||
}); | |||
setDataSource(newData); | |||
} else { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '更新失败' }) | |||
} | |||
}; | |||
const components = { | |||
body: { | |||
row: EditableRow, | |||
cell: EditableCell, | |||
}, | |||
}; | |||
useEffect(() => { | |||
load(); | |||
}, [searchState]); | |||
const onReset = () => { | |||
searchFrom.resetFields() | |||
load() | |||
} | |||
return ( | |||
<> | |||
<div> | |||
<div className='flex flex-row-reverse mb-2'> | |||
<Button type='primary' size='large' icon={<PlusOutlined />} onClick={() => { setVisible(true) }}> 新增颜色 </Button> | |||
</div> | |||
<Table rowKey="id" | |||
scroll={{ x: true }} | |||
columns={columns as ColumnTypes} | |||
dataSource={dataSource} | |||
components={components} | |||
rowClassName={() => 'editable-row'} | |||
onChange={async (pagination, filters) => { | |||
const state: GoodsAttrPageReqVO = { | |||
attrNameCn: filters.attrNameCn ? filters.attrNameCn[0] as string : undefined, | |||
attrNameEn: filters.attrNameEn ? filters.attrNameEn[0] as string : undefined, | |||
pageNo: pagination.current ?? 1, | |||
pageSize: pagination.pageSize | |||
} | |||
setSearchState(state); | |||
}} | |||
pagination={{ | |||
position: ['bottomRight'], | |||
current: searchState.pageNo, | |||
pageSize: searchState.pageSize, | |||
total | |||
}} /> | |||
</div> | |||
<AttrEditor | |||
visible={visible} | |||
onCancel={() => { setVisible(false) }} | |||
onSave={(data: GoodsAttrVO) => { | |||
setVisible(false); | |||
load(); | |||
}} /> | |||
</> | |||
); | |||
}); | |||
@@ -0,0 +1,78 @@ | |||
import React, { useState, useCallback, useRef, useMemo } from 'react'; | |||
import { Tabs, Card, Button } from 'antd'; | |||
import { KeepAliveTab, useTabs } from '@/hooks/use-tabs'; | |||
import { PlusOutlined } from '@ant-design/icons'; | |||
import type { TabsProps } from 'antd'; | |||
import Classify from './classify'; | |||
import Colors from './colors'; | |||
import Size from './size'; | |||
type TabKey = "1" | "2" | "3"; | |||
export default () => { | |||
const [currentKey, setCurrentKey] = useState<TabKey>("1"); | |||
const [buttonText, setButtonText] = useState("新增分类"); | |||
const { tabs } = useTabs(); | |||
const classifyRef = useRef(); | |||
const colorsRef = useRef(); | |||
const sizeRef = useRef(); | |||
const items = [ | |||
{ | |||
key: '1', | |||
label: '商品分类', | |||
children: (<Classify ref={classifyRef} />), | |||
}, | |||
{ | |||
key: '2', | |||
label: '颜色设置', | |||
children: (<Colors ref={colorsRef} />), | |||
}, | |||
{ | |||
key: '3', | |||
label: '尺码设置', | |||
children: (<Size ref={sizeRef} />), | |||
}, | |||
]; | |||
const onChange = (key: string) => { | |||
setCurrentKey(key as TabKey); | |||
setButtonText(key === "1" ? "新增分类" : (key === "2" ? "新增颜色" : "新增尺码")); | |||
}; | |||
const onAdd = useCallback(() => { | |||
console.log(classifyRef); | |||
console.log(colorsRef); | |||
console.log(sizeRef); | |||
if (currentKey === "1") { | |||
} else if (currentKey === "2") { | |||
} else { | |||
} | |||
}, [classifyRef, colorsRef, sizeRef]) | |||
return ( | |||
<> | |||
<Card className='dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]' bodyStyle={{ | |||
paddingTop: 10, | |||
paddingBottom: 10 | |||
}}> | |||
<div className='static'> | |||
<Tabs defaultActiveKey="1" onChange={onChange} | |||
items={items} | |||
tabPosition={"left"} | |||
// tabBarExtraContent={{ | |||
// right: <Button className="ml-5" type='primary' size='large' icon={<PlusOutlined />} onClick={onAdd}> {buttonText} </Button> | |||
// }} | |||
> | |||
</Tabs> | |||
</div> | |||
</Card> | |||
</> | |||
); | |||
}; | |||
@@ -0,0 +1,415 @@ | |||
import React, { useState, useEffect, useRef, useContext, forwardRef, useImperativeHandle } from 'react'; | |||
import { Space, Table, Button, Input, Modal, Radio, Form, Popconfirm, Badge } from 'antd'; | |||
import type { TableColumnsType } from 'antd'; | |||
import type { InputRef } from 'antd'; | |||
import type { FormInstance } from 'antd/es/form'; | |||
import type { ColumnType, TableProps } from 'antd/es/table'; | |||
import { t } from '@/utils/i18n'; | |||
import { PlusOutlined, ExclamationCircleFilled, SearchOutlined } from '@ant-design/icons'; | |||
import type { GoodsAttrVO, GoodsAttrPageReqVO } from '@/models' | |||
import { antdUtils } from '@/utils/antd'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import goodsAttrService from '@/request/service/goods-attr'; | |||
import { formatDate } from '@/utils/formatTime'; | |||
import { useSetState } from 'ahooks'; | |||
const AttrEditor = (props: { | |||
visible: boolean; | |||
onCancel: (flag?: boolean) => void; | |||
onSave: (role: GoodsAttrVO) => void; | |||
}) => { | |||
const { visible, onCancel, onSave } = props; | |||
const { runAsync: createApi } = useRequest(goodsAttrService.createGoodsAttrApi, { manual: true }); | |||
const [saveLoading, setSaveLoading] = useState(false); | |||
const [form] = Form.useForm(); | |||
useEffect(() => { | |||
if (visible) { | |||
form.setFieldValue("attrStatus", 1); | |||
} else { | |||
form.resetFields(); | |||
} | |||
}, [visible]); | |||
const save = async () => { | |||
await form.validateFields(); | |||
setSaveLoading(true); | |||
const fieldValues = form.getFieldsValue(); | |||
const [error, { msg, code }] = await createApi(fieldValues); | |||
if (!error && code === 0) { | |||
onSave(fieldValues); | |||
} else { | |||
antdUtils.message?.open({ | |||
type: 'error', | |||
content: msg ?? '操作失败', | |||
}); | |||
} | |||
setSaveLoading(false); | |||
} | |||
return ( | |||
<> | |||
<Modal | |||
open={visible} | |||
title={"新建"} | |||
width={640} | |||
onOk={save} | |||
onCancel={() => onCancel()} | |||
confirmLoading={saveLoading} | |||
destroyOnClose | |||
> | |||
<Form | |||
form={form} | |||
onFinish={save} | |||
labelCol={{ flex: '0 0 120px' }} | |||
wrapperCol={{ span: 16 }} | |||
> | |||
<Form.Item name="attrNameCn" label="属性名称(中文):" | |||
rules={[ | |||
{ | |||
required: true, | |||
message: '请输属性中文名称', | |||
}, | |||
]} | |||
> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="attrNameEn" label="属性名称(英文):" | |||
rules={[ | |||
{ | |||
required: true, | |||
message: '请输属性英文名称', | |||
}, | |||
]} | |||
> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="attrStatus" label="可用状态:" | |||
rules={[ | |||
{ | |||
required: true, | |||
message: '请输选择', | |||
}, | |||
]} | |||
> | |||
<Radio.Group options={[ | |||
{ value: 1, label: "正常使用" }, | |||
{ value: 2, label: "停止使用" } | |||
]} optionType="default"> | |||
</Radio.Group> | |||
</Form.Item> | |||
</Form> | |||
</Modal> | |||
</> | |||
) | |||
}; | |||
const EditableContext = React.createContext<FormInstance<any> | null>(null); | |||
interface EditableRowProps { | |||
index: number; | |||
} | |||
const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => { | |||
const [form] = Form.useForm(); | |||
return ( | |||
<Form form={form} component={false}> | |||
<EditableContext.Provider value={form}> | |||
<tr {...props} /> | |||
</EditableContext.Provider> | |||
</Form> | |||
); | |||
}; | |||
interface EditableCellProps { | |||
title: React.ReactNode; | |||
editable: boolean; | |||
children: React.ReactNode; | |||
dataIndex: keyof GoodsAttrVO; | |||
record: GoodsAttrVO; | |||
handleSave: (record: GoodsAttrVO) => void; | |||
} | |||
const EditableCell: React.FC<EditableCellProps> = ({ | |||
title, | |||
editable, | |||
children, | |||
dataIndex, | |||
record, | |||
handleSave, | |||
...restProps | |||
}) => { | |||
const [editing, setEditing] = useState(false); | |||
const inputRef = useRef<InputRef>(null); | |||
const form = useContext(EditableContext)!; | |||
useEffect(() => { | |||
if (editing) { | |||
inputRef.current!.focus(); | |||
} | |||
}, [editing]); | |||
const toggleEdit = () => { | |||
setEditing(!editing); | |||
form.setFieldsValue({ [dataIndex]: record[dataIndex] }); | |||
}; | |||
const save = async () => { | |||
try { | |||
const values = await form.validateFields(); | |||
toggleEdit(); | |||
handleSave({ ...record, ...values }); | |||
} catch (errInfo) { | |||
console.log('Save failed:', errInfo); | |||
} | |||
}; | |||
let childNode = children; | |||
if (editable) { | |||
childNode = editing ? ( | |||
<Form.Item | |||
style={{ margin: 0 }} | |||
name={dataIndex} | |||
rules={[ | |||
{ | |||
required: true, | |||
message: `${title} is required.`, | |||
}, | |||
]} | |||
> | |||
<Input size="middle" ref={inputRef} onPressEnter={save} onBlur={save} /> | |||
</Form.Item> | |||
) : ( | |||
<div className="editable-cell-value-wrap" style={{ paddingRight: 24 }} onClick={toggleEdit}> | |||
{children} | |||
</div> | |||
); | |||
} | |||
return <td {...restProps}>{childNode}</td>; | |||
}; | |||
type EditableTableProps = Parameters<typeof Table>[0]; | |||
type ColumnTypes = Exclude<EditableTableProps['columns'], undefined>; | |||
export default forwardRef((props, ref) => { | |||
useImperativeHandle(ref, () => { | |||
addItem | |||
}); | |||
const [visible, setVisible] = useState<boolean>(false); | |||
const [dataSource, setDataSource] = useState<GoodsAttrVO[]>([]); | |||
const [searchFrom] = Form.useForm(); | |||
const [searchState, setSearchState] = useSetState<GoodsAttrPageReqVO>({ | |||
pageNo: 1, | |||
pageSize: 10 | |||
}); | |||
const [total, setTotal] = useState(0) | |||
const searchInput = useRef<InputRef>(null); | |||
const [onSearching, setOnSearching] = useState(false); | |||
const { runAsync: getPageApi } = useRequest(goodsAttrService.getGoodsAttrPageApi, { manual: true }); | |||
const { runAsync: updateApi } = useRequest(goodsAttrService.updateGoodsAttrApi, { manual: true }); | |||
const { runAsync: verifyApi } = useRequest(goodsAttrService.attrNameEnVerifyUnique, { manual: true }); | |||
const { runAsync: deleteApi } = useRequest(goodsAttrService.deleteGoodsAttrApi, { manual: true }); | |||
const load = async () => { | |||
const [error, { data }] = await getPageApi(searchState); | |||
if (!error) { | |||
setDataSource(data.list); | |||
setTotal(data.total); | |||
} | |||
}; | |||
const deleteItem = async (data: GoodsAttrVO) => { | |||
const [error, { code, msg }] = await deleteApi(data.id); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }) | |||
} else { | |||
antdUtils.message?.open({ type: 'success', content: '删除成功' }) | |||
} | |||
await load(); | |||
}; | |||
const addItem = () => { | |||
console.log("on call add Item") | |||
} | |||
const getColumnSearchProps = (placeholder: string): ColumnType<GoodsAttrVO> => ({ | |||
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => ( | |||
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}> | |||
<Input.Search | |||
ref={searchInput} | |||
placeholder={placeholder} | |||
value={selectedKeys[0]} | |||
onChange={(e) => { | |||
setSelectedKeys(e.target.value && e.target.value !== '' ? [e.target.value] : []) | |||
}} | |||
onSearch={(value) => { | |||
if (value === '' && clearFilters) { | |||
clearFilters!!() | |||
} | |||
confirm(); | |||
}} | |||
onPressEnter={() => confirm()} | |||
allowClear | |||
style={{ marginBottom: 8, display: 'block' }} | |||
enterButton="搜索" | |||
size="middle" | |||
loading={onSearching} | |||
/> | |||
</div> | |||
), | |||
filterIcon: (filtered: boolean) => ( | |||
<SearchOutlined style={{ color: filtered ? 'primaryColor' : undefined }} /> | |||
), | |||
onFilterDropdownOpenChange: (visible) => { | |||
if (visible) { | |||
setTimeout(() => searchInput.current?.select(), 100); | |||
} | |||
}, | |||
}); | |||
const defaultColumns: (ColumnTypes[number] & { editable?: boolean; dataIndex: string })[] = [ | |||
{ | |||
title: '尺码中文名称', | |||
dataIndex: 'attrNameCn', | |||
key: 'attrNameCn', | |||
align: 'left', | |||
width: '30%', | |||
editable: true, | |||
}, | |||
{ | |||
title: '尺码英文名称', | |||
dataIndex: 'attrNameEn', | |||
key: 'attrNameEn', | |||
align: 'left', | |||
width: '30%', | |||
editable: true, | |||
}, | |||
{ | |||
title: '状态', | |||
key: 'attrStatus', | |||
dataIndex: 'attrStatus', | |||
width: 100, | |||
align: 'center', | |||
render: (value: number) => { | |||
return (value === 1 ? <Badge status="success" text="已开启" /> : <Badge status="error" text="已关闭" />) | |||
} | |||
}, | |||
{ | |||
title: '创建时间', | |||
dataIndex: 'createTime', | |||
key: 'createTime', | |||
align: 'center', | |||
render: (value: number) => { | |||
return formatDate(new Date(value), "YYYY-mm-dd HH:MM:SS") | |||
} | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
dataIndex: 'operation', | |||
key: 'action', | |||
render: (_, record, index) => | |||
dataSource.length >= 1 ? ( | |||
<Popconfirm title="确认要将该属性删除吗?" onConfirm={() => deleteItem(dataSource[index])}> | |||
<a>删除</a> | |||
</Popconfirm> | |||
) : null, | |||
}, | |||
]; | |||
const columns = defaultColumns.map((col) => { | |||
if (!col.editable) { | |||
return col; | |||
} | |||
return { | |||
...col, | |||
...getColumnSearchProps("请输入尺码名称"), | |||
onCell: (record: GoodsAttrVO) => ({ | |||
record, | |||
editable: col.editable, | |||
dataIndex: col.dataIndex, | |||
title: col.title, | |||
handleSave, | |||
}), | |||
}; | |||
}); | |||
const handleSave = async (row: GoodsAttrVO) => { | |||
const [error, { code, msg }] = await updateApi(row); | |||
if (!error && code === 0) { | |||
const newData = [...dataSource]; | |||
const index = newData.findIndex((item) => row.id === item.id); | |||
const item = newData[index]; | |||
newData.splice(index, 1, { | |||
...item, | |||
...row, | |||
}); | |||
setDataSource(newData); | |||
} else { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '更新失败' }) | |||
} | |||
}; | |||
const components = { | |||
body: { | |||
row: EditableRow, | |||
cell: EditableCell, | |||
}, | |||
}; | |||
useEffect(() => { | |||
load(); | |||
}, [searchState]); | |||
const onReset = () => { | |||
searchFrom.resetFields() | |||
load() | |||
} | |||
return ( | |||
<> | |||
<div> | |||
<div className='flex flex-row-reverse mb-2'> | |||
<Button type='primary' size='large' icon={<PlusOutlined />} onClick={() => { setVisible(true) }}> 新增尺码 </Button> | |||
</div> | |||
<Table rowKey="id" | |||
scroll={{ x: true }} | |||
columns={columns as ColumnTypes} | |||
dataSource={dataSource} | |||
components={components} | |||
rowClassName={() => 'editable-row'} | |||
onChange={async (pagination, filters) => { | |||
const state: GoodsAttrPageReqVO = { | |||
attrNameCn: filters.attrNameCn ? filters.attrNameCn[0] as string : undefined, | |||
attrNameEn: filters.attrNameEn ? filters.attrNameEn[0] as string : undefined, | |||
pageNo: pagination.current ?? 1, | |||
pageSize: pagination.pageSize | |||
} | |||
setSearchState(state); | |||
}} | |||
pagination={{ | |||
position: ['bottomRight'], | |||
current: searchState.pageNo, | |||
pageSize: searchState.pageSize, | |||
total | |||
}} /> | |||
</div> | |||
<AttrEditor | |||
visible={visible} | |||
onCancel={() => { setVisible(false) }} | |||
onSave={(data: GoodsAttrVO) => { | |||
setVisible(false); | |||
load(); | |||
}} /> | |||
</> | |||
); | |||
}); | |||
@@ -0,0 +1,152 @@ | |||
import { Space, Table, Button, Input, Select, Divider, Tag, Card, Badge, Form, InputRef } from 'antd'; | |||
import type { TableColumnsType } from 'antd'; | |||
import { t } from '@/utils/i18n'; | |||
import React, { useState, useEffect, useRef } from 'react'; | |||
import { PlusOutlined, ExclamationCircleFilled, SearchOutlined, UndoOutlined } from '@ant-design/icons'; | |||
import type { CategoryPageReqVO, CategoryVO } from '@/models' | |||
import { antdUtils } from '@/utils/antd'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import platformShopService from '@/request/service/category'; | |||
import { formatDate } from '@/utils/formatTime'; | |||
import { useSetState } from 'ahooks'; | |||
export default () => { | |||
const [editorVisable, seEditorVisable] = useState<boolean>(false); | |||
const [editData, seEditData] = useState<CategoryVO>(); | |||
const [dataSource, setDataSource] = useState<CategoryVO[]>([]); | |||
const [searchFrom] = Form.useForm(); | |||
const [searchState, setSearchState] = useSetState<CategoryPageReqVO>({ | |||
pageNo: 1, | |||
pageSize: 10 | |||
}); | |||
const [total, setTotal] = useState(0) | |||
const searchInput = useRef<InputRef>(null); | |||
const [onSearching, setOnSearching] = useState(false); | |||
const { runAsync: getPageApi } = useRequest(platformShopService.getCategoryPageApi, { manual: true }); | |||
const { runAsync: deleteApi } = useRequest(platformShopService.deleteCategoryApi, { manual: true }); | |||
const load = async () => { | |||
const [error, { data }] = await getPageApi(searchFrom.getFieldsValue()); | |||
if (!error) { | |||
setDataSource(data.list); | |||
} | |||
}; | |||
const showDeleteConfirm = (data: CategoryVO) => { | |||
antdUtils.modal?.confirm({ | |||
title: '确认要将该分类删除吗?', | |||
icon: <ExclamationCircleFilled />, | |||
content: '请注意删除以后不可恢复!', | |||
okText: '删除', | |||
okType: 'danger', | |||
cancelText: '取消', | |||
onOk() { | |||
return new Promise(async (resolve) => { | |||
const [error, { code, msg }] = await deleteApi(data.id); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }) | |||
} else { | |||
antdUtils.message?.open({ type: 'success', content: '删除成功' }) | |||
} | |||
await load(); | |||
resolve('') | |||
}).catch(() => antdUtils.message?.open({ | |||
type: 'error', | |||
content: '操作失败', | |||
})); | |||
}, | |||
onCancel() { | |||
}, | |||
}); | |||
}; | |||
const columns: TableColumnsType<CategoryVO> = [ | |||
{ | |||
title: '类目名称(CN)', | |||
dataIndex: 'categoryName', | |||
key: 'categoryName', | |||
align: 'right', | |||
width: 200, | |||
}, | |||
{ | |||
title: '类目名称(EN)', | |||
dataIndex: 'categoryNameEn', | |||
key: 'categoryNameEn', | |||
align: 'center', | |||
width: 200, | |||
}, | |||
{ | |||
title: '敦煌类目', | |||
dataIndex: '', | |||
key: '', | |||
align: 'center', | |||
width: 200, | |||
}, | |||
{ | |||
title: 'sds类目', | |||
dataIndex: '', | |||
key: '', | |||
align: 'center', | |||
width: 200 | |||
}, | |||
{ | |||
title: '虾皮类目', | |||
dataIndex: '', | |||
key: '', | |||
align: 'center', | |||
width: 200 | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
key: 'action', | |||
render: (value: CategoryVO, record) => ( | |||
<Space size="small" split={(<Divider type='vertical' />)}> | |||
<a onClick={() => { | |||
seEditData(value); | |||
seEditorVisable(true); | |||
}}> | |||
编辑 | |||
</a> | |||
<a onClick={() => { | |||
showDeleteConfirm(value) | |||
}}> | |||
删除 | |||
</a> | |||
</Space> | |||
), | |||
}, | |||
]; | |||
useEffect(() => { | |||
load(); | |||
}, []); | |||
const onReset = () => { | |||
searchFrom.resetFields() | |||
load() | |||
} | |||
return ( | |||
<> | |||
<div> | |||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||
<Table rowKey="id" | |||
scroll={{ x: true }} | |||
columns={columns} | |||
dataSource={dataSource} | |||
className='bg-transparent' | |||
pagination={{ | |||
position: ['bottomRight'], | |||
current: searchState.pageNo, | |||
pageSize: searchState.pageSize, | |||
total | |||
}} /> | |||
</Card> | |||
</div> | |||
</> | |||
); | |||
}; | |||
@@ -0,0 +1,272 @@ | |||
import React, { useState, useEffect, useRef } from 'react'; | |||
import { useSetState } from 'ahooks'; | |||
import { Space, Table, Button, Input, FloatButton, Divider, Tag, Card, Tooltip } from 'antd'; | |||
import type { TableColumnsType, InputRef } from 'antd'; | |||
import type { ColumnType, TableProps } from 'antd/es/table'; | |||
import { t } from '@/utils/i18n'; | |||
import { DownloadOutlined, SearchOutlined, ExclamationCircleFilled } from '@ant-design/icons'; | |||
import { antdUtils } from '@/utils/antd'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import { formatDate } from '@/utils/formatTime' | |||
import apiLogService from '@/request/service/api-log'; | |||
import { | |||
ApiAccessLogPageReqVO, | |||
ApiAccessLogExportReqVO, | |||
ApiAccessLogVO | |||
} from '@/models'; | |||
type DataIndex = keyof ApiAccessLogVO; | |||
const mapOperation = (operation: number) => { | |||
if (operation === 1) { | |||
return (<Tag color="purple">通知</Tag>) | |||
} else if (operation === 2) { | |||
return (<Tag color="purple">新增</Tag>) | |||
} else if (operation === 3) { | |||
return (<Tag color="yellow">修改</Tag>) | |||
} else if (operation === 4) { | |||
return (<Tag color="red">删除</Tag>) | |||
} else if (operation === 5) { | |||
return (<Tag color="blue">导出</Tag>) | |||
} else { | |||
return (<Tag color="red">未知({operation})</Tag>) | |||
} | |||
} | |||
export default () => { | |||
const [dataSource, setDataSource] = useState<ApiAccessLogVO[]>([]); | |||
const { runAsync: getPageData } = useRequest(apiLogService.getApiAccessLogPageApi, { manual: true }); | |||
const { runAsync: exportOperateLog } = useRequest(apiLogService.exportApiAccessLogApi, { manual: true }); | |||
const [searchState, setSearchState] = useSetState<ApiAccessLogPageReqVO>({ | |||
pageNo: 1, | |||
pageSize: 10 | |||
}); | |||
const [total, setTotal] = useState(0) | |||
const searchInput = useRef<InputRef>(null); | |||
const [onSearching, setOnSearching] = useState(false); | |||
const load = async () => { | |||
console.log(searchState) | |||
const [error, { code, msg, data }] = await getPageData(searchState); | |||
setOnSearching(false); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }); | |||
return | |||
} | |||
setTotal(data.total); | |||
setDataSource(data.list); | |||
}; | |||
const getColumnSearchProps = (dataIndex: DataIndex, placeholder: string): ColumnType<ApiAccessLogVO> => ({ | |||
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => ( | |||
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}> | |||
<Input.Search | |||
ref={searchInput} | |||
placeholder={placeholder} | |||
value={selectedKeys[0]} | |||
onChange={(e) => { | |||
setSelectedKeys(e.target.value && e.target.value !== '' ? [e.target.value] : []) | |||
}} | |||
onSearch={(value) => { | |||
if (value === '' && clearFilters) { | |||
clearFilters!!() | |||
} | |||
confirm(); | |||
}} | |||
onPressEnter={() => confirm()} | |||
allowClear | |||
style={{ marginBottom: 8, display: 'block' }} | |||
enterButton="搜索" | |||
size="middle" | |||
loading={onSearching} | |||
/> | |||
</div> | |||
), | |||
filterIcon: (filtered: boolean) => ( | |||
<SearchOutlined style={{ color: filtered ? 'primaryColor' : undefined }} /> | |||
), | |||
onFilterDropdownOpenChange: (visible) => { | |||
if (visible) { | |||
setTimeout(() => searchInput.current?.select(), 100); | |||
} | |||
}, | |||
}); | |||
const columns: TableColumnsType<ApiAccessLogVO> = [ | |||
{ | |||
title: '日志编号', | |||
dataIndex: 'id', | |||
key: 'id', | |||
align: 'center', | |||
fixed: 'left', | |||
width: 100, | |||
}, | |||
{ | |||
title: '链路追踪', | |||
dataIndex: 'traceId', | |||
key: 'traceId', | |||
align: 'center', | |||
width: 100, | |||
}, | |||
{ | |||
title: '用户编号', | |||
dataIndex: 'userId', | |||
key: 'userId', | |||
align: 'center', | |||
width: 100, | |||
...getColumnSearchProps('userId', "请输入用户编号") | |||
}, | |||
{ | |||
title: '用户类型', | |||
dataIndex: 'userType', | |||
key: 'userType', | |||
fixed: 'left', | |||
align: 'center', | |||
width: 100, | |||
filters: [ | |||
{ text: '会员', value: 1 }, | |||
{ text: '管理员', value: 2 }, | |||
], | |||
filterMultiple: false, | |||
filterSearch: false, | |||
render: (value: number) => { | |||
if (value === 1) { | |||
return <Tag color="purple">会员</Tag> | |||
} else if (value === 2) { | |||
return <Tag color="blue">管理员</Tag> | |||
} else { | |||
return <Tag color="red">未知</Tag> | |||
} | |||
} | |||
}, | |||
{ | |||
title: '应用名', | |||
dataIndex: 'applicationName', | |||
key: 'applicationName', | |||
align: 'center', | |||
width: 120, | |||
...getColumnSearchProps('applicationName', "请输入应用名") | |||
}, | |||
{ | |||
title: '请求方法', | |||
dataIndex: 'requestMethod', | |||
key: 'requestMethod', | |||
align: 'center', | |||
width: 100, | |||
}, | |||
{ | |||
title: '请求地址', | |||
dataIndex: 'requestUrl', | |||
key: 'requestUrl', | |||
align: 'center', | |||
width: 150, | |||
render: (text, record) => ( | |||
<Tooltip title={text}> | |||
<div className='text-ellipsis overflow-hidden whitespace-nowrap max-w-xs'> | |||
{text} | |||
</div> | |||
</Tooltip> | |||
), | |||
...getColumnSearchProps('requestUrl', "请输入请求地址") | |||
}, | |||
{ | |||
title: '请求时间', | |||
dataIndex: 'beginTime', | |||
key: 'beginTime', | |||
align: 'center', | |||
width: 150, | |||
render: (value: number) => { | |||
return formatDate(new Date(value), "YYYY-mm-dd HH:MM:SS") | |||
} | |||
}, | |||
{ | |||
title: '执行时长', | |||
dataIndex: 'duration', | |||
key: 'duration', | |||
align: 'center', | |||
}, | |||
{ | |||
title: '操作结果', | |||
dataIndex: 'resultCode', | |||
key: 'resultCode', | |||
align: 'center', | |||
width: 150, | |||
render: (value: number) => { | |||
if (value === 0) { | |||
return <Tag color="purple">成功</Tag> | |||
} else if (value === 1) { | |||
return <Tag color="red">失败</Tag> | |||
} else { | |||
return <Tag color="red">未知</Tag> | |||
} | |||
} | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
key: 'action', | |||
fixed: 'right', | |||
align: 'center', | |||
width: 100, | |||
render: (value: ApiAccessLogVO, record) => ( | |||
<Space size="small" split={(<Divider type='vertical' />)}> | |||
<a onClick={() => { | |||
}}> 详情 </a> | |||
</Space> | |||
), | |||
}, | |||
]; | |||
const exportLogs = async () => { | |||
await exportOperateLog(); | |||
} | |||
useEffect(() => { | |||
load(); | |||
}, [searchState]); | |||
const onChange: TableProps<ApiAccessLogVO>['onChange'] = (pagination, filters, sorter, extra) => { | |||
const state: ApiAccessLogPageReqVO = { | |||
applicationName: filters.applicationName ? filters.applicationName[0] as string : undefined, | |||
requestUrl: filters.requestUrl ? filters.requestUrl[0] as string : undefined, | |||
userId: filters.userId ? parseInt(filters.userId[0] as string) : undefined, | |||
userType: filters.userType ? filters.userType[0] as number : undefined, | |||
pageNo: pagination.current, | |||
pageSize: pagination.pageSize | |||
} | |||
setOnSearching(true); | |||
setSearchState(state); | |||
}; | |||
return ( | |||
<> | |||
<div> | |||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||
<Table rowKey="id" | |||
scroll={{ x: true }} | |||
columns={columns} | |||
dataSource={dataSource} | |||
onChange={onChange} | |||
className='bg-transparent' | |||
pagination={{ | |||
position: ['bottomRight'], | |||
current: searchState.pageNo, | |||
pageSize: searchState.pageSize, | |||
total | |||
}} | |||
/> | |||
</Card> | |||
<FloatButton | |||
type='primary' | |||
tooltip={<div>导出日志</div>} | |||
icon={<DownloadOutlined />} | |||
onClick={exportLogs} | |||
/> | |||
</div> | |||
</> | |||
); | |||
}; |
@@ -145,9 +145,14 @@ export default () => { | |||
dataIndex: 'userType', | |||
key: 'userType', | |||
fixed: 'left', | |||
filterSearch: true, | |||
align: 'center', | |||
width: 100, | |||
filters: [ | |||
{ text: '会员', value: 1 }, | |||
{ text: '管理员', value: 2 }, | |||
], | |||
filterMultiple: false, | |||
filterSearch: false, | |||
render: (value: number) => { | |||
if (value === 1) { | |||
return <Tag color="purple">会员</Tag> | |||
@@ -179,7 +184,15 @@ export default () => { | |||
key: 'requestUrl', | |||
align: 'center', | |||
width: 150, | |||
render: (text, record) => ( | |||
<Tooltip title={text}> | |||
<div className='text-ellipsis overflow-hidden whitespace-nowrap max-w-xs'> | |||
{text} | |||
</div> | |||
</Tooltip> | |||
), | |||
...getColumnSearchProps('requestUrl', "请输入请求地址") | |||
}, | |||
{ | |||
title: '异常时间', | |||
@@ -196,7 +209,13 @@ export default () => { | |||
dataIndex: 'exceptionName', | |||
key: 'exceptionName', | |||
align: 'center', | |||
width: 150, | |||
render: (text, record) => ( | |||
<Tooltip title={text}> | |||
<p className='text-ellipsis overflow-hidden whitespace-nowrap max-w-xs'> | |||
{text} | |||
</p> | |||
</Tooltip> | |||
) | |||
}, | |||
{ | |||
title: '处理状态', | |||
@@ -206,13 +225,13 @@ export default () => { | |||
width: 150, | |||
render: (value: number) => { | |||
if (value === 0) { | |||
return <Tag color="red">未处理</Tag> | |||
return <Tag color="yellow">未处理</Tag> | |||
} else if (value === 1) { | |||
return <Tag color="purple">已处理</Tag> | |||
} else if (value === 2) { | |||
return <Tag color="blue">已忽略</Tag> | |||
} else { | |||
return <Tag color="purple">内置</Tag> | |||
return <Tag color="red">未知</Tag> | |||
} | |||
} | |||
}, | |||
@@ -243,7 +262,7 @@ export default () => { | |||
]; | |||
const exportLogs = async () => { | |||
await exportOperateLog() | |||
await exportOperateLog(); | |||
} | |||
useEffect(() => { | |||
@@ -255,6 +274,7 @@ export default () => { | |||
applicationName: filters.applicationName ? filters.applicationName[0] as string : undefined, | |||
requestUrl: filters.requestUrl ? filters.requestUrl[0] as string : undefined, | |||
userId: filters.userId ? parseInt(filters.userId[0] as string) : undefined, | |||
userType: filters.userType ? filters.userType[0] as number : undefined, | |||
pageNo: pagination.current, | |||
pageSize: pagination.pageSize | |||
} | |||
@@ -61,7 +61,7 @@ const DepartmentEditor: React.FC<EditorProps> = (props) => { | |||
<> | |||
<Modal | |||
open={visible} | |||
title="新建" | |||
title={isEdit ? "编辑" : "新建"} | |||
width={640} | |||
onOk={save} | |||
onCancel={() => onCancel()} | |||
@@ -0,0 +1,258 @@ | |||
import React, { useState, useEffect, useRef } from 'react'; | |||
import { useSetState } from 'ahooks'; | |||
import { Space, Table, Input, Divider, Tag, Card, Badge, Image } from 'antd'; | |||
import type { TableColumnsType, InputRef } from 'antd'; | |||
import type { ColumnType, TableProps } from 'antd/es/table'; | |||
import { t } from '@/utils/i18n'; | |||
import { SearchOutlined, ExclamationCircleFilled } from '@ant-design/icons'; | |||
import { antdUtils } from '@/utils/antd'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import { formatDate } from '@/utils/formatTime' | |||
import oauth2Service from '@/request/service/oauth2'; | |||
import { | |||
OAuth2ClientVO, | |||
OAuth2ClientPageReqVO | |||
} from '@/models'; | |||
const defaultFallbackImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg==" | |||
type DataIndex = keyof OAuth2ClientVO; | |||
export default () => { | |||
const [dataSource, setDataSource] = useState<OAuth2ClientVO[]>([]); | |||
const { runAsync: getPageData } = useRequest(oauth2Service.getOAuth2ClientPageApi, { manual: true }); | |||
const { runAsync: deleteClient } = useRequest(oauth2Service.deleteOAuth2ClientApi, { manual: true }); | |||
const [searchState, setSearchState] = useSetState<OAuth2ClientPageReqVO>({ | |||
pageNo: 1, | |||
pageSize: 10 | |||
}); | |||
const [total, setTotal] = useState(0) | |||
const searchInput = useRef<InputRef>(null); | |||
const [onSearching, setOnSearching] = useState(false); | |||
const load = async () => { | |||
console.log(searchState) | |||
const [error, { code, msg, data }] = await getPageData(searchState); | |||
setOnSearching(false); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }); | |||
return | |||
} | |||
setTotal(data.total); | |||
setDataSource(data.list); | |||
}; | |||
const getColumnSearchProps = (dataIndex: DataIndex, placeholder: string): ColumnType<OAuth2ClientVO> => ({ | |||
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => ( | |||
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}> | |||
<Input.Search | |||
ref={searchInput} | |||
placeholder={placeholder} | |||
value={selectedKeys[0]} | |||
onChange={(e) => { | |||
setSelectedKeys(e.target.value && e.target.value !== '' ? [e.target.value] : []) | |||
}} | |||
onSearch={(value) => { | |||
if (value === '' && clearFilters) { | |||
clearFilters!!() | |||
} | |||
confirm(); | |||
}} | |||
onPressEnter={() => confirm()} | |||
allowClear | |||
style={{ marginBottom: 8, display: 'block' }} | |||
enterButton="搜索" | |||
size="middle" | |||
loading={onSearching} | |||
/> | |||
</div> | |||
), | |||
filterIcon: (filtered: boolean) => ( | |||
<SearchOutlined style={{ color: filtered ? 'primaryColor' : undefined }} /> | |||
), | |||
onFilterDropdownOpenChange: (visible) => { | |||
if (visible) { | |||
setTimeout(() => searchInput.current?.select(), 100); | |||
} | |||
}, | |||
}); | |||
const deleteItem = async (params: OAuth2ClientVO) => { | |||
antdUtils.modal?.confirm({ | |||
title: '系统提示', | |||
icon: <ExclamationCircleFilled />, | |||
content: '是否删除所选中数据?', | |||
okText: '删除', | |||
okType: 'danger', | |||
cancelText: '取消', | |||
onOk() { | |||
return new Promise(async (resolve) => { | |||
const [error, { code, msg }] = await deleteClient(params.id); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }) | |||
} else { | |||
antdUtils.message?.open({ type: 'success', content: '操作成功' }) | |||
} | |||
resolve(''); | |||
await load(); | |||
}).catch(() => antdUtils.message?.open({ | |||
type: 'error', | |||
content: '操作失败', | |||
})); | |||
}, | |||
onCancel() { | |||
}, | |||
}); | |||
} | |||
const columns: TableColumnsType<OAuth2ClientVO> = [ | |||
{ | |||
title: '客户端密钥', | |||
dataIndex: 'secret', | |||
key: 'secret', | |||
align: 'center', | |||
width: 120, | |||
}, | |||
{ | |||
title: '应用名', | |||
dataIndex: 'name', | |||
key: 'name', | |||
align: 'center', | |||
width: 100, | |||
}, | |||
{ | |||
title: '应用图标', | |||
dataIndex: 'logo', | |||
key: 'logo', | |||
align: 'center', | |||
width: 100, | |||
render: (value: string) => { | |||
return <Image src={value} fallback={defaultFallbackImage}/> | |||
} | |||
}, | |||
{ | |||
title: '状态', | |||
dataIndex: 'status', | |||
key: 'status', | |||
align: 'center', | |||
width: 100, | |||
filters: [ | |||
{ text: '会员', value: 1 }, | |||
{ text: '管理员', value: 2 }, | |||
], | |||
filterMultiple: false, | |||
filterSearch: false, | |||
render: (value: number) => { | |||
return (value === 0 ? <Badge status="success" text="已开启" /> : <Badge status="error" text="已关闭" />) | |||
} | |||
}, | |||
{ | |||
title: '访问令牌的有效期', | |||
dataIndex: 'accessTokenValiditySeconds', | |||
key: 'accessTokenValiditySeconds', | |||
align: 'center', | |||
width: 100, | |||
render: (value) => { | |||
return `${value}秒` | |||
} | |||
}, | |||
{ | |||
title: '刷新令牌的有效期', | |||
dataIndex: 'refreshTokenValiditySeconds', | |||
key: 'refreshTokenValiditySeconds', | |||
align: 'center', | |||
width: 100, | |||
render: (value) => { | |||
return `${value}秒` | |||
} | |||
}, | |||
{ | |||
title: '授权类型', | |||
dataIndex: 'authorizedGrantTypes', | |||
key: 'authorizedGrantTypes', | |||
fixed: 'left', | |||
align: 'center', | |||
width: 100, | |||
filterMultiple: false, | |||
filterSearch: false, | |||
render: (value: string[]) => { | |||
return (<> | |||
{ | |||
...value.map(it => { | |||
return <Tag color="blue">{it}</Tag> | |||
}) | |||
} | |||
</>) | |||
} | |||
}, | |||
{ | |||
title: '创建时间', | |||
dataIndex: 'createTime', | |||
key: 'createTime', | |||
align: 'center', | |||
width: 150, | |||
render: (value: number) => { | |||
return formatDate(new Date(value), "YYYY-mm-dd HH:MM:SS") | |||
} | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
key: 'action', | |||
fixed: 'right', | |||
align: 'center', | |||
width: 250, | |||
render: (value: OAuth2ClientVO, record) => ( | |||
<Space size="small" split={(<Divider type='vertical' />)}> | |||
<a onClick={() => { | |||
}}> 详情 </a> | |||
<a onClick={() => { | |||
deleteItem(value) | |||
}}> 删除 </a> | |||
</Space> | |||
), | |||
}, | |||
]; | |||
useEffect(() => { | |||
load(); | |||
}, [searchState]); | |||
const onChange: TableProps<OAuth2ClientVO>['onChange'] = (pagination, filters, sorter, extra) => { | |||
const state: OAuth2ClientPageReqVO = { | |||
name: filters.name ? filters.name[0] as string : undefined, | |||
status: filters.status ? filters.status[0] as number : undefined, | |||
pageNo: pagination.current, | |||
pageSize: pagination.pageSize | |||
} | |||
setOnSearching(true); | |||
setSearchState(state); | |||
}; | |||
return ( | |||
<> | |||
<div> | |||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||
<Table rowKey="id" | |||
scroll={{ x: true }} | |||
columns={columns} | |||
dataSource={dataSource} | |||
onChange={onChange} | |||
className='bg-transparent' | |||
pagination={{ | |||
position: ['bottomRight'], | |||
current: searchState.pageNo, | |||
pageSize: searchState.pageSize, | |||
total | |||
}} | |||
/> | |||
</Card> | |||
</div> | |||
</> | |||
); | |||
}; |
@@ -0,0 +1,242 @@ | |||
import React, { useState, useEffect, useRef } from 'react'; | |||
import { useSetState } from 'ahooks'; | |||
import { Space, Table, Input, Divider, Tag, Card } from 'antd'; | |||
import type { TableColumnsType, InputRef } from 'antd'; | |||
import type { ColumnType, TableProps } from 'antd/es/table'; | |||
import { t } from '@/utils/i18n'; | |||
import { SearchOutlined, ExclamationCircleFilled } from '@ant-design/icons'; | |||
import { antdUtils } from '@/utils/antd'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import { formatDate } from '@/utils/formatTime' | |||
import oauth2Service from '@/request/service/oauth2'; | |||
import { | |||
OAuth2TokenVO, | |||
OAuth2TokenPageReqVO | |||
} from '@/models'; | |||
type DataIndex = keyof OAuth2TokenVO; | |||
export default () => { | |||
const [dataSource, setDataSource] = useState<OAuth2TokenVO[]>([]); | |||
const { runAsync: getPageData } = useRequest(oauth2Service.getAccessTokenPageApi, { manual: true }); | |||
const { runAsync: deleteAccessToken } = useRequest(oauth2Service.deleteAccessTokenApi, { manual: true }); | |||
const [searchState, setSearchState] = useSetState<OAuth2TokenPageReqVO>({ | |||
pageNo: 1, | |||
pageSize: 10 | |||
}); | |||
const [total, setTotal] = useState(0) | |||
const searchInput = useRef<InputRef>(null); | |||
const [onSearching, setOnSearching] = useState(false); | |||
const load = async () => { | |||
console.log(searchState) | |||
const [error, { code, msg, data }] = await getPageData(searchState); | |||
setOnSearching(false); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }); | |||
return | |||
} | |||
setTotal(data.total); | |||
setDataSource(data.list); | |||
}; | |||
const getColumnSearchProps = (dataIndex: DataIndex, placeholder: string): ColumnType<OAuth2TokenVO> => ({ | |||
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => ( | |||
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}> | |||
<Input.Search | |||
ref={searchInput} | |||
placeholder={placeholder} | |||
value={selectedKeys[0]} | |||
onChange={(e) => { | |||
setSelectedKeys(e.target.value && e.target.value !== '' ? [e.target.value] : []) | |||
}} | |||
onSearch={(value) => { | |||
if (value === '' && clearFilters) { | |||
clearFilters!!() | |||
} | |||
confirm(); | |||
}} | |||
onPressEnter={() => confirm()} | |||
allowClear | |||
style={{ marginBottom: 8, display: 'block' }} | |||
enterButton="搜索" | |||
size="middle" | |||
loading={onSearching} | |||
/> | |||
</div> | |||
), | |||
filterIcon: (filtered: boolean) => ( | |||
<SearchOutlined style={{ color: filtered ? 'primaryColor' : undefined }} /> | |||
), | |||
onFilterDropdownOpenChange: (visible) => { | |||
if (visible) { | |||
setTimeout(() => searchInput.current?.select(), 100); | |||
} | |||
}, | |||
}); | |||
const forceQuite = async (params: OAuth2TokenVO) => { | |||
antdUtils.modal?.confirm({ | |||
title: '系统提示', | |||
icon: <ExclamationCircleFilled />, | |||
content: '是否要强制退出用户', | |||
okText: '确定', | |||
okType: 'danger', | |||
cancelText: '取消', | |||
onOk() { | |||
return new Promise(async (resolve) => { | |||
const [error, { code, msg }] = await deleteAccessToken(params.accessToken); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }) | |||
} else { | |||
antdUtils.message?.open({ type: 'success', content: '操作成功' }) | |||
} | |||
resolve(''); | |||
await load(); | |||
}).catch(() => antdUtils.message?.open({ | |||
type: 'error', | |||
content: '操作失败', | |||
})); | |||
}, | |||
onCancel() { | |||
}, | |||
}); | |||
} | |||
const columns: TableColumnsType<OAuth2TokenVO> = [ | |||
{ | |||
title: '用户编号', | |||
dataIndex: 'userId', | |||
key: 'userId', | |||
align: 'center', | |||
width: 100, | |||
...getColumnSearchProps('userId', "请输入用户编号") | |||
}, | |||
{ | |||
title: '访问令牌', | |||
dataIndex: 'accessToken', | |||
key: 'accessToken', | |||
align: 'center', | |||
width: 120, | |||
}, | |||
{ | |||
title: '刷新令牌', | |||
dataIndex: 'refreshToken', | |||
key: 'refreshToken', | |||
align: 'center', | |||
width: 100, | |||
}, | |||
{ | |||
title: '客户端编号', | |||
dataIndex: 'clientId', | |||
key: 'clientId', | |||
align: 'center', | |||
width: 150, | |||
...getColumnSearchProps('clientId', "客户端编号") | |||
}, | |||
{ | |||
title: '用户类型', | |||
dataIndex: 'userType', | |||
key: 'userType', | |||
fixed: 'left', | |||
align: 'center', | |||
width: 100, | |||
filters: [ | |||
{ text: '会员', value: 1 }, | |||
{ text: '管理员', value: 2 }, | |||
], | |||
filterMultiple: false, | |||
filterSearch: false, | |||
render: (value: number) => { | |||
if (value === 1) { | |||
return <Tag color="purple">会员</Tag> | |||
} else if (value === 2) { | |||
return <Tag color="blue">管理员</Tag> | |||
} else { | |||
return <Tag color="red">未知</Tag> | |||
} | |||
} | |||
}, | |||
{ | |||
title: '创建时间', | |||
dataIndex: 'createTime', | |||
key: 'createTime', | |||
align: 'center', | |||
width: 150, | |||
render: (value: number) => { | |||
return formatDate(new Date(value), "YYYY-mm-dd HH:MM:SS") | |||
} | |||
}, | |||
{ | |||
title: '过期时间', | |||
dataIndex: 'expiresTime', | |||
key: 'expiresTime', | |||
align: 'center', | |||
width: 150, | |||
render: (value: number) => { | |||
return formatDate(new Date(value), "YYYY-mm-dd HH:MM:SS") | |||
} | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
key: 'action', | |||
fixed: 'right', | |||
align: 'center', | |||
width: 250, | |||
render: (value: OAuth2TokenVO, record) => ( | |||
<Space size="small" split={(<Divider type='vertical' />)}> | |||
<a onClick={() => { | |||
}}> 详情 </a> | |||
<a onClick={() => { | |||
forceQuite(value) | |||
}}> 强制退出 </a> | |||
</Space> | |||
), | |||
}, | |||
]; | |||
useEffect(() => { | |||
load(); | |||
}, [searchState]); | |||
const onChange: TableProps<OAuth2TokenVO>['onChange'] = (pagination, filters, sorter, extra) => { | |||
const state: OAuth2TokenPageReqVO = { | |||
clientId: filters.clientId ? filters.clientId[0] as string : undefined, | |||
userId: filters.userId ? parseInt(filters.userId[0] as string) : undefined, | |||
userType: filters.userType ? filters.userType[0] as number : undefined, | |||
pageNo: pagination.current, | |||
pageSize: pagination.pageSize | |||
} | |||
setOnSearching(true); | |||
setSearchState(state); | |||
}; | |||
return ( | |||
<> | |||
<div> | |||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||
<Table rowKey="id" | |||
scroll={{ x: true }} | |||
columns={columns} | |||
dataSource={dataSource} | |||
onChange={onChange} | |||
className='bg-transparent' | |||
pagination={{ | |||
position: ['bottomRight'], | |||
current: searchState.pageNo, | |||
pageSize: searchState.pageSize, | |||
total | |||
}} | |||
/> | |||
</Card> | |||
</div> | |||
</> | |||
); | |||
}; |
@@ -61,7 +61,7 @@ const PositionEditor: React.FC<EditorProps> = (props) => { | |||
<> | |||
<Modal | |||
open={visible} | |||
title="新建" | |||
title={isEdit ? "编辑" : "新建"} | |||
width={640} | |||
onOk={save} | |||
onCancel={() => onCancel()} | |||
@@ -0,0 +1,240 @@ | |||
import { Space, Table, Button, Input, Select, Divider, Tag, Card, Badge, Form } from 'antd'; | |||
import type { TableColumnsType } from 'antd'; | |||
import { t } from '@/utils/i18n'; | |||
import React, { useState, useEffect } from 'react'; | |||
import { PlusOutlined, ExclamationCircleFilled, SearchOutlined, UndoOutlined } from '@ant-design/icons'; | |||
import type { PlatformShop } from '@/models' | |||
import { antdUtils } from '@/utils/antd'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import platformShopService from '@/request/service/platform-shop'; | |||
import { formatDate } from '@/utils/formatTime' | |||
import ShopEditor from './shop-editor'; | |||
export default () => { | |||
const [editorVisable, seEditorVisable] = useState<boolean>(false); | |||
const [editData, seEditData] = useState<PlatformShop>(); | |||
const [dataSource, setDataSource] = useState<PlatformShop[]>([]); | |||
const [searchFrom] = Form.useForm(); | |||
const { runAsync: getPageApi } = useRequest(platformShopService.getPlatformShopList, { manual: true }); | |||
const { runAsync: deleteApi } = useRequest(platformShopService.deletePlatformShop, { manual: true }); | |||
const load = async () => { | |||
const [error, { data }] = await getPageApi(searchFrom.getFieldsValue()); | |||
if (!error) { | |||
setDataSource(data.list); | |||
} | |||
}; | |||
const showDeleteConfirm = (data: PlatformShop) => { | |||
antdUtils.modal?.confirm({ | |||
title: '确认要将该店铺删除吗?', | |||
icon: <ExclamationCircleFilled />, | |||
content: '请注意删除以后不可恢复!', | |||
okText: '删除', | |||
okType: 'danger', | |||
cancelText: '取消', | |||
onOk() { | |||
return new Promise(async (resolve) => { | |||
const [error, { code, msg }] = await deleteApi(data.platformShopId); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }) | |||
} else { | |||
antdUtils.message?.open({ type: 'success', content: '删除成功' }) | |||
} | |||
await load(); | |||
resolve('') | |||
}).catch(() => antdUtils.message?.open({ | |||
type: 'error', | |||
content: '操作失败', | |||
})); | |||
}, | |||
onCancel() { | |||
}, | |||
}); | |||
}; | |||
const columns: TableColumnsType<PlatformShop> = [ | |||
{ | |||
title: '名称', | |||
dataIndex: 'shopName', | |||
key: 'shopName', | |||
align: 'right', | |||
width: 100, | |||
}, | |||
{ | |||
title: '平台', | |||
dataIndex: 'platformId', | |||
key: 'platformId', | |||
align: 'center', | |||
width: 150, | |||
render: (value: number) => { | |||
//TODO: get platform name by id | |||
return "虾皮" | |||
} | |||
}, | |||
{ | |||
title: '商户信息', | |||
dataIndex: 'packageId', | |||
key: 'packageId', | |||
align: 'center', | |||
width: 100, | |||
}, | |||
{ | |||
title: '店长', | |||
dataIndex: 'shopManager', | |||
key: 'shopManager', | |||
align: 'center', | |||
width: 150 | |||
}, | |||
{ | |||
title: '店员', | |||
dataIndex: 'shopAssistantList', | |||
key: 'shopAssistantList', | |||
align: 'center', | |||
width: 150 | |||
}, | |||
{ | |||
title: '客服', | |||
dataIndex: 'shopCustomerList', | |||
key: 'shopCustomerList', | |||
align: 'center', | |||
width: 150 | |||
}, | |||
{ | |||
title: '状态', | |||
key: 'shopStatus', | |||
dataIndex: 'shopStatus', | |||
width: 100, | |||
align: 'center', | |||
render: (value: number) => { | |||
return (value === 1 ? <Badge status="success" text="已开启" /> : <Badge status="error" text="已停用" />) | |||
} | |||
}, | |||
{ | |||
title: '授权状态', | |||
dataIndex: 'tokenFlag', | |||
key: 'tokenFlag', | |||
align: 'center', | |||
width: 200, | |||
render: (value: number) => { | |||
//1:成功, 2:失败,0:未授权 | |||
if(value === 0) { | |||
return <Tag color='yellow'>未授权</Tag> | |||
} else if(value === 1) { | |||
return <Tag color='purple'>成功</Tag> | |||
} else if(value === 2) { | |||
return <Tag color='red'>失败</Tag> | |||
} | |||
return <Tag >未知状态</Tag> | |||
} | |||
}, | |||
{ | |||
title: '授权过期时间', | |||
key: 'expiresTime', | |||
dataIndex: 'expiresTime', | |||
width: 200, | |||
render: (value: number) => { | |||
return formatDate(new Date(value), "YYYY-mm-dd HH:MM:SS") | |||
} | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
key: 'action', | |||
render: (value: PlatformShop, record) => ( | |||
<Space size="small" split={(<Divider type='vertical' />)}> | |||
<a onClick={() => { | |||
seEditData(value); | |||
seEditorVisable(true); | |||
}}> | |||
编辑 | |||
</a> | |||
<a onClick={() => { | |||
showDeleteConfirm(value) | |||
}}> | |||
删除 | |||
</a> | |||
</Space> | |||
), | |||
}, | |||
]; | |||
useEffect(() => { | |||
load(); | |||
}, []); | |||
const onReset = () => { | |||
searchFrom.resetFields() | |||
load() | |||
} | |||
return ( | |||
<> | |||
<div> | |||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]' bodyStyle={{ paddingTop: 4, paddingBottom: 4 }}> | |||
<div className='flex justify-between content-center'> | |||
<div className='flex justify-normal items-center'> | |||
<Form layout='inline' form={searchFrom}> | |||
<Form.Item name="shopName" label="店铺名称"> | |||
<Input className='w-[140px]' placeholder='请输入名称' allowClear /> | |||
</Form.Item> | |||
<Form.Item name="platformId" label="平台"> | |||
<Input className='w-[140px]' placeholder='请选择' allowClear /> | |||
</Form.Item> | |||
<Form.Item name="shopManagerId" label="所属店长"> | |||
<Input className='w-[140px]' placeholder='请输入' allowClear /> | |||
</Form.Item> | |||
<Form.Item name="shopStaffId" label="所属商户"> | |||
<Input className='w-[140px]' placeholder='请输入' allowClear /> | |||
</Form.Item> | |||
<Form.Item className='ml-2 w-[130px]' name="shopStatus" label="状态"> | |||
<Select placeholder="请选择" allowClear > | |||
<Select.Option value="">全部</Select.Option> | |||
<Select.Option value="0">开启</Select.Option> | |||
<Select.Option value="1">关闭</Select.Option> | |||
</Select> | |||
</Form.Item> | |||
<Form.Item className='ml-2 w-[150px]' name="tokenFlag" label="授权状态"> | |||
<Select placeholder="请选择" allowClear > | |||
<Select.Option value="">全部</Select.Option> | |||
<Select.Option value="0">成功</Select.Option> | |||
<Select.Option value="1">失败</Select.Option> | |||
</Select> | |||
</Form.Item> | |||
</Form> | |||
<Space.Compact className="ml-5"> | |||
<Button type='primary' size='large' icon={<SearchOutlined />} onClick={load}> 搜索 </Button> | |||
<Button type='primary' size='large' icon={<UndoOutlined />} onClick={onReset}> 重置 </Button> | |||
</Space.Compact> | |||
</div> | |||
<div className="py-[4px]"> | |||
<Button className="ml-5" type='primary' size='large' icon={<PlusOutlined />} onClick={() => { | |||
seEditData(undefined); | |||
seEditorVisable(true); | |||
}}> 新增店铺 </Button> | |||
</div> | |||
</div> | |||
</Card> | |||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||
<Table rowKey="id" | |||
scroll={{ x: true }} | |||
columns={columns} | |||
dataSource={dataSource} | |||
className='bg-transparent' | |||
pagination={false} /> | |||
</Card> | |||
</div> | |||
<ShopEditor | |||
onSave={() => { | |||
load(); | |||
seEditorVisable(false); | |||
}} | |||
onCancel={() => { seEditorVisable(false) }} | |||
visible={editorVisable} | |||
data={editData} /> | |||
</> | |||
); | |||
}; | |||
@@ -0,0 +1,227 @@ | |||
import React, { useEffect, useState } from 'react'; | |||
import { Form, Input, InputNumber, Radio, Modal, Steps, DatePicker, Button } from 'antd'; | |||
import platformShopService from '@/request/service/platform-shop'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import type { PlatformShop } from '@/models' | |||
import { antdUtils } from '@/utils/antd'; | |||
import customParseFormat from 'dayjs/plugin/customParseFormat'; | |||
import dayjs from 'dayjs'; | |||
dayjs.extend(customParseFormat); | |||
const layout = { | |||
labelCol: { span: 4, }, | |||
wrapperCol: { span: 16 } | |||
}; | |||
interface EditorProps { | |||
visible: boolean; | |||
onCancel: (flag?: boolean) => void; | |||
onSave: (role: PlatformShop) => void; | |||
data?: PlatformShop | null; | |||
} | |||
const TenantEditor: React.FC<EditorProps> = (props) => { | |||
const { visible, onCancel, onSave, data } = props; | |||
const { runAsync: updateApi } = useRequest(platformShopService.updatePlatformShop, { manual: true }); | |||
const { runAsync: createApi } = useRequest(platformShopService.createPlatformShop, { manual: true }); | |||
const isEdit = !!data; | |||
const [current, setCurrent] = useState(0); | |||
const next = async () => { | |||
const isValidate = await form.validateFields(); | |||
console.log(isValidate) | |||
setCurrent(current + 1); | |||
}; | |||
const prev = () => { | |||
setCurrent(current - 1); | |||
}; | |||
const [saveLoading, setSaveLoading] = useState(false); | |||
const [form] = Form.useForm(); | |||
const [shopForm] = Form.useForm(); | |||
useEffect(() => { | |||
if (visible) { | |||
if (data) { | |||
form.setFieldsValue(data); | |||
form.setFieldValue('expiresTime', dayjs(data.expiresTime)); | |||
shopForm.setFieldsValue(data.shopConfig); | |||
} else { | |||
form.setFieldValue("shopStatus", 0); | |||
} | |||
} else { | |||
form.resetFields(); | |||
shopForm.resetFields(); | |||
} | |||
setCurrent(0); | |||
}, [visible]); | |||
const save = async () => { | |||
await shopForm.validateFields(); | |||
setSaveLoading(true); | |||
const fieldValues = form.getFieldsValue(); | |||
const expiresTime = dayjs(form.getFieldValue('expiresTime')).toDate().getTime(); | |||
const shopFieldValues = shopForm.getFieldsValue(); | |||
const newValue = isEdit ? { ...data, ...fieldValues, expiresTime, shopConfig: { ...shopFieldValues } } : { ...fieldValues, expiresTime, shopConfig: { ...shopFieldValues } }; | |||
const [error, { msg, code }] = isEdit ? await updateApi(newValue) : await createApi(newValue); | |||
if (!error && code === 0) { | |||
onSave(newValue); | |||
} else { | |||
antdUtils.message?.open({ | |||
type: 'error', | |||
content: msg ?? '操作失败', | |||
}); | |||
} | |||
setSaveLoading(false); | |||
} | |||
return ( | |||
<> | |||
<Modal | |||
open={visible} | |||
title={isEdit ? "编辑" : "新建"} | |||
width={640} | |||
onOk={save} | |||
onCancel={() => onCancel()} | |||
footer={null} | |||
confirmLoading={saveLoading} | |||
destroyOnClose | |||
> | |||
<Steps className="my-5" current={current} items={[ | |||
{ | |||
title: '商户设置' | |||
}, | |||
{ | |||
title: '店铺配置', | |||
} | |||
]} /> | |||
<Form | |||
form={form} | |||
{...layout} | |||
onFinish={save} | |||
labelCol={{ flex: '0 0 100px' }} | |||
wrapperCol={{ span: 16 }} | |||
hidden={current === 1} | |||
> | |||
<Form.Item name="shopName" label="店铺名称" | |||
rules={[ | |||
{ | |||
required: true, | |||
message: '请输入店铺名称', | |||
}, | |||
]} | |||
> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="platformId" label="平台" | |||
rules={[ | |||
{ | |||
required: true, | |||
message: '请选择', | |||
}, | |||
]} | |||
> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="userName" label="账号"> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="password" label="密码"> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="expiresTime" label="过期时间" | |||
rules={[ | |||
{ | |||
required: true, | |||
message: '请设置过期时间', | |||
}, | |||
]}> | |||
<DatePicker showTime format="YYYY-MM-DD HH:mm:ss" /> | |||
</Form.Item> | |||
<Form.Item name="shopStatus" label="状态" rules={[{ | |||
required: true, | |||
message: '请设置状态', | |||
},]}> | |||
<Radio.Group options={[ | |||
{ value: 1, label: "开启" }, | |||
{ value: 2, label: "停用" } | |||
]} optionType="default"> | |||
</Radio.Group> | |||
</Form.Item> | |||
<Form.Item name="tokenFlag" label="授权状态" rules={[{ | |||
required: true, | |||
message: '请设置授权状态', | |||
},]}> | |||
<Radio.Group options={[ | |||
{ value: 0, label: "未授权不能为空" }, | |||
{ value: 1, label: "成功" }, | |||
{ value: 2, label: "失败" } | |||
]} optionType="default"> | |||
</Radio.Group> | |||
</Form.Item> | |||
</Form> | |||
<Form | |||
form={shopForm} | |||
{...layout} | |||
onFinish={save} | |||
labelCol={{ flex: '0 0 100px' }} | |||
wrapperCol={{ span: 16 }} | |||
hidden={current === 0} | |||
> | |||
<Form.Item name="shopId" label="店铺" | |||
rules={[ | |||
{ | |||
required: true, | |||
message: '请设置店铺', | |||
}, | |||
]}> | |||
<InputNumber /> | |||
</Form.Item> | |||
<Form.Item name="iossNo" label="欧盟税号"> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="vat" label="vat税号"> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="language" label="语言"> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="dayMaxPublishNumber" label="最大刊登量"> | |||
<InputNumber min={0} /> | |||
</Form.Item> | |||
</Form> | |||
<div className='flex justify-end'> | |||
{ | |||
(current === 0 ? | |||
(<Button type='primary' size='large' onClick={next}> 下一步 </Button>) : | |||
( | |||
<> | |||
<Button size='large' onClick={prev} disabled={saveLoading}> 上一步 </Button> | |||
<Button type='primary' size='large' className='ml-2' onClick={save}> 提交 </Button> | |||
</> | |||
) | |||
) | |||
} | |||
</div> | |||
</Modal> | |||
</> | |||
) | |||
} | |||
export default TenantEditor; |
@@ -74,7 +74,7 @@ const TenantEditor: React.FC<EditorProps> = (props) => { | |||
<> | |||
<Modal | |||
open={visible} | |||
title="新建" | |||
title={isEdit ? "编辑" : "新建"} | |||
width={640} | |||
onOk={save} | |||
onCancel={() => onCancel()} | |||
@@ -89,7 +89,7 @@ const TenantPackageEditor: React.FC<EditorProps> = (props) => { | |||
<> | |||
<Modal | |||
open={visible} | |||
title="新建" | |||
title={isEdit ? "编辑" : "新建"} | |||
width={640} | |||
onOk={save} | |||
confirmLoading={saveLoading} | |||
@@ -0,0 +1,48 @@ | |||
import request from '@/request'; | |||
import { CategoryVO, CategoryPageReqVO, CategoryTreeVO, PageData } from '@/models'; | |||
const BASE_URL = '/admin-api/main/category'; | |||
export default { | |||
// 修改类目 | |||
updateCategoryApi: (data: CategoryVO) => { | |||
return request.put(`${BASE_URL}/update`, data); | |||
}, | |||
// 创建类目 | |||
createCategoryApi: (data: CategoryVO) => { | |||
return request.post(`${BASE_URL}/create`, data); | |||
}, | |||
// 获得类目分页 | |||
getCategoryPageApi: (params: CategoryPageReqVO) => { | |||
return request.get<PageData<CategoryVO>>(`${BASE_URL}/page`, { params }); | |||
}, | |||
// 获得所有类目(树结构) | |||
getCategoryTreeApi: () => { | |||
return request.get<CategoryTreeVO[]>(`${BASE_URL}/categoryAllTree`) | |||
}, | |||
// 查询类目详情 | |||
getCategoryApi: (id: number) => { | |||
return request.get(`${BASE_URL}/get?id=${id}`); | |||
}, | |||
// 删除类目 | |||
deleteCategoryApi: (id: number) => { | |||
return request.delete(`${BASE_URL}/delete?id=${id}`); | |||
}, | |||
// 类目名称唯一校验 | |||
categoryNameVerifyUnique: (categoryName: string) => { | |||
return request.get(`${BASE_URL}/categoryNameVerifyUnique?categoryName=${categoryName}`); | |||
}, | |||
// 类目英文名称唯一校验 | |||
categoryNameEnVerifyUnique: (categoryNameEn: string) => { | |||
return request.get(`${BASE_URL}/categoryNameEnVerifyUnique?categoryNameEn=${categoryNameEn}`); | |||
}, | |||
}; |
@@ -0,0 +1,31 @@ | |||
import request from '@/request'; | |||
import { GoodsAttrValVO, AttrValNameEnVerifyParam } from '@/models'; | |||
const BASE_URL = '/admin-api/main/attr-val'; | |||
export default { | |||
// 更新主属性 | |||
updateGoodsAttrApi: (data: GoodsAttrValVO) => { | |||
return request.put(`${BASE_URL}/update`, data); | |||
}, | |||
// 创建主属性 | |||
createGoodsAttrApi: (data: GoodsAttrValVO) => { | |||
return request.post(`${BASE_URL}/create`, data); | |||
}, | |||
// 查询主属性详情 | |||
getGoodsAttrApi: (id: number) => { | |||
return request.get(`${BASE_URL}/get?id=${id}`); | |||
}, | |||
// 删除主属性 | |||
deleteGoodsAttrApi: (id: number) => { | |||
return request.delete(`${BASE_URL}/delete?id=${id}`); | |||
}, | |||
// 主属性名称唯一校验 | |||
valNameEnVerifyUnique: (params: AttrValNameEnVerifyParam) => { | |||
return request.get(`${BASE_URL}/valNameEnVerifyUnique`, { params }); | |||
}, | |||
}; |
@@ -0,0 +1,46 @@ | |||
import request from '@/request'; | |||
import { GoodsAttrVO, GoodsAttrPageReqVO, AttrAndAttrValByParam, PageData } from '@/models'; | |||
const BASE_URL = '/admin-api/main/attr'; | |||
export default { | |||
// 更新主属性 | |||
updateGoodsAttrApi: (data: GoodsAttrVO) => { | |||
return request.put(`${BASE_URL}/update`, data); | |||
}, | |||
// 更新主属性 | |||
updateAttrStatusByIdApi: (data: GoodsAttrVO) => { | |||
return request.put(`${BASE_URL}/updateAttrStatusById`, data); | |||
}, | |||
// 创建主属性 | |||
createGoodsAttrApi: (data: GoodsAttrVO) => { | |||
return request.post(`${BASE_URL}/create`, data); | |||
}, | |||
// 获得主属性分页 | |||
getGoodsAttrPageApi: (params: GoodsAttrPageReqVO) => { | |||
return request.get<PageData<GoodsAttrVO>>(`${BASE_URL}/page`, { params }); | |||
}, | |||
// 查询主属性详情 | |||
getGoodsAttrApi: (id: number) => { | |||
return request.get(`${BASE_URL}/get?id=${id}`); | |||
}, | |||
// 删除主属性 | |||
deleteGoodsAttrApi: (id: number) => { | |||
return request.delete(`${BASE_URL}/delete?id=${id}`); | |||
}, | |||
// 查询主属性详情 | |||
getAttrAndAttrValByParamApi: (params: AttrAndAttrValByParam) => { | |||
return request.get(`${BASE_URL}/getAttrAndAttrValByParam`, { params }); | |||
}, | |||
// 主属性名称唯一校验 | |||
attrNameEnVerifyUnique: (attrNameEn: string) => { | |||
return request.get(`${BASE_URL}/attrNameEnVerifyUnique?attrNameEn=${attrNameEn}`); | |||
}, | |||
}; |
@@ -0,0 +1,37 @@ | |||
import request from '@/request'; | |||
import { GoodsClassifyVO, GoodsClassifyPageReqVO, PageData } from '@/models'; | |||
const BASE_URL = '/admin-api/main/classify'; | |||
export default { | |||
// 更新商品分类 | |||
updateGoodsClassifyApi: (data: GoodsClassifyVO) => { | |||
return request.put(`${BASE_URL}/update`, data); | |||
}, | |||
// 创建商品分类 | |||
createGoodsClassifyApi: (data: GoodsClassifyVO) => { | |||
return request.post(`${BASE_URL}/create`, data); | |||
}, | |||
// 获得商品分类分页 | |||
getGoodsClassifyPageApi: (params: GoodsClassifyPageReqVO) => { | |||
return request.get<PageData<GoodsClassifyVO>>(`${BASE_URL}/page`, { params }); | |||
}, | |||
// 查询商品分类详情 | |||
getGoodsClassifyApi: (id: number) => { | |||
return request.get(`${BASE_URL}/get?id=${id}`); | |||
}, | |||
// 删除商品分类 | |||
deleteGoodsClassifyApi: (id: number) => { | |||
return request.delete(`${BASE_URL}/delete?id=${id}`); | |||
}, | |||
// 商品分类名称唯一校验 | |||
classifyNameVerifyUnique: (classifyName: string) => { | |||
return request.get(`${BASE_URL}/classifyNameVerifyUnique?classifyName=${classifyName}`); | |||
}, | |||
}; |
@@ -0,0 +1,43 @@ | |||
import request from '@/request'; | |||
import { MaterialClassifyVO, MaterialClassifyPageReqVO, PageData } from '@/models'; | |||
const BASE_URL = '/admin-api/material/classify'; | |||
export default { | |||
// 更新素材分类 | |||
updateMaterialClassifyApi: (data: MaterialClassifyVO) => { | |||
return request.put(`${BASE_URL}/update`, data); | |||
}, | |||
// 创建素材分类 | |||
createMaterialClassifyApi: (data: MaterialClassifyVO) => { | |||
return request.post(`${BASE_URL}/create`, data); | |||
}, | |||
// 获得素材分类分页 | |||
getMaterialClassifyPageApi: (params: MaterialClassifyPageReqVO) => { | |||
return request.get<PageData<MaterialClassifyVO>>(`${BASE_URL}/page`, { params }); | |||
}, | |||
// 获得所有素材分类(树结构) | |||
getMaterialClassifyTreeApi: () => { | |||
return request.get<MaterialClassifyVO[]>(`${BASE_URL}/getMaterialClassifyAllTree`) | |||
}, | |||
// 查询素材分类详情 | |||
getMaterialClassifyApi: (id: number) => { | |||
return request.get(`${BASE_URL}/get?id=${id}`); | |||
}, | |||
// 删除素材分类 | |||
deleteMaterialClassifyApi: (id: number) => { | |||
return request.get(`${BASE_URL}/delete?id=${id}`); | |||
}, | |||
// 素材分类名称唯一校验 | |||
MaterialClassifyNameVerifyUnique: (materialClassifyName: string) => { | |||
return request.get(`${BASE_URL}/materialClassifyNameVerifyUnique?materialClassifyName=${materialClassifyName}`); | |||
}, | |||
}; |
@@ -0,0 +1,44 @@ | |||
import request from '@/request'; | |||
import { OAuth2ClientVO, OAuth2TokenPageReqVO, OAuth2TokenVO, OAuth2ClientPageReqVO, PageData } from '@/models'; | |||
const BASE_TOKEN_URL = '/admin-api/system/oauth2-token'; | |||
const BASE_CLIENT_URL = '/admin-api/system/oauth2-client'; | |||
export default { | |||
// 查询 token列表 | |||
getAccessTokenPageApi: (params: OAuth2TokenPageReqVO) => { | |||
return request.get<PageData<OAuth2TokenVO>>(`${BASE_TOKEN_URL}/page`, {params}) | |||
}, | |||
// 删除 token | |||
deleteAccessTokenApi: (accessToken: number) => { | |||
return request.delete(`${BASE_TOKEN_URL}/delete?accessToken=${accessToken}`) | |||
}, | |||
// 查询 OAuth2列表 | |||
getOAuth2ClientPageApi: (params: OAuth2ClientPageReqVO) => { | |||
return request.get<PageData<OAuth2ClientVO>>(`${BASE_CLIENT_URL}/page`, {params}) | |||
}, | |||
// 查询 OAuth2详情 | |||
getOAuth2ClientApi: (id: number) => { | |||
return request.get<OAuth2ClientVO>(`${BASE_CLIENT_URL}/get?id=${id}`) | |||
}, | |||
// 新增 OAuth2 | |||
createOAuth2ClientApi: (data: OAuth2ClientVO) => { | |||
return request.post(`${BASE_CLIENT_URL}/create`, data) | |||
}, | |||
// 修改 OAuth2 | |||
updateOAuth2ClientApi: (data: OAuth2ClientVO) => { | |||
return request.put(`${BASE_CLIENT_URL}/update`, data) | |||
}, | |||
// 删除 OAuth2 | |||
deleteOAuth2ClientApi: (id: number) => { | |||
return request.delete(`${BASE_CLIENT_URL}/delete?id=${id}`) | |||
}, | |||
}; | |||
@@ -0,0 +1,26 @@ | |||
import request from '@/request'; | |||
import { PlatformShop, PlatformShopPageReqVO, PageData } from '@/models'; | |||
const BASE_URL = '/admin-api/platform/shop'; | |||
export default { | |||
// 查询平台店铺列表 | |||
getPlatformShopList: (params: PlatformShopPageReqVO) => { | |||
return request.get<PageData<PlatformShop>>(`${BASE_URL}/page`, {params}); | |||
}, | |||
// 创建平台店铺 | |||
createPlatformShop: (data: PlatformShop) => { | |||
return request.post(`${BASE_URL}/create`, data); | |||
}, | |||
// 修改平台店铺 | |||
updatePlatformShop: (data: PlatformShop) => { | |||
return request.post(`${BASE_URL}/update`, data); | |||
}, | |||
// 删除平台店铺 | |||
deletePlatformShop: (id: number) => { | |||
return request.delete(`${BASE_URL}/delete?id=${id}`); | |||
}, | |||
}; |
@@ -0,0 +1,35 @@ | |||
import request from '@/request'; | |||
import { | |||
TemplateInfoVO, | |||
TemplateInfoPageReqVO, | |||
PageData | |||
} from '@/models'; | |||
const BASE_URL = '/admin-api/template/platform'; | |||
export default { | |||
//获得模版分页 | |||
pageApi: (params: TemplateInfoPageReqVO) => { | |||
return request.get<PageData<TemplateInfoVO>>(`${BASE_URL}/page`, { params }); | |||
}, | |||
//创建模版 | |||
createApi: (data: TemplateInfoVO) => { | |||
return request.post(`${BASE_URL}`, data); | |||
}, | |||
//修改模版 | |||
updateApi: (data: TemplateInfoVO) => { | |||
return request.put(`${BASE_URL}`, data); | |||
}, | |||
//获得模版 | |||
getApi: (id: number) => { | |||
return request.get(`${BASE_URL}/get?id=${id}`); | |||
}, | |||
//删除模版 | |||
deleteApi: (id: number) => { | |||
return request.delete(`${BASE_URL}/delete?id=${id}`); | |||
}, | |||
} |
@@ -0,0 +1,33 @@ | |||
import request from '@/request'; | |||
import { | |||
SHProductVO, | |||
SHProductPageReqVO, | |||
UpdateSHProductNameReqVO, | |||
UpdateSHProductPriceReqVO, | |||
UpdateSHProductDescReqVO, | |||
PageData | |||
} from '@/models'; | |||
const BASE_URL = '/admin-api/shopee/product'; | |||
export default { | |||
pageApi: (params: SHProductPageReqVO) => { | |||
return request.get<PageData<SHProductVO>>(`${BASE_URL}/page`, { params }); | |||
}, | |||
//批量修改标题 | |||
batchUpdateNameApi: (data: UpdateSHProductNameReqVO[]) => { | |||
return request.put(`${BASE_URL}/batchUpdateName`, data); | |||
}, | |||
//批量修改价格 | |||
batchUpdatePriceApi: (data: UpdateSHProductPriceReqVO[]) => { | |||
return request.put(`${BASE_URL}/batchUpdatePrice`, data); | |||
}, | |||
//批量修改描述 | |||
batchUpdateDescApi: (data: UpdateSHProductDescReqVO[]) => { | |||
return request.put(`${BASE_URL}/batchUpdateDesc`, data); | |||
} | |||
} |
@@ -0,0 +1,36 @@ | |||
import request from '@/request'; | |||
import { TemplateDictDetaiPageReqVO, TemplateDictDetailVO, PageData } from '@/models'; | |||
const BASE_URL = '/admin-api/template/dict-detail'; | |||
export default { | |||
//获取字典详情分页数据 | |||
getPageApi(params: TemplateDictDetaiPageReqVO) { | |||
return request.get<PageData<TemplateDictDetailVO>>(`${BASE_URL}/page`, { params }); | |||
}, | |||
//创建模板-数据字典详情 | |||
createApi(data: TemplateDictDetailVO) { | |||
return request.post<TemplateDictDetailVO>(`${BASE_URL}/create`, data); | |||
}, | |||
//更新模板-数据字典详情 | |||
updateApi(data: TemplateDictDetailVO) { | |||
return request.put<TemplateDictDetailVO>(`${BASE_URL}/update`, data); | |||
}, | |||
//删除模板-数据字典详情 | |||
deleteApi(id: number) { | |||
return request.delete<boolean>(`${BASE_URL}/delete?id=${id}`); | |||
}, | |||
//获取模板-数据字典详情 | |||
getApi(id: number) { | |||
return request.get<TemplateDictDetailVO>(`${BASE_URL}/get?id=${id}`); | |||
}, | |||
//获得模板-数据字典详情列表 | |||
getListApi(ids: string[]) { | |||
return request.get<PageData<TemplateDictDetailVO>>(`${BASE_URL}/list`, { params: { ids }}); | |||
}, | |||
} |
@@ -1,15 +1,30 @@ | |||
import request from '@/request'; | |||
import { DataDictVO, DataDictDetailVO } from '@/models'; | |||
import { TemplateDictVO, TemplateDictDetailVO, TemplateDictPageReqVO, PageData } from '@/models'; | |||
const BASE_URL = '/admin-api/system/dict'; | |||
const BASE_URL = '/admin-api/template/dict'; | |||
export default { | |||
createDictApi: (data: DataDictVO) => { | |||
//获取字典分页 | |||
getDictPageApi: (params: TemplateDictPageReqVO) => { | |||
return request.get<PageData<TemplateDictVO>>(`${BASE_URL}/page`, { params }); | |||
}, | |||
//获取字典详情 | |||
getDictDetailApi: (id: number) => { | |||
return request.get(`${BASE_URL}/get?id=${id}`); | |||
}, | |||
//获取字典列表 | |||
getDictListApi: (ids: string[]) => { | |||
return request.get(`${BASE_URL}/list?ids=${ids.join(",")}`); | |||
}, | |||
createDictApi: (data: TemplateDictVO) => { | |||
return request.post(`${BASE_URL}/create`, data); | |||
}, | |||
updateDictApi: (data: DataDictVO) => { | |||
updateDictApi: (data: TemplateDictVO) => { | |||
return request.put(`${BASE_URL}/update`, data); | |||
}, | |||
@@ -17,7 +32,7 @@ export default { | |||
return request.delete(`${BASE_URL}/delete?id=${id}`); | |||
}, | |||
updateDictDetailApi: (data: DataDictDetailVO) => { | |||
updateDictDetailApi: (data: TemplateDictDetailVO) => { | |||
return request.put(`${BASE_URL}/update`, data); | |||
}, | |||