@@ -65,6 +65,7 @@ const DraggableTab: React.FC<TabsProps & { onItemsChange?: (items: any[]) => voi | |||||
return ( | return ( | ||||
<Tabs | <Tabs | ||||
style={{ position: 'relative' }} | |||||
renderTabBar={(tabBarProps, DefaultTabBar) => ( | renderTabBar={(tabBarProps, DefaultTabBar) => ( | ||||
<DndContext sensors={[sensor]} onDragEnd={onDragEnd} modifiers={[restrictToHorizontalAxis]}> | <DndContext sensors={[sensor]} onDragEnd={onDragEnd} modifiers={[restrictToHorizontalAxis]}> | ||||
<SortableContext items={items.map((i) => i.key)} strategy={horizontalListSortingStrategy}> | <SortableContext items={items.map((i) => i.key)} strategy={horizontalListSortingStrategy}> | ||||
@@ -80,7 +81,7 @@ const DraggableTab: React.FC<TabsProps & { onItemsChange?: (items: any[]) => voi | |||||
)} | )} | ||||
{...props} | {...props} | ||||
items={items} | items={items} | ||||
tabBarStyle={{ marginBottom: 8 }} | |||||
tabBarStyle={{ marginBottom: 8, position: 'sticky', top: 0, zIndex: 997}} | |||||
className='tab-layout' | className='tab-layout' | ||||
/> | /> | ||||
); | ); | ||||
@@ -18,7 +18,6 @@ const Content: FC<any> = ({ children }) => { | |||||
style={{ | style={{ | ||||
borderRadius: '8px', | borderRadius: '8px', | ||||
marginLeft: collapsed ? 100 : defaultSetting.slideWidth, | marginLeft: collapsed ? 100 : defaultSetting.slideWidth, | ||||
minHeight: 'calc(100vh - 60px)', | |||||
transition: "all 200ms cubic-bezier(0.4, 0, 0.6, 1) 0ms", | transition: "all 200ms cubic-bezier(0.4, 0, 0.6, 1) 0ms", | ||||
width: `calc(100vw - ${isPC ? collapsed ? 100 : defaultSetting.slideWidth : 32}px)` | 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.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) | routes.push(item) | ||||
if(item.children && item.children.length > 0) { | if(item.children && item.children.length > 0) { | ||||
const parentPaths= item.path.replace('/index.tsx', '').split("/").filter(it => it!== '').map(it => `/${it}`) | const parentPaths= item.path.replace('/index.tsx', '').split("/").filter(it => it!== '').map(it => `/${it}`) | ||||
@@ -73,8 +73,17 @@ const TabsLayout: React.FC = () => { | |||||
<div | <div | ||||
key={tab.key} | key={tab.key} | ||||
className='px-[16px]' | className='px-[16px]' | ||||
style={{ | |||||
height: 'calc(100vh - 107px)' | |||||
}} | |||||
> | > | ||||
{tab.children} | |||||
<div style={{ | |||||
height: '100%', | |||||
overflow: 'auto' | |||||
}}> | |||||
{tab.children} | |||||
</div> | |||||
</div> | </div> | ||||
), | ), | ||||
closable: tabs.length > 1, // 剩最后一个就不能删除了 | 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 './data-source.data.ts' | ||||
export * from './redis.data.ts' | export * from './redis.data.ts' | ||||
export * from './api-log.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>{ | export interface ResponseDTO<T>{ | ||||
code: number; | 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 { | export interface ProductKeywordGpt { | ||||
dynamicTableName: string; | |||||
id: number; | id: number; | ||||
idList: string[], | |||||
keyword: string; | keyword: string; | ||||
keywordType: number; | keywordType: number; | ||||
lang: string; | |||||
spuCode: string; | |||||
tableName: string; | |||||
} | } | ||||
export interface ShpopeeProductVO { | |||||
catePubId: string; | |||||
catePubName: string; | |||||
export interface UpdateSHProductPriceReqVO { | |||||
id: number; | id: number; | ||||
imgUrl: string; | |||||
isDelete: number; | |||||
isPublish: number; | |||||
itemId: number; | |||||
itemName: string; | |||||
originalPrice: number; | 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; | description: string; | ||||
id: number; | id: number; | ||||
name: string; | name: string; | ||||
createTime?: number; | |||||
} | } | ||||
//数据字典详情 | |||||
export interface DataDictDetailVO { | |||||
//模板字典详情 | |||||
export interface TemplateDictDetailVO { | |||||
dictId: number; | dictId: number; | ||||
dictSort: number; | dictSort: number; | ||||
id: number; | id: number; | ||||
label: string; | label: string; | ||||
value: string; | value: string; | ||||
createTime: number; | |||||
} | } | ||||
export interface ShopeeTemplateVO { | |||||
/** | |||||
* TemplateInfoVO,模板数据 | |||||
*/ | |||||
export interface TemplateInfoVO { | |||||
/** | |||||
* 背景ids | |||||
*/ | |||||
backgroupIds?: number[]; | |||||
/** | |||||
* 类别 | |||||
*/ | |||||
catePubId: string; | |||||
/** | |||||
* 每天最大刊登量 | |||||
*/ | |||||
dayMaxPublishNumber?: number; | |||||
/** | |||||
* 模版id | |||||
*/ | |||||
id: number; | id: number; | ||||
categoryName: string; | |||||
createName: string; | |||||
createTime: string; | |||||
/** | |||||
* 图片信息 | |||||
*/ | |||||
imageInfo: TemplateImgVO[]; | |||||
/** | |||||
* 物流渠道 | |||||
*/ | |||||
logistics: TemplateLogisticsVO[]; | |||||
/** | |||||
* 素材组ids | |||||
*/ | |||||
materialClassifyIds?: number[]; | |||||
/** | |||||
* 店铺最大上架数量 | |||||
*/ | |||||
maxPrintCount: number; | |||||
/** | |||||
* 产品价格 | |||||
*/ | |||||
originalPrice: number; | |||||
/** | |||||
* 平台编码 | |||||
*/ | |||||
platformCode: string; | platformCode: string; | ||||
remark: string; | |||||
shopName: string; | |||||
/** | |||||
* 单价折扣 | |||||
*/ | |||||
priceDiscount?: number; | |||||
/** | |||||
* 变种比例 | |||||
*/ | |||||
proportion?: number; | |||||
/** | |||||
* 模板备注 | |||||
*/ | |||||
remark?: string; | |||||
/** | |||||
* 店铺id | |||||
*/ | |||||
shopId: number; | |||||
/** | |||||
* 类目属性 | |||||
*/ | |||||
templateAttrs: TemplateAttrInfoVO[]; | |||||
templateDimension: TemplateDimensionVO; | |||||
/** | |||||
* 模板名称编码 | |||||
*/ | |||||
templateName: string; | 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 { Space, Table, Form, Button, Card, Input, TreeSelect, Image } from 'antd'; | ||||
import type { ColumnsType } from 'antd/es/table'; | import type { ColumnsType } from 'antd/es/table'; | ||||
import { t } from '@/utils/i18n'; | import { t } from '@/utils/i18n'; | ||||
import React, { useState } from 'react'; | |||||
import { useNavigate } from 'react-router-dom'; | |||||
import { | import { | ||||
ExclamationCircleFilled, | |||||
PlusOutlined, | |||||
CarryOutOutlined, | CarryOutOutlined, | ||||
SearchOutlined, | SearchOutlined, | ||||
UndoOutlined | UndoOutlined | ||||
} from '@ant-design/icons'; | } from '@ant-design/icons'; | ||||
import { antdUtils } from '@/utils/antd'; | 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 = [ | const treeData = [ | ||||
{ | { | ||||
@@ -55,35 +54,30 @@ const treeData = [ | |||||
export default () => { | 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: '缩略图', | title: '缩略图', | ||||
dataIndex: 'imgUrl', | dataIndex: 'imgUrl', | ||||
@@ -127,30 +121,17 @@ export default () => { | |||||
title: '刊登时间', | title: '刊登时间', | ||||
key: 'publishTime', | key: 'publishTime', | ||||
dataIndex: '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 [treeLine, setTreeLine] = useState(true); | ||||
const [showLeafIcon, setShowLeafIcon] = useState(false); | const [showLeafIcon, setShowLeafIcon] = useState(false); | ||||
const [showIcon, setShowIcon] = useState<boolean>(false); | const [showIcon, setShowIcon] = useState<boolean>(false); | ||||
useEffect(() => { | |||||
load(); | |||||
}, [searchFrom, searchState]); | |||||
return ( | return ( | ||||
<div> | <div> | ||||
<div> | <div> | ||||
@@ -182,8 +163,17 @@ export default () => { | |||||
</div> | </div> | ||||
</Card> | </Card> | ||||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | <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> | </Card> | ||||
</div> | </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 { PlusOutlined, ExclamationCircleFilled, DeleteOutlined, SearchOutlined } from '@ant-design/icons'; | ||||
import { antdUtils } from '@/utils/antd'; | import { antdUtils } from '@/utils/antd'; | ||||
import mData from '../../../../../mock/findMaterialPage.json' | import mData from '../../../../../mock/findMaterialPage.json' | ||||
import MaterialClassifyView from './classify'; | |||||
const { Search } = Input; | const { Search } = Input; | ||||
@@ -311,6 +312,7 @@ const TablePage: React.FC = () => { | |||||
<> | <> | ||||
<div className='flex flex-row'> | <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]'> | <Card className='basis-1/4 w-[100px] mb-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | ||||
<MaterialClassifyView/> | |||||
</Card> | </Card> | ||||
<Card className='basis-3/4 mb-[10px] ml-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]' bodyStyle={{ | <Card className='basis-3/4 mb-[10px] ml-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]' bodyStyle={{ | ||||
paddingTop: 0, | paddingTop: 0, | ||||
@@ -1,18 +1,18 @@ | |||||
import React, { useEffect, useMemo, useState } from 'react' | import React, { useEffect, useMemo, useState } from 'react' | ||||
import { CloseOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'; | 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 { RcFile, UploadProps } from 'antd/es/upload'; | ||||
import type { UploadFile } from 'antd/es/upload/interface'; | import type { UploadFile } from 'antd/es/upload/interface'; | ||||
import './editor.css'; | |||||
const layout = { | const layout = { | ||||
labelCol: { span: 4, }, | |||||
wrapperCol: { span: 16 }, | |||||
bordered: false, | |||||
labelCol: { span: 5, }, | |||||
wrapperCol: { span: 15 }, | |||||
}; | }; | ||||
interface AttributeValue { | interface AttributeValue { | ||||
id: number; | |||||
attrId: number; | |||||
id?: number; | |||||
attrId?: number; | |||||
valName: string; | valName: string; | ||||
imgId?: number; | imgId?: number; | ||||
imgUrl?: string; | imgUrl?: string; | ||||
@@ -29,7 +29,6 @@ export interface SampleAttribute { | |||||
interface CreateSampleAttrProps { | interface CreateSampleAttrProps { | ||||
visible: boolean; | visible: boolean; | ||||
onCancel: (flag?: boolean) => void; | onCancel: (flag?: boolean) => void; | ||||
curRecord?: SampleAttribute[] | null; | |||||
onSave: () => void; | onSave: () => void; | ||||
editData?: SampleAttribute[] | null; | editData?: SampleAttribute[] | null; | ||||
} | } | ||||
@@ -45,13 +44,15 @@ const getBase64 = (file: RcFile): Promise<string> => | |||||
const SampleAttrEditor: React.FC<CreateSampleAttrProps> = (props) => { | 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 [saveLoading, setSaveLoading] = useState(false); | ||||
const [previewOpen, setPreviewOpen] = useState(false); | const [previewOpen, setPreviewOpen] = useState(false); | ||||
const [previewImage, setPreviewImage] = useState(''); | const [previewImage, setPreviewImage] = useState(''); | ||||
const [previewTitle, setPreviewTitle] = useState(''); | const [previewTitle, setPreviewTitle] = useState(''); | ||||
const [form] = Form.useForm(); | const [form] = Form.useForm(); | ||||
const isEdit = !!editData; | |||||
useEffect(() => { | useEffect(() => { | ||||
if (visible) { | if (visible) { | ||||
setInitValue(); | setInitValue(); | ||||
@@ -61,6 +62,9 @@ const SampleAttrEditor: React.FC<CreateSampleAttrProps> = (props) => { | |||||
}, [visible]); | }, [visible]); | ||||
async function setInitValue() { | async function setInitValue() { | ||||
if (editData) { | |||||
form.setFieldsValue({ attrData: editData }); | |||||
} | |||||
} | } | ||||
const save = async (values: any) => { | const save = async (values: any) => { | ||||
@@ -83,60 +87,80 @@ const SampleAttrEditor: React.FC<CreateSampleAttrProps> = (props) => { | |||||
}; | }; | ||||
const uploadButton = ( | const uploadButton = ( | ||||
<div> | |||||
<div className='flex'> | |||||
<PlusOutlined /> | <PlusOutlined /> | ||||
<div style={{ marginTop: 8 }}>上传</div> | |||||
<div style={{ marginLeft: 8 }}>上传</div> | |||||
</div> | </div> | ||||
); | ); | ||||
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { | 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 | <Popconfirm | ||||
title="删除属性" | title="删除属性" | ||||
description="确认是否删除当前属性?" | description="确认是否删除当前属性?" | ||||
@@ -144,62 +168,71 @@ const SampleAttrEditor: React.FC<CreateSampleAttrProps> = (props) => { | |||||
okText="确认" | okText="确认" | ||||
cancelText="取消" | cancelText="取消" | ||||
> | > | ||||
<DeleteOutlined style={{ fontSize: '16px' }} className='hover:(bg-[rgb(94,53,177)]'/> | |||||
<Tooltip title="删除属性"> | |||||
<CloseOutlined style={{ fontSize: '16px' }} className='delete-icon' /> | |||||
</Tooltip> | |||||
</Popconfirm> | </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> | ||||
<Form.Item label="属性值"> | |||||
<Form.Item label="属性值" labelCol={{ span: 4 }}> | |||||
<Form.List name={[field.name, 'attrVals']}> | <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.List> | ||||
</Form.Item> | </Form.Item> | ||||
</Card> | </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 ( | return ( | ||||
<> | <> | ||||
<Drawer | <Drawer | ||||
open={visible} | open={visible} | ||||
title="新建" | |||||
title={isEdit ? "编辑" : "新建"} | |||||
width={640} | width={640} | ||||
onClose={() => { onCancel() }} | onClose={() => { onCancel() }} | ||||
extra={ | extra={ | ||||
<Space> | <Space> | ||||
<Button type="primary" size='middle' onClick={() => {save}}> | |||||
<Button type="primary" size='middle' onClick={() => { save }}> | |||||
保存 | 保存 | ||||
</Button> | </Button> | ||||
</Space> | </Space> | ||||
} | } | ||||
destroyOnClose | 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}> | <Modal open={previewOpen} title={previewTitle} footer={null} onCancel={handleCancel}> | ||||
<img alt="example" style={{ width: '100%' }} src={previewImage} /> | <img alt="example" style={{ width: '100%' }} src={previewImage} /> | ||||
</Modal> | </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; | padding: 0; | ||||
border-radius: 0 0 8px 8px; | 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; | padding: 0; | ||||
border-radius: 0 0 8px 8px; | 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; | 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; | 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 { ImageStencil } from "./components/ImageStencil"; | ||||
import { DeleteOutlined, EditOutlined, UploadOutlined } from '@ant-design/icons'; | import { DeleteOutlined, EditOutlined, UploadOutlined } from '@ant-design/icons'; | ||||
import cn from 'classnames'; | import cn from 'classnames'; | ||||
import './index.css' | |||||
import './index.css'; | |||||
import property from '../../../../../../mock/propertyById.json' | import property from '../../../../../../mock/propertyById.json' | ||||
interface SampleProperty { | interface SampleProperty { | ||||
@@ -113,8 +113,8 @@ export default () => { | |||||
return ( | return ( | ||||
<div className='flex'> | <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 | <Anchor | ||||
direction="horizontal" | direction="horizontal" | ||||
items={[ | items={[ | ||||
@@ -133,21 +133,21 @@ export default () => { | |||||
</div> | </div> | ||||
<div > | <div > | ||||
<div > | <div > | ||||
<div id="part-1" > | |||||
<div id="part-1"> | |||||
<List dataSource={sample.prototypeImgs.filter(it => it.imgType === 2)} | <List dataSource={sample.prototypeImgs.filter(it => it.imgType === 2)} | ||||
grid={{ | grid={{ | ||||
gutter: 2, | gutter: 2, | ||||
column: 2 | column: 2 | ||||
}} | }} | ||||
header={( | header={( | ||||
<div className='flex justify-center'> | |||||
<div className='flex justify-center' > | |||||
<Upload {...props}> | <Upload {...props}> | ||||
<Button icon={<UploadOutlined />}>点击上传主图</Button> | <Button icon={<UploadOutlined />}>点击上传主图</Button> | ||||
</Upload> | </Upload> | ||||
</div> | </div> | ||||
)} | )} | ||||
renderItem={(item) => (renderListItem(item))} | renderItem={(item) => (renderListItem(item))} | ||||
className='workbench' | |||||
/> | /> | ||||
</div> | </div> | ||||
<div id="part-2"> | <div id="part-2"> | ||||
@@ -165,13 +165,14 @@ export default () => { | |||||
</div> | </div> | ||||
)} | )} | ||||
renderItem={(item) => (renderListItem(item))} | renderItem={(item) => (renderListItem(item))} | ||||
className='workbench' | |||||
/> | /> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
</Card> | </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 flex-col justify-center'> | ||||
<div className='flex justify-center text-3xl p-5 font-extrabold'>工作台</div> | <div className='flex justify-center text-3xl p-5 font-extrabold'>工作台</div> | ||||
<div className='flex justify-center'> | <div className='flex justify-center'> | ||||
@@ -201,7 +202,7 @@ export default () => { | |||||
</div> | </div> | ||||
</Card> | </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'> | <div className='flex justify-center pt-2'> | ||||
<Radio.Group options={options} | <Radio.Group options={options} | ||||
onChange={onStyleChanged} | onChange={onStyleChanged} | ||||
@@ -384,7 +384,6 @@ const TablePage: React.FC = () => { | |||||
onSave={saveAttributeHandle} | onSave={saveAttributeHandle} | ||||
onCancel={cancelHandle} | onCancel={cancelHandle} | ||||
visible={attrEditorVisible} | visible={attrEditorVisible} | ||||
curRecord={attrData} | |||||
editData={attrData} /> | editData={attrData} /> | ||||
<MaskPictureEditor | <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 React, { useEffect, useState } from 'react' | ||||
import { Form, Input, InputNumber, Modal } from 'antd'; | 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 { useRequest } from '@/hooks/use-request'; | ||||
import type { DataDictDetailVO } from '@/models' | |||||
import type { TemplateDictDetailVO } from '@/models' | |||||
import { antdUtils } from '@/utils/antd'; | import { antdUtils } from '@/utils/antd'; | ||||
const layout = { | const layout = { | ||||
@@ -13,14 +13,15 @@ const layout = { | |||||
export default (props: { | export default (props: { | ||||
visible: boolean; | visible: boolean; | ||||
onCancel: (flag?: boolean) => void; | 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; | const isEdit = !!data; | ||||
@@ -41,7 +42,7 @@ export default (props: { | |||||
const save = async () => { | const save = async () => { | ||||
setSaveLoading(true); | setSaveLoading(true); | ||||
const fieldValues = form.getFieldsValue(); | 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); | const [error, { msg, code }] = isEdit ? await updateApi(newValue) : await createApi(newValue); | ||||
if (!error && code === 0) { | if (!error && code === 0) { | ||||
onSave(newValue); | onSave(newValue); | ||||
@@ -58,7 +59,7 @@ export default (props: { | |||||
<> | <> | ||||
<Modal | <Modal | ||||
open={visible} | open={visible} | ||||
title="新建" | |||||
title={isEdit ? "编辑" : "新建"} | |||||
width={640} | width={640} | ||||
onOk={save} | onOk={save} | ||||
onCancel={() => onCancel()} | 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 { Form, Input, Modal } from 'antd'; | ||||
import dictService from '@/request/service/template-dict'; | import dictService from '@/request/service/template-dict'; | ||||
import { useRequest } from '@/hooks/use-request'; | import { useRequest } from '@/hooks/use-request'; | ||||
import type { DataDictVO } from '@/models' | |||||
import type { TemplateDictVO } from '@/models' | |||||
import { antdUtils } from '@/utils/antd'; | import { antdUtils } from '@/utils/antd'; | ||||
const layout = { | const layout = { | ||||
@@ -13,8 +13,8 @@ const layout = { | |||||
export default (props: { | export default (props: { | ||||
visible: boolean; | visible: boolean; | ||||
onCancel: (flag?: boolean) => void; | onCancel: (flag?: boolean) => void; | ||||
onSave: (role: DataDictVO) => void; | |||||
data?: DataDictVO | null; | |||||
onSave: (role: TemplateDictVO) => void; | |||||
data?: TemplateDictVO | null; | |||||
}) => { | }) => { | ||||
const { visible, onCancel, onSave, data } = props; | const { visible, onCancel, onSave, data } = props; | ||||
@@ -58,7 +58,7 @@ export default (props: { | |||||
<> | <> | ||||
<Modal | <Modal | ||||
open={visible} | open={visible} | ||||
title="新建" | |||||
title={isEdit ? "编辑" : "新建"} | |||||
width={640} | width={640} | ||||
onOk={save} | onOk={save} | ||||
onCancel={() => onCancel()} | 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 () => { | 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 ( | return ( | ||||
<> | <> | ||||
<div className='flex flex-row'> | <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> | </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 { Space, Table, Form, Button, Card, Input, TreeSelect } from 'antd'; | ||||
import type { ColumnsType } from 'antd/es/table'; | import type { ColumnsType } from 'antd/es/table'; | ||||
import { t } from '@/utils/i18n'; | import { t } from '@/utils/i18n'; | ||||
import React, { useState } from 'react'; | |||||
import { useNavigate } from 'react-router-dom'; | import { useNavigate } from 'react-router-dom'; | ||||
import { | import { | ||||
ExclamationCircleFilled, | ExclamationCircleFilled, | ||||
@@ -11,7 +11,10 @@ import { | |||||
UndoOutlined | UndoOutlined | ||||
} from '@ant-design/icons'; | } from '@ant-design/icons'; | ||||
import { antdUtils } from '@/utils/antd'; | 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 = [ | const treeData = [ | ||||
{ | { | ||||
@@ -53,42 +56,38 @@ const treeData = [ | |||||
]; | ]; | ||||
export default () => { | 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({ | antdUtils.modal?.confirm({ | ||||
title: `确认删除标题为: ${item.shopName} 的模板吗?`, | |||||
title: `确认删除标题为: ${item.templateName} 的模板吗?`, | |||||
icon: <ExclamationCircleFilled />, | icon: <ExclamationCircleFilled />, | ||||
content: `请注意删除以后不可恢复!`, | content: `请注意删除以后不可恢复!`, | ||||
okText: '删除', | okText: '删除', | ||||
@@ -114,7 +113,7 @@ export default () => { | |||||
}); | }); | ||||
}; | }; | ||||
const columns: ColumnsType<ShopeeTemplateVO> = [ | |||||
const columns: ColumnsType<TemplateInfoVO> = [ | |||||
{ | { | ||||
title: '店铺', | title: '店铺', | ||||
dataIndex: 'shopName', | 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 ( | return ( | ||||
<div> | <div> | ||||
@@ -204,8 +199,18 @@ export default () => { | |||||
</div> | </div> | ||||
</Card> | </Card> | ||||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | <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> | </Card> | ||||
</div> | </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', | dataIndex: 'userType', | ||||
key: 'userType', | key: 'userType', | ||||
fixed: 'left', | fixed: 'left', | ||||
filterSearch: true, | |||||
align: 'center', | align: 'center', | ||||
width: 100, | width: 100, | ||||
filters: [ | |||||
{ text: '会员', value: 1 }, | |||||
{ text: '管理员', value: 2 }, | |||||
], | |||||
filterMultiple: false, | |||||
filterSearch: false, | |||||
render: (value: number) => { | render: (value: number) => { | ||||
if (value === 1) { | if (value === 1) { | ||||
return <Tag color="purple">会员</Tag> | return <Tag color="purple">会员</Tag> | ||||
@@ -179,7 +184,15 @@ export default () => { | |||||
key: 'requestUrl', | key: 'requestUrl', | ||||
align: 'center', | align: 'center', | ||||
width: 150, | width: 150, | ||||
render: (text, record) => ( | |||||
<Tooltip title={text}> | |||||
<div className='text-ellipsis overflow-hidden whitespace-nowrap max-w-xs'> | |||||
{text} | |||||
</div> | |||||
</Tooltip> | |||||
), | |||||
...getColumnSearchProps('requestUrl', "请输入请求地址") | ...getColumnSearchProps('requestUrl', "请输入请求地址") | ||||
}, | }, | ||||
{ | { | ||||
title: '异常时间', | title: '异常时间', | ||||
@@ -196,7 +209,13 @@ export default () => { | |||||
dataIndex: 'exceptionName', | dataIndex: 'exceptionName', | ||||
key: 'exceptionName', | key: 'exceptionName', | ||||
align: 'center', | 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: '处理状态', | title: '处理状态', | ||||
@@ -206,13 +225,13 @@ export default () => { | |||||
width: 150, | width: 150, | ||||
render: (value: number) => { | render: (value: number) => { | ||||
if (value === 0) { | if (value === 0) { | ||||
return <Tag color="red">未处理</Tag> | |||||
return <Tag color="yellow">未处理</Tag> | |||||
} else if (value === 1) { | } else if (value === 1) { | ||||
return <Tag color="purple">已处理</Tag> | return <Tag color="purple">已处理</Tag> | ||||
} else if (value === 2) { | } else if (value === 2) { | ||||
return <Tag color="blue">已忽略</Tag> | return <Tag color="blue">已忽略</Tag> | ||||
} else { | } else { | ||||
return <Tag color="purple">内置</Tag> | |||||
return <Tag color="red">未知</Tag> | |||||
} | } | ||||
} | } | ||||
}, | }, | ||||
@@ -243,7 +262,7 @@ export default () => { | |||||
]; | ]; | ||||
const exportLogs = async () => { | const exportLogs = async () => { | ||||
await exportOperateLog() | |||||
await exportOperateLog(); | |||||
} | } | ||||
useEffect(() => { | useEffect(() => { | ||||
@@ -255,6 +274,7 @@ export default () => { | |||||
applicationName: filters.applicationName ? filters.applicationName[0] as string : undefined, | applicationName: filters.applicationName ? filters.applicationName[0] as string : undefined, | ||||
requestUrl: filters.requestUrl ? filters.requestUrl[0] as string : undefined, | requestUrl: filters.requestUrl ? filters.requestUrl[0] as string : undefined, | ||||
userId: filters.userId ? parseInt(filters.userId[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, | pageNo: pagination.current, | ||||
pageSize: pagination.pageSize | pageSize: pagination.pageSize | ||||
} | } | ||||
@@ -61,7 +61,7 @@ const DepartmentEditor: React.FC<EditorProps> = (props) => { | |||||
<> | <> | ||||
<Modal | <Modal | ||||
open={visible} | open={visible} | ||||
title="新建" | |||||
title={isEdit ? "编辑" : "新建"} | |||||
width={640} | width={640} | ||||
onOk={save} | onOk={save} | ||||
onCancel={() => onCancel()} | 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 | <Modal | ||||
open={visible} | open={visible} | ||||
title="新建" | |||||
title={isEdit ? "编辑" : "新建"} | |||||
width={640} | width={640} | ||||
onOk={save} | onOk={save} | ||||
onCancel={() => onCancel()} | 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 | <Modal | ||||
open={visible} | open={visible} | ||||
title="新建" | |||||
title={isEdit ? "编辑" : "新建"} | |||||
width={640} | width={640} | ||||
onOk={save} | onOk={save} | ||||
onCancel={() => onCancel()} | onCancel={() => onCancel()} | ||||
@@ -89,7 +89,7 @@ const TenantPackageEditor: React.FC<EditorProps> = (props) => { | |||||
<> | <> | ||||
<Modal | <Modal | ||||
open={visible} | open={visible} | ||||
title="新建" | |||||
title={isEdit ? "编辑" : "新建"} | |||||
width={640} | width={640} | ||||
onOk={save} | onOk={save} | ||||
confirmLoading={saveLoading} | 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 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 { | 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); | return request.post(`${BASE_URL}/create`, data); | ||||
}, | }, | ||||
updateDictApi: (data: DataDictVO) => { | |||||
updateDictApi: (data: TemplateDictVO) => { | |||||
return request.put(`${BASE_URL}/update`, data); | return request.put(`${BASE_URL}/update`, data); | ||||
}, | }, | ||||
@@ -17,7 +32,7 @@ export default { | |||||
return request.delete(`${BASE_URL}/delete?id=${id}`); | return request.delete(`${BASE_URL}/delete?id=${id}`); | ||||
}, | }, | ||||
updateDictDetailApi: (data: DataDictDetailVO) => { | |||||
updateDictDetailApi: (data: TemplateDictDetailVO) => { | |||||
return request.put(`${BASE_URL}/update`, data); | return request.put(`${BASE_URL}/update`, data); | ||||
}, | }, | ||||