Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
admin
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
887936cc
authored
Sep 30, 2024
by
YunaiV
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'dev' of
https://gitee.com/yudaocode/yudao-ui-admin-vue3
parents
fd907ddf
4a2eddbd
Hide whitespace changes
Inline
Side-by-side
Showing
47 changed files
with
2530 additions
and
290 deletions
+2530
-290
.env.dev
+0
-2
.env.local
+0
-2
.env.prod
+0
-2
.env.stage
+0
-2
.env.test
+0
-2
.vscode/settings.json
+1
-1
src/api/login/index.ts
+6
-1
src/api/login/types.ts
+7
-0
src/api/mall/promotion/point/index.ts
+91
-0
src/api/mall/promotion/reward/rewardActivity.ts
+6
-1
src/api/pay/order/index.ts
+8
-2
src/components/AppLinkInput/data.ts
+8
-0
src/components/DiyEditor/components/mobile/ProductCard/index.vue
+1
-1
src/components/DiyEditor/components/mobile/PromotionPoint/config.ts
+96
-0
src/components/DiyEditor/components/mobile/PromotionPoint/index.vue
+202
-0
src/components/DiyEditor/components/mobile/PromotionPoint/property.vue
+154
-0
src/components/Editor/src/Editor.vue
+3
-2
src/components/UploadFile/src/UploadImgs.vue
+9
-14
src/components/UploadFile/src/useUpload.ts
+20
-11
src/utils/dict.ts
+0
-1
src/views/Login/components/RegisterForm.vue
+251
-114
src/views/knowledge/dataset-form/form-step1.vue
+151
-0
src/views/knowledge/dataset-form/form-step2.vue
+168
-0
src/views/knowledge/dataset.vue
+152
-0
src/views/mall/product/property/value/index.vue
+1
-1
src/views/mall/product/spu/components/SkuList.vue
+5
-5
src/views/mall/product/spu/form/InfoForm.vue
+1
-1
src/views/mall/promotion/combination/activity/index.vue
+44
-40
src/views/mall/promotion/components/SpuAndSkuList.vue
+9
-9
src/views/mall/promotion/coupon/components/CouponSelect.vue
+2
-1
src/views/mall/promotion/coupon/formatter.ts
+18
-3
src/views/mall/promotion/coupon/template/CouponTemplateForm.vue
+4
-1
src/views/mall/promotion/coupon/template/index.vue
+7
-1
src/views/mall/promotion/discountActivity/DiscountActivityForm.vue
+69
-33
src/views/mall/promotion/discountActivity/discountActivity.data.ts
+11
-2
src/views/mall/promotion/point/activity/PointActivityForm.vue
+227
-0
src/views/mall/promotion/point/activity/index.vue
+219
-0
src/views/mall/promotion/point/activity/pointActivity.data.ts
+55
-0
src/views/mall/promotion/point/components/PointShowcase.vue
+154
-0
src/views/mall/promotion/point/components/PointTableSelect.vue
+300
-0
src/views/mall/promotion/rewardActivity/RewardForm.vue
+8
-2
src/views/mall/promotion/rewardActivity/components/RewardRule.vue
+12
-1
src/views/mall/promotion/rewardActivity/index.vue
+32
-5
src/views/mp/components/wx-account-select/main.vue
+11
-0
src/views/mp/statistics/index.vue
+6
-25
src/views/pay/cashier/index.vue
+1
-1
types/env.d.ts
+0
-1
No files found.
.env.dev
View file @
887936cc
...
...
@@ -8,8 +8,6 @@ VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server
# 上传路径
VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
# 接口地址
VITE_API_URL=/admin-api
...
...
.env.local
View file @
887936cc
...
...
@@ -8,8 +8,6 @@ VITE_BASE_URL='http://localhost:48080'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
VITE_UPLOAD_TYPE=server
# 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
# 接口地址
VITE_API_URL=/admin-api
...
...
.env.prod
View file @
887936cc
...
...
@@ -8,8 +8,6 @@ VITE_BASE_URL='http://localhost:48080'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server
# 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
# 接口地址
VITE_API_URL=/admin-api
...
...
.env.stage
View file @
887936cc
...
...
@@ -8,8 +8,6 @@ VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server
# 上传路径
VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
# 接口地址
VITE_API_URL=/admin-api
...
...
.env.test
View file @
887936cc
...
...
@@ -8,8 +8,6 @@ VITE_BASE_URL='http://localhost:48080'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE
=
server
# 上传路径
VITE_UPLOAD_URL
=
'http://localhost:48080/admin-api/infra/file/upload'
# 接口地址
VITE_API_URL
=/
admin
-
api
...
...
.vscode/settings.json
View file @
887936cc
...
...
@@ -87,7 +87,7 @@
"source.fixAll.stylelint"
:
"explicit"
},
"[vue]"
:
{
"editor.defaultFormatter"
:
"
rvest.vs-code-prettier-eslint
"
"editor.defaultFormatter"
:
"
esbenp.prettier-vscode
"
},
"i18n-ally.localesPaths"
:
[
"src/locales"
],
"i18n-ally.keystyle"
:
"nested"
,
...
...
src/api/login/index.ts
View file @
887936cc
import
request
from
'@/config/axios'
import
{
getRefreshToken
}
from
'@/utils/auth'
import
type
{
UserLoginVO
}
from
'./types'
import
type
{
RegisterVO
,
UserLoginVO
}
from
'./types'
export
interface
SmsCodeVO
{
mobile
:
string
...
...
@@ -17,6 +17,11 @@ export const login = (data: UserLoginVO) => {
return
request
.
post
({
url
:
'/system/auth/login'
,
data
})
}
// 注册
export
const
register
=
(
data
:
RegisterVO
)
=>
{
return
request
.
post
({
url
:
'/system/auth/register'
,
data
})
}
// 刷新访问令牌
export
const
refreshToken
=
()
=>
{
return
request
.
post
({
url
:
'/system/auth/refresh-token?refreshToken='
+
getRefreshToken
()
})
...
...
src/api/login/types.ts
View file @
887936cc
...
...
@@ -29,3 +29,10 @@ export type UserVO = {
loginIp
:
string
loginDate
:
string
}
export
type
RegisterVO
=
{
tenantName
:
string
username
:
string
password
:
string
captchaVerification
:
string
}
src/api/mall/promotion/point/index.ts
0 → 100644
View file @
887936cc
import
request
from
'@/config/axios'
import
{
Sku
,
Spu
}
from
'@/api/mall/product/spu'
// 积分商城活动 VO
// 积分商城活动 VO
export
interface
PointActivityVO
{
id
:
number
// 积分商城活动编号
spuId
:
number
// 积分商城活动商品
status
:
number
// 活动状态
stock
:
number
// 积分商城活动库存
totalStock
:
number
// 积分商城活动总库存
remark
?:
string
// 备注
sort
:
number
// 排序
createTime
:
string
// 创建时间
products
:
PointProductVO
[]
// 积分商城商品
// ========== 商品字段 ==========
spuName
:
string
// 商品名称
picUrl
:
string
// 商品主图
marketPrice
:
number
// 商品市场价,单位:分
//======================= 显示所需兑换积分最少的 sku 信息 =======================
point
:
number
// 兑换积分
price
:
number
// 兑换金额,单位:分
}
// 秒杀活动所需属性
export
interface
PointProductVO
{
id
?:
number
// 积分商城商品编号
activityId
?:
number
// 积分商城活动 id
spuId
?:
number
// 商品 SPU 编号
skuId
:
number
// 商品 SKU 编号
count
:
number
// 可兑换数量
point
:
number
// 兑换积分
price
:
number
// 兑换金额,单位:分
stock
:
number
// 积分商城商品库存
activityStatus
?:
number
// 积分商城商品状态
}
// 扩展 Sku 配置
export
type
SkuExtension
=
Sku
&
{
productConfig
:
PointProductVO
}
export
interface
SpuExtension
extends
Spu
{
skus
:
SkuExtension
[]
// 重写类型
}
export
interface
SpuExtension0
extends
Spu
{
pointStock
:
number
// 积分商城活动库存
pointTotalStock
:
number
// 积分商城活动总库存
point
:
number
// 兑换积分
pointPrice
:
number
// 兑换金额,单位:分
}
// 积分商城活动 API
export
const
PointActivityApi
=
{
// 查询积分商城活动分页
getPointActivityPage
:
async
(
params
:
any
)
=>
{
return
await
request
.
get
({
url
:
`/promotion/point-activity/page`
,
params
})
},
// 查询积分商城活动详情
getPointActivity
:
async
(
id
:
number
)
=>
{
return
await
request
.
get
({
url
:
`/promotion/point-activity/get?id=`
+
id
})
},
// 查询积分商城活动列表,基于活动编号数组
getPointActivityListByIds
:
async
(
ids
:
number
[])
=>
{
return
request
.
get
({
url
:
`/promotion/point-activity/list-by-ids?ids=
${
ids
}
`
})
},
// 新增积分商城活动
createPointActivity
:
async
(
data
:
PointActivityVO
)
=>
{
return
await
request
.
post
({
url
:
`/promotion/point-activity/create`
,
data
})
},
// 修改积分商城活动
updatePointActivity
:
async
(
data
:
PointActivityVO
)
=>
{
return
await
request
.
put
({
url
:
`/promotion/point-activity/update`
,
data
})
},
// 删除积分商城活动
deletePointActivity
:
async
(
id
:
number
)
=>
{
return
await
request
.
delete
({
url
:
`/promotion/point-activity/delete?id=`
+
id
})
},
// 关闭秒杀活动
closePointActivity
:
async
(
id
:
number
)
=>
{
return
await
request
.
put
({
url
:
'/promotion/point-activity/close?id='
+
id
})
}
}
src/api/mall/promotion/reward/rewardActivity.ts
View file @
887936cc
...
...
@@ -47,7 +47,12 @@ export const getReward = async (id: number) => {
return
await
request
.
get
({
url
:
'/promotion/reward-activity/get?id='
+
id
})
}
// 删除
限时折扣
活动
// 删除
满减送
活动
export
const
deleteRewardActivity
=
async
(
id
:
number
)
=>
{
return
await
request
.
delete
({
url
:
'/promotion/reward-activity/delete?id='
+
id
})
}
// 关闭满减送活动
export
const
closeRewardActivity
=
async
(
id
:
number
)
=>
{
return
await
request
.
put
({
url
:
'/promotion/reward-activity/close?id='
+
id
})
}
src/api/pay/order/index.ts
View file @
887936cc
...
...
@@ -84,8 +84,14 @@ export const getOrderPage = async (params: OrderPageReqVO) => {
}
// 查询详情支付订单
export
const
getOrder
=
async
(
id
:
number
)
=>
{
return
await
request
.
get
({
url
:
'/pay/order/get?id='
+
id
})
export
const
getOrder
=
async
(
id
:
number
,
sync
?:
boolean
)
=>
{
return
await
request
.
get
({
url
:
'/pay/order/get'
,
params
:
{
id
,
sync
}
})
}
// 获得支付订单的明细
...
...
src/components/AppLinkInput/data.ts
View file @
887936cc
...
...
@@ -5,6 +5,7 @@ export interface AppLinkGroup {
// 链接列表
links
:
AppLink
[]
}
// APP 链接
export
interface
AppLink
{
// 链接名称
...
...
@@ -21,6 +22,8 @@ export const enum APP_LINK_TYPE_ENUM {
ACTIVITY_COMBINATION
,
// 秒杀活动
ACTIVITY_SECKILL
,
// 积分商城活动
ACTIVITY_POINT
,
// 文章详情
ARTICLE_DETAIL
,
// 优惠券详情
...
...
@@ -131,6 +134,11 @@ export const APP_LINK_GROUP_LIST = [
type
:
APP_LINK_TYPE_ENUM
.
ACTIVITY_SECKILL
},
{
name
:
'积分商城活动'
,
path
:
'/pages/activity/point/list'
,
type
:
APP_LINK_TYPE_ENUM
.
ACTIVITY_POINT
},
{
name
:
'签到中心'
,
path
:
'/pages/app/sign'
},
...
...
src/components/DiyEditor/components/mobile/ProductCard/index.vue
View file @
887936cc
...
...
@@ -67,7 +67,7 @@
class=
"text-16px"
:style=
"
{ color: property.fields.price.color }"
>
¥
{{
fenToYuan
(
spu
.
price
)
}}
¥
{{
fenToYuan
(
spu
.
price
as
any
)
}}
</span>
<!-- 市场价 -->
<span
...
...
src/components/DiyEditor/components/mobile/PromotionPoint/config.ts
0 → 100644
View file @
887936cc
import
{
ComponentStyle
,
DiyComponent
}
from
'@/components/DiyEditor/util'
/** 积分商城属性 */
export
interface
PromotionPointProperty
{
// 布局类型:单列 | 三列
layoutType
:
'oneColBigImg'
|
'oneColSmallImg'
|
'twoCol'
// 商品字段
fields
:
{
// 商品名称
name
:
PromotionPointFieldProperty
// 商品简介
introduction
:
PromotionPointFieldProperty
// 商品价格
price
:
PromotionPointFieldProperty
// 市场价
marketPrice
:
PromotionPointFieldProperty
// 商品销量
salesCount
:
PromotionPointFieldProperty
// 商品库存
stock
:
PromotionPointFieldProperty
}
// 角标
badge
:
{
// 是否显示
show
:
boolean
// 角标图片
imgUrl
:
string
}
// 按钮
btnBuy
:
{
// 类型:文字 | 图片
type
:
'text'
|
'img'
// 文字
text
:
string
// 文字按钮:背景渐变起始颜色
bgBeginColor
:
string
// 文字按钮:背景渐变结束颜色
bgEndColor
:
string
// 图片按钮:图片地址
imgUrl
:
string
}
// 上圆角
borderRadiusTop
:
number
// 下圆角
borderRadiusBottom
:
number
// 间距
space
:
number
// 秒杀活动编号
activityIds
:
number
[]
// 组件样式
style
:
ComponentStyle
}
// 商品字段
export
interface
PromotionPointFieldProperty
{
// 是否显示
show
:
boolean
// 颜色
color
:
string
}
// 定义组件
export
const
component
=
{
id
:
'PromotionPoint'
,
name
:
'积分商城'
,
icon
:
'ep:present'
,
property
:
{
layoutType
:
'oneColBigImg'
,
fields
:
{
name
:
{
show
:
true
,
color
:
'#000'
},
introduction
:
{
show
:
true
,
color
:
'#999'
},
price
:
{
show
:
true
,
color
:
'#ff3000'
},
marketPrice
:
{
show
:
true
,
color
:
'#c4c4c4'
},
salesCount
:
{
show
:
true
,
color
:
'#c4c4c4'
},
stock
:
{
show
:
false
,
color
:
'#c4c4c4'
}
},
badge
:
{
show
:
false
,
imgUrl
:
''
},
btnBuy
:
{
type
:
'text'
,
text
:
'立即兑换'
,
bgBeginColor
:
'#FF6000'
,
bgEndColor
:
'#FE832A'
,
imgUrl
:
''
},
borderRadiusTop
:
8
,
borderRadiusBottom
:
8
,
space
:
8
,
style
:
{
bgType
:
'color'
,
bgColor
:
''
,
marginLeft
:
8
,
marginRight
:
8
,
marginBottom
:
8
}
as
ComponentStyle
}
}
as
DiyComponent
<
PromotionPointProperty
>
src/components/DiyEditor/components/mobile/PromotionPoint/index.vue
0 → 100644
View file @
887936cc
<
template
>
<div
ref=
"containerRef"
:class=
"`box-content min-h-30px w-full flex flex-row flex-wrap`"
>
<div
v-for=
"(spu, index) in spuList"
:key=
"index"
:style=
"
{
...calculateSpace(index),
...calculateWidth(),
borderTopLeftRadius: `${property.borderRadiusTop}px`,
borderTopRightRadius: `${property.borderRadiusTop}px`,
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
borderBottomRightRadius: `${property.borderRadiusBottom}px`
}"
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
>
<!-- 角标 -->
<div
v-if=
"property.badge.show"
class=
"absolute left-0 top-0 z-1 items-center justify-center"
>
<el-image
:src=
"property.badge.imgUrl"
class=
"h-26px w-38px"
fit=
"cover"
/>
</div>
<!-- 商品封面图 -->
<div
:class=
"[
'h-140px',
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-140px': property.layoutType === 'oneColSmallImg'
}
]"
>
<el-image
:src=
"spu.picUrl"
class=
"h-full w-full"
fit=
"cover"
/>
</div>
<div
:class=
"[
' flex flex-col gap-8px p-8px box-border',
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg'
}
]"
>
<!-- 商品名称 -->
<div
v-if=
"property.fields.name.show"
:class=
"[
'text-14px ',
{
truncate: property.layoutType !== 'oneColSmallImg',
'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
}
]"
:style="{ color: property.fields.name.color }"
>
{{
spu
.
name
}}
</div>
<!-- 商品简介 -->
<div
v-if=
"property.fields.introduction.show"
:style=
"
{ color: property.fields.introduction.color }"
class="truncate text-12px"
>
{{
spu
.
introduction
}}
</div>
<div>
<!-- 积分 -->
<span
v-if=
"property.fields.price.show"
:style=
"
{ color: property.fields.price.color }"
class="text-16px"
>
{{
spu
.
point
}}
积分
{{
!
spu
.
pointPrice
||
spu
.
pointPrice
===
0
?
''
:
`+${fenToYuan(spu.pointPrice)
}
元`
}}
<
/span
>
<!--
市场价
-->
<
span
v
-
if
=
"property.fields.marketPrice.show && spu.marketPrice"
:
style
=
"
{
color
:
property
.
fields
.
marketPrice
.
color
}
"
class="
ml
-
4
px
text
-
10
px
line
-
through
"
>
¥
{{
fenToYuan
(
spu
.
marketPrice
)
}}
<
/span
>
<
/div
>
<
div
class
=
"text-12px"
>
<!--
销量
-->
<
span
v
-
if
=
"property.fields.salesCount.show"
:
style
=
"
{
color
:
property
.
fields
.
salesCount
.
color
}"
>
已兑
{{
(
spu
.
pointTotalStock
||
0
)
-
(
spu
.
pointStock
||
0
)
}}
件
<
/span
>
<!--
库存
-->
<
span
v
-
if
=
"property.fields.stock.show"
:
style
=
"
{
color
:
property
.
fields
.
stock
.
color
}"
>
库存
{{
spu
.
pointTotalStock
||
0
}}
<
/span
>
<
/div
>
<
/div
>
<!--
购买按钮
-->
<
div
class
=
"absolute bottom-8px right-8px"
>
<!--
文字按钮
-->
<
span
v
-
if
=
"property.btnBuy.type === 'text'"
:
style
=
"
{
background
:
`linear-gradient(to right, ${property.btnBuy.bgBeginColor
}
, ${property.btnBuy.bgEndColor
}
`
}
"
class="
rounded
-
full
p
-
x
-
12
px
p
-
y
-
4
px
text
-
12
px
text
-
white
"
>
{{
property
.
btnBuy
.
text
}}
<
/span
>
<!--
图片按钮
-->
<
el
-
image
v
-
else
:
src
=
"property.btnBuy.imgUrl"
class
=
"h-28px w-28px rounded-full"
fit
=
"cover"
/>
<
/div
>
<
/div
>
<
/div
>
<
/template
>
<
script
lang
=
"ts"
setup
>
import
{
PromotionPointProperty
}
from
'./config'
import
*
as
ProductSpuApi
from
'@/api/mall/product/spu'
import
{
PointActivityApi
,
PointActivityVO
,
SpuExtension0
}
from
'@/api/mall/promotion/point'
import
{
fenToYuan
}
from
'@/utils'
/** 积分商城卡片 */
defineOptions
({
name
:
'PromotionPoint'
}
)
// 定义属性
const
props
=
defineProps
<
{
property
:
PromotionPointProperty
}
>
()
// 商品列表
const
spuList
=
ref
<
SpuExtension0
[]
>
([])
const
spuIdList
=
ref
<
number
[]
>
([])
const
pointActivityList
=
ref
<
PointActivityVO
[]
>
([])
watch
(
()
=>
props
.
property
.
activityIds
,
async
()
=>
{
try
{
// 新添加的积分商城组件,是没有活动ID的
const
activityIds
=
props
.
property
.
activityIds
// 检查活动ID的有效性
if
(
Array
.
isArray
(
activityIds
)
&&
activityIds
.
length
>
0
)
{
// 获取积分商城活动详情列表
pointActivityList
.
value
=
await
PointActivityApi
.
getPointActivityListByIds
(
activityIds
)
// 获取积分商城活动的 SPU 详情列表
spuList
.
value
=
[]
spuIdList
.
value
=
pointActivityList
.
value
.
map
((
activity
)
=>
activity
.
spuId
)
if
(
spuIdList
.
value
.
length
>
0
)
{
spuList
.
value
=
await
ProductSpuApi
.
getSpuDetailList
(
spuIdList
.
value
)
}
// 更新 SPU 的最低兑换积分和所需兑换金额
pointActivityList
.
value
.
forEach
((
activity
)
=>
{
// 匹配spuId
const
spu
=
spuList
.
value
.
find
((
spu
)
=>
spu
.
id
===
activity
.
spuId
)
if
(
spu
)
{
spu
.
pointStock
=
activity
.
stock
spu
.
pointTotalStock
=
activity
.
totalStock
spu
.
point
=
activity
.
point
spu
.
pointPrice
=
activity
.
price
}
}
)
}
}
catch
(
error
)
{
console
.
error
(
'获取积分商城活动细节或 SPU 细节时出错:'
,
error
)
}
}
,
{
immediate
:
true
,
deep
:
true
}
)
/**
* 计算商品的间距
* @param index 商品索引
*/
const
calculateSpace
=
(
index
:
number
)
=>
{
// 商品的列数
const
columns
=
props
.
property
.
layoutType
===
'twoCol'
?
2
:
1
// 第一列没有左边距
const
marginLeft
=
index
%
columns
===
0
?
'0'
:
props
.
property
.
space
+
'px'
// 第一行没有上边距
const
marginTop
=
index
<
columns
?
'0'
:
props
.
property
.
space
+
'px'
return
{
marginLeft
,
marginTop
}
}
// 容器
const
containerRef
=
ref
()
// 计算商品的宽度
const
calculateWidth
=
()
=>
{
let
width
=
'100%'
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
if
(
props
.
property
.
layoutType
===
'twoCol'
)
{
width
=
`${(containerRef.value.offsetWidth - props.property.space) / 2
}
px`
}
return
{
width
}
}
<
/script
>
<
style
lang
=
"scss"
scoped
><
/style
>
src/components/DiyEditor/components/mobile/PromotionPoint/property.vue
0 → 100644
View file @
887936cc
<
template
>
<ComponentContainerProperty
v-model=
"formData.style"
>
<el-form
:model=
"formData"
label-width=
"80px"
>
<el-card
class=
"property-group"
header=
"积分商城活动"
shadow=
"never"
>
<PointShowcase
v-model=
"formData.activityIds"
/>
</el-card>
<el-card
class=
"property-group"
header=
"商品样式"
shadow=
"never"
>
<el-form-item
label=
"布局"
prop=
"type"
>
<el-radio-group
v-model=
"formData.layoutType"
>
<el-tooltip
class=
"item"
content=
"单列大图"
placement=
"bottom"
>
<el-radio-button
value=
"oneColBigImg"
>
<Icon
icon=
"fluent:text-column-one-24-filled"
/>
</el-radio-button>
</el-tooltip>
<el-tooltip
class=
"item"
content=
"单列小图"
placement=
"bottom"
>
<el-radio-button
value=
"oneColSmallImg"
>
<Icon
icon=
"fluent:text-column-two-left-24-filled"
/>
</el-radio-button>
</el-tooltip>
<el-tooltip
class=
"item"
content=
"双列"
placement=
"bottom"
>
<el-radio-button
value=
"twoCol"
>
<Icon
icon=
"fluent:text-column-two-24-filled"
/>
</el-radio-button>
</el-tooltip>
<!--
<el-tooltip
class=
"item"
content=
"三列"
placement=
"bottom"
>
<el-radio-button
value=
"threeCol"
>
<Icon
icon=
"fluent:text-column-three-24-filled"
/>
</el-radio-button>
</el-tooltip>
-->
</el-radio-group>
</el-form-item>
<el-form-item
label=
"商品名称"
prop=
"fields.name.show"
>
<div
class=
"flex gap-8px"
>
<ColorInput
v-model=
"formData.fields.name.color"
/>
<el-checkbox
v-model=
"formData.fields.name.show"
/>
</div>
</el-form-item>
<el-form-item
label=
"商品简介"
prop=
"fields.introduction.show"
>
<div
class=
"flex gap-8px"
>
<ColorInput
v-model=
"formData.fields.introduction.color"
/>
<el-checkbox
v-model=
"formData.fields.introduction.show"
/>
</div>
</el-form-item>
<el-form-item
label=
"商品价格"
prop=
"fields.price.show"
>
<div
class=
"flex gap-8px"
>
<ColorInput
v-model=
"formData.fields.price.color"
/>
<el-checkbox
v-model=
"formData.fields.price.show"
/>
</div>
</el-form-item>
<el-form-item
label=
"市场价"
prop=
"fields.marketPrice.show"
>
<div
class=
"flex gap-8px"
>
<ColorInput
v-model=
"formData.fields.marketPrice.color"
/>
<el-checkbox
v-model=
"formData.fields.marketPrice.show"
/>
</div>
</el-form-item>
<el-form-item
label=
"商品销量"
prop=
"fields.salesCount.show"
>
<div
class=
"flex gap-8px"
>
<ColorInput
v-model=
"formData.fields.salesCount.color"
/>
<el-checkbox
v-model=
"formData.fields.salesCount.show"
/>
</div>
</el-form-item>
<el-form-item
label=
"商品库存"
prop=
"fields.stock.show"
>
<div
class=
"flex gap-8px"
>
<ColorInput
v-model=
"formData.fields.stock.color"
/>
<el-checkbox
v-model=
"formData.fields.stock.show"
/>
</div>
</el-form-item>
</el-card>
<el-card
class=
"property-group"
header=
"角标"
shadow=
"never"
>
<el-form-item
label=
"角标"
prop=
"badge.show"
>
<el-switch
v-model=
"formData.badge.show"
/>
</el-form-item>
<el-form-item
v-if=
"formData.badge.show"
label=
"角标"
prop=
"badge.imgUrl"
>
<UploadImg
v-model=
"formData.badge.imgUrl"
height=
"44px"
width=
"72px"
>
<template
#
tip
>
建议尺寸:36 * 22
</
template
>
</UploadImg>
</el-form-item>
</el-card>
<el-card
class=
"property-group"
header=
"按钮"
shadow=
"never"
>
<el-form-item
label=
"按钮类型"
prop=
"btnBuy.type"
>
<el-radio-group
v-model=
"formData.btnBuy.type"
>
<el-radio-button
value=
"text"
>
文字
</el-radio-button>
<el-radio-button
value=
"img"
>
图片
</el-radio-button>
</el-radio-group>
</el-form-item>
<
template
v-if=
"formData.btnBuy.type === 'text'"
>
<el-form-item
label=
"按钮文字"
prop=
"btnBuy.text"
>
<el-input
v-model=
"formData.btnBuy.text"
/>
</el-form-item>
<el-form-item
label=
"左侧背景"
prop=
"btnBuy.bgBeginColor"
>
<ColorInput
v-model=
"formData.btnBuy.bgBeginColor"
/>
</el-form-item>
<el-form-item
label=
"右侧背景"
prop=
"btnBuy.bgEndColor"
>
<ColorInput
v-model=
"formData.btnBuy.bgEndColor"
/>
</el-form-item>
</
template
>
<
template
v-else
>
<el-form-item
label=
"图片"
prop=
"btnBuy.imgUrl"
>
<UploadImg
v-model=
"formData.btnBuy.imgUrl"
height=
"56px"
width=
"56px"
>
<template
#
tip
>
建议尺寸:56 * 56
</
template
>
</UploadImg>
</el-form-item>
</template>
</el-card>
<el-card
class=
"property-group"
header=
"商品样式"
shadow=
"never"
>
<el-form-item
label=
"上圆角"
prop=
"borderRadiusTop"
>
<el-slider
v-model=
"formData.borderRadiusTop"
:max=
"100"
:min=
"0"
:show-input-controls=
"false"
input-size=
"small"
show-input
/>
</el-form-item>
<el-form-item
label=
"下圆角"
prop=
"borderRadiusBottom"
>
<el-slider
v-model=
"formData.borderRadiusBottom"
:max=
"100"
:min=
"0"
:show-input-controls=
"false"
input-size=
"small"
show-input
/>
</el-form-item>
<el-form-item
label=
"间隔"
prop=
"space"
>
<el-slider
v-model=
"formData.space"
:max=
"100"
:min=
"0"
:show-input-controls=
"false"
input-size=
"small"
show-input
/>
</el-form-item>
</el-card>
</el-form>
</ComponentContainerProperty>
</template>
<
script
lang=
"ts"
setup
>
import
{
PromotionPointProperty
}
from
'./config'
import
{
usePropertyForm
}
from
'@/components/DiyEditor/util'
import
PointShowcase
from
'@/views/mall/promotion/point/components/PointShowcase.vue'
// 秒杀属性面板
defineOptions
({
name
:
'PromotionPointProperty'
})
const
props
=
defineProps
<
{
modelValue
:
PromotionPointProperty
}
>
()
const
emit
=
defineEmits
([
'update:modelValue'
])
const
{
formData
}
=
usePropertyForm
(
props
.
modelValue
,
emit
)
</
script
>
<
style
lang=
"scss"
scoped
></
style
>
src/components/Editor/src/Editor.vue
View file @
887936cc
...
...
@@ -7,6 +7,7 @@ import { isNumber } from '@/utils/is'
import
{
ElMessage
}
from
'element-plus'
import
{
useLocaleStore
}
from
'@/store/modules/locale'
import
{
getAccessToken
,
getTenantId
}
from
'@/utils/auth'
import
{
getUploadUrl
}
from
'@/components/UploadFile/src/useUpload'
defineOptions
({
name
:
'Editor'
})
...
...
@@ -88,7 +89,7 @@ const editorConfig = computed((): IEditorConfig => {
scroll
:
true
,
MENU_CONF
:
{
[
'uploadImage'
]:
{
server
:
import
.
meta
.
env
.
VITE_UPLOAD_URL
,
server
:
getUploadUrl
()
,
// 单个文件的最大体积限制,默认为 2M
maxFileSize
:
5
*
1024
*
1024
,
// 最多可上传几个文件,默认为 100
...
...
@@ -136,7 +137,7 @@ const editorConfig = computed((): IEditorConfig => {
}
},
[
'uploadVideo'
]:
{
server
:
import
.
meta
.
env
.
VITE_UPLOAD_URL
,
server
:
getUploadUrl
()
,
// 单个文件的最大体积限制,默认为 10M
maxFileSize
:
10
*
1024
*
1024
,
// 最多可上传几个文件,默认为 100
...
...
src/components/UploadFile/src/UploadImgs.vue
View file @
887936cc
...
...
@@ -25,7 +25,7 @@
<template
#
file=
"
{ file }">
<img
:src=
"file.url"
class=
"upload-image"
/>
<div
class=
"upload-handle"
@
click
.
stop
>
<div
class=
"handle-icon"
@
click=
"
handlePictureCardPreview(file
)"
>
<div
class=
"handle-icon"
@
click=
"
imagePreview(file.url!
)"
>
<Icon
icon=
"ep:zoom-in"
/>
<span>
查看
</span>
</div>
...
...
@@ -39,16 +39,12 @@
<div
class=
"el-upload__tip"
>
<slot
name=
"tip"
></slot>
</div>
<el-image-viewer
v-if=
"imgViewVisible"
:url-list=
"[viewImageUrl]"
@
close=
"imgViewVisible = false"
/>
</div>
</template>
<
script
lang=
"ts"
setup
>
import
type
{
UploadFile
,
UploadProps
,
UploadUserFile
}
from
'element-plus'
import
{
ElNotification
}
from
'element-plus'
import
{
createImageViewer
}
from
'@/components/ImageViewer'
import
{
propTypes
}
from
'@/utils/propTypes'
import
{
useUpload
}
from
'@/components/UploadFile/src/useUpload'
...
...
@@ -56,6 +52,13 @@ import { useUpload } from '@/components/UploadFile/src/useUpload'
defineOptions
({
name
:
'UploadImgs'
})
const
message
=
useMessage
()
// 消息弹窗
// 查看图片
const
imagePreview
=
(
imgUrl
:
string
)
=>
{
createImageViewer
({
zIndex
:
9999999
,
urlList
:
[
imgUrl
]
})
}
type
FileTypes
=
|
'image/apng'
...
...
@@ -178,14 +181,6 @@ const handleExceed = () => {
type
:
'warning'
})
}
// 图片预览
const
viewImageUrl
=
ref
(
''
)
const
imgViewVisible
=
ref
(
false
)
const
handlePictureCardPreview
:
UploadProps
[
'onPreview'
]
=
(
uploadFile
)
=>
{
viewImageUrl
.
value
=
uploadFile
.
url
!
imgViewVisible
.
value
=
true
}
</
script
>
<
style
lang=
"scss"
scoped
>
...
...
src/components/UploadFile/src/useUpload.ts
View file @
887936cc
...
...
@@ -3,9 +3,16 @@ import CryptoJS from 'crypto-js'
import
{
UploadRawFile
,
UploadRequestOptions
}
from
'element-plus/es/components/upload/src/upload'
import
axios
from
'axios'
/**
* 获得上传 URL
*/
export
const
getUploadUrl
=
():
string
=>
{
return
import
.
meta
.
env
.
VITE_BASE_URL
+
import
.
meta
.
env
.
VITE_API_URL
+
'/infra/file/upload'
}
export
const
useUpload
=
()
=>
{
// 后端上传地址
const
uploadUrl
=
import
.
meta
.
env
.
VITE_UPLOAD_URL
const
uploadUrl
=
getUploadUrl
()
// 是否使用前端直连上传
const
isClientUpload
=
UPLOAD_TYPE
.
CLIENT
===
import
.
meta
.
env
.
VITE_UPLOAD_TYPE
// 重写ElUpload上传方法
...
...
@@ -17,16 +24,18 @@ export const useUpload = () => {
// 1.2 获取文件预签名地址
const
presignedInfo
=
await
FileApi
.
getFilePresignedUrl
(
fileName
)
// 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传,Minio 不支持)
return
axios
.
put
(
presignedInfo
.
uploadUrl
,
options
.
file
,
{
headers
:
{
'Content-Type'
:
options
.
file
.
type
,
}
}).
then
(()
=>
{
// 1.4. 记录文件信息到后端(异步)
createFile
(
presignedInfo
,
fileName
,
options
.
file
)
// 通知成功,数据格式保持与后端上传的返回结果一致
return
{
data
:
presignedInfo
.
url
}
})
return
axios
.
put
(
presignedInfo
.
uploadUrl
,
options
.
file
,
{
headers
:
{
'Content-Type'
:
options
.
file
.
type
}
})
.
then
(()
=>
{
// 1.4. 记录文件信息到后端(异步)
createFile
(
presignedInfo
,
fileName
,
options
.
file
)
// 通知成功,数据格式保持与后端上传的返回结果一致
return
{
data
:
presignedInfo
.
url
}
})
}
else
{
// 模式二:后端上传
// 重写 el-upload httpRequest 文件上传成功会走成功的钩子,失败走失败的钩子
...
...
src/utils/dict.ts
View file @
887936cc
...
...
@@ -194,7 +194,6 @@ export enum DICT_TYPE {
PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE
=
'promotion_coupon_template_validity_type'
,
// 优惠劵模板的有限期类型
PROMOTION_COUPON_STATUS
=
'promotion_coupon_status'
,
// 优惠劵的状态
PROMOTION_COUPON_TAKE_TYPE
=
'promotion_coupon_take_type'
,
// 优惠劵的领取方式
PROMOTION_ACTIVITY_STATUS
=
'promotion_activity_status'
,
// 优惠活动的状态
PROMOTION_CONDITION_TYPE
=
'promotion_condition_type'
,
// 营销的条件类型枚举
PROMOTION_BARGAIN_RECORD_STATUS
=
'promotion_bargain_record_status'
,
// 砍价记录的状态
PROMOTION_COMBINATION_RECORD_STATUS
=
'promotion_combination_record_status'
,
// 拼团记录的状态
...
...
src/views/Login/components/RegisterForm.vue
View file @
887936cc
<
template
>
<
F
orm
<
el-f
orm
v-show=
"getShow"
:rules=
"rules
"
:
schema=
"schema
"
class=
"w-[100%] dark:(border-1 border-[var(--el-border-color)] border-solid)
"
hide-required-asterisk
ref=
"formLogin
"
:
model=
"registerData.registerForm
"
:rules=
"registerRules
"
class=
"login-form"
label-position=
"top"
label-width=
"120px"
size=
"large"
@
register=
"register"
>
<template
#
title
>
<LoginFormTitle
style=
"width: 100%"
/>
</
template
>
<
template
#
code=
"form"
>
<div
class=
"w-[100%] flex"
>
<el-input
v-model=
"form['code']"
:placeholder=
"t('login.codePlaceholder')"
/>
</div>
</
template
>
<
template
#
register
>
<div
class=
"w-[100%]"
>
<XButton
:loading=
"loading"
:title=
"t('login.register')"
class=
"w-[100%]"
type=
"primary"
@
click=
"loginRegister()"
/>
</div>
<div
class=
"mt-15px w-[100%]"
>
<XButton
:title=
"t('login.hasUser')"
class=
"w-[100%]"
@
click=
"handleBackLogin()"
/>
</div>
</
template
>
</Form>
<el-row
style=
"margin-right: -10px; margin-left: -10px"
>
<el-col
:span=
"24"
style=
"padding-right: 10px; padding-left: 10px"
>
<el-form-item>
<LoginFormTitle
style=
"width: 100%"
/>
</el-form-item>
</el-col>
<el-col
:span=
"24"
style=
"padding-right: 10px; padding-left: 10px"
>
<el-form-item
v-if=
"registerData.tenantEnable === 'true'"
prop=
"tenantName"
>
<el-input
v-model=
"registerData.registerForm.tenantName"
:placeholder=
"t('login.tenantname')"
:prefix-icon=
"iconHouse"
link
type=
"primary"
size=
"large"
/>
</el-form-item>
</el-col>
<el-col
:span=
"24"
style=
"padding-right: 10px; padding-left: 10px"
>
<el-form-item
prop=
"username"
>
<el-input
v-model=
"registerData.registerForm.username"
:placeholder=
"t('login.username')"
size=
"large"
:prefix-icon=
"iconAvatar"
/>
</el-form-item>
</el-col>
<el-col
:span=
"24"
style=
"padding-right: 10px; padding-left: 10px"
>
<el-form-item
prop=
"username"
>
<el-input
v-model=
"registerData.registerForm.nickname"
placeholder=
"昵称"
size=
"large"
:prefix-icon=
"iconAvatar"
/>
</el-form-item>
</el-col>
<el-col
:span=
"24"
style=
"padding-right: 10px; padding-left: 10px"
>
<el-form-item
prop=
"password"
>
<el-input
v-model=
"registerData.registerForm.password"
type=
"password"
auto-complete=
"off"
:placeholder=
"t('login.password')"
size=
"large"
:prefix-icon=
"iconLock"
show-password
/>
</el-form-item>
</el-col>
<el-col
:span=
"24"
style=
"padding-right: 10px; padding-left: 10px"
>
<el-form-item
prop=
"confirmPassword"
>
<el-input
v-model=
"registerData.registerForm.confirmPassword"
type=
"password"
size=
"large"
auto-complete=
"off"
:placeholder=
"t('login.checkPassword')"
:prefix-icon=
"iconLock"
show-password
/>
</el-form-item>
</el-col>
<el-col
:span=
"24"
style=
"padding-right: 10px; padding-left: 10px"
>
<el-form-item>
<XButton
:loading=
"loginLoading"
:title=
"t('login.register')"
class=
"w-[100%]"
type=
"primary"
@
click=
"getCode()"
/>
</el-form-item>
</el-col>
<Verify
ref=
"verify"
:captchaType=
"captchaType"
:imgSize=
"
{ width: '400px', height: '200px' }"
mode="pop"
@success="handleRegister"
/>
</el-row>
<XButton
:title=
"t('login.hasUser')"
class=
"w-[100%]"
@
click=
"handleBackLogin()"
/>
</el-form>
</
template
>
<
script
lang=
"ts"
setup
>
import
type
{
FormRules
}
from
'element-plus'
import
{
useForm
}
from
'@/hooks/web/useForm'
import
{
useValidator
}
from
'@/hooks/web/useValidator'
import
{
ElLoading
}
from
'element-plus'
import
LoginFormTitle
from
'./LoginFormTitle.vue'
import
type
{
RouteLocationNormalizedLoaded
}
from
'vue-router'
import
{
useIcon
}
from
'@/hooks/web/useIcon'
import
*
as
authUtil
from
'@/utils/auth'
import
{
usePermissionStore
}
from
'@/store/modules/permission'
import
*
as
LoginApi
from
'@/api/login'
import
{
LoginStateEnum
,
useLoginState
}
from
'./useLogin'
import
{
FormSchema
}
from
'@/types/form'
defineOptions
({
name
:
'RegisterForm'
})
const
{
t
}
=
useI18n
()
const
{
required
}
=
useValidator
()
const
{
register
,
elFormRef
}
=
useForm
()
const
iconHouse
=
useIcon
({
icon
:
'ep:house'
})
const
iconAvatar
=
useIcon
({
icon
:
'ep:avatar'
})
const
iconLock
=
useIcon
({
icon
:
'ep:lock'
})
const
formLogin
=
ref
()
const
{
handleBackLogin
,
getLoginState
}
=
useLoginState
()
const
{
currentRoute
,
push
}
=
useRouter
()
const
permissionStore
=
usePermissionStore
()
const
redirect
=
ref
<
string
>
(
''
)
const
loginLoading
=
ref
(
false
)
const
verify
=
ref
()
const
captchaType
=
ref
(
'blockPuzzle'
)
// blockPuzzle 滑块 clickWord 点击文字
const
getShow
=
computed
(()
=>
unref
(
getLoginState
)
===
LoginStateEnum
.
REGISTER
)
const
schema
=
reactive
<
FormSchema
[]
>
([
{
field
:
'title'
,
colProps
:
{
span
:
24
const
equalToPassword
=
(
rule
,
value
,
callback
)
=>
{
if
(
registerData
.
registerForm
.
password
!==
value
)
{
callback
(
new
Error
(
'两次输入的密码不一致'
))
}
else
{
callback
()
}
}
const
registerRules
=
{
tenantName
:
[
{
required
:
true
,
trigger
:
'blur'
,
message
:
'请输入您所属的租户'
},
{
min
:
2
,
max
:
20
,
message
:
'租户账号长度必须介于 2 和 20 之间'
,
trigger
:
'blur'
}
],
username
:
[
{
required
:
true
,
trigger
:
'blur'
,
message
:
'请输入您的账号'
},
{
min
:
4
,
max
:
30
,
message
:
'用户账号长度必须介于 4 和 30 之间'
,
trigger
:
'blur'
}
],
nickname
:
[
{
required
:
true
,
trigger
:
'blur'
,
message
:
'请输入您的昵称'
},
{
min
:
0
,
max
:
30
,
message
:
'昵称长度必须介于 0 和 30 之间'
,
trigger
:
'blur'
}
],
password
:
[
{
required
:
true
,
trigger
:
'blur'
,
message
:
'请输入您的密码'
},
{
min
:
5
,
max
:
20
,
message
:
'用户密码长度必须介于 5 和 20 之间'
,
trigger
:
'blur'
},
{
pattern
:
/^
[^
<>"'|
\\]
+$/
,
message
:
'不能包含非法字符:
<
>
"
\
' \\\ |'
,
trigger
:
'blur'
}
],
confirmPassword
:
[
{
required
:
true
,
trigger
:
'blur'
,
message
:
'请再次输入您的密码'
},
{
required
:
true
,
validator
:
equalToPassword
,
trigger
:
'blur'
}
]
}
const
registerData
=
reactive
({
isShowPassword
:
false
,
captchaEnable
:
import
.
meta
.
env
.
VITE_APP_CAPTCHA_ENABLE
,
tenantEnable
:
import
.
meta
.
env
.
VITE_APP_TENANT_ENABLE
,
registerForm
:
{
tenantName
:
import
.
meta
.
env
.
VITE_APP_DEFAULT_LOGIN_TENANT
||
''
,
nickname
:
''
,
tenantId
:
0
,
username
:
''
,
password
:
''
,
confirmPassword
:
''
,
captchaVerification
:
''
}
})
// 提交注册
const
handleRegister
=
async
(
params
:
any
)
=>
{
loading
.
value
=
true
try
{
if
(
registerData
.
tenantEnable
)
{
await
getTenantId
()
registerData
.
registerForm
.
tenantId
=
authUtil
.
getTenantId
()
}
},
{
field
:
'username'
,
label
:
t
(
'login.username'
),
value
:
''
,
component
:
'Input'
,
colProps
:
{
span
:
24
},
componentProps
:
{
placeholder
:
t
(
'login.usernamePlaceholder'
)
if
(
registerData
.
captchaEnable
)
{
registerData
.
registerForm
.
captchaVerification
=
params
.
captchaVerification
}
},
{
field
:
'password'
,
label
:
t
(
'login.password'
),
value
:
''
,
component
:
'InputPassword'
,
colProps
:
{
span
:
24
},
componentProps
:
{
style
:
{
width
:
'100%'
},
strength
:
true
,
placeholder
:
t
(
'login.passwordPlaceholder'
)
const
res
=
await
LoginApi
.
register
(
registerData
.
registerForm
)
if
(
!
res
)
{
return
}
},
{
field
:
'check_password'
,
label
:
t
(
'login.checkPassword'
),
value
:
''
,
component
:
'InputPassword'
,
colProps
:
{
span
:
24
},
componentProps
:
{
style
:
{
width
:
'100%'
},
strength
:
true
,
placeholder
:
t
(
'login.passwordPlaceholder'
)
loading
.
value
=
ElLoading
.
service
({
lock
:
true
,
text
:
'正在加载系统中...'
,
background
:
'rgba(0, 0, 0, 0.7)'
})
authUtil
.
removeLoginForm
()
authUtil
.
setToken
(
res
)
if
(
!
redirect
.
value
)
{
redirect
.
value
=
'/'
}
},
{
field
:
'code'
,
label
:
t
(
'login.code'
),
colProps
:
{
span
:
24
// 判断是否为SSO登录
if
(
redirect
.
value
.
indexOf
(
'sso'
)
!==
-
1
)
{
window
.
location
.
href
=
window
.
location
.
href
.
replace
(
'/login?redirect='
,
''
)
}
else
{
push
({
path
:
redirect
.
value
||
permissionStore
.
addRouters
[
0
].
path
})
}
}
finally
{
loginLoading
.
value
=
false
loading
.
value
.
close
()
}
}
// 获取验证码
const
getCode
=
async
()
=>
{
// 情况一,未开启:则直接注册
if
(
registerData
.
captchaEnable
===
'false'
)
{
await
handleRegister
({})
}
else
{
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行注册
// 弹出验证码
verify
.
value
.
show
()
}
}
// 获取租户 ID
const
getTenantId
=
async
()
=>
{
if
(
registerData
.
tenantEnable
===
'true'
)
{
const
res
=
await
LoginApi
.
getTenantIdByName
(
registerData
.
registerForm
.
tenantName
)
authUtil
.
setTenantId
(
res
)
}
}
// 根据域名,获得租户信息
const
getTenantByWebsite
=
async
()
=>
{
const
website
=
location
.
host
const
res
=
await
LoginApi
.
getTenantByWebsite
(
website
)
if
(
res
)
{
registerData
.
registerForm
.
tenantName
=
res
.
name
authUtil
.
setTenantId
(
res
.
id
)
}
}
const
loading
=
ref
()
// ElLoading.service 返回的实例
watch
(
()
=>
currentRoute
.
value
,
(
route
:
RouteLocationNormalizedLoaded
)
=>
{
redirect
.
value
=
route
?.
query
?.
redirect
as
string
},
{
field
:
'register'
,
colProps
:
{
span
:
24
}
immediate
:
true
}
])
)
onMounted
(()
=>
{
// getCookie()
getTenantByWebsite
()
})
</
script
>
const
rules
:
FormRules
=
{
username
:
[
required
()],
password
:
[
required
()],
check_password
:
[
required
()],
code
:
[
required
()]
<
style
lang=
"scss"
scoped
>
:deep
(
.anticon
)
{
&:hover
{
color
:
var
(
--el-color-primary
)
!important
;
}
}
const
loading
=
ref
(
false
)
const
loginRegister
=
async
()
=>
{
const
formRef
=
unref
(
elFormRef
)
formRef
?.
validate
(
async
(
valid
)
=>
{
if
(
valid
)
{
try
{
loading
.
value
=
true
}
finally
{
loading
.
value
=
false
}
}
})
.login-code
{
float
:
right
;
width
:
100%
;
height
:
38px
;
img
{
width
:
100%
;
height
:
auto
;
max-width
:
100px
;
vertical-align
:
middle
;
cursor
:
pointer
;
}
}
</
s
cript
>
</
s
tyle
>
src/views/knowledge/dataset-form/form-step1.vue
0 → 100644
View file @
887936cc
<
template
>
<div
class=
"upload-container"
>
<!-- 标题 -->
<div
class=
"title"
>
<div>
选择数据源
</div>
</div>
<!-- 数据源选择 -->
<div
class=
"resource-btn"
>
导入已有文本
</div>
<!-- 上传文件区域 -->
<el-form>
<div
class=
"upload-section"
>
<div
class=
"upload-label"
>
上传文本文件
</div>
<el-upload
class=
"upload-area"
action=
"#"
:file-list=
"fileList"
:on-remove=
"handleRemove"
:before-upload=
"beforeUpload"
list-type=
"text"
drag
>
<i
class=
"el-icon-upload"
></i>
<div
class=
"el-upload__text"
>
拖拽文件至此,或者
<em>
选择文件
</em></div>
<div
class=
"el-upload__tip"
>
已支持 TXT、MARKDOWN、PDF、HTML、XLSX、XLS、DOCX、CSV、EML、MSG、PPTX、PPT、XML、EPUB,每个文件不超过 15MB。
</div>
</el-upload>
</div>
<!-- 下一步按钮 -->
<div
class=
"next-button"
>
<el-button
type=
"primary"
:disabled=
"!fileList.length"
>
下一步
</el-button>
</div>
</el-form>
<!-- 知识库创建 -->
<div
class=
"create-knowledge"
>
<el-link
type=
"primary"
underline
>
创建一个空知识库
</el-link>
</div>
</div>
</
template
>
<
script
setup
>
import
{
ref
}
from
'vue'
const
fileList
=
ref
([])
const
handleRemove
=
(
file
,
fileList
)
=>
{
console
.
log
(
file
,
fileList
)
}
const
beforeUpload
=
(
file
)
=>
{
fileList
.
value
.
push
(
file
)
return
false
}
</
script
>
<
style
scoped
lang=
"scss"
>
.upload-container
{
width
:
600px
;
margin
:
0
auto
;
padding
:
20px
;
background-color
:
#fff
;
border-radius
:
8px
;
border
:
1px
solid
#ebebeb
;
}
.title
{
font-size
:
22px
;
font-weight
:
bold
;
}
.resource-btn
{
margin-top
:
20px
;
border-radius
:
10px
;
cursor
:
pointer
;
width
:
150px
;
border
:
1.5px
solid
#528bff
;
padding
:
10px
;
text-align
:
center
;
font-weight
:
500
;
font-size
:
14px
;
line-height
:
30px
;
color
:
#101828
;
}
.upload-section
{
margin
:
20px
0
;
padding-top
:
10px
;
}
.upload-label
{
font-size
:
16px
;
font-weight
:
bold
;
margin-bottom
:
10px
;
color
:
#303133
;
}
.upload-area
{
margin-top
:
10px
;
border
:
1px
dashed
#d9d9d9
;
padding
:
40px
;
text-align
:
center
;
background-color
:
#f5f7fa
;
border-radius
:
8px
;
}
.el-upload__text
em
{
color
:
#409eff
;
cursor
:
pointer
;
}
.el-upload__tip
{
margin-top
:
10px
;
font-size
:
12px
;
color
:
#909399
;
}
.next-button
{
text-align
:
left
;
margin-top
:
20px
;
}
.create-knowledge
{
text-align
:
left
;
margin-top
:
20px
;
}
.el-form-item
{
margin-bottom
:
0
;
}
.source-radio-group
{
display
:
flex
;
justify-content
:
space-between
;
}
.el-radio-button
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
font-size
:
14px
;
padding
:
10px
20px
;
}
.el-radio-button
.el-icon
{
margin-right
:
8px
;
}
</
style
>
src/views/knowledge/dataset-form/form-step2.vue
0 → 100644
View file @
887936cc
<
template
>
<el-row>
<!-- Left Section -->
<el-col
:span=
"12"
>
<el-card>
<!-- 分段设置 -->
<el-form>
<el-form-item
label=
"分段设置"
>
<el-radio-group
v-model=
"segmentSetting"
>
<el-radio
label=
"自动分段与清洗"
>
自动分段与清洗
</el-radio>
<el-radio
label=
"自定义"
>
自定义
</el-radio>
</el-radio-group>
</el-form-item>
<!-- 索引方式 -->
<el-form-item
label=
"索引方式"
>
<el-radio-group
v-model=
"indexingMethod"
>
<el-radio
label=
"高质量"
>
高质量
</el-radio>
<el-radio
label=
"经济"
>
经济
</el-radio>
</el-radio-group>
</el-form-item>
<!-- Embedding 模型 -->
<el-form-item
label=
"Embedding 模型"
>
<el-select
v-model=
"embeddingModel"
placeholder=
"Select Embedding Model"
>
<el-option
label=
"text-embedding-3-large"
value=
"text-embedding-3-large"
/>
</el-select>
</el-form-item>
<!-- 检索设置 -->
<el-form-item
label=
"检索设置"
>
<el-card
style=
"width: 400px;"
>
<div
class=
"card-header"
>
<span>
向量检索
</span>
</div>
<el-slider
v-model=
"topK"
:min=
"1"
:max=
"10"
label=
"Top K"
/>
<el-slider
v-model=
"scoreThreshold"
:min=
"0"
:max=
"1"
step=
"0.1"
label=
"Score 阈值"
/>
</el-card>
<el-card
style=
"width: 400px;"
>
<div
class=
"card-header"
>
<span>
全文检索
</span>
</div>
<el-slider
v-model=
"topK"
:min=
"1"
:max=
"10"
label=
"Top K"
/>
<el-slider
v-model=
"scoreThreshold"
:min=
"0"
:max=
"1"
step=
"0.1"
label=
"Score 阈值"
/>
</el-card>
<el-card
style=
"width: 400px;"
>
<div
class=
"card-header"
>
<span>
混合检索
</span>
</div>
<el-slider
v-model=
"topK"
:min=
"1"
:max=
"10"
label=
"Top K"
/>
<el-slider
v-model=
"scoreThreshold"
:min=
"0"
:max=
"1"
step=
"0.1"
label=
"Score 阈值"
/>
</el-card>
</el-form-item>
</el-form>
</el-card>
</el-col>
<!-- Right Section: 分段预览 -->
<el-col
:span=
"9"
>
<el-card
shadow=
"never"
>
<div
class=
"previews-title"
>
分段预览
</div>
<template
v-for=
"(segment, index) in segmentPreviews"
:key=
"index"
>
<div
class=
"segment-preview"
>
<div
class=
"title"
>
<div
class=
"left"
>
<svg
width=
"12"
height=
"12"
viewBox=
"0 0 12 12"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<path
d=
"M4.74999 1.5L3.24999 10.5M8.74998 1.5L7.24998 10.5M10.25 4H1.75M9.75 8H1.25"
stroke=
"#98A2B3"
stroke-linecap=
"round"
stroke-linejoin=
"round"
/>
</svg>
<span
class=
"id"
>
{{
segment
.
number
}}
</span>
</div>
<div
class=
"right"
>
<svg
width=
"12"
height=
"12"
viewBox=
"0 0 12 12"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<path
d=
"M4 3.5H8M6 3.5V8.5M3.9 10.5H8.1C8.94008 10.5 9.36012 10.5 9.68099 10.3365C9.96323 10.1927 10.1927 9.96323 10.3365 9.68099C10.5 9.36012 10.5 8.94008 10.5 8.1V3.9C10.5 3.05992 10.5 2.63988 10.3365 2.31901C10.1927 2.03677 9.96323 1.8073 9.68099 1.66349C9.36012 1.5 8.94008 1.5 8.1 1.5H3.9C3.05992 1.5 2.63988 1.5 2.31901 1.66349C2.03677 1.8073 1.8073 2.03677 1.66349 2.31901C1.5 2.63988 1.5 3.05992 1.5 3.9V8.1C1.5 8.94008 1.5 9.36012 1.66349 9.68099C1.8073 9.96323 2.03677 10.1927 2.31901 10.3365C2.63988 10.5 3.05992 10.5 3.9 10.5Z"
stroke=
"#667085"
stroke-linecap=
"round"
stroke-linejoin=
"round"
/>
</svg>
<span
class=
"char-size"
>
7777 字符
</span>
</div>
</div>
<div
class=
"content"
>
{{
segment
.
text
}}
</div>
</div>
</
template
>
</el-card>
</el-col>
</el-row>
</template>
<
script
setup
>
import
{
ref
}
from
'vue'
;
// Reactive variables for form control
const
segmentSetting
=
ref
(
'自动分段与清洗'
);
const
indexingMethod
=
ref
(
'高质量'
);
const
embeddingModel
=
ref
(
'text-embedding-3-large'
);
const
directionalSearch
=
ref
(
true
);
const
topK
=
ref
(
3
);
const
scoreThreshold
=
ref
(
0.5
);
// Mock data for segment previews
const
segmentPreviews
=
ref
([
{
number
:
'001'
,
text
:
"同步obs模型...'UAE-large-V1'"
},
{
number
:
'002'
,
text
:
"同步obs模型...'plip'"
},
{
number
:
'003'
,
text
:
"同步obs模型...'phoBERT-base-v2'"
},
{
number
:
'004'
,
text
:
"同步obs模型...'lama3-bb-bnb-4bit'"
},
{
number
:
'005'
,
text
:
"同步obs模型...'t5-base-split-and-rephrase'"
}
]);
</
script
>
<
style
scoped
lang=
"scss"
>
/* Add any custom styles here */
.previews-title
{
font-size
:
18px
;
font-weight
:
500
;
}
.segment-preview
{
background-color
:
rgba
(
228
,
228
,
228
,
0.38
);
border-radius
:
10px
;
padding
:
15px
;
margin-top
:
15px
;
.title
{
display
:
flex
;
justify-content
:
space-between
;
.left
{
border-right
:
5px
;
font-size
:
13px
;
font-style
:
italic
;
font-weight
:
500
;
color
:
#676767
;
box-sizing
:
border-box
;
align-items
:
center
;
.id
{
margin-left
:
5px
;
}
}
.right
{
display
:
flex
;
flex-direction
:
row
;
align-items
:
center
;
.char-size
{
margin-left
:
5px
;
font-size
:
13px
;
color
:
rgba
(
57
,
57
,
57
,
0.66
);
}
}
}
.content
{
margin-top
:
10px
;
font-size
:
15px
;
font-weight
:
500
;
}
}
</
style
>
src/views/knowledge/dataset.vue
0 → 100644
View file @
887936cc
<
template
>
<div
class=
"knowledge-base-container"
>
<div
class=
"card-container"
>
<el-card
class=
"create-card"
shadow=
"hover"
>
<div
class=
"create-content"
>
<el-icon
class=
"create-icon"
><Plus
/></el-icon>
<span
class=
"create-text"
>
创建知识库
</span>
</div>
<div
class=
"create-footer"
>
导入您自己的文本数据或通过 Webhook 实时写入数据以增强 LLM 的上下文。
</div>
</el-card>
<el-card
class=
"document-card"
shadow=
"hover"
v-for=
"index in 4"
:key=
"index"
>
<div
class=
"document-header"
>
<el-icon><Folder
/></el-icon>
<span>
接口鉴权示例代码.md
</span>
</div>
<div
class=
"document-info"
>
<el-tag
size=
"small"
>
1 文档
</el-tag>
<el-tag
size=
"small"
type=
"info"
>
5 千字符
</el-tag>
<el-tag
size=
"small"
type=
"warning"
>
0 关联应用
</el-tag>
</div>
<p
class=
"document-description"
>
useful for when you want to answer queries about the 接口鉴权示例代码.md
</p>
</el-card>
</div>
<div
class=
"pagination-container"
>
<el-pagination
v-model:current-page=
"currentPage"
v-model:page-size=
"pageSize"
:page-sizes=
"[10, 20, 30, 40]"
:small=
"false"
:disabled=
"false"
:background=
"true"
layout=
"total, sizes, prev, pager, next, jumper"
:total=
"total"
@
size-change=
"handleSizeChange"
@
current-change=
"handleCurrentChange"
/>
</div>
</div>
</
template
>
<
script
setup
>
import
{
ref
}
from
'vue'
import
{
Folder
,
Plus
}
from
'@element-plus/icons-vue'
const
currentPage
=
ref
(
1
)
const
pageSize
=
ref
(
10
)
const
total
=
ref
(
100
)
// 假设总共有100条数据
const
handleSizeChange
=
(
val
)
=>
{
console
.
log
(
`每页
${
val
}
条`
)
}
const
handleCurrentChange
=
(
val
)
=>
{
console
.
log
(
`当前页:
${
val
}
`
)
}
</
script
>
<
style
scoped
>
.knowledge-base-container
{
font-family
:
'Helvetica Neue'
,
Helvetica
,
'PingFang SC'
,
'Hiragino Sans GB'
,
'Microsoft YaHei'
,
'微软雅黑'
,
Arial
,
sans-serif
;
position
:
absolute
;
padding
:
20px
;
margin
:
0
auto
;
display
:
flex
;
flex-direction
:
column
;
top
:
0
;
bottom
:
40px
;
width
:
100%
;
}
.card-container
{
display
:
flex
;
flex-wrap
:
wrap
;
/* Enable wrapping */
gap
:
20px
;
margin-bottom
:
auto
;
/* Pushes pagination to the bottom */
}
.create-card
,
.document-card
{
flex
:
1
1
360px
;
/* Allow cards to grow and shrink */
min-width
:
0
;
max-width
:
400px
;
border-radius
:
10px
;
cursor
:
pointer
;
}
.create-card
{
background-color
:
rgba
(
168
,
168
,
168
,
0.22
);
}
.create-card
:hover
{
background-color
:
#fff
;
}
.create-content
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
margin-bottom
:
15px
;
}
.create-icon
{
font-size
:
24px
;
color
:
#409EFF
;
}
.create-text
{
font-size
:
18px
;
font-weight
:
bold
;
color
:
#303133
;
}
.create-footer
{
font-size
:
14px
;
color
:
#909399
;
line-height
:
1.5
;
}
.document-header
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
font-size
:
16px
;
font-weight
:
bold
;
margin-bottom
:
15px
;
}
.document-info
{
display
:
flex
;
gap
:
10px
;
margin-bottom
:
15px
;
}
.document-description
{
color
:
#606266
;
font-size
:
14px
;
line-height
:
1.5
;
}
.pagination-container
{
position
:
absolute
;
width
:
100%
;
bottom
:
0
;
display
:
flex
;
justify-content
:
center
;
margin-top
:
20px
;
}
</
style
>
src/views/mall/product/property/value/index.vue
View file @
887936cc
...
...
@@ -105,7 +105,7 @@ const list = ref([]) // 列表的数据
const
queryParams
=
reactive
({
pageNo
:
1
,
pageSize
:
10
,
propertyId
:
Number
(
params
.
propertyId
)
,
propertyId
:
params
.
propertyId
,
name
:
undefined
})
const
queryFormRef
=
ref
()
// 搜索的表单
...
...
src/views/mall/product/spu/components/SkuList.vue
View file @
887936cc
...
...
@@ -180,17 +180,17 @@
</el-table-column>
<el-table-column
align=
"center"
label=
"销售价(元)"
min-width=
"80"
>
<
template
#
default=
"{ row }"
>
{{
formatToFraction
(
row
.
price
)
}}
{{
row
.
price
}}
</
template
>
</el-table-column>
<el-table-column
align=
"center"
label=
"市场价(元)"
min-width=
"80"
>
<
template
#
default=
"{ row }"
>
{{
formatToFraction
(
row
.
marketPrice
)
}}
{{
row
.
marketPrice
}}
</
template
>
</el-table-column>
<el-table-column
align=
"center"
label=
"成本价(元)"
min-width=
"80"
>
<
template
#
default=
"{ row }"
>
{{
formatToFraction
(
row
.
costPrice
)
}}
{{
row
.
costPrice
}}
</
template
>
</el-table-column>
<el-table-column
align=
"center"
label=
"库存"
min-width=
"80"
>
...
...
@@ -211,12 +211,12 @@
<
template
v-if=
"formData!.subCommissionType"
>
<el-table-column
align=
"center"
label=
"一级返佣(元)"
min-width=
"80"
>
<template
#
default=
"
{ row }">
{{
formatToFraction
(
row
.
firstBrokeragePrice
)
}}
{{
row
.
firstBrokeragePrice
}}
</
template
>
</el-table-column>
<el-table-column
align=
"center"
label=
"二级返佣(元)"
min-width=
"80"
>
<
template
#
default=
"{ row }"
>
{{
formatToFraction
(
row
.
secondBrokeragePrice
)
}}
{{
row
.
secondBrokeragePrice
}}
</
template
>
</el-table-column>
</template>
...
...
src/views/mall/product/spu/form/InfoForm.vue
View file @
887936cc
...
...
@@ -45,7 +45,7 @@
:show-word-limit="true"
class="w-80!"
maxlength="128"
placeholder="请输入商品
名称
"
placeholder="请输入商品
简介
"
type="textarea"
/>
</el-form-item>
...
...
src/views/mall/promotion/combination/activity/index.vue
View file @
887936cc
...
...
@@ -4,27 +4,27 @@
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class=
"-mb-15px"
:model=
"queryParams"
ref=
"queryFormRef"
:inline=
"true"
:model=
"queryParams"
class=
"-mb-15px"
label-width=
"68px"
>
<el-form-item
label=
"活动名称"
prop=
"name"
>
<el-input
v-model=
"queryParams.name"
placeholder=
"请输入活动名称
"
class=
"!w-240px
"
clearable
placeholder=
"请输入活动名称"
@
keyup
.
enter=
"handleQuery"
class=
"!w-240px"
/>
</el-form-item>
<el-form-item
label=
"活动状态"
prop=
"status"
>
<el-select
v-model=
"queryParams.status"
placeholder=
"请选择活动状态"
clearable
class=
"!w-240px"
clearable
placeholder=
"请选择活动状态"
>
<el-option
v-for=
"dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
...
...
@@ -35,15 +35,22 @@
</el-select>
</el-form-item>
<el-form-item>
<el-button
@
click=
"handleQuery"
><Icon
icon=
"ep:search"
class=
"mr-5px"
/>
搜索
</el-button>
<el-button
@
click=
"resetQuery"
><Icon
icon=
"ep:refresh"
class=
"mr-5px"
/>
重置
</el-button>
<el-button
@
click=
"handleQuery"
>
<Icon
class=
"mr-5px"
icon=
"ep:search"
/>
搜索
</el-button>
<el-button
@
click=
"resetQuery"
>
<Icon
class=
"mr-5px"
icon=
"ep:refresh"
/>
重置
</el-button>
<el-button
type=
"primary
"
v-hasPermi=
"['promotion:combination-activity:create']
"
plain
type=
"primary"
@
click=
"openForm('create')"
v-hasPermi=
"['promotion:combination-activity:create']"
>
<Icon
icon=
"ep:plus"
class=
"mr-5px"
/>
新增
<Icon
class=
"mr-5px"
icon=
"ep:plus"
/>
新增
</el-button>
</el-form-item>
</el-form>
...
...
@@ -51,77 +58,77 @@
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading=
"loading"
:data=
"list"
:s
tripe=
"true"
:show-overflow-tooltip
=
"true"
>
<el-table-column
label=
"活动编号"
prop=
"id"
min-width=
"80
"
/>
<el-table-column
label=
"活动名称"
prop=
"name"
min-width=
"140
"
/>
<el-table
v-loading=
"loading"
:data=
"list"
:s
how-overflow-tooltip=
"true"
:stripe
=
"true"
>
<el-table-column
label=
"活动编号"
min-width=
"80"
prop=
"id
"
/>
<el-table-column
label=
"活动名称"
min-width=
"140"
prop=
"name
"
/>
<el-table-column
label=
"活动时间"
min-width=
"210"
>
<template
#
default=
"scope"
>
{{
formatDate
(
scope
.
row
.
startTime
,
'YYYY-MM-DD'
)
}}
~
{{
formatDate
(
scope
.
row
.
endTime
,
'YYYY-MM-DD'
)
}}
</
template
>
</el-table-column>
<el-table-column
label=
"商品图片"
prop=
"spuName"
min-width=
"80
"
>
<el-table-column
label=
"商品图片"
min-width=
"80"
prop=
"spuName
"
>
<
template
#
default=
"scope"
>
<el-image
:preview-src-list=
"[scope.row.picUrl]"
:src=
"scope.row.picUrl"
class=
"h-40px w-40px"
:preview-src-list=
"[scope.row.picUrl]"
preview-teleported
/>
</
template
>
</el-table-column>
<el-table-column
label=
"商品标题"
prop=
"spuName"
min-width=
"300
"
/>
<el-table-column
label=
"商品标题"
min-width=
"300"
prop=
"spuName
"
/>
<el-table-column
:formatter=
"fenToYuanFormat"
label=
"原价"
prop=
"marketPrice"
min-width=
"100"
:formatter=
"fenToYuanFormat
"
prop=
"marketPrice
"
/>
<el-table-column
label=
"拼团价"
prop=
"seckillPrice"
min-width=
"100
"
>
<el-table-column
label=
"拼团价"
min-width=
"100"
prop=
"seckillPrice
"
>
<
template
#
default=
"scope"
>
{{
formatCombinationPrice
(
scope
.
row
.
products
)
}}
</
template
>
</el-table-column>
<el-table-column
label=
"开团组数"
prop=
"groupCount"
min-width=
"100
"
/>
<el-table-column
label=
"成团组数"
prop=
"groupSuccessCount"
min-width=
"100
"
/>
<el-table-column
label=
"购买次数"
prop=
"recordCount"
min-width=
"100
"
/>
<el-table-column
label=
"活动状态"
align=
"center"
prop=
"status"
min-width=
"100
"
>
<el-table-column
label=
"开团组数"
min-width=
"100"
prop=
"groupCount
"
/>
<el-table-column
label=
"成团组数"
min-width=
"100"
prop=
"groupSuccessCount
"
/>
<el-table-column
label=
"购买次数"
min-width=
"100"
prop=
"recordCount
"
/>
<el-table-column
align=
"center"
label=
"活动状态"
min-width=
"100"
prop=
"status
"
>
<
template
#
default=
"scope"
>
<dict-tag
:type=
"DICT_TYPE.COMMON_STATUS"
:value=
"scope.row.status"
/>
</
template
>
</el-table-column>
<el-table-column
label=
"创建时间
"
:formatter=
"dateFormatter
"
align=
"center"
label=
"创建时间"
prop=
"createTime"
:formatter=
"dateFormatter"
width=
"180px"
/>
<el-table-column
label=
"操作"
align=
"center"
width=
"150px"
fixed=
"right
"
>
<el-table-column
align=
"center"
fixed=
"right"
label=
"操作"
width=
"150px
"
>
<
template
#
default=
"scope"
>
<el-button
v-hasPermi=
"['promotion:combination-activity:update']"
link
type=
"primary"
@
click=
"openForm('update', scope.row.id)"
v-hasPermi=
"['promotion:combination-activity:update']"
>
编辑
</el-button>
<el-button
v-if=
"scope.row.status === 0"
v-hasPermi=
"['promotion:combination-activity:close']"
link
type=
"danger"
@
click=
"handleClose(scope.row.id)"
v-if=
"scope.row.status === 0"
v-hasPermi=
"['promotion:combination-activity:close']"
>
关闭
</el-button>
<el-button
v-else
v-hasPermi=
"['promotion:combination-activity:delete']"
link
type=
"danger"
@
click=
"handleDelete(scope.row.id)"
v-else
v-hasPermi=
"['promotion:combination-activity:delete']"
>
删除
</el-button>
...
...
@@ -130,9 +137,9 @@
</el-table>
<!-- 分页 -->
<Pagination
:total=
"total"
v-model:page=
"queryParams.pageNo"
v-model:limit=
"queryParams.pageSize"
v-model:page=
"queryParams.pageNo"
:total=
"total"
@
pagination=
"getList"
/>
</ContentWrap>
...
...
@@ -141,12 +148,11 @@
<CombinationActivityForm
ref=
"formRef"
@
success=
"getList"
/>
</template>
<
script
setup
lang=
"ts"
>
<
script
lang=
"ts"
setup
>
import
{
DICT_TYPE
,
getIntDictOptions
}
from
'@/utils/dict'
import
{
dateFormatter
}
from
'@/utils/formatTime'
import
{
dateFormatter
,
formatDate
}
from
'@/utils/formatTime'
import
*
as
CombinationActivityApi
from
'@/api/mall/promotion/combination/combinationActivity'
import
CombinationActivityForm
from
'./CombinationActivityForm.vue'
import
{
formatDate
}
from
'@/utils/formatTime'
import
{
fenToYuanFormat
}
from
'@/utils/formatter'
import
{
fenToYuan
}
from
'@/utils'
...
...
@@ -165,7 +171,6 @@ const queryParams = reactive({
status
:
null
})
const
queryFormRef
=
ref
()
// 搜索的表单
const
exportLoading
=
ref
(
false
)
// 导出的加载中
/** 查询列表 */
const
getList
=
async
()
=>
{
...
...
@@ -197,12 +202,11 @@ const openForm = (type: string, id?: number) => {
formRef
.
value
.
open
(
type
,
id
)
}
// TODO 芋艿:这里要改下
/** 关闭按钮操作 */
const
handleClose
=
async
(
id
:
number
)
=>
{
try
{
// 关闭的二次确认
await
message
.
confirm
(
'确认关闭该
秒杀
活动吗?'
)
await
message
.
confirm
(
'确认关闭该
拼团
活动吗?'
)
// 发起关闭
await
CombinationActivityApi
.
closeCombinationActivity
(
id
)
message
.
success
(
'关闭成功'
)
...
...
src/views/mall/promotion/components/SpuAndSkuList.vue
View file @
887936cc
...
...
@@ -30,13 +30,13 @@
<el-table-column
align=
"center"
label=
"销量"
min-width=
"90"
prop=
"salesCount"
/>
<el-table-column
align=
"center"
label=
"库存"
min-width=
"90"
prop=
"stock"
/>
<el-table-column
v-if=
"spuData.length > 1 &&
isDelet
e"
v-if=
"spuData.length > 1 &&
deletabl
e"
align=
"center"
label=
"操作"
min-width=
"90"
>
<
template
#
default=
"scope"
>
<el-button
type=
"primary"
link
@
click=
"deleteSpu(scope.row.id)"
>
删除
</el-button>
<el-button
link
type=
"primary"
@
click=
"deleteSpu(scope.row.id)"
>
删除
</el-button>
</
template
>
</el-table-column>
</el-table>
...
...
@@ -56,13 +56,13 @@ const props = defineProps<{
spuList
:
T
[]
ruleConfig
:
RuleConfig
[]
spuPropertyListP
:
SpuProperty
<
T
>
[]
isDelete
?:
boolean
// SPU 是否可删除;TODO deletable 换成这个名字好点。
deletable
?:
boolean
// SPU 是否可删除;
}
>
()
const
spuData
=
ref
<
Spu
[]
>
([])
// spu 详情数据列表
const
skuListRef
=
ref
()
// 商品属性列表Ref
const
spuPropertyList
=
ref
<
SpuProperty
<
T
>
[]
>
([])
// spuId 对应的 sku 的属性列表
const
expandRowKeys
=
ref
<
number
[]
>
(
)
// 控制展开行需要设置 row-key 属性才能使用,该属性为展开行的 keys 数组。
const
expandRowKeys
=
ref
<
string
[]
>
([]
)
// 控制展开行需要设置 row-key 属性才能使用,该属性为展开行的 keys 数组。
/**
* 获取所有 sku 活动配置
...
...
@@ -71,10 +71,10 @@ const expandRowKeys = ref<number[]>() // 控制展开行需要设置 row-key 属
*/
const
getSkuConfigs
=
(
extendedAttribute
:
string
)
=>
{
skuListRef
.
value
.
validateSku
()
const
seckillProducts
=
[]
const
seckillProducts
:
any
[]
=
[]
spuPropertyList
.
value
.
forEach
((
item
)
=>
{
item
.
spuDetail
.
skus
.
forEach
((
sku
)
=>
{
seckillProducts
.
push
(
sku
[
extendedAttribute
])
item
.
spuDetail
.
skus
?.
forEach
((
sku
:
any
)
=>
{
seckillProducts
.
push
(
sku
[
extendedAttribute
]
as
any
)
})
})
return
seckillProducts
...
...
@@ -124,10 +124,10 @@ watch(
()
=>
props
.
spuPropertyListP
,
(
data
)
=>
{
if
(
!
data
)
return
spuPropertyList
.
value
=
data
as
SpuProperty
<
T
>
[]
spuPropertyList
.
value
=
data
as
SpuProperty
<
T
>
[]
as
any
// 解决如果之前选择的是单规格 spu 的话后面选择多规格 sku 多规格属性信息不展示的问题。解决方法:让 SkuList 组件重新渲染(行折叠会干掉包含的组件展开时会重新加载)
setTimeout
(()
=>
{
expandRowKeys
.
value
=
data
.
map
((
item
)
=>
item
.
spuId
)
expandRowKeys
.
value
=
data
.
map
((
item
)
=>
item
.
spuId
+
''
)
},
200
)
},
{
...
...
src/views/mall/promotion/coupon/components/CouponSelect.vue
View file @
887936cc
...
...
@@ -116,6 +116,7 @@ import {
validityTypeFormat
}
from
'@/views/mall/promotion/coupon/formatter'
import
*
as
CouponTemplateApi
from
'@/api/mall/promotion/coupon/couponTemplate'
import
{
CouponTemplateTakeTypeEnum
}
from
'@/utils/constants'
defineOptions
({
name
:
'CouponSelect'
})
...
...
@@ -138,7 +139,7 @@ const queryParams = reactive({
pageSize
:
10
,
name
:
null
,
discountType
:
null
,
canTakeTypes
:
null
canTakeTypes
:
[
CouponTemplateTakeTypeEnum
.
USER
.
type
]
// 只获得直接领取的券
})
const
queryFormRef
=
ref
()
// 搜索的表单
const
selectedCouponList
=
ref
<
CouponTemplateApi
.
CouponTemplateVO
[]
>
([])
// 选择的数据
...
...
src/views/mall/promotion/coupon/formatter.ts
View file @
887936cc
...
...
@@ -16,10 +16,14 @@ export const discountFormat = (row: CouponTemplateVO) => {
// 格式化【领取上限】
export
const
takeLimitCountFormat
=
(
row
:
CouponTemplateVO
)
=>
{
if
(
row
.
takeLimitCount
===
-
1
)
{
return
'无领取限制'
if
(
row
.
takeLimitCount
)
{
if
(
row
.
takeLimitCount
===
-
1
)
{
return
'无领取限制'
}
return
`
${
row
.
takeLimitCount
}
张/人`
}
else
{
return
' '
}
return
`
${
row
.
takeLimitCount
}
张/人`
}
// 格式化【有效期限】
...
...
@@ -33,8 +37,19 @@ export const validityTypeFormat = (row: CouponTemplateVO) => {
return
'未知【'
+
row
.
validityType
+
'】'
}
// 格式化【totalCount】
export
const
totalCountFormat
=
(
row
:
CouponTemplateVO
)
=>
{
if
(
row
.
totalCount
===
-
1
)
{
return
'不限制'
}
return
row
.
totalCount
}
// 格式化【剩余数量】
export
const
remainedCountFormat
=
(
row
:
CouponTemplateVO
)
=>
{
if
(
row
.
totalCount
===
-
1
)
{
return
'不限制'
}
return
row
.
totalCount
-
row
.
takeCount
}
...
...
src/views/mall/promotion/coupon/template/CouponTemplateForm.vue
View file @
887936cc
...
...
@@ -115,6 +115,7 @@
<el-radio-group
v-model=
"formData.takeType"
>
<el-radio
:key=
"1"
:value=
"1"
>
直接领取
</el-radio>
<el-radio
:key=
"2"
:value=
"2"
>
指定发放
</el-radio>
<el-radio
:key=
"2"
:value=
"3"
>
新人卷
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if=
"formData.takeType === 1"
label=
"发放数量"
prop=
"totalCount"
>
...
...
@@ -309,7 +310,9 @@ const submitForm = async () => {
validEndTime
:
formData
.
value
.
validTimes
&&
formData
.
value
.
validTimes
.
length
===
2
?
formData
.
value
.
validTimes
[
1
]
:
undefined
:
undefined
,
totalCount
:
formData
.
value
.
takeType
===
1
?
formData
.
value
.
totalCount
:
-
1
,
takeLimitCount
:
formData
.
value
.
takeType
===
1
?
formData
.
value
.
takeLimitCount
:
-
1
}
as
unknown
as
CouponTemplateApi
.
CouponTemplateVO
// 设置商品范围
...
...
src/views/mall/promotion/coupon/template/index.vue
View file @
887936cc
...
...
@@ -109,7 +109,12 @@
prop=
"validityType"
width=
"185"
/>
<el-table-column
align=
"center"
label=
"发放数量"
prop=
"totalCount"
/>
<el-table-column
:formatter=
"totalCountFormat"
align=
"center"
label=
"发放数量"
prop=
"totalCount"
/>
<el-table-column
:formatter=
"remainedCountFormat"
align=
"center"
...
...
@@ -189,6 +194,7 @@ import {
discountFormat
,
remainedCountFormat
,
takeLimitCountFormat
,
totalCountFormat
,
validityTypeFormat
}
from
'@/views/mall/promotion/coupon/formatter'
...
...
src/views/mall/promotion/discountActivity/DiscountActivityForm.vue
View file @
887936cc
...
...
@@ -8,28 +8,40 @@
:schema=
"allSchemas.formSchema"
>
<!-- 先选择 -->
<!-- TODO @zhangshuai:商品允许选择多个 -->
<!-- TODO @zhangshuai:选择后的 SKU,需要后面加个【删除】按钮 -->
<!-- TODO @zhangshuai:展示的金额,貌似不对,大了 100 倍,需要看下 -->
<!-- TODO @zhangshuai:“优惠类型”,是每个 SKU 可以自定义已设置哈。因为每个商品 SKU 的折扣和减少价格,可能不同。具体交互,可以注册一个 youzan.com 看看;它的交互方式是,如果设置了“优惠金额”,则算“减价”;如果再次设置了“折扣百分比”,就算“打折”;这样形成一个互斥的优惠类型 -->
<template
#
spuId
>
<el-button
@
click=
"spuSelectRef.open()"
>
选择商品
</el-button>
<SpuAndSkuList
ref=
"spuAndSkuListRef"
:deletable=
"true"
:rule-config=
"ruleConfig"
:spu-list=
"spuList"
:spu-property-list-p=
"spuPropertyList"
:isDelete=
"true"
@
delete=
"deleteSpu"
>
<el-table-column
align=
"center"
label=
"优惠金额"
min-width=
"168"
>
<template
#
default=
"
{ row: sku }">
<el-input-number
v-model=
"sku.productConfig.discountPrice"
:min=
"0"
class=
"w-100%"
/>
<template
#
default=
"
{ row }">
<el-input-number
v-model=
"row.productConfig.discountPrice"
:max=
"parseFloat(fenToYuan(row.price))"
:min=
"0"
:precision=
"2"
:step=
"0.1"
class=
"w-100%"
@
change=
"handleSkuDiscountPriceChange(row)"
/>
</
template
>
</el-table-column>
<el-table-column
align=
"center"
label=
"折扣百分比(%)"
min-width=
"168"
>
<
template
#
default=
"{ row: sku }"
>
<el-input-number
v-model=
"sku.productConfig.discountPercent"
class=
"w-100%"
/>
<
template
#
default=
"{ row }"
>
<el-input-number
v-model=
"row.productConfig.discountPercent"
:max=
"100"
:min=
"0"
:precision=
"2"
:step=
"0.1"
class=
"w-100%"
@
change=
"handleSkuDiscountPercentChange(row)"
/>
</
template
>
</el-table-column>
</SpuAndSkuList>
...
...
@@ -45,11 +57,12 @@
<
script
lang=
"ts"
setup
>
import
{
SpuAndSkuList
,
SpuProperty
,
SpuSelect
}
from
'../components'
import
{
allSchemas
,
rules
}
from
'./discountActivity.data'
import
{
cloneDeep
}
from
'lodash-es'
import
{
cloneDeep
,
debounce
}
from
'lodash-es'
import
*
as
DiscountActivityApi
from
'@/api/mall/promotion/discount/discountActivity'
import
*
as
ProductSpuApi
from
'@/api/mall/product/spu'
import
{
getPropertyList
,
RuleConfig
}
from
'@/views/mall/product/spu/components'
import
{
formatToFraction
}
from
'@/utils'
import
{
convertToInteger
,
erpCalculatePercentage
,
fenToYuan
,
yuanToFen
}
from
'@/utils'
import
{
PromotionDiscountTypeEnum
}
from
'@/utils/constants'
defineOptions
({
name
:
'PromotionDiscountActivityForm'
})
...
...
@@ -65,7 +78,13 @@ const formRef = ref() // 表单 Ref
const
spuSelectRef
=
ref
()
// 商品和属性选择 Ref
const
spuAndSkuListRef
=
ref
()
// sku 限时折扣 配置组件Ref
const
ruleConfig
:
RuleConfig
[]
=
[]
const
ruleConfig
:
RuleConfig
[]
=
[
{
name
:
'productConfig.discountPrice'
,
rule
:
(
arg
)
=>
arg
>
0
,
message
:
'商品优惠金额不能为 0 !!!'
}
]
const
spuList
=
ref
<
DiscountActivityApi
.
SpuExtension
[]
>
([])
// 选择的 spu
const
spuPropertyList
=
ref
<
SpuProperty
<
DiscountActivityApi
.
SpuExtension
>
[]
>
([])
const
spuIds
=
ref
<
number
[]
>
([])
...
...
@@ -101,21 +120,20 @@ const getSpuDetails = async (
selectSkus
?.
forEach
((
sku
)
=>
{
let
config
:
DiscountActivityApi
.
DiscountProductVO
=
{
skuId
:
sku
.
id
!
,
spuId
:
spu
.
id
,
spuId
:
spu
.
id
!
,
discountType
:
1
,
discountPercent
:
0
,
discountPrice
:
0
}
if
(
typeof
products
!==
'undefined'
)
{
const
product
=
products
.
find
((
item
)
=>
item
.
skuId
===
sku
.
id
)
if
(
product
)
{
product
.
discountPercent
=
fenToYuan
(
product
.
discountPercent
)
as
any
product
.
discountPrice
=
fenToYuan
(
product
.
discountPrice
)
as
any
}
config
=
product
||
config
}
sku
.
productConfig
=
config
sku
.
price
=
formatToFraction
(
sku
.
price
)
sku
.
marketPrice
=
formatToFraction
(
sku
.
marketPrice
)
sku
.
costPrice
=
formatToFraction
(
sku
.
costPrice
)
sku
.
firstBrokeragePrice
=
formatToFraction
(
sku
.
firstBrokeragePrice
)
sku
.
secondBrokeragePrice
=
formatToFraction
(
sku
.
secondBrokeragePrice
)
})
spu
.
skus
=
selectSkus
as
DiscountActivityApi
.
SkuExtension
[]
spuPropertyList
.
value
.
push
({
...
...
@@ -168,25 +186,13 @@ const submitForm = async () => {
// 提交请求
formLoading
.
value
=
true
try
{
const
data
=
formRef
.
value
.
formModel
as
DiscountActivityApi
.
DiscountActivityVO
// 获取折扣商品配置
const
products
=
cloneDeep
(
spuAndSkuListRef
.
value
.
getSkuConfigs
(
'productConfig'
))
// 校验优惠金额、折扣百分比,是否正确
// TODO @puhui999:这个交互,可以参考下 youzan 的
let
discountInvalid
=
false
products
.
forEach
((
item
:
DiscountActivityApi
.
DiscountProductVO
)
=>
{
if
(
item
.
discountPrice
!=
null
&&
item
.
discountPrice
>
0
)
{
item
.
discountType
=
1
}
else
if
(
item
.
discountPercent
!=
null
&&
item
.
discountPercent
>
0
)
{
item
.
discountType
=
2
}
else
{
discountInvalid
=
true
}
item
.
discountPercent
=
convertToInteger
(
item
.
discountPercent
)
item
.
discountPrice
=
convertToInteger
(
yuanToFen
(
item
.
discountPrice
))
})
if
(
discountInvalid
)
{
message
.
error
(
'优惠金额和折扣百分比需要填写一个'
)
return
}
const
data
=
cloneDeep
(
formRef
.
value
.
formModel
)
as
DiscountActivityApi
.
DiscountActivityVO
data
.
products
=
products
// 真正提交
if
(
formType
.
value
===
'create'
)
{
...
...
@@ -204,6 +210,36 @@ const submitForm = async () => {
}
}
/** 处理 sku 优惠金额变动 */
const
handleSkuDiscountPriceChange
=
debounce
((
row
:
any
)
=>
{
// 校验边界
if
(
row
.
productConfig
.
discountPrice
<=
0
)
{
return
}
// 设置优惠类型:满减
row
.
productConfig
.
discountType
=
PromotionDiscountTypeEnum
.
PRICE
.
type
// 设置折扣
row
.
productConfig
.
discountPercent
=
erpCalculatePercentage
(
row
.
price
-
yuanToFen
(
row
.
productConfig
.
discountPrice
),
row
.
price
)
},
200
)
/** 处理 sku 优惠折扣变动 */
const
handleSkuDiscountPercentChange
=
debounce
((
row
:
any
)
=>
{
// 校验边界
if
(
row
.
productConfig
.
discountPercent
<=
0
||
row
.
productConfig
.
discountPercent
>=
100
)
{
return
}
// 设置优惠类型:折扣
row
.
productConfig
.
discountType
=
PromotionDiscountTypeEnum
.
PERCENT
.
type
// 设置满减金额
row
.
productConfig
.
discountPrice
=
fenToYuan
(
row
.
price
-
row
.
price
*
(
row
.
productConfig
.
discountPercent
/
100.0
||
0
)
)
},
200
)
/** 重置表单 */
const
resetForm
=
async
()
=>
{
spuList
.
value
=
[]
...
...
src/views/mall/promotion/discountActivity/discountActivity.data.ts
View file @
887936cc
import
type
{
CrudSchema
}
from
'@/hooks/web/useCrudSchemas'
import
{
dateFormatter2
}
from
'@/utils/formatTime'
// TODO @zhangshai:
// 表单校验
export
const
rules
=
reactive
({
spuId
:
[
required
],
name
:
[
required
],
startTime
:
[
required
],
endTime
:
[
required
],
...
...
@@ -73,6 +71,17 @@ const crudSchemas = reactive<CrudSchema[]>([
}
},
{
label
:
'优惠类型'
,
field
:
'discountType'
,
dictType
:
DICT_TYPE
.
PROMOTION_DISCOUNT_TYPE
,
dictClass
:
'number'
,
isSearch
:
true
,
form
:
{
component
:
'Radio'
,
value
:
1
}
},
{
label
:
'活动商品'
,
field
:
'spuId'
,
isTable
:
true
,
...
...
src/views/mall/promotion/point/activity/PointActivityForm.vue
0 → 100644
View file @
887936cc
<
template
>
<Dialog
v-model=
"dialogVisible"
:title=
"dialogTitle"
width=
"65%"
>
<Form
ref=
"formRef"
v-loading=
"formLoading"
:isCol=
"true"
:rules=
"rules"
:schema=
"allSchemas.formSchema"
>
<!-- 先选择 -->
<template
#
spuId
>
<el-button
v-if=
"!isFormUpdate"
@
click=
"spuSelectRef.open()"
>
选择商品
</el-button>
<SpuAndSkuList
ref=
"spuAndSkuListRef"
:rule-config=
"ruleConfig"
:spu-list=
"spuList"
:spu-property-list-p=
"spuPropertyList"
>
<el-table-column
align=
"center"
label=
"可兑换库存"
min-width=
"168"
>
<template
#
default=
"
{ row: sku }">
<el-input-number
v-model=
"sku.productConfig.stock"
:max=
"sku.stock"
:min=
"0"
class=
"w-100%"
/>
</
template
>
</el-table-column>
<el-table-column
align=
"center"
label=
"可兑换次数"
min-width=
"168"
>
<
template
#
default=
"{ row: sku }"
>
<el-input-number
v-model=
"sku.productConfig.count"
:min=
"0"
class=
"w-100%"
/>
</
template
>
</el-table-column>
<el-table-column
align=
"center"
label=
"所需积分"
min-width=
"168"
>
<
template
#
default=
"{ row: sku }"
>
<el-input-number
v-model=
"sku.productConfig.point"
:min=
"0"
class=
"w-100%"
/>
</
template
>
</el-table-column>
<el-table-column
align=
"center"
label=
"所需金额(元)"
min-width=
"168"
>
<
template
#
default=
"{ row: sku }"
>
<el-input-number
v-model=
"sku.productConfig.price"
:min=
"0"
:precision=
"2"
:step=
"0.1"
class=
"w-100%"
/>
</
template
>
</el-table-column>
</SpuAndSkuList>
</template>
</Form>
<
template
#
footer
>
<el-button
:disabled=
"formLoading"
type=
"primary"
@
click=
"submitForm"
>
确 定
</el-button>
<el-button
@
click=
"dialogVisible = false"
>
取 消
</el-button>
</
template
>
</Dialog>
<SpuSelect
ref=
"spuSelectRef"
:isSelectSku=
"true"
@
confirm=
"selectSpu"
/>
</template>
<
script
lang=
"ts"
setup
>
import
{
SpuAndSkuList
,
SpuProperty
,
SpuSelect
}
from
'../../components'
import
{
allSchemas
,
rules
}
from
'./pointActivity.data'
import
{
cloneDeep
}
from
'lodash-es'
import
{
PointActivityApi
,
PointActivityVO
,
PointProductVO
,
SkuExtension
,
SpuExtension
}
from
'@/api/mall/promotion/point'
import
*
as
ProductSpuApi
from
'@/api/mall/product/spu'
import
{
getPropertyList
,
RuleConfig
}
from
'@/views/mall/product/spu/components'
import
{
convertToInteger
,
formatToFraction
}
from
'@/utils'
defineOptions
({
name
:
'PromotionSeckillActivityForm'
})
const
{
t
}
=
useI18n
()
// 国际化
const
message
=
useMessage
()
// 消息弹窗
const
dialogVisible
=
ref
(
false
)
// 弹窗的是否展示
const
dialogTitle
=
ref
(
''
)
// 弹窗的标题
const
formLoading
=
ref
(
false
)
// 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const
formType
=
ref
(
''
)
// 表单的类型:create - 新增;update - 修改
const
formRef
=
ref
()
// 表单 Ref
const
isFormUpdate
=
ref
(
false
)
// 是否更新表单
// ================= 商品选择相关 =================
const
spuSelectRef
=
ref
()
// 商品和属性选择 Ref
const
spuAndSkuListRef
=
ref
()
// sku 积分商城商品配置组件Ref
const
ruleConfig
:
RuleConfig
[]
=
[
{
name
:
'productConfig.stock'
,
rule
:
(
arg
)
=>
arg
>=
1
,
message
:
'商品可兑换库存必须大于等于 1 !!!'
},
{
name
:
'productConfig.point'
,
rule
:
(
arg
)
=>
arg
>=
1
,
message
:
'商品所需兑换积分必须大于等于 1 !!!'
},
{
name
:
'productConfig.count'
,
rule
:
(
arg
)
=>
arg
>=
1
,
message
:
'商品可兑换次数必须大于等于 1 !!!'
}
]
const
spuList
=
ref
<
SpuExtension
[]
>
([])
// 选择的 spu
const
spuPropertyList
=
ref
<
SpuProperty
<
SpuExtension
>
[]
>
([])
const
selectSpu
=
(
spuId
:
number
,
skuIds
:
number
[])
=>
{
formRef
.
value
.
setValues
({
spuId
})
getSpuDetails
(
spuId
,
skuIds
)
}
/**
* 获取 SPU 详情
*/
const
getSpuDetails
=
async
(
spuId
:
number
,
skuIds
:
number
[]
|
undefined
,
products
?:
PointProductVO
[]
)
=>
{
const
spuProperties
:
SpuProperty
<
SpuExtension
>
[]
=
[]
const
res
=
(
await
ProductSpuApi
.
getSpuDetailList
([
spuId
]))
as
SpuExtension
[]
if
(
res
.
length
==
0
)
{
return
}
spuList
.
value
=
[]
// 因为只能选择一个
const
spu
=
res
[
0
]
const
selectSkus
=
typeof
skuIds
===
'undefined'
?
spu
?.
skus
:
spu
?.
skus
?.
filter
((
sku
)
=>
skuIds
.
includes
(
sku
.
id
!
))
selectSkus
?.
forEach
((
sku
)
=>
{
let
config
:
PointProductVO
=
{
skuId
:
sku
.
id
!
,
stock
:
0
,
price
:
0
,
point
:
0
,
count
:
0
}
if
(
typeof
products
!==
'undefined'
)
{
const
product
=
products
.
find
((
item
)
=>
item
.
skuId
===
sku
.
id
)
if
(
product
)
{
product
.
price
=
formatToFraction
(
product
.
price
)
as
any
}
config
=
product
||
config
}
sku
.
productConfig
=
config
})
spu
.
skus
=
selectSkus
as
SkuExtension
[]
spuProperties
.
push
({
spuId
:
spu
.
id
!
,
spuDetail
:
spu
,
propertyList
:
getPropertyList
(
spu
)
})
spuList
.
value
.
push
(
spu
)
spuPropertyList
.
value
=
spuProperties
}
// ================= end =================
/** 打开弹窗 */
const
open
=
async
(
type
:
string
,
id
?:
number
)
=>
{
dialogVisible
.
value
=
true
dialogTitle
.
value
=
t
(
'action.'
+
type
)
formType
.
value
=
type
await
resetForm
()
// 修改时,设置数据
if
(
id
)
{
formLoading
.
value
=
true
try
{
const
data
=
(
await
PointActivityApi
.
getPointActivity
(
id
))
as
PointActivityVO
isFormUpdate
.
value
=
true
await
getSpuDetails
(
data
.
spuId
!
,
data
.
products
?.
map
((
sku
)
=>
sku
.
skuId
),
data
.
products
)
formRef
.
value
.
setValues
(
data
)
}
finally
{
formLoading
.
value
=
false
}
}
}
defineExpose
({
open
})
// 提供 open 方法,用于打开弹窗
/** 提交表单 */
const
emit
=
defineEmits
([
'success'
])
// 定义 success 事件,用于操作成功后的回调
const
submitForm
=
async
()
=>
{
// 校验表单
if
(
!
formRef
)
return
const
valid
=
await
formRef
.
value
.
getElFormRef
().
validate
()
if
(
!
valid
)
return
// 提交请求
formLoading
.
value
=
true
try
{
// 获取秒杀商品配置
const
products
=
cloneDeep
(
spuAndSkuListRef
.
value
.
getSkuConfigs
(
'productConfig'
))
products
.
forEach
((
item
:
PointProductVO
)
=>
{
item
.
price
=
convertToInteger
(
item
.
price
)
})
const
data
=
formRef
.
value
.
formModel
as
PointActivityVO
data
.
products
=
products
// 真正提交
if
(
formType
.
value
===
'create'
)
{
await
PointActivityApi
.
createPointActivity
(
data
)
message
.
success
(
t
(
'common.createSuccess'
))
}
else
{
await
PointActivityApi
.
updatePointActivity
(
data
)
message
.
success
(
t
(
'common.updateSuccess'
))
}
dialogVisible
.
value
=
false
// 发送操作成功的事件
emit
(
'success'
)
}
finally
{
formLoading
.
value
=
false
}
}
/** 重置表单 */
const
resetForm
=
async
()
=>
{
spuList
.
value
=
[]
spuPropertyList
.
value
=
[]
isFormUpdate
.
value
=
false
await
nextTick
()
formRef
.
value
.
getElFormRef
().
resetFields
()
}
</
script
>
src/views/mall/promotion/point/activity/index.vue
0 → 100644
View file @
887936cc
<
template
>
<doc-alert
title=
"【营销】积分商城活动"
url=
"https://doc.iocoder.cn/mall/promotion-point/"
/>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
ref=
"queryFormRef"
:inline=
"true"
:model=
"queryParams"
class=
"-mb-15px"
label-width=
"68px"
>
<el-form-item
label=
"活动状态"
prop=
"status"
>
<el-select
v-model=
"queryParams.status"
class=
"!w-240px"
clearable
placeholder=
"请选择活动状态"
>
<el-option
v-for=
"dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key=
"dict.value"
:label=
"dict.label"
:value=
"dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button
@
click=
"handleQuery"
>
<Icon
class=
"mr-5px"
icon=
"ep:search"
/>
搜索
</el-button>
<el-button
@
click=
"resetQuery"
>
<Icon
class=
"mr-5px"
icon=
"ep:refresh"
/>
重置
</el-button>
<el-button
v-hasPermi=
"['promotion:point-activity:create']"
plain
type=
"primary"
@
click=
"openForm('create')"
>
<Icon
class=
"mr-5px"
icon=
"ep:plus"
/>
新增
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading=
"loading"
:data=
"list"
:show-overflow-tooltip=
"true"
:stripe=
"true"
>
<el-table-column
label=
"活动编号"
min-width=
"80"
prop=
"id"
/>
<el-table-column
label=
"商品图片"
min-width=
"80"
prop=
"spuName"
>
<template
#
default=
"scope"
>
<el-image
:preview-src-list=
"[scope.row.picUrl]"
:src=
"scope.row.picUrl"
class=
"h-40px w-40px"
preview-teleported
/>
</
template
>
</el-table-column>
<el-table-column
label=
"商品标题"
min-width=
"300"
prop=
"spuName"
/>
<el-table-column
:formatter=
"fenToYuanFormat"
label=
"原价"
min-width=
"100"
prop=
"marketPrice"
/>
<el-table-column
label=
"原价"
min-width=
"100"
prop=
"marketPrice"
/>
<el-table-column
align=
"center"
label=
"活动状态"
min-width=
"100"
prop=
"status"
>
<
template
#
default=
"scope"
>
<dict-tag
:type=
"DICT_TYPE.COMMON_STATUS"
:value=
"scope.row.status"
/>
</
template
>
</el-table-column>
<el-table-column
align=
"center"
label=
"库存"
min-width=
"80"
prop=
"stock"
/>
<el-table-column
align=
"center"
label=
"总库存"
min-width=
"80"
prop=
"totalStock"
/>
<el-table-column
align=
"center"
label=
"已兑换数量"
min-width=
"100"
prop=
"redeemedQuantity"
>
<
template
#
default=
"{ row }"
>
{{
getRedeemedQuantity
(
row
)
}}
</
template
>
</el-table-column>
<el-table-column
:formatter=
"dateFormatter"
align=
"center"
label=
"创建时间"
prop=
"createTime"
width=
"180px"
/>
<el-table-column
align=
"center"
fixed=
"right"
label=
"操作"
width=
"150px"
>
<
template
#
default=
"scope"
>
<el-button
v-hasPermi=
"['promotion:point-activity:update']"
link
type=
"primary"
@
click=
"openForm('update', scope.row.id)"
>
编辑
</el-button>
<el-button
v-if=
"scope.row.status === 0"
v-hasPermi=
"['promotion:point-activity:close']"
link
type=
"danger"
@
click=
"handleClose(scope.row.id)"
>
关闭
</el-button>
<el-button
v-else
v-hasPermi=
"['promotion:point-activity:delete']"
link
type=
"danger"
@
click=
"handleDelete(scope.row.id)"
>
删除
</el-button>
</
template
>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit=
"queryParams.pageSize"
v-model:page=
"queryParams.pageNo"
:total=
"total"
@
pagination=
"getList"
/>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<PointActivityForm
ref=
"formRef"
@
success=
"getList"
/>
</template>
<
script
lang=
"ts"
setup
>
import
{
DICT_TYPE
,
getIntDictOptions
}
from
'@/utils/dict'
import
{
dateFormatter
}
from
'@/utils/formatTime'
import
PointActivityForm
from
'./PointActivityForm.vue'
import
{
fenToYuanFormat
}
from
'@/utils/formatter'
import
{
PointActivityApi
}
from
'@/api/mall/promotion/point'
defineOptions
({
name
:
'PointActivity'
})
const
message
=
useMessage
()
// 消息弹窗
const
{
t
}
=
useI18n
()
// 国际化
const
loading
=
ref
(
true
)
// 列表的加载中
const
total
=
ref
(
0
)
// 列表的总页数
const
list
=
ref
([])
// 列表的数据
const
queryParams
=
reactive
({
pageNo
:
1
,
pageSize
:
10
,
name
:
null
,
status
:
null
})
const
queryFormRef
=
ref
()
// 搜索的表单
const
getRedeemedQuantity
=
computed
(()
=>
(
row
:
any
)
=>
(
row
.
totalStock
||
0
)
-
(
row
.
stock
||
0
))
// 获得商品已兑换数量
/** 查询列表 */
const
getList
=
async
()
=>
{
loading
.
value
=
true
try
{
const
data
=
await
PointActivityApi
.
getPointActivityPage
(
queryParams
)
list
.
value
=
data
.
list
total
.
value
=
data
.
total
}
finally
{
loading
.
value
=
false
}
}
/** 搜索按钮操作 */
const
handleQuery
=
()
=>
{
queryParams
.
pageNo
=
1
getList
()
}
/** 重置按钮操作 */
const
resetQuery
=
()
=>
{
queryFormRef
.
value
.
resetFields
()
handleQuery
()
}
/** 添加/修改操作 */
const
formRef
=
ref
()
const
openForm
=
(
type
:
string
,
id
?:
number
)
=>
{
formRef
.
value
.
open
(
type
,
id
)
}
/** 关闭按钮操作 */
const
handleClose
=
async
(
id
:
number
)
=>
{
try
{
// 关闭的二次确认
await
message
.
confirm
(
'确认关闭该积分商城活动吗?'
)
// 发起关闭
await
PointActivityApi
.
closePointActivity
(
id
)
message
.
success
(
'关闭成功'
)
// 刷新列表
await
getList
()
}
catch
{}
}
/** 删除按钮操作 */
const
handleDelete
=
async
(
id
:
number
)
=>
{
try
{
// 删除的二次确认
await
message
.
delConfirm
()
// 发起删除
await
PointActivityApi
.
deletePointActivity
(
id
)
message
.
success
(
t
(
'common.delSuccess'
))
// 刷新列表
await
getList
()
}
catch
{}
}
/** 初始化 **/
onMounted
(
async
()
=>
{
await
getList
()
})
</
script
>
src/views/mall/promotion/point/activity/pointActivity.data.ts
0 → 100644
View file @
887936cc
import
type
{
CrudSchema
}
from
'@/hooks/web/useCrudSchemas'
// 表单校验
export
const
rules
=
reactive
({
spuId
:
[
required
],
sort
:
[
required
]
})
// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
const
crudSchemas
=
reactive
<
CrudSchema
[]
>
([
{
label
:
'排序'
,
field
:
'sort'
,
form
:
{
component
:
'InputNumber'
,
value
:
0
},
table
:
{
width
:
80
}
},
{
label
:
'积分商城活动商品'
,
field
:
'spuId'
,
isTable
:
true
,
isSearch
:
false
,
form
:
{
colProps
:
{
span
:
24
}
},
table
:
{
width
:
300
}
},
{
label
:
'备注'
,
field
:
'remark'
,
isSearch
:
false
,
form
:
{
component
:
'Input'
,
componentProps
:
{
type
:
'textarea'
,
rows
:
4
},
colProps
:
{
span
:
24
}
},
table
:
{
width
:
300
}
}
])
export
const
{
allSchemas
}
=
useCrudSchemas
(
crudSchemas
)
src/views/mall/promotion/point/components/PointShowcase.vue
0 → 100644
View file @
887936cc
<
template
>
<div
class=
"flex flex-wrap items-center gap-8px"
>
<div
v-for=
"(pointActivity, index) in pointActivityList"
:key=
"pointActivity.id"
class=
"select-box spu-pic"
>
<el-tooltip
:content=
"pointActivity.name"
>
<div
class=
"relative h-full w-full"
>
<el-image
:src=
"pointActivity.picUrl"
class=
"h-full w-full"
/>
<Icon
v-show=
"!disabled"
class=
"del-icon"
icon=
"ep:circle-close-filled"
@
click=
"handleRemoveActivity(index)"
/>
</div>
</el-tooltip>
</div>
<el-tooltip
v-if=
"canAdd"
content=
"选择活动"
>
<div
class=
"select-box"
@
click=
"openSeckillActivityTableSelect"
>
<Icon
icon=
"ep:plus"
/>
</div>
</el-tooltip>
</div>
<!-- 拼团活动选择对话框(表格形式) -->
<PointTableSelect
ref=
"pointActivityTableSelectRef"
:multiple=
"limit != 1"
@
change=
"handleActivitySelected"
/>
</
template
>
<
script
lang=
"ts"
setup
>
import
PointTableSelect
from
'./PointTableSelect.vue'
import
{
PointActivityApi
,
PointActivityVO
}
from
'@/api/mall/promotion/point'
import
{
propTypes
}
from
'@/utils/propTypes'
import
{
oneOfType
}
from
'vue-types'
import
{
isArray
}
from
'@/utils/is'
// 活动橱窗,一般用于装修时使用
// 提供功能:展示活动列表、添加活动、删除活动
defineOptions
({
name
:
'PointShowcase'
})
const
props
=
defineProps
({
modelValue
:
oneOfType
<
number
|
Array
<
number
>>
([
Number
,
Array
]).
isRequired
,
// 限制数量:默认不限制
limit
:
propTypes
.
number
.
def
(
Number
.
MAX_VALUE
),
disabled
:
propTypes
.
bool
.
def
(
false
)
})
// 计算是否可以添加
const
canAdd
=
computed
(()
=>
{
// 情况一:禁用时不可以添加
if
(
props
.
disabled
)
return
false
// 情况二:未指定限制数量时,可以添加
if
(
!
props
.
limit
)
return
true
// 情况三:检查已添加数量是否小于限制数量
return
pointActivityList
.
value
.
length
<
props
.
limit
})
// 拼团活动列表
const
pointActivityList
=
ref
<
PointActivityVO
[]
>
([])
watch
(
()
=>
props
.
modelValue
,
async
()
=>
{
const
ids
=
isArray
(
props
.
modelValue
)
?
// 情况一:多选
props
.
modelValue
:
// 情况二:单选
props
.
modelValue
?
[
props
.
modelValue
]
:
[]
// 不需要返显
if
(
ids
.
length
===
0
)
{
pointActivityList
.
value
=
[]
return
}
// 只有活动发生变化之后,才会查询活动
if
(
pointActivityList
.
value
.
length
===
0
||
pointActivityList
.
value
.
some
((
pointActivity
)
=>
!
ids
.
includes
(
pointActivity
.
id
!
))
)
{
pointActivityList
.
value
=
await
PointActivityApi
.
getPointActivityListByIds
(
ids
)
}
},
{
immediate
:
true
}
)
/** 活动表格选择对话框 */
const
pointActivityTableSelectRef
=
ref
()
// 打开对话框
const
openSeckillActivityTableSelect
=
()
=>
{
pointActivityTableSelectRef
.
value
.
open
(
pointActivityList
.
value
)
}
/**
* 选择活动后触发
* @param activityList 选中的活动列表
*/
const
handleActivitySelected
=
(
activityList
:
PointActivityVO
|
PointActivityVO
[])
=>
{
pointActivityList
.
value
=
isArray
(
activityList
)
?
activityList
:
[
activityList
]
emitActivityChange
()
}
/**
* 删除活动
* @param index 活动索引
*/
const
handleRemoveActivity
=
(
index
:
number
)
=>
{
pointActivityList
.
value
.
splice
(
index
,
1
)
emitActivityChange
()
}
const
emit
=
defineEmits
([
'update:modelValue'
,
'change'
])
const
emitActivityChange
=
()
=>
{
if
(
props
.
limit
===
1
)
{
const
pointActivity
=
pointActivityList
.
value
.
length
>
0
?
pointActivityList
.
value
[
0
]
:
null
emit
(
'update:modelValue'
,
pointActivity
?.
id
||
0
)
emit
(
'change'
,
pointActivity
)
}
else
{
emit
(
'update:modelValue'
,
pointActivityList
.
value
.
map
((
pointActivity
)
=>
pointActivity
.
id
)
)
emit
(
'change'
,
pointActivityList
.
value
)
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
.select-box
{
display
:
flex
;
width
:
60px
;
height
:
60px
;
border
:
1px
dashed
var
(
--el-border-color-darker
);
border-radius
:
8px
;
align-items
:
center
;
justify-content
:
center
;
cursor
:
pointer
;
}
.spu-pic
{
position
:
relative
;
}
.del-icon
{
position
:
absolute
;
top
:
-10px
;
right
:
-10px
;
z-index
:
1
;
width
:
20px
!important
;
height
:
20px
!important
;
}
</
style
>
src/views/mall/promotion/point/components/PointTableSelect.vue
0 → 100644
View file @
887936cc
<
template
>
<Dialog
v-model=
"dialogVisible"
:appendToBody=
"true"
title=
"选择活动"
width=
"70%"
>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
ref=
"queryFormRef"
:inline=
"true"
:model=
"queryParams"
class=
"-mb-15px"
label-width=
"68px"
>
<el-form-item
label=
"活动状态"
prop=
"status"
>
<el-select
v-model=
"queryParams.status"
class=
"!w-240px"
clearable
placeholder=
"请选择活动状态"
>
<el-option
v-for=
"dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key=
"dict.value"
:label=
"dict.label"
:value=
"dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button
@
click=
"handleQuery"
>
<Icon
class=
"mr-5px"
icon=
"ep:search"
/>
搜索
</el-button>
<el-button
@
click=
"resetQuery"
>
<Icon
class=
"mr-5px"
icon=
"ep:refresh"
/>
重置
</el-button>
</el-form-item>
</el-form>
<el-table
v-loading=
"loading"
:data=
"list"
show-overflow-tooltip
>
<!-- 1. 多选模式(不能使用type="selection",Element会忽略Header插槽) -->
<el-table-column
v-if=
"multiple"
width=
"55"
>
<template
#
header
>
<el-checkbox
v-model=
"isCheckAll"
:indeterminate=
"isIndeterminate"
@
change=
"handleCheckAll"
/>
</
template
>
<
template
#
default=
"{ row }"
>
<el-checkbox
v-model=
"checkedStatus[row.id]"
@
change=
"(checked: boolean) => handleCheckOne(checked, row, true)"
/>
</
template
>
</el-table-column>
<!-- 2. 单选模式 -->
<el-table-column
v-else
label=
"#"
width=
"55"
>
<
template
#
default=
"{ row }"
>
<el-radio
v-model=
"selectedActivityId"
:value=
"row.id"
@
change=
"handleSingleSelected(row)"
>
<!-- 空格不能省略,是为了让单选框不显示label,如果不指定label不会有选中的效果 -->
</el-radio>
</
template
>
</el-table-column>
<el-table-column
label=
"活动编号"
min-width=
"80"
prop=
"id"
/>
<el-table-column
label=
"商品图片"
min-width=
"80"
prop=
"spuName"
>
<
template
#
default=
"scope"
>
<el-image
:preview-src-list=
"[scope.row.picUrl]"
:src=
"scope.row.picUrl"
class=
"h-40px w-40px"
preview-teleported
/>
</
template
>
</el-table-column>
<el-table-column
label=
"商品标题"
min-width=
"300"
prop=
"spuName"
/>
<el-table-column
:formatter=
"fenToYuanFormat"
label=
"原价"
min-width=
"100"
prop=
"marketPrice"
/>
<el-table-column
label=
"原价"
min-width=
"100"
prop=
"marketPrice"
/>
<el-table-column
align=
"center"
label=
"活动状态"
min-width=
"100"
prop=
"status"
>
<
template
#
default=
"scope"
>
<dict-tag
:type=
"DICT_TYPE.COMMON_STATUS"
:value=
"scope.row.status"
/>
</
template
>
</el-table-column>
<el-table-column
align=
"center"
label=
"库存"
min-width=
"80"
prop=
"stock"
/>
<el-table-column
align=
"center"
label=
"总库存"
min-width=
"80"
prop=
"totalStock"
/>
<el-table-column
align=
"center"
label=
"已兑换数量"
min-width=
"100"
prop=
"redeemedQuantity"
>
<
template
#
default=
"{ row }"
>
{{
getRedeemedQuantity
(
row
)
}}
</
template
>
</el-table-column>
<el-table-column
:formatter=
"dateFormatter"
align=
"center"
label=
"创建时间"
prop=
"createTime"
width=
"180px"
/>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit=
"queryParams.pageSize"
v-model:page=
"queryParams.pageNo"
:total=
"total"
@
pagination=
"getList"
/>
</ContentWrap>
<
template
v-if=
"multiple"
#
footer
>
<el-button
type=
"primary"
@
click=
"handleEmitChange"
>
确 定
</el-button>
<el-button
@
click=
"dialogVisible = false"
>
取 消
</el-button>
</
template
>
</Dialog>
</template>
<
script
lang=
"ts"
setup
>
import
{
propTypes
}
from
'@/utils/propTypes'
import
{
CHANGE_EVENT
}
from
'element-plus'
import
{
PointActivityApi
,
PointActivityVO
}
from
'@/api/mall/promotion/point'
import
{
fenToYuanFormat
}
from
'@/utils/formatter'
import
{
DICT_TYPE
,
getIntDictOptions
}
from
'@/utils/dict'
import
{
dateFormatter
}
from
'@/utils/formatTime'
/**
* 活动表格选择对话框
* 1. 单选模式:
* 1.1 点击表格左侧的单选框时,结束选择,并关闭对话框
* 1.2 再次打开时,保持选中状态
* 2. 多选模式:
* 2.1 点击表格左侧的多选框时,记录选中的活动
* 2.2 切换分页时,保持活动的选中状态
* 2.3 点击右下角的确定按钮时,结束选择,关闭对话框
* 2.4 再次打开时,保持选中状态
*/
defineOptions
({
name
:
'PointTableSelect'
})
defineProps
({
// 多选模式
multiple
:
propTypes
.
bool
.
def
(
false
)
})
// 列表的总页数
const
total
=
ref
(
0
)
// 列表的数据
const
list
=
ref
<
PointActivityVO
[]
>
([])
// 列表的加载中
const
loading
=
ref
(
false
)
// 弹窗的是否展示
const
dialogVisible
=
ref
(
false
)
// 查询参数
const
queryParams
=
ref
({
pageNo
:
1
,
pageSize
:
10
,
name
:
null
,
status
:
undefined
})
const
getRedeemedQuantity
=
computed
(()
=>
(
row
:
any
)
=>
(
row
.
totalStock
||
0
)
-
(
row
.
stock
||
0
))
// 获得商品已兑换数量
/** 打开弹窗 */
const
open
=
(
pointList
?:
PointActivityVO
[])
=>
{
// 重置
checkedActivities
.
value
=
[]
checkedStatus
.
value
=
{}
isCheckAll
.
value
=
false
isIndeterminate
.
value
=
false
// 处理已选中
if
(
pointList
&&
pointList
.
length
>
0
)
{
checkedActivities
.
value
=
[...
pointList
]
checkedStatus
.
value
=
Object
.
fromEntries
(
pointList
.
map
((
activityVO
)
=>
[
activityVO
.
id
,
true
]))
}
dialogVisible
.
value
=
true
resetQuery
()
}
// 提供 open 方法,用于打开弹窗
defineExpose
({
open
})
/** 查询列表 */
const
getList
=
async
()
=>
{
loading
.
value
=
true
try
{
const
data
=
await
PointActivityApi
.
getPointActivityPage
(
queryParams
.
value
)
list
.
value
=
data
.
list
total
.
value
=
data
.
total
// checkbox绑定undefined会有问题,需要给一个bool值
list
.
value
.
forEach
(
(
activityVO
)
=>
(
checkedStatus
.
value
[
activityVO
.
id
]
=
checkedStatus
.
value
[
activityVO
.
id
]
||
false
)
)
// 计算全选框状态
calculateIsCheckAll
()
}
finally
{
loading
.
value
=
false
}
}
/** 搜索按钮操作 */
const
handleQuery
=
()
=>
{
queryParams
.
value
.
pageNo
=
1
getList
()
}
/** 重置按钮操作 */
const
resetQuery
=
()
=>
{
queryParams
.
value
=
{
pageNo
:
1
,
pageSize
:
10
,
name
:
null
,
status
:
undefined
}
getList
()
}
// 是否全选
const
isCheckAll
=
ref
(
false
)
// 全选框是否处于中间状态:不是全部选中 && 任意一个选中
const
isIndeterminate
=
ref
(
false
)
// 选中的活动
const
checkedActivities
=
ref
<
PointActivityVO
[]
>
([])
// 选中状态:key为活动ID,value为是否选中
const
checkedStatus
=
ref
<
Record
<
string
,
boolean
>>
({})
// 选中的活动 activityId
const
selectedActivityId
=
ref
()
/** 单选中时触发 */
const
handleSingleSelected
=
(
pointActivityVO
:
PointActivityVO
)
=>
{
emits
(
CHANGE_EVENT
,
pointActivityVO
)
// 关闭弹窗
dialogVisible
.
value
=
false
// 记住上次选择的ID
selectedActivityId
.
value
=
pointActivityVO
.
id
}
/** 多选完成 */
const
handleEmitChange
=
()
=>
{
// 关闭弹窗
dialogVisible
.
value
=
false
emits
(
CHANGE_EVENT
,
[...
checkedActivities
.
value
])
}
/** 确认选择时的触发事件 */
const
emits
=
defineEmits
<
{
(
e
:
CHANGE_EVENT
,
v
:
PointActivityVO
|
PointActivityVO
[]
|
any
):
void
}
>
()
/** 全选/全不选 */
const
handleCheckAll
=
(
checked
:
boolean
)
=>
{
isCheckAll
.
value
=
checked
isIndeterminate
.
value
=
false
list
.
value
.
forEach
((
pointActivity
)
=>
handleCheckOne
(
checked
,
pointActivity
,
false
))
}
/**
* 选中一行
* @param checked 是否选中
* @param pointActivity 活动
* @param isCalcCheckAll 是否计算全选
*/
const
handleCheckOne
=
(
checked
:
boolean
,
pointActivity
:
PointActivityVO
,
isCalcCheckAll
:
boolean
)
=>
{
if
(
checked
)
{
checkedActivities
.
value
.
push
(
pointActivity
)
checkedStatus
.
value
[
pointActivity
.
id
]
=
true
}
else
{
const
index
=
findCheckedIndex
(
pointActivity
)
if
(
index
>
-
1
)
{
checkedActivities
.
value
.
splice
(
index
,
1
)
checkedStatus
.
value
[
pointActivity
.
id
]
=
false
isCheckAll
.
value
=
false
}
}
// 计算全选框状态
if
(
isCalcCheckAll
)
{
calculateIsCheckAll
()
}
}
// 查找活动在已选中活动列表中的索引
const
findCheckedIndex
=
(
activityVO
:
PointActivityVO
)
=>
checkedActivities
.
value
.
findIndex
((
item
)
=>
item
.
id
===
activityVO
.
id
)
// 计算全选框状态
const
calculateIsCheckAll
=
()
=>
{
isCheckAll
.
value
=
list
.
value
.
every
((
activityVO
)
=>
checkedStatus
.
value
[
activityVO
.
id
])
// 计算中间状态:不是全部选中 && 任意一个选中
isIndeterminate
.
value
=
!
isCheckAll
.
value
&&
list
.
value
.
some
((
activityVO
)
=>
checkedStatus
.
value
[
activityVO
.
id
])
}
</
script
>
src/views/mall/promotion/rewardActivity/RewardForm.vue
View file @
887936cc
...
...
@@ -56,7 +56,7 @@
label=
"分类"
prop=
"productCategoryIds"
>
<ProductCategorySelect
v-model=
"formData.productCategoryIds"
/>
<ProductCategorySelect
v-model=
"formData.productCategoryIds"
:multiple=
"true"
/>
</el-form-item>
<el-form-item
label=
"备注"
prop=
"remark"
>
<el-input
v-model=
"formData.remark"
placeholder=
"请输入备注"
/>
...
...
@@ -119,6 +119,9 @@ const open = async (type: string, id?: number) => {
// 规则分转元
data
.
rules
?.
forEach
((
item
:
any
)
=>
{
item
.
discountPrice
=
fenToYuan
(
item
.
discountPrice
||
0
)
if
(
data
.
conditionType
===
PromotionConditionTypeEnum
.
PRICE
.
type
)
{
item
.
limit
=
fenToYuan
(
item
.
limit
||
0
)
}
})
formData
.
value
=
data
// 获得商品范围
...
...
@@ -151,6 +154,9 @@ const submitForm = async () => {
// 规则元转分
data
.
rules
.
forEach
((
item
)
=>
{
item
.
discountPrice
=
yuanToFen
(
item
.
discountPrice
||
0
)
if
(
data
.
conditionType
===
PromotionConditionTypeEnum
.
PRICE
.
type
)
{
item
.
limit
=
yuanToFen
(
item
.
limit
||
0
)
}
})
// 设置商品范围
setProductScopeValues
(
data
)
...
...
@@ -188,7 +194,7 @@ const getProductScope = async () => {
case
PromotionProductScopeEnum
.
CATEGORY
.
scope
:
await
nextTick
()
let
productCategoryIds
=
formData
.
value
.
productScopeValues
as
any
if
(
Array
.
isArray
(
productCategoryIds
)
&&
productCategoryIds
.
length
>
0
)
{
if
(
Array
.
isArray
(
productCategoryIds
)
&&
productCategoryIds
.
length
===
1
)
{
// 单选时使用数组不能反显
productCategoryIds
=
productCategoryIds
[
0
]
}
...
...
src/views/mall/promotion/rewardActivity/components/RewardRule.vue
View file @
887936cc
...
...
@@ -10,14 +10,25 @@
<el-form
ref=
"formRef"
:model=
"rule"
>
<el-form-item
label=
"优惠门槛:"
label-width=
"100px"
prop=
"limit"
>
满
<el-input-number
v-if=
"PromotionConditionTypeEnum.PRICE.type === formData.conditionType"
v-model=
"rule.limit"
:min=
"0"
:precision=
"2"
:step=
"0.1"
class=
"w-150px! p-x-20px!"
placeholder=
""
type=
"number"
controls-position=
"right"
/>
<el-input
v-else
v-model=
"rule.limit"
:min=
"0"
class=
"w-150px! p-x-20px!"
placeholder=
""
type=
"number"
/>
<!-- TODO @puhui999:走字典数据? -->
{{
PromotionConditionTypeEnum
.
PRICE
.
type
===
formData
.
conditionType
?
'元'
:
'件'
}}
</el-form-item>
<el-form-item
label=
"优惠内容:"
label-width=
"100px"
>
...
...
src/views/mall/promotion/rewardActivity/index.vue
View file @
887936cc
...
...
@@ -27,7 +27,7 @@
placeholder=
"请选择活动状态"
>
<el-option
v-for=
"dict in getIntDictOptions(DICT_TYPE.
PROMOTION_ACTIVITY
_STATUS)"
v-for=
"dict in getIntDictOptions(DICT_TYPE.
COMMON
_STATUS)"
:key=
"dict.value"
:label=
"dict.label"
:value=
"dict.value"
...
...
@@ -55,7 +55,7 @@
重置
</el-button>
<el-button
v-hasPermi=
"['pro
duct:brand
:create']"
v-hasPermi=
"['pro
motion:reward-activity
:create']"
plain
type=
"primary"
@
click=
"openForm('create')"
...
...
@@ -71,6 +71,11 @@
<ContentWrap>
<el-table
v-loading=
"loading"
:data=
"list"
default-expand-all
row-key=
"id"
>
<el-table-column
label=
"活动名称"
prop=
"name"
/>
<el-table-column
label=
"活动范围"
prop=
"productScope"
>
<template
#
default=
"scope"
>
<dict-tag
:type=
"DICT_TYPE.PROMOTION_PRODUCT_SCOPE"
:value=
"scope.row.productScope"
/>
</
template
>
</el-table-column>
<el-table-column
:formatter=
"dateFormatter"
align=
"center"
...
...
@@ -85,7 +90,7 @@
/>
<el-table-column
align=
"center"
label=
"状态"
prop=
"status"
>
<
template
#
default=
"scope"
>
<dict-tag
:type=
"DICT_TYPE.
PROMOTION_ACTIVITY
_STATUS"
:value=
"scope.row.status"
/>
<dict-tag
:type=
"DICT_TYPE.
COMMON
_STATUS"
:value=
"scope.row.status"
/>
</
template
>
</el-table-column>
<el-table-column
...
...
@@ -98,7 +103,7 @@
<el-table-column
align=
"center"
label=
"操作"
>
<
template
#
default=
"scope"
>
<el-button
v-hasPermi=
"['pro
duct:brand
:update']"
v-hasPermi=
"['pro
motion:reward-activity
:update']"
link
type=
"primary"
@
click=
"openForm('update', scope.row.id)"
...
...
@@ -106,7 +111,16 @@
编辑
</el-button>
<el-button
v-hasPermi=
"['product:brand:delete']"
v-if=
"scope.row.status === 0"
v-hasPermi=
"['promotion:reward-activity:close']"
link
type=
"danger"
@
click=
"handleClose(scope.row.id)"
>
关闭
</el-button>
<el-button
v-hasPermi=
"['promotion:reward-activity:delete']"
link
type=
"danger"
@
click=
"handleDelete(scope.row.id)"
...
...
@@ -180,6 +194,19 @@ const openForm = (type: string, id?: number) => {
formRef
.
value
?.
open
(
type
,
id
)
}
/** 关闭按钮操作 */
const
handleClose
=
async
(
id
:
number
)
=>
{
try
{
// 关闭的二次确认
await
message
.
confirm
(
'确认关闭该满减活动吗?'
)
// 发起关闭
await
RewardActivityApi
.
closeRewardActivity
(
id
)
message
.
success
(
'关闭成功'
)
// 刷新列表
await
getList
()
}
catch
{}
}
/** 删除按钮操作 */
const
handleDelete
=
async
(
id
:
number
)
=>
{
try
{
...
...
src/views/mp/components/wx-account-select/main.vue
View file @
887936cc
...
...
@@ -6,6 +6,11 @@
<
script
lang=
"ts"
setup
>
import
*
as
MpAccountApi
from
'@/api/mp/account'
import
{
useTagsViewStore
}
from
'@/store/modules/tagsView'
const
message
=
useMessage
()
// 消息弹窗
const
{
delView
}
=
useTagsViewStore
()
// 视图操作
const
{
push
,
currentRoute
}
=
useRouter
()
// 路由
defineOptions
({
name
:
'WxAccountSelect'
})
...
...
@@ -22,6 +27,12 @@ const emit = defineEmits<{
const
handleQuery
=
async
()
=>
{
accountList
.
value
=
await
MpAccountApi
.
getSimpleAccountList
()
if
(
accountList
.
value
.
length
==
0
)
{
message
.
error
(
'未配置公众号,请在【公众号管理 -> 账号管理】菜单,进行配置'
)
delView
(
unref
(
currentRoute
))
await
push
({
name
:
'MpAccount'
})
return
}
// 默认选中第一个
if
(
accountList
.
value
.
length
>
0
)
{
account
.
id
=
accountList
.
value
[
0
].
id
...
...
src/views/mp/statistics/index.vue
View file @
887936cc
...
...
@@ -3,14 +3,7 @@
<ContentWrap>
<el-form
class=
"-mb-15px"
ref=
"queryForm"
:inline=
"true"
label-width=
"68px"
>
<el-form-item
label=
"公众号"
prop=
"accountId"
>
<el-select
v-model=
"accountId"
@
change=
"getSummary"
class=
"!w-240px"
>
<el-option
v-for=
"item in accountList"
:key=
"item.id"
:label=
"item.name"
:value=
"item.id"
/>
</el-select>
<WxAccountSelect
@
change=
"onAccountChanged"
/>
</el-form-item>
<el-form-item
label=
"时间范围"
prop=
"dateRange"
>
<el-date-picker
...
...
@@ -76,7 +69,7 @@
<
script
lang=
"ts"
setup
>
import
{
formatDate
,
addTime
,
betweenDay
,
beginOfDay
,
endOfDay
}
from
'@/utils/formatTime'
import
*
as
StatisticsApi
from
'@/api/mp/statistics'
import
*
as
MpAccountApi
from
'@/api/mp/accoun
t'
import
WxAccountSelect
from
'@/views/mp/components/wx-account-selec
t'
defineOptions
({
name
:
'MpStatistics'
})
...
...
@@ -88,7 +81,6 @@ const dateRange = ref([
endOfDay
(
new
Date
(
new
Date
().
getTime
()
-
3600
*
1000
*
24
))
])
const
accountId
=
ref
(
-
1
)
// 选中的公众号编号
const
accountList
=
ref
<
MpAccountApi
.
AccountVO
[]
>
([])
// 公众号账号列表
const
xAxisDate
=
ref
([]
as
any
[])
// X 轴的日期范围
// 用户增减数据图表配置项
...
...
@@ -230,13 +222,10 @@ const interfaceSummaryOption = reactive({
]
})
/** 加载公众号账号的列表 */
const
getAccountList
=
async
()
=>
{
accountList
.
value
=
await
MpAccountApi
.
getSimpleAccountList
()
// 默认选中第一个
if
(
accountList
.
value
.
length
>
0
)
{
accountId
.
value
=
accountList
.
value
[
0
].
id
!
}
/** 侦听公众号变化 **/
const
onAccountChanged
=
(
id
:
number
)
=>
{
accountId
.
value
=
id
getSummary
()
}
/** 加载数据 */
...
...
@@ -357,12 +346,4 @@ const interfaceSummaryChart = async () => {
})
}
catch
{}
}
/** 初始化 */
onMounted
(
async
()
=>
{
// 获取公众号下拉列表
await
getAccountList
()
// 加载数据
getSummary
()
})
</
script
>
src/views/pay/cashier/index.vue
View file @
887936cc
...
...
@@ -231,7 +231,7 @@ const getDetail = async () => {
goReturnUrl
(
'cancel'
)
return
}
const
data
=
await
PayOrderApi
.
getOrder
(
id
.
value
)
const
data
=
await
PayOrderApi
.
getOrder
(
id
.
value
,
true
)
payOrder
.
value
=
data
// 1.2 无法查询到支付信息
if
(
!
data
)
{
...
...
types/env.d.ts
View file @
887936cc
...
...
@@ -19,7 +19,6 @@ interface ImportMetaEnv {
readonly
VITE_APP_DEFAULT_LOGIN_PASSWORD
:
string
readonly
VITE_APP_DOCALERT_ENABLE
:
string
readonly
VITE_BASE_URL
:
string
readonly
VITE_UPLOAD_URL
:
string
readonly
VITE_API_URL
:
string
readonly
VITE_BASE_PATH
:
string
readonly
VITE_DROP_DEBUGGER
:
string
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment