Переглянути джерело

Merge branch 'feature' into preview

dev
powersir 1 рік тому
джерело
коміт
965a1663c1
53 змінених файлів з 4994 додано та 549 видалено
  1. +2
    -1
      src/components/draggable-tab/index.tsx
  2. +0
    -1
      src/layout/content/index.tsx
  3. +3
    -1
      src/layout/index.tsx
  4. +10
    -1
      src/layout/tabs-layout.tsx
  5. +144
    -0
      src/models/category.data.ts
  6. +58
    -0
      src/models/goods-attr.data.ts
  7. +18
    -0
      src/models/goods-classify.data.ts
  8. +6
    -0
      src/models/index.ts
  9. +15
    -0
      src/models/material-classify.data.ts
  10. +42
    -0
      src/models/oauth2.data.ts
  11. +245
    -21
      src/models/platform-product.data.ts
  12. +82
    -0
      src/models/platform.data.ts
  13. +339
    -11
      src/models/template.data.ts
  14. +43
    -53
      src/pages/custom/platform-product/shopee/index.tsx
  15. +205
    -0
      src/pages/custom/product/material/classify.tsx
  16. +2
    -0
      src/pages/custom/product/material/index.tsx
  17. +119
    -86
      src/pages/custom/product/sample/components/attr-editor.tsx
  18. +45
    -0
      src/pages/custom/product/sample/components/editor.css
  19. +13
    -4
      src/pages/custom/product/sample/editor/index.css
  20. +9
    -8
      src/pages/custom/product/sample/editor/index.tsx
  21. +0
    -1
      src/pages/custom/product/sample/index.tsx
  22. +202
    -0
      src/pages/custom/template/dict/dict-data.tsx
  23. +10
    -9
      src/pages/custom/template/dict/dict-detail-editor.tsx
  24. +178
    -0
      src/pages/custom/template/dict/dict-detail.tsx
  25. +4
    -4
      src/pages/custom/template/dict/dict-editor.tsx
  26. +15
    -289
      src/pages/custom/template/dict/index.tsx
  27. +50
    -45
      src/pages/custom/template/shopee/index.tsx
  28. +408
    -0
      src/pages/goods/main/attribute/classify.tsx
  29. +415
    -0
      src/pages/goods/main/attribute/colors.tsx
  30. +78
    -0
      src/pages/goods/main/attribute/index.tsx
  31. +415
    -0
      src/pages/goods/main/attribute/size.tsx
  32. +152
    -0
      src/pages/goods/main/category/index.tsx
  33. +272
    -0
      src/pages/infra/log/api-access-log/index.tsx
  34. +25
    -5
      src/pages/infra/log/api-error-log/index.tsx
  35. +1
    -1
      src/pages/system/dept/create-department.tsx
  36. +258
    -0
      src/pages/system/oauth2/client/index.tsx
  37. +242
    -0
      src/pages/system/oauth2/token/index.tsx
  38. +1
    -1
      src/pages/system/post/create-position.tsx
  39. +240
    -0
      src/pages/system/shop/manage/index.tsx
  40. +227
    -0
      src/pages/system/shop/manage/shop-editor.tsx
  41. +1
    -1
      src/pages/system/tenant/list/tenant-editor.tsx
  42. +1
    -1
      src/pages/system/tenant/package/tenant-pkg-editor.tsx
  43. +48
    -0
      src/request/service/category.ts
  44. +31
    -0
      src/request/service/goods-attr-val.ts
  45. +46
    -0
      src/request/service/goods-attr.ts
  46. +37
    -0
      src/request/service/goods-classify.ts
  47. +43
    -0
      src/request/service/material-classify.ts
  48. +44
    -0
      src/request/service/oauth2.ts
  49. +26
    -0
      src/request/service/platform-shop.ts
  50. +35
    -0
      src/request/service/platform-template.ts
  51. +33
    -0
      src/request/service/shopee-product.ts
  52. +36
    -0
      src/request/service/template-dict-detail.ts
  53. +20
    -5
      src/request/service/template-dict.ts

+ 2
- 1
src/components/draggable-tab/index.tsx Переглянути файл

@@ -65,6 +65,7 @@ const DraggableTab: React.FC<TabsProps & { onItemsChange?: (items: any[]) => voi

return (
<Tabs
style={{ position: 'relative' }}
renderTabBar={(tabBarProps, DefaultTabBar) => (
<DndContext sensors={[sensor]} onDragEnd={onDragEnd} modifiers={[restrictToHorizontalAxis]}>
<SortableContext items={items.map((i) => i.key)} strategy={horizontalListSortingStrategy}>
@@ -80,7 +81,7 @@ const DraggableTab: React.FC<TabsProps & { onItemsChange?: (items: any[]) => voi
)}
{...props}
items={items}
tabBarStyle={{ marginBottom: 8 }}
tabBarStyle={{ marginBottom: 8, position: 'sticky', top: 0, zIndex: 997}}
className='tab-layout'
/>
);


+ 0
- 1
src/layout/content/index.tsx Переглянути файл

@@ -18,7 +18,6 @@ const Content: FC<any> = ({ children }) => {
style={{
borderRadius: '8px',
marginLeft: collapsed ? 100 : defaultSetting.slideWidth,
minHeight: 'calc(100vh - 60px)',
transition: "all 200ms cubic-bezier(0.4, 0, 0.6, 1) 0ms",
width: `calc(100vw - ${isPC ? collapsed ? 100 : defaultSetting.slideWidth : 32}px)`
}}


+ 3
- 1
src/layout/index.tsx Переглянути файл

@@ -67,7 +67,9 @@ const BasicLayout: React.FC = () => {
item.path = [...item.parentPaths, item.path].join('')
}
item.component = item.component ? (item.path.endsWith('/index.tsx') ? item.path : `${item.path}/index.tsx`) : item.component
if(item.component) {
item.component = `${item.component.startsWith('/')?'':'/'}${item.component}${item.component.endsWith('.tsx')?'':'.tsx'}`
}
routes.push(item)
if(item.children && item.children.length > 0) {
const parentPaths= item.path.replace('/index.tsx', '').split("/").filter(it => it!== '').map(it => `/${it}`)


+ 10
- 1
src/layout/tabs-layout.tsx Переглянути файл

@@ -73,8 +73,17 @@ const TabsLayout: React.FC = () => {
<div
key={tab.key}
className='px-[16px]'
style={{
height: 'calc(100vh - 107px)'
}}
>
{tab.children}
<div style={{
height: '100%',
overflow: 'auto'
}}>
{tab.children}
</div>

</div>
),
closable: tabs.length > 1, // 剩最后一个就不能删除了


+ 144
- 0
src/models/category.data.ts Переглянути файл

@@ -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[];
}

+ 58
- 0
src/models/goods-attr.data.ts Переглянути файл

@@ -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;
}

+ 18
- 0
src/models/goods-classify.data.ts Переглянути файл

@@ -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[];
}

+ 6
- 0
src/models/index.ts Переглянути файл

@@ -13,6 +13,12 @@ export * from './error-code.data.ts'
export * from './data-source.data.ts'
export * from './redis.data.ts'
export * from './api-log.data.ts'
export * from './oauth2.data.ts'
export * from './platform.data.ts'
export * from './category.data.ts'
export * from './material-classify.data.ts'
export * from './goods-classify.data.ts'
export * from './goods-attr.data.ts'

export interface ResponseDTO<T>{
code: number;


+ 15
- 0
src/models/material-classify.data.ts Переглянути файл

@@ -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;
}

+ 42
- 0
src/models/oauth2.data.ts Переглянути файл

@@ -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
}

+ 245
- 21
src/models/platform-product.data.ts Переглянути файл

@@ -1,32 +1,256 @@
/**
* SHProductVO 虾皮商品
*/
export interface SHProductVO {
/**
* 品牌id(类目属性冗余过来,这个必填)
*/
brandId: number;
/**
* 类目id
*/
catePubId: string;
/**
* 类目名称
*/
catePubName: string;
/**
* 项目状况,可以是新的或二手的
*/
conditionVal?: string;
/**
* 创建时间
*/
createTime: Date;
/**
* 保证发货订单的天数
*/
daysToShip?: number;
/**
* 是否删除 0:未删除 1:删除
*/
deleted: boolean;
/**
* 描述-需要确认大小
*/
description: string;
/**
* description_type(正常,扩展)--- 不要了,白名单卖家字段关联,那个先不管
*/
descriptionType?: string;
/**
* 虾皮折扣活动ID
*/
discountId?: number;
/**
* (仅适用于 BR 本地卖家)全球贸易项目代码。如果已上传,请通过get_item_base_info api 检查项目的gtin_code
*/
gtinCode?: string;
/**
* 自增id
*/
id: number;
/**
* 图片
*/
imgUrl: string;
/**
* 产品备货数量
*/
inventory?: number;
/**
* 商品是否为预购商品(0.否 1.是)
*/
isPreOrder: number;
/**
* 是否已刊登到店铺1: 是, 2: 否 3.仍需手动重新刊登
*/
isPublish: string;
/**
* 此字段仅适用于印度尼西亚和马来西亚的本地卖家。使用此字段可确定产品是否为危险产品。0 表示非危险品,1 表示危险品。有关更多信息,请访问市场相应的卖家教育中心。)
*/
itemDangerous?: string;
/**
* 产品id
*/
itemId?: number;
/**
* 产品名称
*/
itemName: string;
/**
* 产品状态(UNLIST:下架状态; NORMAL:上架状态)
*/
itemStatus?: string;
/**
* 产品价格
*/
originalPrice?: number;
/**
* 单价折扣
*/
priceDiscount?: number;
/**
* 刊登失败原因
*/
publishErrInfo?: string;
/**
* 刊登时间
*/
publishTime?: Date;
/**
* 用于错误跟踪的 API 请求的标识符
*/
requestId?: string;
/**
* 店铺id
*/
shopId: number;
/**
* 尺码ID
*/
sizeId?: number;
/**
* 产品spu编码
*/
spuCode: string;
/**
* 成品spuCode
*/
tbSpuCodes: string[];
/**
* 模版Id
*/
templateId: number;
/**
* 模版名称
*/
templateName: string;
}


export interface SHProductPageReqVO extends PageParam {
/**
* 品牌id(类目属性冗余过来,这个必填)
*/
brandId?: number;
/**
* 类目id
*/
catePubId?: string;
/**
* 项目状况,可以是新的或二手的
*/
conditionVal?: string;
/**
* 创建时间
*/
createTime?: string[];
/**
* 保证发货订单的天数
*/
daysToShip?: number;
/**
* 描述-需要确认大小
*/
description?: string;
/**
* description_type(正常,扩展)--- 不要了,白名单卖家字段关联,那个先不管
*/
descriptionType?: string;
/**
* 虾皮折扣活动ID
*/
discountId?: number;
/**
* (仅适用于 BR 本地卖家)全球贸易项目代码。如果已上传,请通过get_item_base_info api 检查项目的gtin_code
*/
gtinCode?: string;
/**
* 产品备货数量
*/
inventory?: number;
/**
* 商品是否为预购商品(0.否 1.是)
*/
isPreOrder?: number;
/**
* 是否已刊登到店铺1: 是, 2: 否 3.仍需手动重新刊登
*/
isPublish?: string;
/**
* 此字段仅适用于印度尼西亚和马来西亚的本地卖家。使用此字段可确定产品是否为危险产品。0 表示非危险品,1 表示危险品。有关更多信息,请访问市场相应的卖家教育中心。)
*/
itemDangerous?: string;
/**
* 产品id
*/
itemId?: number;
/**
* 产品名称
*/
itemName?: string;
/**
* 产品状态(UNLIST:下架状态; NORMAL:上架状态)
*/
itemStatus?: string;
/**
* 产品价格
*/
originalPrice?: number;
/**
* 单价折扣
*/
priceDiscount?: number;
/**
* 刊登失败原因
*/
publishErrInfo?: string;
/**
* 刊登时间
*/
publishTime?: string[];
/**
* 用于错误跟踪的 API 请求的标识符
*/
requestId?: string;
/**
* 店铺id
*/
shopId?: number;
/**
* 尺码ID
*/
sizeId?: number;
/**
* 产品spu编码
*/
spuCode?: string;
/**
* 模版Id
*/
templateId?: number;
}

export interface UpdateSHProductNameReqVO {
id: number;
spuCode: string;
gpts: ProductKeywordGpt[];
}

export interface ProductKeywordGpt {
dynamicTableName: string;
id: number;
idList: string[],
keyword: string;
keywordType: number;
lang: string;
spuCode: string;
tableName: string;
}

export interface ShpopeeProductVO {
catePubId: string;
catePubName: string;
export interface UpdateSHProductPriceReqVO {
id: number;
imgUrl: string;
isDelete: number;
isPublish: number;
itemId: number;
itemName: string;
originalPrice: number;
publishErrInfo: string;
publishTime: string;
requestId: string;
spuCode: string;
tbProductKeywordGpts: ProductKeywordGpt[];
tbSpuCodes: string[],
templateId: number;
templateName: string;
}


export interface UpdateSHProductDescReqVO {
id: number;
description: string;
}

+ 82
- 0
src/models/platform.data.ts Переглянути файл

@@ -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
}

+ 339
- 11
src/models/template.data.ts Переглянути файл

@@ -1,28 +1,356 @@

//数据字典
export interface DataDictVO{
//模板字典
export interface TemplateDictVO{
description: string;
id: number;
name: string;
createTime?: number;
}

//数据字典详情
export interface DataDictDetailVO {
//模板字典详情
export interface TemplateDictDetailVO {
dictId: number;
dictSort: number;
id: number;
label: string;
value: string;
createTime: number;
}


export interface ShopeeTemplateVO {
/**
* TemplateInfoVO,模板数据
*/
export interface TemplateInfoVO {
/**
* 背景ids
*/
backgroupIds?: number[];
/**
* 类别
*/
catePubId: string;
/**
* 每天最大刊登量
*/
dayMaxPublishNumber?: number;
/**
* 模版id
*/
id: number;
categoryName: string;
createName: string;
createTime: string;
/**
* 图片信息
*/
imageInfo: TemplateImgVO[];
/**
* 物流渠道
*/
logistics: TemplateLogisticsVO[];
/**
* 素材组ids
*/
materialClassifyIds?: number[];
/**
* 店铺最大上架数量
*/
maxPrintCount: number;
/**
* 产品价格
*/
originalPrice: number;
/**
* 平台编码
*/
platformCode: string;
remark: string;
shopName: string;
/**
* 单价折扣
*/
priceDiscount?: number;
/**
* 变种比例
*/
proportion?: number;
/**
* 模板备注
*/
remark?: string;
/**
* 店铺id
*/
shopId: number;
/**
* 类目属性
*/
templateAttrs: TemplateAttrInfoVO[];
templateDimension: TemplateDimensionVO;
/**
* 模板名称编码
*/
templateName: string;
templateProduct: TemplateProductVO;
templateSkuSpace?: TemplateSkuSpaceVO;
/**
* 图片信息
*/
videoInfo: TemplateVideoVO[];
}

/**
* TemplateImgVO,模板图片信息
*/
export interface TemplateImgVO {
/**
* 图片id
*/
id?: number;
/**
* 图片Url信息
*/
url: string;
}

/**
* TemplateLogisticsVO,模板物流通道
*/
export interface TemplateLogisticsVO {
/**
* 是否为此项目启用通道(0.否 1.是)
*/
enabled: number;
/**
* 是否为买家支付运费(0.否 1.是)
*/
isFree?: number;
/**
* 频道的 ID
*/
logisticId: number;
/**
* 运费
*/
shippingFee?: number;
/**
* 大小 ID
*/
sizeId?: number;
}

/**
* TemplateAttrInfoVO,模板属性信息封装VO
*/
export interface TemplateAttrInfoVO {
templateAttr: TemplateAttrVO;
/**
* 模板属性值信息
*/
templateAttrVals: TemplateAttrValVO[];
}

/**
* TemplateAttrVO,模板属性信息
*/
export interface TemplateAttrVO {
/**
* 属性ID
*/
attrId: number;
/**
* 属性名称
*/
attrName: string;
}

/**
* TemplateAttrValVO,模板属性值信息
*/
export interface TemplateAttrValVO {
/**
* 属性值ID
*/
attrValId: number;
/**
* 属性值名称
*/
attrValName: string;
/**
* 价值单位(仅限定量属性)
*/
valueUnit?: string;
}

/**
* TemplateDimensionVO,模板包装尺寸
*/
export interface TemplateDimensionVO {
/**
* 包装高度,单位为厘米
*/
packageHeight: number;
/**
* 包装长度,单位为厘米
*/
packageLength: number;
/**
* 包装宽度,单位为厘米
*/
packageWidth: number;
/**
* 重量,单位为千克
*/
weight: number;
}

/**
* TemplateProductVO,模板产品信息
*/
export interface TemplateProductVO {
/**
* 品牌ID
*/
brandId?: number;
/**
* 品牌名称
*/
brandName?: string;
/**
* 项目的状况,可以是二手的或新的
*/
conditionVal?: string;
/**
* 保证发货数量
*/
daysToShip?: number;
/**
* 描述数据字典ID
*/
description: string;
/**
* 产品Id
*/
id: number;
/**
* 商品是否为预购商品(0.否 1.是)
*/
isPreOrder: number;
/**
* 产品名称数据字典ID
*/
itemName: string;
/**
* 产品状态
*/
itemStatus: string;
}

/**
* TemplateSkuSpaceVO,模板sku
*/
export interface TemplateSkuSpaceVO {
/**
* 销售数量
*/
inventory?: number;
/**
* 价格
*/
originalPrice?: number;
/**
* 尺码
*/
sizeId?: number;
/**
* 批发价格
*/
templateWholesale?: TemplateWholesaleVO[];
}

/**
* TemplateWholesaleVO,模板批发价格
*/
export interface TemplateWholesaleVO {
/**
* 此层的最大计数
*/
maxCount: number;
/**
* 此层的最小计数
*/
minCount: number;
/**
* 价格
*/
unitPrice: number;
}

/**
* TemplateVideoVO,模板视频信息
*/
export interface TemplateVideoVO {
/**
* 视频id
*/
id?: number;
/**
* 视频信息
*/
url: string;
}


//模板分页请求参数
export interface TemplateInfoPageReqVO extends PageParam {
/**
* 背景id(a,b,c)
*/
backgroupIds?: string;
/**
* 类目id
*/
catePubId?: string;
/**
* 创建时间
*/
createTime?: string[];
/**
* 素材id(a,b,c)
*/
materialClassifyIds?: string;
/**
* 最大上刊数量
*/
maxPrintCount?: number;
/**
* 平台编码
*/
platformCode?: string;
/**
* 变种比例
*/
proportion?: number;
/**
* 备注
*/
remark?: string;
/**
* 店铺id
*/
shopId?: number;
/**
* 模板名称
*/
templateName?: string;
}


export interface TemplateDictPageReqVO extends PageParam {
createTime?: string[];
description?: string;
name?: string;
}

export interface TemplateDictDetaiPageReqVO extends PageParam {
createTime?: string[];
dictId?: number;
dictSort?: number;
label?: string;
value?: string;
}

+ 43
- 53
src/pages/custom/platform-product/shopee/index.tsx Переглянути файл

@@ -1,18 +1,17 @@
import React, { useEffect, useState } from 'react';
import { Space, Table, Form, Button, Card, Input, TreeSelect, Image } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { t } from '@/utils/i18n';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
ExclamationCircleFilled,
PlusOutlined,
CarryOutOutlined,
SearchOutlined,
UndoOutlined
} from '@ant-design/icons';
import { antdUtils } from '@/utils/antd';
import { ShpopeeProductVO } from '@/models';
import mockData from '../../../../../mock/shopeeProduct.json'
import { useSetState } from 'ahooks';
import { SHProductVO, SHProductPageReqVO } from '@/models';
import { useRequest } from '@/hooks/use-request';
import shopeeProductService from '@/request/service/shopee-product';

const treeData = [
{
@@ -55,35 +54,30 @@ const treeData = [

export default () => {

const showDeleteConfirm = (item: ShpopeeProductVO) => {
antdUtils.modal?.confirm({
title: `确认删除编码为: ${item.spuCode} 的产品吗?`,
icon: <ExclamationCircleFilled />,
content: `请注意删除以后不可恢复!`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk() {
return new Promise((resolve, reject) => {
setTimeout(() => {
antdUtils.message?.open({
type: 'success',
content: '删除成功',
});
resolve(null)
}, 1000);
//TODO: batch update title、price、desc

}).catch(() => antdUtils.message?.open({
type: 'error',
content: '操作失败',
}));
},
onCancel() {
},
});
};
const [dataSource, setDataSource] = useState<SHProductVO[]>([]);
const [searchFrom] = Form.useForm();
const [searchState, setSearchState] = useSetState<SHProductPageReqVO>({});
const [total, setTotal] = useState(0);

const { runAsync: getPageApi } = useRequest(shopeeProductService.pageApi, { manual: true });
const { runAsync: batchUpdateNameApi } = useRequest(shopeeProductService.batchUpdateNameApi, { manual: true });
const { runAsync: batchUpdateDescApi } = useRequest(shopeeProductService.batchUpdateDescApi, { manual: true });
const { runAsync: batchUpdatePriceApi } = useRequest(shopeeProductService.batchUpdatePriceApi, { manual: true });

const columns: ColumnsType<ShpopeeProductVO> = [
const load = async () => {
searchFrom.setFieldValue("pageSize", searchState.pageSize);
searchFrom.setFieldValue("pageNo", searchState.pageNo);
//TODO: merge search params
const [error, { data }] = await getPageApi(searchFrom.getFieldsValue());
if (!error) {
setDataSource(data.list);
setTotal(data.total);
}
}

const columns: ColumnsType<SHProductVO> = [
{
title: '缩略图',
dataIndex: 'imgUrl',
@@ -127,30 +121,17 @@ export default () => {
title: '刊登时间',
key: 'publishTime',
dataIndex: 'publishTime'
},
{
title: t("QkOmYwne" /* 操作 */),
key: 'action',
render: (_, record) => (
<Space size="middle">
<a onClick={() => {
}}>编辑</a>
<a onClick={() => {
showDeleteConfirm(record)
}}>删除</a>
</Space>
),
},
}
];

const [searchFrom] = Form.useForm();
const navigate = useNavigate();


const [treeLine, setTreeLine] = useState(true);
const [showLeafIcon, setShowLeafIcon] = useState(false);
const [showIcon, setShowIcon] = useState<boolean>(false);

useEffect(() => {
load();
}, [searchFrom, searchState]);

return (
<div>
<div>
@@ -182,8 +163,17 @@ export default () => {
</div>
</Card>
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'>
<Table rowKey="id" scroll={{ x: true }} columns={columns} dataSource={mockData as ShpopeeProductVO[]} className='bg-transparent'
pagination={{ position: ['bottomRight'] }}
<Table rowKey="id"
scroll={{ x: true }}
columns={columns}
dataSource={dataSource}
className='bg-transparent'
pagination={{
position: ['bottomRight'],
current: searchState.pageNo,
pageSize: searchState.pageSize,
total
}}
/>
</Card>
</div>


+ 205
- 0
src/pages/custom/product/material/classify.tsx Переглянути файл

@@ -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>
</>
);
};

+ 2
- 0
src/pages/custom/product/material/index.tsx Переглянути файл

@@ -7,6 +7,7 @@ import { t } from '@/utils/i18n';
import { PlusOutlined, ExclamationCircleFilled, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
import { antdUtils } from '@/utils/antd';
import mData from '../../../../../mock/findMaterialPage.json'
import MaterialClassifyView from './classify';

const { Search } = Input;

@@ -311,6 +312,7 @@ const TablePage: React.FC = () => {
<>
<div className='flex flex-row'>
<Card className='basis-1/4 w-[100px] mb-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'>
<MaterialClassifyView/>
</Card>
<Card className='basis-3/4 mb-[10px] ml-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]' bodyStyle={{
paddingTop: 0,


+ 119
- 86
src/pages/custom/product/sample/components/attr-editor.tsx Переглянути файл

@@ -1,18 +1,18 @@
import React, { useEffect, useMemo, useState } from 'react'
import { CloseOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import { Drawer, Form, Input, Card, Space, Button, Upload, Popconfirm, Modal, FormListOperation, FormListFieldData } from 'antd'
import { Drawer, Form, Input, Card, Space, Button, Upload, Popconfirm, Modal, FormListOperation, FormListFieldData, Tooltip } from 'antd'
import type { RcFile, UploadProps } from 'antd/es/upload';
import type { UploadFile } from 'antd/es/upload/interface';
import './editor.css';

const layout = {
labelCol: { span: 4, },
wrapperCol: { span: 16 },
bordered: false,
labelCol: { span: 5, },
wrapperCol: { span: 15 },
};

interface AttributeValue {
id: number;
attrId: number;
id?: number;
attrId?: number;
valName: string;
imgId?: number;
imgUrl?: string;
@@ -29,7 +29,6 @@ export interface SampleAttribute {
interface CreateSampleAttrProps {
visible: boolean;
onCancel: (flag?: boolean) => void;
curRecord?: SampleAttribute[] | null;
onSave: () => void;
editData?: SampleAttribute[] | null;
}
@@ -45,13 +44,15 @@ const getBase64 = (file: RcFile): Promise<string> =>

const SampleAttrEditor: React.FC<CreateSampleAttrProps> = (props) => {

const { visible, onCancel, curRecord, onSave, editData } = props;
const { visible, onCancel, onSave, editData } = props;
const [saveLoading, setSaveLoading] = useState(false);
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState('');
const [previewTitle, setPreviewTitle] = useState('');
const [form] = Form.useForm();

const isEdit = !!editData;

useEffect(() => {
if (visible) {
setInitValue();
@@ -61,6 +62,9 @@ const SampleAttrEditor: React.FC<CreateSampleAttrProps> = (props) => {
}, [visible]);

async function setInitValue() {
if (editData) {
form.setFieldsValue({ attrData: editData });
}
}

const save = async (values: any) => {
@@ -83,60 +87,80 @@ const SampleAttrEditor: React.FC<CreateSampleAttrProps> = (props) => {
};

const uploadButton = (
<div>
<div className='flex'>
<PlusOutlined />
<div style={{ marginTop: 8 }}>上传</div>
<div style={{ marginLeft: 8 }}>上传</div>
</div>
);
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
// setFileList(newFileList);
}

const readerAttrValueEditForm = (subFields: FormListFieldData[], {remove, add}: FormListOperation, isContainImg: boolean) => {
return (<div style={{ display: 'flex', flexDirection: 'column', rowGap: 16 }}>
{subFields.map((subField) => (
<Space key={subField.key}>
<Form.Item noStyle name={[subField.name, 'valName']}>
<Input size='middle'/>
</Form.Item>
{isContainImg && <Form.Item noStyle shouldUpdate name={[subField.name, 'imgUrl']}>
<Upload
action="https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188"
listType="picture-card"
onPreview={handlePreview}
onChange={handleChange}
showUploadList={false}
maxCount={1}
>
{uploadButton}
</Upload>
</Form.Item>
}
<Popconfirm
title="删除属性值"
description="确认是否删除当前属性值?"
onConfirm={() => { remove(subField.name); }}
okText="确认"
cancelText="取消"
>
<CloseOutlined/>
</Popconfirm>
</Space>
))}
<Button type="dashed" size='middle' onClick={() => add()} block>
+ 添加自定义值
</Button>
</div>)
const readerAttrValueEditForm = (subFields: FormListFieldData[], attrVals: AttributeValue[], { remove, add }: FormListOperation, isContainImg: boolean) => {
return (
<div style={{ display: 'flex', flexDirection: 'column', rowGap: 16 }}>
{subFields.map((subField) => {
const item = attrVals[subField.key];
const uploadFile: UploadFile | null = item && item.imgUrl && item.imgId ? {
uid: `${item.imgId}`,
name: '',
status: 'done',
url: item.imgUrl
} : null
const fileList: UploadFile[] = [];
if (uploadFile) {
fileList.push(uploadFile)
}
return (
<Form.Item {...subField} style={{marginBottom: 0}}>
<Space key={subField.key}>
<Form.Item noStyle name={[subField.name, 'valName']}>
<Input size='middle' />
</Form.Item>
{isContainImg && <Form.Item noStyle shouldUpdate name={[subField.name, 'imgUrl']}>
<Upload className='attr-image'
action="https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188"
listType="picture-card"
onPreview={handlePreview}
onChange={handleChange}
maxCount={1}
defaultFileList={[...fileList]}
>
{fileList.length > 0 ? null : uploadButton}
</Upload>
</Form.Item>
}
<Popconfirm
title="删除属性值"
description="确认是否删除当前属性值?"
onConfirm={() => { remove(subField.name); }}
okText="确认"
cancelText="取消"
>
<Tooltip title="删除属性值">
<DeleteOutlined className='delete-icon'/>
</Tooltip>
</Popconfirm>
</Space>
</Form.Item>
)
})}
<Button type="dashed" size='middle' onClick={() => add({ valName: '', imgUrl: '', imgId: 0 })} block>
+ 添加自定义值
</Button>
</div>)
}

const renderAttrEditForm = (fields: FormListFieldData[], {remove, add}: FormListOperation, dataSource: SampleAttribute[]) => {
return (
<div style={{ display: 'flex', rowGap: 16, flexDirection: 'column' }}>
{fields.map((field) => {
const data = dataSource[field.key]
return (
<Card size="small" title={field.name} key={field.key}
extra={
const renderAttrEditForm = (fields: FormListFieldData[], { remove, add }: FormListOperation, dataSource: SampleAttribute[]) => {
return (
<div style={{ display: 'flex', rowGap: 16, flexDirection: 'column' }}>
{fields.map((field) => {
const data = dataSource[field.key];
return (
<Form.Item {...field} style={{marginBottom: 4}}>
<Card size="small" title={field.name} key={field.key}
style={{ width: 500 }}
extra={
<Popconfirm
title="删除属性"
description="确认是否删除当前属性?"
@@ -144,62 +168,71 @@ const SampleAttrEditor: React.FC<CreateSampleAttrProps> = (props) => {
okText="确认"
cancelText="取消"
>
<DeleteOutlined style={{ fontSize: '16px' }} className='hover:(bg-[rgb(94,53,177)]'/>
<Tooltip title="删除属性">
<CloseOutlined style={{ fontSize: '16px' }} className='delete-icon' />
</Tooltip>
</Popconfirm>
}
}
>
<Form.Item label="属性名称" name={[field.name, 'attrName']}>
<Input size='middle'/>
<Form.Item label="属性名称" name={[field.name, 'attrName']} labelCol={{ span: 4 }}>
<Input size='middle' />
</Form.Item>

<Form.Item label="属性值">
<Form.Item label="属性值" labelCol={{ span: 4 }}>
<Form.List name={[field.name, 'attrVals']}>
{(subFields, subOpt) => readerAttrValueEditForm(subFields, subOpt, data.isContainImg === 1)}
{(subFields, subOpt) => {
return readerAttrValueEditForm(subFields, data.attrVals, subOpt, data.isContainImg === 1)
}}
</Form.List>
</Form.Item>
</Card>
)})}

<Button type="dashed" size='middle' onClick={() => add()} block>
+ 添加属性
</Button>
</div>
)
</Form.Item>

)
})}

<Button type="dashed" size='middle' onClick={() => add({
"prototypeId": 88,
"attrName": "",
"isContainImg": 1,
"attrVals": [
]
},)} block>
+ 添加属性
</Button>
</div>
)
}

return (
<>
<Drawer
open={visible}
title="新建"
title={isEdit ? "编辑" : "新建"}
width={640}
onClose={() => { onCancel() }}
extra={
<Space>
<Button type="primary" size='middle' onClick={() => {save}}>
<Button type="primary" size='middle' onClick={() => { save }}>
保存
</Button>
</Space>
}
destroyOnClose
>
<Form
form={form}
{...layout}
initialValues={{ editData }}
onFinish={save}
labelCol={{ flex: '0 0 100px' }}
wrapperCol={{ span: 16 }}
>
<Form.List name="editData">
{(fields, operation) => {
return renderAttrEditForm(fields, operation, editData!!)
}}
</Form.List>

</Form>
</Drawer>
<Form
form={form}
{...layout}
onFinish={save}
labelCol={{ flex: '0 0 100px' }}
wrapperCol={{ span: 16 }}
>
<Form.List name="attrData">
{(fields, operation) => renderAttrEditForm(fields, operation, form.getFieldsValue()['attrData'])}
</Form.List>

</Form>
</Drawer>
<Modal open={previewOpen} title={previewTitle} footer={null} onCancel={handleCancel}>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>


+ 45
- 0
src/pages/custom/product/sample/components/editor.css Переглянути файл

@@ -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;
}

+ 13
- 4
src/pages/custom/product/sample/editor/index.css Переглянути файл

@@ -1,17 +1,26 @@
:where(.css-dev-only-do-not-override-11asvft).ant-card .ant-card-body {
.workbench {}

:where(.workbench).ant-card .ant-card-body {
padding: 0;
border-radius: 0 0 8px 8px;
}

:where(.css-dev-only-do-not-override-phnixs).ant-card .ant-card-body {
:where(.workbench).ant-card .ant-card-body {
padding: 0;
border-radius: 0 0 8px 8px;
}

:where(.css-dev-only-do-not-override-phnixs).ant-list-lg .ant-list-item {
:where(.workbench).ant-list-lg .ant-list-item {
padding: 16px 8px;
}

:where(.css-dev-only-do-not-override-11asvft).ant-list-lg .ant-list-item {
:where(.workbench).ant-list-lg .ant-list-item {
padding: 16px 8px;
}


:where(.workbench).ant-list .ant-spin-container {
position: relative;
transition: opacity 0.3s;
margin-top: 10px;
}

+ 9
- 8
src/pages/custom/product/sample/editor/index.tsx Переглянути файл

@@ -7,7 +7,7 @@ import 'react-advanced-cropper/dist/style.css';
import { ImageStencil } from "./components/ImageStencil";
import { DeleteOutlined, EditOutlined, UploadOutlined } from '@ant-design/icons';
import cn from 'classnames';
import './index.css'
import './index.css';
import property from '../../../../../../mock/propertyById.json'

interface SampleProperty {
@@ -113,8 +113,8 @@ export default () => {

return (
<div className='flex'>
<Card className='flex-none mb-[10px] w-[270px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'>
<div style={{ padding: '20px' }}>
<Card className='flex-none mb-[10px] w-[270px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px] workbench'>
<div style={{ padding: '20px 20px 0 20px' }}>
<Anchor
direction="horizontal"
items={[
@@ -133,21 +133,21 @@ export default () => {
</div>
<div >
<div >
<div id="part-1" >
<div id="part-1">
<List dataSource={sample.prototypeImgs.filter(it => it.imgType === 2)}
grid={{
gutter: 2,
column: 2
}}
header={(
<div className='flex justify-center'>
<div className='flex justify-center' >
<Upload {...props}>
<Button icon={<UploadOutlined />}>点击上传主图</Button>
</Upload>
</div>
)}
renderItem={(item) => (renderListItem(item))}
className='workbench'
/>
</div>
<div id="part-2">
@@ -165,13 +165,14 @@ export default () => {
</div>
)}
renderItem={(item) => (renderListItem(item))}
className='workbench'
/>
</div>
</div>
</div>
</Card>

<Card className='flex-auto mx-[10px] mb-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'>
<Card className='flex-auto mx-[10px] mb-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px] workbench'>
<div className='flex flex-col justify-center'>
<div className='flex justify-center text-3xl p-5 font-extrabold'>工作台</div>
<div className='flex justify-center'>
@@ -201,7 +202,7 @@ export default () => {
</div>
</Card>

<Card className='flex-none mb-[10px] w-[270px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'>
<Card className='flex-none mb-[10px] w-[270px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px] workbench'>
<div className='flex justify-center pt-2'>
<Radio.Group options={options}
onChange={onStyleChanged}


+ 0
- 1
src/pages/custom/product/sample/index.tsx Переглянути файл

@@ -384,7 +384,6 @@ const TablePage: React.FC = () => {
onSave={saveAttributeHandle}
onCancel={cancelHandle}
visible={attrEditorVisible}
curRecord={attrData}
editData={attrData} />

<MaskPictureEditor


+ 202
- 0
src/pages/custom/template/dict/dict-data.tsx Переглянути файл

@@ -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}
/>
</>
);
};

+ 10
- 9
src/pages/custom/template/dict/dict-detail-editor.tsx Переглянути файл

@@ -1,8 +1,8 @@
import React, { useEffect, useState } from 'react'
import { Form, Input, InputNumber, Modal } from 'antd';
import dictService from '@/request/service/template-dict';
import dictService from '@/request/service/template-dict-detail';
import { useRequest } from '@/hooks/use-request';
import type { DataDictDetailVO } from '@/models'
import type { TemplateDictDetailVO } from '@/models'
import { antdUtils } from '@/utils/antd';

const layout = {
@@ -13,14 +13,15 @@ const layout = {
export default (props: {
visible: boolean;
onCancel: (flag?: boolean) => void;
onSave: (role: DataDictDetailVO) => void;
data?: DataDictDetailVO | null;
onSave: (role: TemplateDictDetailVO) => void;
dictId?: number;
data?: TemplateDictDetailVO | null;
}) => {

const { visible, onCancel, onSave, data } = props;
const { visible, onCancel, onSave, dictId, data } = props;

const { runAsync: updateApi } = useRequest(dictService.updateDictDetailApi, { manual: true });
const { runAsync: createApi } = useRequest(dictService.deleteDictDetailApi, { manual: true });
const { runAsync: updateApi } = useRequest(dictService.updateApi, { manual: true });
const { runAsync: createApi } = useRequest(dictService.createApi, { manual: true });

const isEdit = !!data;

@@ -41,7 +42,7 @@ export default (props: {
const save = async () => {
setSaveLoading(true);
const fieldValues = form.getFieldsValue();
const newValue = isEdit ? { ...data, ...fieldValues } : fieldValues;
const newValue = isEdit ? { ...data, ...fieldValues } : {...fieldValues, dictId};
const [error, { msg, code }] = isEdit ? await updateApi(newValue) : await createApi(newValue);
if (!error && code === 0) {
onSave(newValue);
@@ -58,7 +59,7 @@ export default (props: {
<>
<Modal
open={visible}
title="新建"
title={isEdit ? "编辑" : "新建"}
width={640}
onOk={save}
onCancel={() => onCancel()}


+ 178
- 0
src/pages/custom/template/dict/dict-detail.tsx Переглянути файл

@@ -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}
/>
</>
);
});

+ 4
- 4
src/pages/custom/template/dict/dict-editor.tsx Переглянути файл

@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'
import { Form, Input, Modal } from 'antd';
import dictService from '@/request/service/template-dict';
import { useRequest } from '@/hooks/use-request';
import type { DataDictVO } from '@/models'
import type { TemplateDictVO } from '@/models'
import { antdUtils } from '@/utils/antd';

const layout = {
@@ -13,8 +13,8 @@ const layout = {
export default (props: {
visible: boolean;
onCancel: (flag?: boolean) => void;
onSave: (role: DataDictVO) => void;
data?: DataDictVO | null;
onSave: (role: TemplateDictVO) => void;
data?: TemplateDictVO | null;
}) => {

const { visible, onCancel, onSave, data } = props;
@@ -58,7 +58,7 @@ export default (props: {
<>
<Modal
open={visible}
title="新建"
title={isEdit ? "编辑" : "新建"}
width={640}
onOk={save}
onCancel={() => onCancel()}


+ 15
- 289
src/pages/custom/template/dict/index.tsx Переглянути файл

@@ -1,300 +1,26 @@
import React, { useState, useRef } from 'react';
import { Space, Table, Button, Image, Divider, Card, Input } from 'antd';
import type { InputRef } from 'antd';
import type { ColumnType, ColumnsType } from 'antd/es/table';
import type { FilterConfirmProps, TableRowSelection } from 'antd/es/table/interface';
import { t } from '@/utils/i18n';
import { PlusOutlined, ExclamationCircleFilled, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
import { antdUtils } from '@/utils/antd';
import { DataDictVO, DataDictDetailVO } from '@/models';
import DictEditor from './dict-editor';
import DictDetailEditor from './dict-detail-editor';

const dataDicts: DataDictVO[] = [
{
"id": 43,
"name": "爆款词",
"description": "爆款词"
},
{
"id": 42,
"name": "T-T",
"description": "t-shirt"
},
{
"id": 41,
"name": "T-shirt Background",
"description": "T桖背景"
}
]

const dictDetailsA: DataDictDetailVO[] = [
{
"id": 114,
"dictId": 43,
"label": "tshirt for women",
"value": "tshirt for women",
"dictSort": 1
},
{
"id": 113,
"dictId": 43,
"label": "oversize t shirt black",
"value": "oversize t shirt black",
"dictSort": 1
},
{
"id": 112,
"dictId": 43,
"label": "oversize t shirt",
"value": "oversize t shirt",
"dictSort": 1
}
]

const dictDetailsB: DataDictDetailVO[] = [
{
"id": 107,
"dictId": 42,
"label": "three",
"value": "three",
"dictSort": 3
},
{
"id": 106,
"dictId": 42,
"label": "two",
"value": "two",
"dictSort": 2
},
{
"id": 105,
"dictId": 42,
"label": "one",
"value": "one",
"dictSort": 1
}
]

const dictDetailsC: DataDictDetailVO[] = [
{
"id": 111,
"dictId": 41,
"label": "Soothing hot springs",
"value": "Hot spring photos, hot spring water, beautiful scenery, no one, film quality, 8K, meticulous",
"dictSort": 5
},
{
"id": 110,
"dictId": 41,
"label": "Great Eiffel Tower",
"value": "Keywords Paris Tower, photo, high quality, 8K,",
"dictSort": 4
},
{
"id": 109,
"dictId": 41,
"label": "Iconic Eiffel Tower",
"value": "Keywords Paris Tower, photo, high quality, 8K,",
"dictSort": 3
},
{
"id": 108,
"dictId": 41,
"label": "Pleasant Tunisia",
"value": "Venice, photos, high quality, 8K, water, details, film quality, rendering, beautiful scenery.",
"dictSort": 2
},
{
"id": 104,
"dictId": 41,
"label": "Trendy Tunisia",
"value": "Venice, photos, high quality, 8K, water, details, film quality, rendering, beautiful scenery.",
"dictSort": 1
}
]

import React, { useRef } from 'react';
import { TemplateDictVO } from '@/models';
import DictDetail from './dict-detail';
import DictData from './dict-data';
import type { DictDetailRef } from './dict-detail'

export default () => {

const [dictDetail, setDictDetail] = useState<DataDictDetailVO[]>();

const [editorVisable, seEditorVisable] = useState<boolean>(false);
const [editData, seEditData] = useState<DataDictVO>();

const [detailEditorVisable, seEdtailEditorVisable] = useState<boolean>(false);
const [editDetailData, seEditDetailData] = useState<DataDictDetailVO>();

const showDeleteConfirm = (item: DataDictVO) => {
antdUtils.modal?.confirm({
title: `确认删除名称为: ${item.name} 的字典吗?`,
icon: <ExclamationCircleFilled />,
content: '请注意删除以后不可恢复!',
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk() {
return new Promise((resolve, reject) => {
setTimeout(() => {
antdUtils.message?.open({
type: 'success',
content: '删除成功',
});
resolve(null)
}, 1000);

}).catch(() => antdUtils.message?.open({
type: 'error',
content: '操作失败',
}));
},
onCancel() {
},
});
};


const columns: ColumnsType<DataDictDetailVO> = [
{
title: '字典标签',
dataIndex: 'label',
key: 'label',
align: 'center',
width: 150,
},

{
title: '字典值',
key: 'value',
dataIndex: 'value',
align: 'center',
},
{
title: '排序',
key: 'dictSort',
dataIndex: 'dictSort',
width: 100,
},
{
title: t("QkOmYwne" /* 操作 */),
key: 'action',
render: (_, record) => (
<Space size="middle" split={(
<Divider type='vertical' />
)}>
<a
onClick={() => {
seEditDetailData(record)
seEdtailEditorVisable(true);
}}>
编辑
</a>
<a
onClick={() => {

}}>
删除
</a>
</Space>
),
width: 150,
},
];


const updateDactDetail = (data: DataDictVO) => {
if (data.id == 43) {
setDictDetail(dictDetailsA)
} else if (data.id == 42) {
setDictDetail(dictDetailsB)
} else {
setDictDetail(dictDetailsC)
}
}


const dictColumns: ColumnsType<DataDictVO> = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center',
},
{
title: '描述',
key: 'description',
dataIndex: 'description',
align: 'center',
},
{
title: t("QkOmYwne" /* 操作 */),
key: 'action',
align: 'center',
render: (_, record) => (
<Space size="middle" split={(
<Divider type='vertical' />
)}>
<a
onClick={() => {
seEditData(record)
seEditorVisable(true);
}}>
编辑
</a>
<a
onClick={() => {
showDeleteConfirm(record)
}}>
删除
</a>
</Space>
),
width: 150,
},
];
const detailRef = useRef<DictDetailRef>()

return (
<>
<div className='flex flex-row'>
<Card className='basis-2/5 w-[100px] mb-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'>
<Table rowKey="id" scroll={{ x: true }} columns={dictColumns} dataSource={dataDicts} className='bg-transparent'
onRow={(record) => {
return {
onClick: () => {
updateDactDetail(record)
}
};
}}
pagination={{ position: ['bottomRight'] }}
/>
</Card>
<Card className='basis-3/5 mb-[10px] ml-[10px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]' bodyStyle={{
paddingTop: 0,
paddingBottom: 0
}}>
<div className="py-[8px] flex flex-row-reverse">
<Button className="ml-5" type='primary' size='middle' icon={<PlusOutlined />}> 新增 </Button>
</div>
<Table rowKey="id" scroll={{ x: true }} columns={columns} dataSource={dictDetail} className='bg-transparent'
pagination={{ position: ['bottomRight'] }}
/>
</Card>
<div className='basis-2/5 '>
<DictData
onSelectItem={(item: TemplateDictVO) => {
detailRef.current?.updateDictData(item);
}} />
</div>
<div className='basis-3/5 '>
<DictDetail ref={detailRef} />
</div>
</div>
<DictEditor
onSave={() => {
}}
onCancel={() => { seEditorVisable(false) }}
visible={editorVisable}
data={editData}
/>

<DictDetailEditor
onSave={() => {
}}
onCancel={() => { seEdtailEditorVisable(false) }}
visible={detailEditorVisable}
data={editDetailData}
/>
</>
);
};

+ 50
- 45
src/pages/custom/template/shopee/index.tsx Переглянути файл

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Space, Table, Form, Button, Card, Input, TreeSelect } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { t } from '@/utils/i18n';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
ExclamationCircleFilled,
@@ -11,7 +11,10 @@ import {
UndoOutlined
} from '@ant-design/icons';
import { antdUtils } from '@/utils/antd';
import { ShopeeTemplateVO } from '@/models';
import { useSetState } from 'ahooks';
import { useRequest } from '@/hooks/use-request';
import platformShopService from '@/request/service/platform-template';
import { TemplateInfoVO, TemplateInfoPageReqVO } from '@/models';

const treeData = [
{
@@ -53,42 +56,38 @@ const treeData = [
];

export default () => {
const data: ShopeeTemplateVO[] = [
{
"id": 78,
"platformCode": "Shopee",
"shopName": "Trista",
"categoryName": "T-Shirts",
"templateName": "T-SHIRT",
"remark": "1",
"createName": "admin",
"createTime": "2023-06-15 14:45:15"
},
{
"id": 77,
"platformCode": "Shopee",
"shopName": "Trista",
"categoryName": "T-Shirts",
"templateName": "T-SHIRT",
"remark": "1",
"createName": "admin",
"createTime": "2023-06-15 14:44:46"
},
{
"id": 76,
"platformCode": "Shopee",
"shopName": "Trista",
"categoryName": "T-Shirts",
"templateName": "T-SHIRT",
"remark": "",
"createName": "admin",
"createTime": "2023-06-15 14:42:24"

const [dataSource, setDataSource] = useState<TemplateInfoVO[]>([]);
const [total, setTotal] = useState(0);
const [searchFrom] = Form.useForm();
const [searchState, setSearchState] = useSetState<TemplateInfoPageReqVO>({
pageNo: 1,
pageSize: 10
});

const [treeLine, setTreeLine] = useState(true);
const [showLeafIcon, setShowLeafIcon] = useState(false);
const [showIcon, setShowIcon] = useState<boolean>(false);

//获取分页数据
const { runAsync: getPageApi } = useRequest(platformShopService.pageApi, { manual: true });
//删除数据
const { runAsync: deleteApi } = useRequest(platformShopService.deleteApi, { manual: true });

//加载数据
const load = async () => {
const [error, { code, data, msg }] = await getPageApi(searchState);
if (error || code !== 0) {
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' })
} else {
setDataSource(data.list);
setTotal(data.total);
}
];
};

const showDeleteConfirm = (item: ShopeeTemplateVO) => {
const showDeleteConfirm = (item: TemplateInfoVO) => {
antdUtils.modal?.confirm({
title: `确认删除标题为: ${item.shopName} 的模板吗?`,
title: `确认删除标题为: ${item.templateName} 的模板吗?`,
icon: <ExclamationCircleFilled />,
content: `请注意删除以后不可恢复!`,
okText: '删除',
@@ -114,7 +113,7 @@ export default () => {
});
};

const columns: ColumnsType<ShopeeTemplateVO> = [
const columns: ColumnsType<TemplateInfoVO> = [
{
title: '店铺',
dataIndex: 'shopName',
@@ -162,13 +161,9 @@ export default () => {
},
];

const [searchFrom] = Form.useForm();
const navigate = useNavigate();


const [treeLine, setTreeLine] = useState(true);
const [showLeafIcon, setShowLeafIcon] = useState(false);
const [showIcon, setShowIcon] = useState<boolean>(false);
useEffect(() => {
load();
}, [])

return (
<div>
@@ -204,8 +199,18 @@ export default () => {
</div>
</Card>
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'>
<Table rowKey="id" scroll={{ x: true }} columns={columns} dataSource={data} className='bg-transparent'
pagination={{ position: ['bottomRight'] }}
<Table
rowKey="id"
scroll={{ x: true }}
columns={columns}
dataSource={dataSource}
className='bg-transparent'
pagination={{
position: ['bottomRight'],
current: searchState.pageNo,
pageSize: searchState.pageSize,
total
}}
/>
</Card>
</div>


+ 408
- 0
src/pages/goods/main/attribute/classify.tsx Переглянути файл

@@ -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();
}}
/>
</>
);
});


+ 415
- 0
src/pages/goods/main/attribute/colors.tsx Переглянути файл

@@ -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();
}} />
</>
);
});


+ 78
- 0
src/pages/goods/main/attribute/index.tsx Переглянути файл

@@ -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>
</>
);
};


+ 415
- 0
src/pages/goods/main/attribute/size.tsx Переглянути файл

@@ -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();
}} />
</>
);
});


+ 152
- 0
src/pages/goods/main/category/index.tsx Переглянути файл

@@ -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>
</>
);
};


+ 272
- 0
src/pages/infra/log/api-access-log/index.tsx Переглянути файл

@@ -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>
</>
);
};

+ 25
- 5
src/pages/infra/log/api-error-log/index.tsx Переглянути файл

@@ -145,9 +145,14 @@ export default () => {
dataIndex: 'userType',
key: 'userType',
fixed: 'left',
filterSearch: true,
align: 'center',
width: 100,
filters: [
{ text: '会员', value: 1 },
{ text: '管理员', value: 2 },
],
filterMultiple: false,
filterSearch: false,
render: (value: number) => {
if (value === 1) {
return <Tag color="purple">会员</Tag>
@@ -179,7 +184,15 @@ export default () => {
key: 'requestUrl',
align: 'center',
width: 150,
render: (text, record) => (
<Tooltip title={text}>
<div className='text-ellipsis overflow-hidden whitespace-nowrap max-w-xs'>
{text}
</div>
</Tooltip>
),
...getColumnSearchProps('requestUrl', "请输入请求地址")
},
{
title: '异常时间',
@@ -196,7 +209,13 @@ export default () => {
dataIndex: 'exceptionName',
key: 'exceptionName',
align: 'center',
width: 150,
render: (text, record) => (
<Tooltip title={text}>
<p className='text-ellipsis overflow-hidden whitespace-nowrap max-w-xs'>
{text}
</p>
</Tooltip>
)
},
{
title: '处理状态',
@@ -206,13 +225,13 @@ export default () => {
width: 150,
render: (value: number) => {
if (value === 0) {
return <Tag color="red">未处理</Tag>
return <Tag color="yellow">未处理</Tag>
} else if (value === 1) {
return <Tag color="purple">已处理</Tag>
} else if (value === 2) {
return <Tag color="blue">已忽略</Tag>
} else {
return <Tag color="purple">内置</Tag>
return <Tag color="red">未知</Tag>
}
}
},
@@ -243,7 +262,7 @@ export default () => {
];

const exportLogs = async () => {
await exportOperateLog()
await exportOperateLog();
}

useEffect(() => {
@@ -255,6 +274,7 @@ export default () => {
applicationName: filters.applicationName ? filters.applicationName[0] as string : undefined,
requestUrl: filters.requestUrl ? filters.requestUrl[0] as string : undefined,
userId: filters.userId ? parseInt(filters.userId[0] as string) : undefined,
userType: filters.userType ? filters.userType[0] as number : undefined,
pageNo: pagination.current,
pageSize: pagination.pageSize
}


+ 1
- 1
src/pages/system/dept/create-department.tsx Переглянути файл

@@ -61,7 +61,7 @@ const DepartmentEditor: React.FC<EditorProps> = (props) => {
<>
<Modal
open={visible}
title="新建"
title={isEdit ? "编辑" : "新建"}
width={640}
onOk={save}
onCancel={() => onCancel()}


+ 258
- 0
src/pages/system/oauth2/client/index.tsx Переглянути файл

@@ -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>
</>
);
};

+ 242
- 0
src/pages/system/oauth2/token/index.tsx Переглянути файл

@@ -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>
</>
);
};

+ 1
- 1
src/pages/system/post/create-position.tsx Переглянути файл

@@ -61,7 +61,7 @@ const PositionEditor: React.FC<EditorProps> = (props) => {
<>
<Modal
open={visible}
title="新建"
title={isEdit ? "编辑" : "新建"}
width={640}
onOk={save}
onCancel={() => onCancel()}


+ 240
- 0
src/pages/system/shop/manage/index.tsx Переглянути файл

@@ -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} />
</>
);
};


+ 227
- 0
src/pages/system/shop/manage/shop-editor.tsx Переглянути файл

@@ -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;

+ 1
- 1
src/pages/system/tenant/list/tenant-editor.tsx Переглянути файл

@@ -74,7 +74,7 @@ const TenantEditor: React.FC<EditorProps> = (props) => {
<>
<Modal
open={visible}
title="新建"
title={isEdit ? "编辑" : "新建"}
width={640}
onOk={save}
onCancel={() => onCancel()}


+ 1
- 1
src/pages/system/tenant/package/tenant-pkg-editor.tsx Переглянути файл

@@ -89,7 +89,7 @@ const TenantPackageEditor: React.FC<EditorProps> = (props) => {
<>
<Modal
open={visible}
title="新建"
title={isEdit ? "编辑" : "新建"}
width={640}
onOk={save}
confirmLoading={saveLoading}


+ 48
- 0
src/request/service/category.ts Переглянути файл

@@ -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}`);
},
};

+ 31
- 0
src/request/service/goods-attr-val.ts Переглянути файл

@@ -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 });
},
};

+ 46
- 0
src/request/service/goods-attr.ts Переглянути файл

@@ -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}`);
},
};

+ 37
- 0
src/request/service/goods-classify.ts Переглянути файл

@@ -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}`);
},
};

+ 43
- 0
src/request/service/material-classify.ts Переглянути файл

@@ -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}`);
},
};

+ 44
- 0
src/request/service/oauth2.ts Переглянути файл

@@ -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}`)
},

};


+ 26
- 0
src/request/service/platform-shop.ts Переглянути файл

@@ -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}`);
},
};

+ 35
- 0
src/request/service/platform-template.ts Переглянути файл

@@ -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}`);
},
}

+ 33
- 0
src/request/service/shopee-product.ts Переглянути файл

@@ -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);
}
}

+ 36
- 0
src/request/service/template-dict-detail.ts Переглянути файл

@@ -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 }});
},
}

+ 20
- 5
src/request/service/template-dict.ts Переглянути файл

@@ -1,15 +1,30 @@
import request from '@/request';
import { DataDictVO, DataDictDetailVO } from '@/models';
import { TemplateDictVO, TemplateDictDetailVO, TemplateDictPageReqVO, PageData } from '@/models';

const BASE_URL = '/admin-api/system/dict';
const BASE_URL = '/admin-api/template/dict';

export default {

createDictApi: (data: DataDictVO) => {
//获取字典分页
getDictPageApi: (params: TemplateDictPageReqVO) => {
return request.get<PageData<TemplateDictVO>>(`${BASE_URL}/page`, { params });
},

//获取字典详情
getDictDetailApi: (id: number) => {
return request.get(`${BASE_URL}/get?id=${id}`);
},

//获取字典列表
getDictListApi: (ids: string[]) => {
return request.get(`${BASE_URL}/list?ids=${ids.join(",")}`);
},

createDictApi: (data: TemplateDictVO) => {
return request.post(`${BASE_URL}/create`, data);
},

updateDictApi: (data: DataDictVO) => {
updateDictApi: (data: TemplateDictVO) => {
return request.put(`${BASE_URL}/update`, data);
},

@@ -17,7 +32,7 @@ export default {
return request.delete(`${BASE_URL}/delete?id=${id}`);
},

updateDictDetailApi: (data: DataDictDetailVO) => {
updateDictDetailApi: (data: TemplateDictDetailVO) => {
return request.put(`${BASE_URL}/update`, data);
},



Завантаження…
Відмінити
Зберегти