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
e106fb73
authored
Nov 28, 2023
by
dhb52
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'yudao/master' into dhb52_master
parents
c2038b95
57b20f18
Hide whitespace changes
Inline
Side-by-side
Showing
41 changed files
with
1439 additions
and
803 deletions
+1439
-803
.env.base
+2
-2
src/api/crm/contact/index.ts
+8
-6
src/api/crm/customer/index.ts
+5
-0
src/api/crm/permission/index.ts
+3
-3
src/api/mall/promotion/coupon/couponTemplate.ts
+7
-0
src/api/mp/user/index.ts
+1
-1
src/api/system/notice/index.ts
+5
-0
src/components/DiyEditor/components/mobile/CouponCard/component.tsx
+78
-0
src/components/DiyEditor/components/mobile/CouponCard/config.ts
+47
-0
src/components/DiyEditor/components/mobile/CouponCard/index.vue
+142
-0
src/components/DiyEditor/components/mobile/CouponCard/property.vue
+104
-0
src/components/DiyEditor/components/mobile/ProductCard/config.ts
+1
-1
src/components/DiyEditor/components/mobile/ProductList/config.ts
+64
-0
src/components/DiyEditor/components/mobile/ProductList/index.vue
+131
-0
src/components/DiyEditor/components/mobile/ProductList/property.vue
+99
-0
src/components/DiyEditor/util.ts
+7
-3
src/components/RouterSearch/index.vue
+31
-1
src/components/ShortcutDateRangePicker/index.vue
+0
-5
src/layout/components/ToolHeader.vue
+5
-0
src/locales/zh-CN.ts
+2
-1
src/store/modules/app.ts
+2
-0
src/utils/dict.ts
+151
-147
src/utils/formatTime.ts
+4
-1
src/views/crm/components/CrmPermissionForm.vue
+10
-4
src/views/crm/components/CrmPermissionList.vue
+31
-49
src/views/crm/components/index.ts
+3
-5
src/views/crm/contact/ContactForm.vue
+127
-240
src/views/crm/contact/detail/ContactDetails.vue
+15
-25
src/views/crm/contact/detail/index.vue
+7
-22
src/views/crm/contact/index.vue
+9
-34
src/views/crm/customer/detail/index.vue
+27
-14
src/views/crm/customer/index.vue
+8
-9
src/views/infra/webSocket/index.vue
+102
-40
src/views/mall/product/comment/CommentForm.vue
+5
-26
src/views/mall/product/comment/index.vue
+1
-1
src/views/mall/product/spu/components/SpuShowcase.vue
+41
-25
src/views/mall/product/spu/components/SpuTableSelect.vue
+125
-73
src/views/mall/promotion/coupon/components/CouponSelect.vue
+3
-7
src/views/mall/promotion/coupon/template/CouponTemplateForm.vue
+3
-58
src/views/mall/statistics/trade/index.vue
+9
-0
src/views/system/notice/index.vue
+14
-0
No files found.
.env.base
View file @
e106fb73
...
...
@@ -4,10 +4,10 @@ NODE_ENV=development
VITE_DEV=true
# 请求路径
VITE_BASE_URL='http://
localhost
:48080'
VITE_BASE_URL='http://
127.0.0.1
:48080'
# 上传路径
VITE_UPLOAD_URL='http://
localhost
:48080/admin-api/infra/file/upload'
VITE_UPLOAD_URL='http://
127.0.0.1
:48080/admin-api/infra/file/upload'
# 接口前缀
VITE_API_BASEPATH=/dev-api
...
...
src/api/crm/contact/index.ts
View file @
e106fb73
/*
* @Author: zyna
* @Date: 2023-11-05 13:34:41
* @LastEditTime: 2023-11-
11 16:20:19
* @LastEditTime: 2023-11-
26 20:47:04
* @FilePath: \yudao-ui-admin-vue3\src\api\crm\contact\index.ts
* @Description:
*/
...
...
@@ -22,13 +22,15 @@ export interface ContactVO {
id
:
number
parentId
:
number
qq
:
number
we
b
chat
:
string
wechat
:
string
sex
:
number
policyMakers
:
boolean
master
:
boolean
creatorName
:
string
updateTime
?:
Date
createTime
?:
Date
customerName
:
string
customerName
:
string
,
areaName
:
string
,
ownerUserName
:
string
}
// 查询crm联系人列表
...
...
@@ -60,6 +62,6 @@ export const deleteContact = async (id: number) => {
export
const
exportContact
=
async
(
params
)
=>
{
return
await
request
.
download
({
url
:
`/crm/contact/export-excel`
,
params
})
}
export
const
simpleAll
l
ist
=
async
()
=>
{
return
await
request
.
get
({
url
:
`/crm/contact/simple
All
list`
})
export
const
simpleAll
L
ist
=
async
()
=>
{
return
await
request
.
get
({
url
:
`/crm/contact/simple
-all-
list`
})
}
src/api/crm/customer/index.ts
View file @
e106fb73
...
...
@@ -62,3 +62,7 @@ export const deleteCustomer = async (id: number) => {
export
const
exportCustomer
=
async
(
params
)
=>
{
return
await
request
.
download
({
url
:
`/crm/customer/export-excel`
,
params
})
}
//客户列表
export
const
queryAllList
=
async
()
=>
{
return
await
request
.
get
({
url
:
`/crm/customer/query-all-list`
})
}
\ No newline at end of file
src/api/crm/permission/index.ts
View file @
e106fb73
...
...
@@ -6,9 +6,9 @@ export interface PermissionVO {
bizType
:
number
|
undefined
// Crm 类型
bizId
:
number
|
undefined
// Crm 类型数据编号
level
:
number
|
undefined
// 权限级别
deptName
?:
string
// 部门名称
// 岗位名称数组 TODO @puhui999:数组?
deptName
?:
string
// 部门名称
nickname
?:
string
// 用户昵称
postNames
?:
string
// 岗位名称数组 TODO @puhui999:数组?
postNames
?:
string
[]
// 岗位名称数组
createTime
?:
Date
}
...
...
@@ -19,7 +19,7 @@ export const getPermissionList = async (params) => {
// 新增团队成员
export
const
createPermission
=
async
(
data
:
PermissionVO
)
=>
{
return
await
request
.
post
({
url
:
`/crm/permission/
add
`
,
data
})
return
await
request
.
post
({
url
:
`/crm/permission/
create
`
,
data
})
}
// 修改团队成员权限级别
...
...
src/api/mall/promotion/coupon/couponTemplate.ts
View file @
e106fb73
...
...
@@ -73,6 +73,13 @@ export function getCouponTemplatePage(params: PageParam) {
})
}
// 获得优惠劵模板分页
export
function
getCouponTemplateList
(
ids
:
number
[])
{
return
request
.
get
({
url
:
`/promotion/coupon-template/list?ids=
${
ids
}
`
})
}
// 导出优惠劵模板 Excel
export
function
exportCouponTemplateExcel
(
params
:
PageParam
)
{
return
request
.
get
({
...
...
src/api/mp/user/index.ts
View file @
e106fb73
...
...
@@ -26,6 +26,6 @@ export const getUserPage = (query) => {
// 同步公众号粉丝
export
const
syncUser
=
(
accountId
)
=>
{
return
request
.
post
({
url
:
'/mp/
tag
/sync?accountId='
+
accountId
url
:
'/mp/
user
/sync?accountId='
+
accountId
})
}
src/api/system/notice/index.ts
View file @
e106fb73
...
...
@@ -35,3 +35,8 @@ export const updateNotice = (data: NoticeVO) => {
export
const
deleteNotice
=
(
id
:
number
)
=>
{
return
request
.
delete
({
url
:
'/system/notice/delete?id='
+
id
})
}
// 推送公告
export
const
pushNotice
=
(
id
:
number
)
=>
{
return
request
.
post
({
url
:
'/system/notice/push?id='
+
id
})
}
src/components/DiyEditor/components/mobile/CouponCard/component.tsx
0 → 100644
View file @
e106fb73
import
*
as
CouponTemplateApi
from
'@/api/mall/promotion/coupon/couponTemplate'
import
{
CouponTemplateValidityTypeEnum
,
PromotionDiscountTypeEnum
}
from
'@/utils/constants'
import
{
floatToFixed2
}
from
'@/utils'
import
{
formatDate
}
from
'@/utils/formatTime'
// 优惠值
export
const
CouponDiscount
=
defineComponent
({
name
:
'CouponDiscount'
,
props
:
{
coupon
:
{
type
:
CouponTemplateApi
.
CouponTemplateVO
}
},
setup
(
props
)
{
const
coupon
=
props
.
coupon
as
CouponTemplateApi
.
CouponTemplateVO
// 折扣
let
value
=
coupon
.
discountPercent
+
''
let
suffix
=
' 折'
// 满减
if
(
coupon
.
discountType
===
PromotionDiscountTypeEnum
.
PRICE
.
type
)
{
value
=
floatToFixed2
(
coupon
.
discountPrice
)
suffix
=
' 元'
}
return
()
=>
(
<
div
>
<
span
class=
{
'text-20px font-bold'
}
>
{
value
}
</
span
>
<
span
>
{
suffix
}
</
span
>
</
div
>
)
}
})
// 优惠描述
export
const
CouponDiscountDesc
=
defineComponent
({
name
:
'CouponDiscountDesc'
,
props
:
{
coupon
:
{
type
:
CouponTemplateApi
.
CouponTemplateVO
}
},
setup
(
props
)
{
const
coupon
=
props
.
coupon
as
CouponTemplateApi
.
CouponTemplateVO
// 使用条件
const
useCondition
=
coupon
.
usePrice
>
0
?
`满
${
floatToFixed2
(
coupon
.
usePrice
)}
元,`
:
''
// 优惠描述
const
discountDesc
=
coupon
.
discountType
===
PromotionDiscountTypeEnum
.
PRICE
.
type
?
`减
${
floatToFixed2
(
coupon
.
discountPrice
)}
元`
:
`打
${
coupon
.
discountPercent
}
折`
return
()
=>
(
<
div
>
<
span
>
{
useCondition
}
</
span
>
<
span
>
{
discountDesc
}
</
span
>
</
div
>
)
}
})
// 有效期
export
const
CouponValidTerm
=
defineComponent
({
name
:
'CouponValidTerm'
,
props
:
{
coupon
:
{
type
:
CouponTemplateApi
.
CouponTemplateVO
}
},
setup
(
props
)
{
const
coupon
=
props
.
coupon
as
CouponTemplateApi
.
CouponTemplateVO
const
text
=
coupon
.
validityType
===
CouponTemplateValidityTypeEnum
.
DATE
.
type
?
`有效期:
${
formatDate
(
coupon
.
validStartTime
,
'YYYY-MM-DD'
)}
至
${
formatDate
(
coupon
.
validEndTime
,
'YYYY-MM-DD'
)}
`
:
`领取后第
${
coupon
.
fixedStartTerm
}
-
${
coupon
.
fixedEndTerm
}
天内可用`
return
()
=>
<
div
>
{
text
}
</
div
>
}
})
src/components/DiyEditor/components/mobile/CouponCard/config.ts
0 → 100644
View file @
e106fb73
import
{
ComponentStyle
,
DiyComponent
}
from
'@/components/DiyEditor/util'
/** 商品卡片属性 */
export
interface
CouponCardProperty
{
// 列数
columns
:
number
// 背景图
bgImg
:
string
// 文字颜色
textColor
:
string
// 按钮样式
button
:
{
// 颜色
color
:
string
// 背景颜色
bgColor
:
string
}
// 间距
space
:
number
// 优惠券编号列表
couponIds
:
number
[]
// 组件样式
style
:
ComponentStyle
}
// 定义组件
export
const
component
=
{
id
:
'CouponCard'
,
name
:
'优惠券'
,
icon
:
'ep:ticket'
,
property
:
{
columns
:
1
,
bgImg
:
''
,
textColor
:
'#E9B461'
,
button
:
{
color
:
'#434343'
,
bgColor
:
''
},
space
:
0
,
couponIds
:
[],
style
:
{
bgType
:
'color'
,
bgColor
:
''
,
marginBottom
:
8
}
as
ComponentStyle
}
}
as
DiyComponent
<
CouponCardProperty
>
src/components/DiyEditor/components/mobile/CouponCard/index.vue
0 → 100644
View file @
e106fb73
<
template
>
<el-scrollbar
class=
"z-1 min-h-30px"
wrap-class=
"w-full"
ref=
"containerRef"
>
<div
class=
"flex flex-row text-12px"
:style=
"
{
gap: `${property.space}px`,
width: scrollbarWidth
}"
>
<div
class=
"box-content"
:style=
"
{
background: property.bgImg
? `url(${property.bgImg}) 100% center / 100% 100% no-repeat`
: '#fff',
width: `${couponWidth}px`,
color: property.textColor
}"
v-for="(coupon, index) in couponList"
:key="index"
>
<!-- 布局1:1列-->
<div
v-if=
"property.columns === 1"
class=
"m-l-16px flex flex-row justify-between p-8px"
>
<div
class=
"flex flex-col justify-evenly gap-4px"
>
<!-- 优惠值 -->
<CouponDiscount
:coupon=
"coupon"
/>
<!-- 优惠描述 -->
<CouponDiscountDesc
:coupon=
"coupon"
/>
<!-- 有效期 -->
<CouponValidTerm
:coupon=
"coupon"
/>
</div>
<div
class=
"flex flex-col justify-evenly"
>
<div
class=
"rounded-20px p-x-8px p-y-2px"
:style=
"
{
color: property.button.color,
background: property.button.bgColor
}"
>
立即领取
</div>
</div>
</div>
<!-- 布局2:2列-->
<div
v-else-if=
"property.columns === 2"
class=
"m-l-16px flex flex-row justify-between p-8px"
>
<div
class=
"flex flex-col justify-evenly gap-4px"
>
<!-- 优惠值 -->
<CouponDiscount
:coupon=
"coupon"
/>
<div>
{{
coupon
.
name
}}
</div>
</div>
<div
class=
"flex flex-col"
>
<div
class=
"h-full w-20px rounded-20px p-x-2px p-y-8px text-center"
:style=
"
{
color: property.button.color,
background: property.button.bgColor
}"
>
立即领取
</div>
</div>
</div>
<!-- 布局3:3列-->
<div
v-else
class=
"flex flex-col items-center justify-around gap-4px p-4px"
>
<!-- 优惠值 -->
<CouponDiscount
:coupon=
"coupon"
/>
<div>
{{
coupon
.
name
}}
</div>
<div
class=
"rounded-20px p-x-8px p-y-2px"
:style=
"
{
color: property.button.color,
background: property.button.bgColor
}"
>
立即领取
</div>
</div>
</div>
</div>
</el-scrollbar>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
CouponCardProperty
}
from
'./config'
import
*
as
CouponTemplateApi
from
'@/api/mall/promotion/coupon/couponTemplate'
import
{
CouponDiscount
}
from
'./component'
import
{
CouponDiscountDesc
,
CouponValidTerm
}
from
'@/components/DiyEditor/components/mobile/CouponCard/component'
/** 商品卡片 */
defineOptions
({
name
:
'CouponCard'
})
// 定义属性
const
props
=
defineProps
<
{
property
:
CouponCardProperty
}
>
()
// 商品列表
const
couponList
=
ref
<
CouponTemplateApi
.
CouponTemplateVO
[]
>
([])
watch
(
()
=>
props
.
property
.
couponIds
,
async
()
=>
{
if
(
props
.
property
.
couponIds
?.
length
>
0
)
{
couponList
.
value
=
await
CouponTemplateApi
.
getCouponTemplateList
(
props
.
property
.
couponIds
)
}
},
{
immediate
:
true
,
deep
:
true
}
)
// 手机宽度
const
phoneWidth
=
ref
(
375
)
// 容器
const
containerRef
=
ref
()
// 滚动条宽度
const
scrollbarWidth
=
ref
(
'100%'
)
// 优惠券的宽度
const
couponWidth
=
ref
(
375
)
// 计算布局参数
watch
(
()
=>
[
props
.
property
,
phoneWidth
,
couponList
.
value
.
length
],
()
=>
{
// 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
couponWidth
.
value
=
(
phoneWidth
.
value
*
0.95
-
props
.
property
.
space
*
(
props
.
property
.
columns
-
1
))
/
props
.
property
.
columns
// 显示滚动条
scrollbarWidth
.
value
=
`
${
couponWidth
.
value
*
couponList
.
value
.
length
+
props
.
property
.
space
*
(
couponList
.
value
.
length
-
1
)
}
px`
},
{
immediate
:
true
,
deep
:
true
}
)
onMounted
(()
=>
{
// 提取手机宽度
phoneWidth
.
value
=
containerRef
.
value
?.
wrapRef
?.
offsetWidth
||
375
})
</
script
>
<
style
scoped
lang=
"scss"
></
style
>
src/components/DiyEditor/components/mobile/CouponCard/property.vue
0 → 100644
View file @
e106fb73
<
template
>
<ComponentContainerProperty
v-model=
"formData.style"
>
<el-form
label-width=
"80px"
:model=
"formData"
>
<el-card
header=
"优惠券列表"
class=
"property-group"
shadow=
"never"
>
<div
v-for=
"(coupon, index) in couponList"
:key=
"index"
class=
"flex items-center justify-between"
>
<el-text
size=
"large"
truncated
>
{{
coupon
.
name
}}
</el-text>
<el-text
type=
"info"
truncated
>
<span
v-if=
"coupon.usePrice > 0"
>
满
{{
floatToFixed2
(
coupon
.
usePrice
)
}}
元,
</span>
<span
v-if=
"coupon.discountType === PromotionDiscountTypeEnum.PRICE.type"
>
减
{{
floatToFixed2
(
coupon
.
discountPrice
)
}}
元
</span>
<span
v-else
>
打
{{
coupon
.
discountPercent
}}
折
</span>
</el-text>
</div>
<el-form-item
label-width=
"0"
>
<el-button
@
click=
"handleAddCoupon"
type=
"primary"
plain
class=
"m-t-8px w-full"
>
<Icon
icon=
"ep:plus"
class=
"mr-5px"
/>
添加
</el-button>
</el-form-item>
</el-card>
<el-card
header=
"优惠券样式"
class=
"property-group"
shadow=
"never"
>
<el-form-item
label=
"列数"
prop=
"type"
>
<el-radio-group
v-model=
"formData.columns"
>
<el-tooltip
class=
"item"
content=
"一列"
placement=
"bottom"
>
<el-radio-button
:label=
"1"
>
<Icon
icon=
"fluent:text-column-one-24-filled"
/>
</el-radio-button>
</el-tooltip>
<el-tooltip
class=
"item"
content=
"二列"
placement=
"bottom"
>
<el-radio-button
:label=
"2"
>
<Icon
icon=
"fluent:text-column-two-24-filled"
/>
</el-radio-button>
</el-tooltip>
<el-tooltip
class=
"item"
content=
"三列"
placement=
"bottom"
>
<el-radio-button
:label=
"3"
>
<Icon
icon=
"fluent:text-column-three-24-filled"
/>
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item
label=
"背景图片"
prop=
"bgImg"
>
<UploadImg
v-model=
"formData.bgImg"
height=
"80px"
width=
"100%"
class=
"min-w-160px"
/>
</el-form-item>
<el-form-item
label=
"文字颜色"
prop=
"textColor"
>
<ColorInput
v-model=
"formData.textColor"
/>
</el-form-item>
<el-form-item
label=
"按钮背景"
prop=
"button.bgColor"
>
<ColorInput
v-model=
"formData.button.bgColor"
/>
</el-form-item>
<el-form-item
label=
"按钮文字"
prop=
"button.color"
>
<ColorInput
v-model=
"formData.button.color"
/>
</el-form-item>
<el-form-item
label=
"间隔"
prop=
"space"
>
<el-slider
v-model=
"formData.space"
:max=
"100"
:min=
"0"
show-input
input-size=
"small"
:show-input-controls=
"false"
/>
</el-form-item>
</el-card>
</el-form>
</ComponentContainerProperty>
<!-- 优惠券选择 -->
<CouponSelect
ref=
"couponSelectDialog"
v-model:multiple-selection=
"couponList"
/>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
CouponCardProperty
}
from
'./config'
import
{
usePropertyForm
}
from
'@/components/DiyEditor/util'
import
*
as
CouponTemplateApi
from
'@/api/mall/promotion/coupon/couponTemplate'
import
{
floatToFixed2
}
from
'@/utils'
import
{
PromotionDiscountTypeEnum
}
from
'@/utils/constants'
import
CouponSelect
from
'@/views/mall/promotion/coupon/components/CouponSelect.vue'
// 优惠券卡片属性面板
defineOptions
({
name
:
'CouponCardProperty'
})
const
props
=
defineProps
<
{
modelValue
:
CouponCardProperty
}
>
()
const
emit
=
defineEmits
([
'update:modelValue'
])
const
{
formData
}
=
usePropertyForm
(
props
.
modelValue
,
emit
)
// 优惠券列表
const
couponList
=
ref
<
CouponTemplateApi
.
CouponTemplateVO
[]
>
([])
const
couponSelectDialog
=
ref
()
// 添加优惠券
const
handleAddCoupon
=
()
=>
{
couponSelectDialog
.
value
.
open
()
}
watch
(
()
=>
couponList
.
value
,
()
=>
{
formData
.
value
.
couponIds
=
couponList
.
value
.
map
((
coupon
)
=>
coupon
.
id
)
}
)
</
script
>
<
style
scoped
lang=
"scss"
></
style
>
src/components/DiyEditor/components/mobile/ProductCard/config.ts
View file @
e106fb73
...
...
@@ -62,7 +62,7 @@ export interface ProductCardFieldProperty {
export
const
component
=
{
id
:
'ProductCard'
,
name
:
'商品卡片'
,
icon
:
'
system-uicons:carousel
'
,
icon
:
'
fluent:text-column-two-left-24-filled
'
,
property
:
{
layoutType
:
'oneColBigImg'
,
fields
:
{
...
...
src/components/DiyEditor/components/mobile/ProductList/config.ts
0 → 100644
View file @
e106fb73
import
{
ComponentStyle
,
DiyComponent
}
from
'@/components/DiyEditor/util'
/** 商品卡片属性 */
export
interface
ProductListProperty
{
// 布局类型:双列 | 三列 | 水平滑动
layoutType
:
'twoCol'
|
'threeCol'
|
'horizSwiper'
// 商品字段
fields
:
{
// 商品名称
name
:
ProductListFieldProperty
// 商品价格
price
:
ProductListFieldProperty
}
// 角标
badge
:
{
// 是否显示
show
:
boolean
// 角标图片
imgUrl
:
string
}
// 上圆角
borderRadiusTop
:
number
// 下圆角
borderRadiusBottom
:
number
// 间距
space
:
number
// 商品编号列表
spuIds
:
number
[]
// 组件样式
style
:
ComponentStyle
}
// 商品字段
export
interface
ProductListFieldProperty
{
// 是否显示
show
:
boolean
// 颜色
color
:
string
}
// 定义组件
export
const
component
=
{
id
:
'ProductList'
,
name
:
'商品栏'
,
icon
:
'fluent:text-column-two-24-filled'
,
property
:
{
layoutType
:
'twoCol'
,
fields
:
{
name
:
{
show
:
true
,
color
:
'#000'
},
price
:
{
show
:
true
,
color
:
'#ff3000'
}
},
badge
:
{
show
:
false
,
imgUrl
:
''
},
borderRadiusTop
:
8
,
borderRadiusBottom
:
8
,
space
:
8
,
spuIds
:
[],
style
:
{
bgType
:
'color'
,
bgColor
:
''
,
marginLeft
:
8
,
marginRight
:
8
,
marginBottom
:
8
}
as
ComponentStyle
}
}
as
DiyComponent
<
ProductListProperty
>
src/components/DiyEditor/components/mobile/ProductList/index.vue
0 → 100644
View file @
e106fb73
<
template
>
<el-scrollbar
class=
"z-1 min-h-30px"
wrap-class=
"w-full"
ref=
"containerRef"
>
<!-- 商品网格 -->
<div
class=
"grid overflow-x-auto"
:style=
"
{
gridGap: `${property.space}px`,
gridTemplateColumns,
width: scrollbarWidth
}"
>
<!-- 商品 -->
<div
class=
"relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
:style=
"
{
borderTopLeftRadius: `${property.borderRadiusTop}px`,
borderTopRightRadius: `${property.borderRadiusTop}px`,
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
borderBottomRightRadius: `${property.borderRadiusBottom}px`
}"
v-for="(spu, index) in spuList"
:key="index"
>
<!-- 角标 -->
<div
v-if=
"property.badge.show"
class=
"absolute left-0 top-0 z-1 items-center justify-center"
>
<el-image
fit=
"cover"
:src=
"property.badge.imgUrl"
class=
"h-26px w-38px"
/>
</div>
<!-- 商品封面图 -->
<el-image
fit=
"cover"
:src=
"spu.picUrl"
:style=
"
{ width: imageSize, height: imageSize }" />
<div
:class=
"[
'flex flex-col gap-8px p-8px box-border',
{
'w-[calc(100%-64px)]': columns === 2,
'w-full': columns === 3
}
]"
>
<!-- 商品名称 -->
<div
v-if=
"property.fields.name.show"
class=
"truncate text-12px"
:style=
"
{ color: property.fields.name.color }"
>
{{
spu
.
name
}}
</div>
<div>
<!-- 商品价格 -->
<span
v-if=
"property.fields.price.show"
class=
"text-12px"
:style=
"
{ color: property.fields.price.color }"
>
¥
{{
spu
.
price
}}
</span>
</div>
</div>
</div>
</div>
</el-scrollbar>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
ProductListProperty
}
from
'./config'
import
*
as
ProductSpuApi
from
'@/api/mall/product/spu'
/** 商品卡片 */
defineOptions
({
name
:
'ProductList'
})
// 定义属性
const
props
=
defineProps
<
{
property
:
ProductListProperty
}
>
()
// 商品列表
const
spuList
=
ref
<
ProductSpuApi
.
Spu
[]
>
([])
watch
(
()
=>
props
.
property
.
spuIds
,
async
()
=>
{
spuList
.
value
=
await
ProductSpuApi
.
getSpuDetailList
(
props
.
property
.
spuIds
)
},
{
immediate
:
true
,
deep
:
true
}
)
// 手机宽度
const
phoneWidth
=
ref
(
375
)
// 容器
const
containerRef
=
ref
()
// 商品的列数
const
columns
=
ref
(
2
)
// 滚动条宽度
const
scrollbarWidth
=
ref
(
'100%'
)
// 商品图大小
const
imageSize
=
ref
(
'0'
)
// 商品网络列数
const
gridTemplateColumns
=
ref
(
''
)
// 计算布局参数
watch
(
()
=>
[
props
.
property
,
phoneWidth
,
spuList
.
value
.
length
],
()
=>
{
// 计算列数
columns
.
value
=
props
.
property
.
layoutType
===
'twoCol'
?
2
:
3
// 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
const
productWidth
=
(
phoneWidth
.
value
-
props
.
property
.
space
*
(
columns
.
value
-
1
))
/
columns
.
value
// 商品图布局:2列时,左右布局 3列时,上下布局
imageSize
.
value
=
columns
.
value
===
2
?
'64px'
:
`
${
productWidth
}
px`
// 根据布局类型,计算行数、列数
if
(
props
.
property
.
layoutType
===
'horizSwiper'
)
{
// 单行显示
gridTemplateColumns
.
value
=
`repeat(auto-fill,
${
productWidth
}
px)`
// 显示滚动条
scrollbarWidth
.
value
=
`
${
productWidth
*
spuList
.
value
.
length
+
props
.
property
.
space
*
(
spuList
.
value
.
length
-
1
)
}
px`
}
else
{
// 指定列数
gridTemplateColumns
.
value
=
`repeat(
${
columns
.
value
}
, auto)`
// 不滚动
scrollbarWidth
.
value
=
'100%'
}
},
{
immediate
:
true
,
deep
:
true
}
)
onMounted
(()
=>
{
// 提取手机宽度
phoneWidth
.
value
=
containerRef
.
value
?.
wrapRef
?.
offsetWidth
||
375
})
</
script
>
<
style
scoped
lang=
"scss"
></
style
>
src/components/DiyEditor/components/mobile/ProductList/property.vue
0 → 100644
View file @
e106fb73
<
template
>
<ComponentContainerProperty
v-model=
"formData.style"
>
<el-form
label-width=
"80px"
:model=
"formData"
>
<el-card
header=
"商品列表"
class=
"property-group"
shadow=
"never"
>
<SpuShowcase
v-model=
"formData.spuIds"
/>
</el-card>
<el-card
header=
"商品样式"
class=
"property-group"
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
label=
"twoCol"
>
<Icon
icon=
"fluent:text-column-two-24-filled"
/>
</el-radio-button>
</el-tooltip>
<el-tooltip
class=
"item"
content=
"三列"
placement=
"bottom"
>
<el-radio-button
label=
"threeCol"
>
<Icon
icon=
"fluent:text-column-three-24-filled"
/>
</el-radio-button>
</el-tooltip>
<el-tooltip
class=
"item"
content=
"水平滑动"
placement=
"bottom"
>
<el-radio-button
label=
"horizSwiper"
>
<Icon
icon=
"system-uicons:carousel"
/>
</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.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-card>
<el-card
header=
"角标"
class=
"property-group"
shadow=
"never"
>
<el-form-item
label=
"角标"
prop=
"badge.show"
>
<el-switch
v-model=
"formData.badge.show"
/>
</el-form-item>
<el-form-item
label=
"角标"
prop=
"badge.imgUrl"
v-if=
"formData.badge.show"
>
<UploadImg
v-model=
"formData.badge.imgUrl"
height=
"44px"
width=
"72px"
>
<template
#
tip
>
建议尺寸:36 * 22
</
template
>
</UploadImg>
</el-form-item>
</el-card>
<el-card
header=
"商品样式"
class=
"property-group"
shadow=
"never"
>
<el-form-item
label=
"上圆角"
prop=
"borderRadiusTop"
>
<el-slider
v-model=
"formData.borderRadiusTop"
:max=
"100"
:min=
"0"
show-input
input-size=
"small"
:show-input-controls=
"false"
/>
</el-form-item>
<el-form-item
label=
"下圆角"
prop=
"borderRadiusBottom"
>
<el-slider
v-model=
"formData.borderRadiusBottom"
:max=
"100"
:min=
"0"
show-input
input-size=
"small"
:show-input-controls=
"false"
/>
</el-form-item>
<el-form-item
label=
"间隔"
prop=
"space"
>
<el-slider
v-model=
"formData.space"
:max=
"100"
:min=
"0"
show-input
input-size=
"small"
:show-input-controls=
"false"
/>
</el-form-item>
</el-card>
</el-form>
</ComponentContainerProperty>
</template>
<
script
setup
lang=
"ts"
>
import
{
ProductListProperty
}
from
'./config'
import
{
usePropertyForm
}
from
'@/components/DiyEditor/util'
import
SpuShowcase
from
'@/views/mall/product/spu/components/SpuShowcase.vue'
// 商品卡片属性面板
defineOptions
({
name
:
'ProductListProperty'
})
const
props
=
defineProps
<
{
modelValue
:
ProductListProperty
}
>
()
const
emit
=
defineEmits
([
'update:modelValue'
])
const
{
formData
}
=
usePropertyForm
(
props
.
modelValue
,
emit
)
</
script
>
<
style
scoped
lang=
"scss"
></
style
>
src/components/DiyEditor/util.ts
View file @
e106fb73
...
...
@@ -107,11 +107,15 @@ export const PAGE_LIBS = [
extended
:
true
,
components
:
[
'ImageBar'
,
'Carousel'
,
'TitleBar'
,
'VideoPlayer'
,
'Divider'
,
'MagicCube'
]
},
{
name
:
'商品组件'
,
extended
:
true
,
components
:
[
'ProductCard'
]
},
{
name
:
'商品组件'
,
extended
:
true
,
components
:
[
'ProductCard'
,
'ProductList'
]
},
{
name
:
'会员组件'
,
extended
:
true
,
components
:
[
'UserCard'
,
'
OrderCard'
,
'WalletCard'
,
'CouponCard
'
]
components
:
[
'UserCard'
,
'
UserOrder'
,
'UserWallet'
,
'UserCoupon
'
]
},
{
name
:
'营销组件'
,
extended
:
true
,
components
:
[
'Combination'
,
'Seckill'
,
'Point'
,
'Coupon'
]
}
{
name
:
'营销组件'
,
extended
:
true
,
components
:
[
'CombinationCard'
,
'SeckillCard'
,
'PointCard'
,
'CouponCard'
]
}
]
as
DiyComponentLibrary
[]
src/components/RouterSearch/index.vue
View file @
e106fb73
<
template
>
<ElDialog
v-model=
"showSearch"
:show-close=
"false"
title=
"菜单搜索"
>
<ElDialog
v-
if=
"isModal"
v-
model=
"showSearch"
:show-close=
"false"
title=
"菜单搜索"
>
<el-select
filterable
:reserve-keyword=
"false"
...
...
@@ -17,11 +17,34 @@
/>
</el-select>
</ElDialog>
<div
v-else
class=
"custom-hover"
@
click
.
stop=
"showTopSearch = !showTopSearch"
>
<Icon
icon=
"ep:search"
/>
<el-select
filterable
:reserve-keyword=
"false"
remote
placeholder=
"请输入菜单内容"
:remote-method=
"remoteMethod"
class=
"overflow-hidden transition-all-600"
:class=
"showTopSearch ? 'w-220px ml2' : 'w-0'"
@
change=
"handleChange"
>
<el-option
v-for=
"item in options"
:key=
"item.value"
:label=
"item.label"
:value=
"item.value"
/>
</el-select>
</div>
</
template
>
<
script
lang=
"ts"
setup
>
defineProps
({
isModal
:
{
type
:
Boolean
,
default
:
true
}
})
const
router
=
useRouter
()
// 路由对象
const
showSearch
=
ref
(
false
)
// 是否显示弹框
const
showTopSearch
=
ref
(
false
)
// 是否显示顶部搜索框
const
value
:
Ref
=
ref
(
''
)
// 用户输入的值
const
routers
=
router
.
getRoutes
()
// 路由对象
...
...
@@ -50,14 +73,21 @@ function remoteMethod(data) {
function
handleChange
(
path
)
{
router
.
push
({
path
})
hiddenTopSearch
();
}
function
hiddenTopSearch
()
{
showTopSearch
.
value
=
false
}
onMounted
(()
=>
{
window
.
addEventListener
(
'keydown'
,
listenKey
)
window
.
addEventListener
(
'click'
,
hiddenTopSearch
)
})
onUnmounted
(()
=>
{
window
.
removeEventListener
(
'keydown'
,
listenKey
)
window
.
removeEventListener
(
'click'
,
hiddenTopSearch
)
})
// 监听 ctrl + k
...
...
src/components/ShortcutDateRangePicker/index.vue
View file @
e106fb73
...
...
@@ -74,11 +74,6 @@ const emits = defineEmits<{
}
>
()
/** 触发时间范围选中事件 */
const
emitDateRangePicker
=
async
()
=>
{
// 开始与截止在同一天的, 折线图出不来, 需要延长一天
if
(
DateUtil
.
isSameDay
(
times
.
value
[
0
],
times
.
value
[
1
]))
{
// 前天
times
.
value
[
0
]
=
DateUtil
.
formatDate
(
dayjs
(
times
.
value
[
0
]).
subtract
(
1
,
'd'
))
}
emits
(
'change'
,
times
.
value
)
}
...
...
src/layout/components/ToolHeader.vue
View file @
e106fb73
...
...
@@ -7,6 +7,7 @@ import { Screenfull } from '@/layout/components/Screenfull'
import
{
Breadcrumb
}
from
'@/layout/components/Breadcrumb'
import
{
SizeDropdown
}
from
'@/layout/components/SizeDropdown'
import
{
LocaleDropdown
}
from
'@/layout/components/LocaleDropdown'
import
RouterSearch
from
'@/components/RouterSearch/index.vue'
import
{
useAppStore
}
from
'@/store/modules/app'
import
{
useDesign
}
from
'@/hooks/web/useDesign'
...
...
@@ -25,6 +26,9 @@ const hamburger = computed(() => appStore.getHamburger)
// 全屏图标
const
screenfull
=
computed
(()
=>
appStore
.
getScreenfull
)
// 搜索图片
const
search
=
computed
(()
=>
appStore
.
search
)
// 尺寸图标
const
size
=
computed
(()
=>
appStore
.
getSize
)
...
...
@@ -61,6 +65,7 @@ export default defineComponent({
{
screenfull
.
value
?
(
<
Screenfull
class
=
"custom-hover"
color
=
"var(--top-header-text-color)"
><
/Screenfull
>
)
:
undefined
}
{
search
.
value
?
(
<
RouterSearch
isModal
=
{
false
}
/>
)
: undefined
}
{
size
.
value
?
(
<
SizeDropdown
class
=
"custom-hover"
color
=
"var(--top-header-text-color)"
><
/SizeDropdown
>
)
:
undefined
}
...
...
src/locales/zh-CN.ts
View file @
e106fb73
...
...
@@ -437,5 +437,6 @@ export default {
btn_zoom_in
:
'放大'
,
btn_zoom_out
:
'缩小'
,
preview
:
'预览'
}
},
'OAuth 2.0'
:
'OAuth 2.0'
// 避免菜单名是 OAuth 2.0 时,一直 warn 报错
}
src/store/modules/app.ts
View file @
e106fb73
...
...
@@ -16,6 +16,7 @@ interface AppState {
uniqueOpened
:
boolean
hamburger
:
boolean
screenfull
:
boolean
search
:
boolean
size
:
boolean
locale
:
boolean
message
:
boolean
...
...
@@ -52,6 +53,7 @@ export const useAppStore = defineStore('app', {
uniqueOpened
:
true
,
// 是否只保持一个子菜单的展开
hamburger
:
true
,
// 折叠图标
screenfull
:
true
,
// 全屏图标
search
:
true
,
// 搜索图标
size
:
true
,
// 尺寸图标
locale
:
true
,
// 多语言图标
message
:
true
,
// 消息图标
...
...
src/utils/dict.ts
View file @
e106fb73
/**
* 数据字典工具类
*/
import
{
useDictStoreWithOut
}
from
'@/store/modules/dict'
import
{
ElementPlusInfoType
}
from
'@/types/elementPlus'
import
{
useDictStoreWithOut
}
from
'@/store/modules/dict'
import
{
ElementPlusInfoType
}
from
'@/types/elementPlus'
const
dictStore
=
useDictStoreWithOut
()
...
...
@@ -13,51 +13,51 @@ const dictStore = useDictStoreWithOut()
* @returns {*|Array} 数据字典数组
*/
export
interface
DictDataType
{
dictType
:
string
label
:
string
value
:
string
|
number
|
boolean
colorType
:
ElementPlusInfoType
|
''
cssClass
:
string
dictType
:
string
label
:
string
value
:
string
|
number
|
boolean
colorType
:
ElementPlusInfoType
|
''
cssClass
:
string
}
export
const
getDictOptions
=
(
dictType
:
string
)
=>
{
return
dictStore
.
getDictByType
(
dictType
)
||
[]
return
dictStore
.
getDictByType
(
dictType
)
||
[]
}
export
const
getIntDictOptions
=
(
dictType
:
string
):
DictDataType
[]
=>
{
const
dictOption
:
DictDataType
[]
=
[]
const
dictOptions
:
DictDataType
[]
=
getDictOptions
(
dictType
)
dictOptions
.
forEach
((
dict
:
DictDataType
)
=>
{
dictOption
.
push
({
...
dict
,
value
:
parseInt
(
dict
.
value
+
''
)
const
dictOption
:
DictDataType
[]
=
[]
const
dictOptions
:
DictDataType
[]
=
getDictOptions
(
dictType
)
dictOptions
.
forEach
((
dict
:
DictDataType
)
=>
{
dictOption
.
push
({
...
dict
,
value
:
parseInt
(
dict
.
value
+
''
)
})
})
})
return
dictOption
return
dictOption
}
export
const
getStrDictOptions
=
(
dictType
:
string
)
=>
{
const
dictOption
:
DictDataType
[]
=
[]
const
dictOptions
:
DictDataType
[]
=
getDictOptions
(
dictType
)
dictOptions
.
forEach
((
dict
:
DictDataType
)
=>
{
dictOption
.
push
({
...
dict
,
value
:
dict
.
value
+
''
const
dictOption
:
DictDataType
[]
=
[]
const
dictOptions
:
DictDataType
[]
=
getDictOptions
(
dictType
)
dictOptions
.
forEach
((
dict
:
DictDataType
)
=>
{
dictOption
.
push
({
...
dict
,
value
:
dict
.
value
+
''
})
})
})
return
dictOption
return
dictOption
}
export
const
getBoolDictOptions
=
(
dictType
:
string
)
=>
{
const
dictOption
:
DictDataType
[]
=
[]
const
dictOptions
:
DictDataType
[]
=
getDictOptions
(
dictType
)
dictOptions
.
forEach
((
dict
:
DictDataType
)
=>
{
dictOption
.
push
({
...
dict
,
value
:
dict
.
value
+
''
===
'true'
const
dictOption
:
DictDataType
[]
=
[]
const
dictOptions
:
DictDataType
[]
=
getDictOptions
(
dictType
)
dictOptions
.
forEach
((
dict
:
DictDataType
)
=>
{
dictOption
.
push
({
...
dict
,
value
:
dict
.
value
+
''
===
'true'
})
})
})
return
dictOption
return
dictOption
}
/**
...
...
@@ -67,12 +67,12 @@ export const getBoolDictOptions = (dictType: string) => {
* @return DictDataType 字典对象
*/
export
const
getDictObj
=
(
dictType
:
string
,
value
:
any
):
DictDataType
|
undefined
=>
{
const
dictOptions
:
DictDataType
[]
=
getDictOptions
(
dictType
)
for
(
const
dict
of
dictOptions
)
{
if
(
dict
.
value
===
value
+
''
)
{
return
dict
const
dictOptions
:
DictDataType
[]
=
getDictOptions
(
dictType
)
for
(
const
dict
of
dictOptions
)
{
if
(
dict
.
value
===
value
+
''
)
{
return
dict
}
}
}
}
/**
...
...
@@ -83,117 +83,121 @@ export const getDictObj = (dictType: string, value: any): DictDataType | undefin
* @return 字典名称
*/
export
const
getDictLabel
=
(
dictType
:
string
,
value
:
any
):
string
=>
{
const
dictOptions
:
DictDataType
[]
=
getDictOptions
(
dictType
)
const
dictLabel
=
ref
(
''
)
dictOptions
.
forEach
((
dict
:
DictDataType
)
=>
{
if
(
dict
.
value
===
value
+
''
)
{
dictLabel
.
value
=
dict
.
label
}
})
return
dictLabel
.
value
const
dictOptions
:
DictDataType
[]
=
getDictOptions
(
dictType
)
const
dictLabel
=
ref
(
''
)
dictOptions
.
forEach
((
dict
:
DictDataType
)
=>
{
if
(
dict
.
value
===
value
+
''
)
{
dictLabel
.
value
=
dict
.
label
}
})
return
dictLabel
.
value
}
export
enum
DICT_TYPE
{
USER_TYPE
=
'user_type'
,
COMMON_STATUS
=
'common_status'
,
SYSTEM_TENANT_PACKAGE_ID
=
'system_tenant_package_id'
,
TERMINAL
=
'terminal'
,
// 终端
// ========== SYSTEM 模块 ==========
SYSTEM_USER_SEX
=
'system_user_sex'
,
SYSTEM_MENU_TYPE
=
'system_menu_type'
,
SYSTEM_ROLE_TYPE
=
'system_role_type'
,
SYSTEM_DATA_SCOPE
=
'system_data_scope'
,
SYSTEM_NOTICE_TYPE
=
'system_notice_type'
,
SYSTEM_OPERATE_TYPE
=
'system_operate_type'
,
SYSTEM_LOGIN_TYPE
=
'system_login_type'
,
SYSTEM_LOGIN_RESULT
=
'system_login_result'
,
SYSTEM_SMS_CHANNEL_CODE
=
'system_sms_channel_code'
,
SYSTEM_SMS_TEMPLATE_TYPE
=
'system_sms_template_type'
,
SYSTEM_SMS_SEND_STATUS
=
'system_sms_send_status'
,
SYSTEM_SMS_RECEIVE_STATUS
=
'system_sms_receive_status'
,
SYSTEM_ERROR_CODE_TYPE
=
'system_error_code_type'
,
SYSTEM_OAUTH2_GRANT_TYPE
=
'system_oauth2_grant_type'
,
SYSTEM_MAIL_SEND_STATUS
=
'system_mail_send_status'
,
SYSTEM_NOTIFY_TEMPLATE_TYPE
=
'system_notify_template_type'
,
SYSTEM_SOCIAL_TYPE
=
'system_social_type'
,
// ========== INFRA 模块 ==========
INFRA_BOOLEAN_STRING
=
'infra_boolean_string'
,
INFRA_JOB_STATUS
=
'infra_job_status'
,
INFRA_JOB_LOG_STATUS
=
'infra_job_log_status'
,
INFRA_API_ERROR_LOG_PROCESS_STATUS
=
'infra_api_error_log_process_status'
,
INFRA_CONFIG_TYPE
=
'infra_config_type'
,
INFRA_CODEGEN_TEMPLATE_TYPE
=
'infra_codegen_template_type'
,
INFRA_CODEGEN_FRONT_TYPE
=
'infra_codegen_front_type'
,
INFRA_CODEGEN_SCENE
=
'infra_codegen_scene'
,
INFRA_FILE_STORAGE
=
'infra_file_storage'
,
// ========== BPM 模块 ==========
BPM_MODEL_CATEGORY
=
'bpm_model_category'
,
BPM_MODEL_FORM_TYPE
=
'bpm_model_form_type'
,
BPM_TASK_ASSIGN_RULE_TYPE
=
'bpm_task_assign_rule_type'
,
BPM_PROCESS_INSTANCE_STATUS
=
'bpm_process_instance_status'
,
BPM_PROCESS_INSTANCE_RESULT
=
'bpm_process_instance_result'
,
BPM_TASK_ASSIGN_SCRIPT
=
'bpm_task_assign_script'
,
BPM_OA_LEAVE_TYPE
=
'bpm_oa_leave_type'
,
// ========== PAY 模块 ==========
PAY_CHANNEL_CODE
=
'pay_channel_code'
,
// 支付渠道编码类型
PAY_ORDER_STATUS
=
'pay_order_status'
,
// 商户支付订单状态
PAY_REFUND_STATUS
=
'pay_refund_status'
,
// 退款订单状态
PAY_NOTIFY_STATUS
=
'pay_notify_status'
,
// 商户支付回调状态
PAY_NOTIFY_TYPE
=
'pay_notify_type'
,
// 商户支付回调状态
PAY_TRANSFER_STATUS
=
'pay_transfer_status'
,
// 转账订单状态
PAY_TRANSFER_TYPE
=
'pay_transfer_type'
,
// 转账订单状态
// ========== MP 模块 ==========
MP_AUTO_REPLY_REQUEST_MATCH
=
'mp_auto_reply_request_match'
,
// 自动回复请求匹配类型
MP_MESSAGE_TYPE
=
'mp_message_type'
,
// 消息类型
// ========== MALL - 会员模块 ==========
MEMBER_POINT_BIZ_TYPE
=
'member_point_biz_type'
,
// 积分的业务类型
MEMBER_EXPERIENCE_BIZ_TYPE
=
'member_experience_biz_type'
,
// 会员经验业务类型
// ========== MALL - 商品模块 ==========
PRODUCT_UNIT
=
'product_unit'
,
// 商品单位
PRODUCT_SPU_STATUS
=
'product_spu_status'
,
//商品状态
PROMOTION_TYPE_ENUM
=
'promotion_type_enum'
,
// 营销类型枚举
// ========== MALL - 交易模块 ==========
EXPRESS_CHARGE_MODE
=
'trade_delivery_express_charge_mode'
,
//快递的计费方式
TRADE_AFTER_SALE_STATUS
=
'trade_after_sale_status'
,
// 售后 - 状态
TRADE_AFTER_SALE_WAY
=
'trade_after_sale_way'
,
// 售后 - 方式
TRADE_AFTER_SALE_TYPE
=
'trade_after_sale_type'
,
// 售后 - 类型
TRADE_ORDER_TYPE
=
'trade_order_type'
,
// 订单 - 类型
TRADE_ORDER_STATUS
=
'trade_order_status'
,
// 订单 - 状态
TRADE_ORDER_ITEM_AFTER_SALE_STATUS
=
'trade_order_item_after_sale_status'
,
// 订单项 - 售后状态
TRADE_DELIVERY_TYPE
=
'trade_delivery_type'
,
// 配送方式
BROKERAGE_ENABLED_CONDITION
=
'brokerage_enabled_condition'
,
// 分佣模式
BROKERAGE_BIND_MODE
=
'brokerage_bind_mode'
,
// 分销关系绑定模式
BROKERAGE_BANK_NAME
=
'brokerage_bank_name'
,
// 佣金提现银行
BROKERAGE_WITHDRAW_TYPE
=
'brokerage_withdraw_type'
,
// 佣金提现类型
BROKERAGE_RECORD_BIZ_TYPE
=
'brokerage_record_biz_type'
,
// 佣金业务类型
BROKERAGE_RECORD_STATUS
=
'brokerage_record_status'
,
// 佣金状态
BROKERAGE_WITHDRAW_STATUS
=
'brokerage_withdraw_status'
,
// 佣金提现状态
// ========== MALL - 营销模块 ==========
PROMOTION_DISCOUNT_TYPE
=
'promotion_discount_type'
,
// 优惠类型
PROMOTION_PRODUCT_SCOPE
=
'promotion_product_scope'
,
// 营销的商品范围
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'
,
// 拼团记录的状态
PROMOTION_BANNER_POSITION
=
'promotion_banner_position'
,
// banner 定位
// ========== CRM - 客户管理模块 ==========
CRM_RECEIVABLE_CHECK_STATUS
=
'crm_receivable_check_status'
,
CRM_RETURN_TYPE
=
'crm_return_type'
,
CRM_CUSTOMER_INDUSTRY
=
'crm_customer_industry'
,
CRM_CUSTOMER_LEVEL
=
'crm_customer_level'
,
CRM_CUSTOMER_SOURCE
=
'crm_customer_source'
,
CRM_PRODUCT_STATUS
=
'crm_product_status'
USER_TYPE
=
'user_type'
,
COMMON_STATUS
=
'common_status'
,
SYSTEM_TENANT_PACKAGE_ID
=
'system_tenant_package_id'
,
TERMINAL
=
'terminal'
,
// 终端
// ========== SYSTEM 模块 ==========
SYSTEM_USER_SEX
=
'system_user_sex'
,
SYSTEM_MENU_TYPE
=
'system_menu_type'
,
SYSTEM_ROLE_TYPE
=
'system_role_type'
,
SYSTEM_DATA_SCOPE
=
'system_data_scope'
,
SYSTEM_NOTICE_TYPE
=
'system_notice_type'
,
SYSTEM_OPERATE_TYPE
=
'system_operate_type'
,
SYSTEM_LOGIN_TYPE
=
'system_login_type'
,
SYSTEM_LOGIN_RESULT
=
'system_login_result'
,
SYSTEM_SMS_CHANNEL_CODE
=
'system_sms_channel_code'
,
SYSTEM_SMS_TEMPLATE_TYPE
=
'system_sms_template_type'
,
SYSTEM_SMS_SEND_STATUS
=
'system_sms_send_status'
,
SYSTEM_SMS_RECEIVE_STATUS
=
'system_sms_receive_status'
,
SYSTEM_ERROR_CODE_TYPE
=
'system_error_code_type'
,
SYSTEM_OAUTH2_GRANT_TYPE
=
'system_oauth2_grant_type'
,
SYSTEM_MAIL_SEND_STATUS
=
'system_mail_send_status'
,
SYSTEM_NOTIFY_TEMPLATE_TYPE
=
'system_notify_template_type'
,
SYSTEM_SOCIAL_TYPE
=
'system_social_type'
,
// ========== INFRA 模块 ==========
INFRA_BOOLEAN_STRING
=
'infra_boolean_string'
,
INFRA_JOB_STATUS
=
'infra_job_status'
,
INFRA_JOB_LOG_STATUS
=
'infra_job_log_status'
,
INFRA_API_ERROR_LOG_PROCESS_STATUS
=
'infra_api_error_log_process_status'
,
INFRA_CONFIG_TYPE
=
'infra_config_type'
,
INFRA_CODEGEN_TEMPLATE_TYPE
=
'infra_codegen_template_type'
,
INFRA_CODEGEN_FRONT_TYPE
=
'infra_codegen_front_type'
,
INFRA_CODEGEN_SCENE
=
'infra_codegen_scene'
,
INFRA_FILE_STORAGE
=
'infra_file_storage'
,
// ========== BPM 模块 ==========
BPM_MODEL_CATEGORY
=
'bpm_model_category'
,
BPM_MODEL_FORM_TYPE
=
'bpm_model_form_type'
,
BPM_TASK_ASSIGN_RULE_TYPE
=
'bpm_task_assign_rule_type'
,
BPM_PROCESS_INSTANCE_STATUS
=
'bpm_process_instance_status'
,
BPM_PROCESS_INSTANCE_RESULT
=
'bpm_process_instance_result'
,
BPM_TASK_ASSIGN_SCRIPT
=
'bpm_task_assign_script'
,
BPM_OA_LEAVE_TYPE
=
'bpm_oa_leave_type'
,
// ========== PAY 模块 ==========
PAY_CHANNEL_CODE
=
'pay_channel_code'
,
// 支付渠道编码类型
PAY_ORDER_STATUS
=
'pay_order_status'
,
// 商户支付订单状态
PAY_REFUND_STATUS
=
'pay_refund_status'
,
// 退款订单状态
PAY_NOTIFY_STATUS
=
'pay_notify_status'
,
// 商户支付回调状态
PAY_NOTIFY_TYPE
=
'pay_notify_type'
,
// 商户支付回调状态
PAY_TRANSFER_STATUS
=
'pay_transfer_status'
,
// 转账订单状态
PAY_TRANSFER_TYPE
=
'pay_transfer_type'
,
// 转账订单状态
// ========== MP 模块 ==========
MP_AUTO_REPLY_REQUEST_MATCH
=
'mp_auto_reply_request_match'
,
// 自动回复请求匹配类型
MP_MESSAGE_TYPE
=
'mp_message_type'
,
// 消息类型
// ========== MALL - 会员模块 ==========
MEMBER_POINT_BIZ_TYPE
=
'member_point_biz_type'
,
// 积分的业务类型
MEMBER_EXPERIENCE_BIZ_TYPE
=
'member_experience_biz_type'
,
// 会员经验业务类型
// ========== MALL - 商品模块 ==========
PRODUCT_UNIT
=
'product_unit'
,
// 商品单位
PRODUCT_SPU_STATUS
=
'product_spu_status'
,
//商品状态
PROMOTION_TYPE_ENUM
=
'promotion_type_enum'
,
// 营销类型枚举
// ========== MALL - 交易模块 ==========
EXPRESS_CHARGE_MODE
=
'trade_delivery_express_charge_mode'
,
//快递的计费方式
TRADE_AFTER_SALE_STATUS
=
'trade_after_sale_status'
,
// 售后 - 状态
TRADE_AFTER_SALE_WAY
=
'trade_after_sale_way'
,
// 售后 - 方式
TRADE_AFTER_SALE_TYPE
=
'trade_after_sale_type'
,
// 售后 - 类型
TRADE_ORDER_TYPE
=
'trade_order_type'
,
// 订单 - 类型
TRADE_ORDER_STATUS
=
'trade_order_status'
,
// 订单 - 状态
TRADE_ORDER_ITEM_AFTER_SALE_STATUS
=
'trade_order_item_after_sale_status'
,
// 订单项 - 售后状态
TRADE_DELIVERY_TYPE
=
'trade_delivery_type'
,
// 配送方式
BROKERAGE_ENABLED_CONDITION
=
'brokerage_enabled_condition'
,
// 分佣模式
BROKERAGE_BIND_MODE
=
'brokerage_bind_mode'
,
// 分销关系绑定模式
BROKERAGE_BANK_NAME
=
'brokerage_bank_name'
,
// 佣金提现银行
BROKERAGE_WITHDRAW_TYPE
=
'brokerage_withdraw_type'
,
// 佣金提现类型
BROKERAGE_RECORD_BIZ_TYPE
=
'brokerage_record_biz_type'
,
// 佣金业务类型
BROKERAGE_RECORD_STATUS
=
'brokerage_record_status'
,
// 佣金状态
BROKERAGE_WITHDRAW_STATUS
=
'brokerage_withdraw_status'
,
// 佣金提现状态
// ========== MALL - 营销模块 ==========
PROMOTION_DISCOUNT_TYPE
=
'promotion_discount_type'
,
// 优惠类型
PROMOTION_PRODUCT_SCOPE
=
'promotion_product_scope'
,
// 营销的商品范围
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'
,
// 拼团记录的状态
PROMOTION_BANNER_POSITION
=
'promotion_banner_position'
,
// banner 定位
// ========== CRM - 客户管理模块 ==========
CRM_RECEIVABLE_CHECK_STATUS
=
'crm_receivable_check_status'
,
CRM_RETURN_TYPE
=
'crm_return_type'
,
CRM_CUSTOMER_INDUSTRY
=
'crm_customer_industry'
,
CRM_CUSTOMER_LEVEL
=
'crm_customer_level'
,
CRM_CUSTOMER_SOURCE
=
'crm_customer_source'
,
CRM_PRODUCT_STATUS
=
'crm_product_status'
,
// ========== CRM - 数据权限模块 ==========
CRM_BIZ_TYPE
=
'crm_biz_type'
,
// 数据模块类型
CRM_PERMISSION_LEVEL
=
'crm_permission_level'
// 用户数据权限类型
}
src/utils/formatTime.ts
View file @
e106fb73
...
...
@@ -335,5 +335,8 @@ export function getDateRange(
beginDate
:
dayjs
.
ConfigType
,
endDate
:
dayjs
.
ConfigType
):
[
string
,
string
]
{
return
[
dayjs
(
beginDate
).
startOf
(
'd'
).
toString
(),
dayjs
(
endDate
).
endOf
(
'd'
).
toString
()]
return
[
dayjs
(
beginDate
).
startOf
(
'd'
).
format
(
'YYYY-MM-DD HH:mm:ss'
),
dayjs
(
endDate
).
endOf
(
'd'
).
format
(
'YYYY-MM-DD HH:mm:ss'
)
]
}
src/views/crm/components/CrmPermissionForm.vue
View file @
e106fb73
...
...
@@ -19,9 +19,14 @@
</el-form-item>
<el-form-item
label=
"权限级别"
prop=
"level"
>
<el-radio-group
v-model=
"formData.level"
>
<!-- TODO @puhui999:搞个字典配置?然后这里 remove 掉负责人 -->
<el-radio
:label=
"CrmPermissionLevelEnum.READ"
>
只读
</el-radio>
<el-radio
:label=
"CrmPermissionLevelEnum.WRITE"
>
读写
</el-radio>
<template
v-for=
"dict in getIntDictOptions(DICT_TYPE.CRM_PERMISSION_LEVEL)"
:key=
"dict.value"
>
<el-radio
v-if=
"dict.value != CrmPermissionLevelEnum.OWNER"
:label=
"dict.value"
>
{{
dict
.
label
}}
</el-radio>
</
template
>
</el-radio-group>
</el-form-item>
</el-form>
...
...
@@ -34,7 +39,8 @@
<
script
lang=
"ts"
setup
>
import
*
as
UserApi
from
'@/api/system/user'
import
*
as
PermissionApi
from
'@/api/crm/permission'
import
{
CrmPermissionLevelEnum
}
from
'./index'
import
{
DICT_TYPE
,
getIntDictOptions
}
from
'@/utils/dict'
import
{
CrmPermissionLevelEnum
}
from
'@/views/crm/components/index'
defineOptions
({
name
:
'CrmPermissionForm'
})
...
...
src/views/crm/components/Crm
Team
List.vue
→
src/views/crm/components/Crm
Permission
List.vue
View file @
e106fb73
<
template
>
<!-- 操作栏 -->
<el-row
justify=
"end"
>
<el-button
type=
"primary"
@
click=
"
handleAdd
"
>
<el-button
type=
"primary"
@
click=
"
openForm
"
>
<Icon
class=
"mr-5px"
icon=
"ep:plus"
/>
新增
</el-button>
...
...
@@ -9,7 +9,7 @@
<Icon
class=
"mr-5px"
icon=
"ep:edit"
/>
编辑
</el-button>
<el-button
@
click=
"handle
Remov
e"
>
<el-button
@
click=
"handle
Delet
e"
>
<Icon
class=
"mr-5px"
icon=
"ep:delete"
/>
移除
</el-button>
...
...
@@ -30,45 +30,32 @@
<el-table-column
align=
"center"
label=
"岗位"
prop=
"postNames"
/>
<el-table-column
align=
"center"
label=
"权限级别"
prop=
"level"
>
<template
#
default=
"
{ row }">
<
el-tag>
{{
getLevelName
(
row
.
level
)
}}
</el-tag
>
<
dict-tag
:type=
"DICT_TYPE.CRM_PERMISSION_LEVEL"
:value=
"row.level"
/
>
</
template
>
</el-table-column>
<el-table-column
:formatter=
"dateFormatter"
align=
"center"
label=
"加入时间"
prop=
"createTime"
/>
</el-table>
<CrmPermissionForm
ref=
"
crmPermissionFormRef
"
/>
<CrmPermissionForm
ref=
"
permissionFormRef"
@
success=
"getList
"
/>
</template>
<
script
lang=
"ts"
setup
>
// TODO @puhui999:改成 CrmPermissionList
import
{
dateFormatter
}
from
'@/utils/formatTime'
import
{
ElTable
}
from
'element-plus'
import
*
as
PermissionApi
from
'@/api/crm/permission'
import
{
useUserStoreWithOut
}
from
'@/store/modules/user'
import
CrmPermissionForm
from
'./CrmPermissionForm.vue'
import
{
CrmPermissionLevelEnum
}
from
'./index'
import
{
DICT_TYPE
}
from
'@/utils/dict'
defineOptions
({
name
:
'Crm
Team
'
})
defineOptions
({
name
:
'Crm
PermissionList
'
})
const
message
=
useMessage
()
// 消息
const
props
=
defineProps
<
{
bizType
:
number
bizId
:
number
bizType
:
number
// 模块类型
bizId
:
number
// 模块数据编号
}
>
()
const
loading
=
ref
(
true
)
// 列表的加载中
const
list
=
ref
<
PermissionApi
.
PermissionVO
[]
>
([
// TODO 测试数据
{
id
:
1
,
// 数据权限编号
userId
:
1
,
// 用户编号
bizType
:
1
,
// Crm 类型
bizId
:
1
,
// Crm 类型数据编号
level
:
1
,
// 权限级别
deptName
:
'研发部门'
,
// 部门名称
nickname
:
'芋道源码'
,
// 用户昵称
postNames
:
'全栈开发工程师'
,
// 岗位名称数组
createTime
:
new
Date
()
}
])
// 列表的数据
const
list
=
ref
<
PermissionApi
.
PermissionVO
[]
>
([])
// 列表的数据
/** 查询列表 */
const
getList
=
async
()
=>
{
...
...
@@ -83,40 +70,28 @@ const getList = async () => {
loading
.
value
=
false
}
}
// TODO @puhui999:字典格式化
/**
* 获得权限级别名称
* @param level 权限级别
*/
const
getLevelName
=
computed
(()
=>
(
level
:
number
)
=>
{
switch
(
level
)
{
case
CrmPermissionLevelEnum
.
OWNER
:
return
'负责人'
case
CrmPermissionLevelEnum
.
READ
:
return
'只读'
case
CrmPermissionLevelEnum
.
WRITE
:
return
'读写'
default
:
break
}
})
// TODO @puhui999:空行稍微注意下哈;一些注释补齐下;
const
multipleSelection
=
ref
<
PermissionApi
.
PermissionVO
[]
>
([])
const
multipleSelection
=
ref
<
PermissionApi
.
PermissionVO
[]
>
([])
// 选择的团队成员
const
handleSelectionChange
=
(
val
:
PermissionApi
.
PermissionVO
[])
=>
{
multipleSelection
.
value
=
val
}
// TODO @puhui999:一些变量命名,看看有没可能跟列表界面的 index.vue 保持他统一的风格;
const
crmPermissionFormRef
=
ref
<
InstanceType
<
typeof
CrmPermissionForm
>>
()
const
permissionFormRef
=
ref
<
InstanceType
<
typeof
CrmPermissionForm
>>
()
// 权限表单 Ref
/**
* 编辑团队成员
*/
const
handleEdit
=
()
=>
{
if
(
multipleSelection
.
value
?.
length
===
0
)
{
message
.
warning
(
'请先选择团队成员后操作!'
)
return
}
const
ids
=
multipleSelection
.
value
?.
map
((
item
)
=>
item
.
id
)
crmP
ermissionFormRef
.
value
?.
open
(
'update'
,
props
.
bizType
,
props
.
bizId
,
ids
)
p
ermissionFormRef
.
value
?.
open
(
'update'
,
props
.
bizType
,
props
.
bizId
,
ids
)
}
const
handleRemove
=
async
()
=>
{
/**
* 移除团队成员
*/
const
handleDelete
=
async
()
=>
{
if
(
multipleSelection
.
value
?.
length
===
0
)
{
message
.
warning
(
'请先选择团队成员后操作!'
)
return
...
...
@@ -129,11 +104,18 @@ const handleRemove = async () => {
ids
})
}
const
handleAdd
=
()
=>
{
crmPermissionFormRef
.
value
?.
open
(
'create'
,
props
.
bizType
,
props
.
bizId
)
/**
* 添加团队成员
*/
const
openForm
=
()
=>
{
permissionFormRef
.
value
?.
open
(
'create'
,
props
.
bizType
,
props
.
bizId
)
}
const
userStore
=
useUserStoreWithOut
()
const
userStore
=
useUserStoreWithOut
()
// 用户信息缓存
/**
* 退出团队
*/
const
handleQuit
=
async
()
=>
{
const
permission
=
list
.
value
.
find
(
(
item
)
=>
item
.
userId
===
userStore
.
getUser
.
id
&&
item
.
level
===
CrmPermissionLevelEnum
.
OWNER
...
...
src/views/crm/components/index.ts
View file @
e106fb73
import
Crm
Team
from
'./CrmTeam
List.vue'
import
Crm
PermissionList
from
'./CrmPermission
List.vue'
enum
CrmBizTypeEnum
{
CRM_LEADS
=
1
,
// 线索
...
...
@@ -9,9 +9,7 @@ enum CrmBizTypeEnum {
}
enum
CrmPermissionLevelEnum
{
OWNER
=
1
,
// 负责人
READ
=
2
,
// 读
WRITE
=
3
// 写
OWNER
=
1
// 负责人
}
export
{
Crm
Team
,
CrmBizTypeEnum
,
CrmPermissionLevelEnum
}
export
{
Crm
PermissionList
,
CrmBizTypeEnum
,
CrmPermissionLevelEnum
}
src/views/crm/contact/ContactForm.vue
View file @
e106fb73
<
template
>
<Dialog
:title=
"dialogTitle"
v-model=
"dialogVisible"
:width=
"800"
>
<el-form
ref=
"formRef"
:model=
"formData"
:rules=
"formRules"
label-width=
"130px"
v-loading=
"formLoading"
:inline=
"true"
>
<el-form-item
label=
"姓名"
prop=
"name"
>
<el-input
v-model=
"formData.name"
placeholder=
"请输入姓名"
/>
</el-form-item>
<el-form-item
label=
"负责人"
prop=
"ownerUserId"
>
<el-select
v-model=
"ownerUserList"
placeholder=
"请选择负责人"
multiple
value-key=
"id"
lable-key=
"nickname"
@
click=
"openOwerForm('open')"
>
<el-option
v-for=
"item in ownerUserList"
:key=
"item.id"
:label=
"item.nickname"
:value=
"item"
/>
</el-select>
</el-form-item>
<!-- TODO 芋艿:封装成一个组件 -->
<el-form-item
label=
"客户名称"
prop=
"customerName"
>
<el-popover
placement=
"bottom"
:width=
"600"
trigger=
"click"
:teleported=
"false"
:visible=
"showCustomer"
:offset=
"10"
>
<template
#
reference
>
<el-input
placeholder=
"请选择"
@
click=
"openCustomerSelect"
v-model=
"formData.customerName"
/>
</
template
>
<el-table
:data=
"list"
ref=
"multipleTableRef"
@
select=
"handleSelectionChange"
>
<el-table-column
label=
"选择"
type=
"selection"
width=
"55"
/>
<el-table-column
width=
"100"
property=
"id"
label=
"编号"
/>
<el-table-column
width=
"150"
property=
"name"
label=
"客户名称"
/>
<el-table-column
label=
"客户来源"
align=
"center"
prop=
"source"
width=
"100"
>
<
template
#
default=
"scope"
>
<dict-tag
:type=
"DICT_TYPE.CRM_CUSTOMER_SOURCE"
:value=
"scope.row.source"
/>
</
template
>
</el-table-column>
<el-table-column
label=
"客户等级"
align=
"center"
prop=
"level"
width=
"120"
>
<
template
#
default=
"scope"
>
<dict-tag
:type=
"DICT_TYPE.CRM_CUSTOMER_LEVEL"
:value=
"scope.row.level"
/>
</
template
>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-row
:gutter=
"20"
>
<el-col>
<Pagination
:total=
"total"
v-model:page=
"queryParams.pageNo"
v-model:limit=
"queryParams.pageSize"
@
pagination=
"getList"
layout=
"sizes, prev, pager, next"
/>
</el-col>
</el-row>
<el-row
:gutter=
"20"
>
<el-col
:span=
"10"
:offset=
"13"
>
<el-button
@
click=
"selectCustomer"
>
确认
</el-button>
<el-button
@
click=
"showCustomer = false"
>
取消
</el-button>
</el-col>
</el-row>
</el-popover>
</el-form-item>
<el-form-item
label=
"性别"
prop=
"sex"
>
<el-select
v-model=
"formData.sex"
placeholder=
"请选择"
>
<el-option
v-for=
"dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
:key=
"dict.value"
:label=
"dict.label"
:value=
"dict.value"
/>
</el-select>
</el-form-item>
<el-form-item
label=
"手机号"
prop=
"mobile"
>
<el-input
v-model=
"formData.mobile"
placeholder=
"请输入手机号"
/>
</el-form-item>
<el-form-item
label=
"座机"
prop=
"telephone"
>
<el-input
v-model=
"formData.telephone"
placeholder=
"请输入座机"
style=
"width: 215px"
/>
</el-form-item>
<el-form-item
label=
"邮箱"
prop=
"email"
>
<el-input
v-model=
"formData.email"
placeholder=
"请输入邮箱"
/>
</el-form-item>
<el-form-item
label=
"QQ"
prop=
"qq"
>
<el-input
v-model=
"formData.qq"
placeholder=
"请输入QQ"
style=
"width: 215px"
/>
</el-form-item>
<el-form-item
label=
"微信"
prop=
"webchat"
>
<el-input
v-model=
"formData.webchat"
placeholder=
"请输入微信"
/>
</el-form-item>
<el-form-item
label=
"下次联系时间"
prop=
"nextTime"
>
<el-date-picker
v-model=
"formData.nextTime"
type=
"date"
value-format=
"x"
placeholder=
"选择下次联系时间"
/>
</el-form-item>
<el-form-item
label=
"地址"
prop=
"address"
>
<el-input
v-model=
"formData.address"
placeholder=
"请输入地址"
/>
</el-form-item>
<el-form-item
label=
"直属上级"
prop=
"parentId"
>
<el-select
v-model=
"formData.parentId"
placeholder=
"请选择"
>
<el-option
v-for=
"item in allContactList"
:key=
"item.id"
:label=
"item.name"
:value=
"item.id"
:disabled=
"item.id == formData.id"
/>
</el-select>
</el-form-item>
<el-form-item
label=
"职位"
prop=
"post"
>
<el-input
v-model=
"formData.post"
placeholder=
"请输入职位"
/>
</el-form-item>
<el-form-item
label=
"是否关键决策人"
prop=
"policyMakers"
style=
"width: 400px"
>
<el-radio-group
v-model=
"formData.policyMakers"
>
<el-radio
v-for=
"dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key=
"dict.value"
:label=
"dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label=
"备注"
prop=
"remark"
>
<el-input
v-model=
"formData.remark"
placeholder=
"请输入备注"
/>
</el-form-item>
<Dialog
:title=
"dialogTitle"
v-model=
"dialogVisible"
:width=
"820"
>
<el-form
ref=
"formRef"
:model=
"formData"
:rules=
"formRules"
label-width=
"110px"
v-loading=
"formLoading"
>
<el-row
:gutter=
"20"
>
<el-col
:span=
"12"
>
<el-form-item
label=
"姓名"
prop=
"name"
>
<el-input
input-style=
"width:190px;"
v-model=
"formData.name"
placeholder=
"请输入姓名"
/>
</el-form-item>
</el-col>
<el-col
:span=
"12"
>
<el-form-item
label=
"负责人"
prop=
"ownerUserId"
>
<el-select
v-model=
"formData.ownerUserId"
placeholder=
"请选择负责人"
value-key=
"id"
lable-key=
"nickname"
>
<el-option
v-for=
"item in userList"
:key=
"item.id"
:label=
"item.nickname"
:value=
"item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col
:span=
"12"
>
<el-form-item
label=
"客户名称"
prop=
"customerName"
>
<el-select
v-model=
"formData.customerId"
placeholder=
"请选择客户"
value-key=
"id"
lable-key=
"name"
>
<el-option
v-for=
"item in customerList"
:key=
"item.id"
:label=
"item.name"
:value=
"item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col
:span=
"12"
><el-form-item
label=
"性别"
prop=
"sex"
>
<el-select
v-model=
"formData.sex"
placeholder=
"请选择"
>
<el-option
v-for=
"dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
:key=
"dict.value"
:label=
"dict.label"
:value=
"dict.value"
/>
</el-select>
</el-form-item></el-col>
</el-row>
<el-row>
<el-col
:span=
"12"
>
<el-form-item
label=
"手机号"
prop=
"mobile"
>
<el-input
input-style=
"width:190px;"
v-model=
"formData.mobile"
placeholder=
"请输入手机号"
/>
</el-form-item>
</el-col>
<el-col
:span=
"12"
>
<el-form-item
label=
"座机"
prop=
"telephone"
>
<el-input
v-model=
"formData.telephone"
placeholder=
"请输入座机"
style=
"width: 215px"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col
:span=
"12"
>
<el-form-item
label=
"邮箱"
prop=
"email"
>
<el-input
input-style=
"width:190px;"
v-model=
"formData.email"
placeholder=
"请输入邮箱"
/>
</el-form-item>
</el-col>
<el-col
:span=
"12"
>
<el-form-item
label=
"QQ"
prop=
"qq"
>
<el-input
v-model=
"formData.qq"
placeholder=
"请输入QQ"
style=
"width: 215px"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col
:span=
"12"
>
<el-form-item
label=
"微信"
prop=
"wechat"
>
<el-input
input-style=
"width:190px;"
v-model=
"formData.wechat"
placeholder=
"请输入微信"
/>
</el-form-item>
</el-col>
<el-col
:span=
"12"
>
<el-form-item
label=
"下次联系时间"
prop=
"nextTime"
>
<el-date-picker
v-model=
"formData.nextTime"
type=
"date"
value-format=
"x"
placeholder=
"选择下次联系时间"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col
:span=
"12"
>
<el-form-item
label=
"所在地"
prop=
"areaId"
>
<el-tree-select
v-model=
"formData.areaId"
:data=
"areaList"
:props=
"defaultProps"
:render-after-expand=
"true"
/>
</el-form-item>
</el-col>
<el-col
:span=
"12"
>
<el-form-item
label=
"地址"
prop=
"address"
>
<el-input
input-style=
"width:190px;"
v-model=
"formData.address"
placeholder=
"请输入地址"
/>
</el-form-item>
</el-col>
</el-row><el-row>
<el-col
:span=
"12"
>
<el-form-item
label=
"直属上级"
prop=
"parentId"
>
<el-select
v-model=
"formData.parentId"
placeholder=
"请选择"
>
<el-option
v-for=
"item in allContactList"
:key=
"item.id"
:label=
"item.name"
:value=
"item.id"
:disabled=
"item.id == formData.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col
:span=
"12"
>
<el-form-item
label=
"职位"
prop=
"post"
>
<el-input
input-style=
"width:190px;"
v-model=
"formData.post"
placeholder=
"请输入职位"
/>
</el-form-item>
</el-col>
</el-row><el-row>
<el-col
:span=
"12"
><el-form-item
label=
"是否关键决策人"
prop=
"master"
style=
"width: 400px"
>
<el-radio-group
v-model=
"formData.master"
>
<el-radio
v-for=
"dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key=
"dict.value"
:label=
"dict.value"
>
{{
dict
.
label
}}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col
:span=
"24"
><el-form-item
label=
"备注"
prop=
"remark"
>
<el-input
v-model=
"formData.remark"
placeholder=
"请输入备注"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template
#
footer
>
<el-button
@
click=
"submitForm"
type=
"primary"
:disabled=
"formLoading"
>
确 定
</el-button>
<el-button
@
click=
"dialogVisible = false"
>
取 消
</el-button>
</
template
>
</Dialog>
<OwerSelect
ref=
"owerRef"
@
confirmOwerSelect=
"owerSelectValue"
:initOwerUser=
"formData.ownerUserId"
/>
</template>
<
script
setup
lang=
"ts"
>
import
*
as
ContactApi
from
'@/api/crm/contact'
import
{
DICT_TYPE
,
getIntDictOptions
,
getBoolDictOptions
}
from
'@/utils/dict'
import
OwerSelect
from
'./OwerSelect.vue'
import
*
as
UserApi
from
'@/api/system/user'
import
*
as
CustomerApi
from
'@/api/crm/customer'
import
{
ElTable
}
from
'element-plus'
import
*
as
AreaApi
from
'@/api/system/area'
import
{
defaultProps
}
from
'@/utils/tree'
const
{
t
}
=
useI18n
()
// 国际化
const
message
=
useMessage
()
// 消息弹窗
...
...
@@ -172,6 +133,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const
dialogTitle
=
ref
(
''
)
// 弹窗的标题
const
formLoading
=
ref
(
false
)
// 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const
formType
=
ref
(
''
)
// 表单的类型:create - 新增;update - 修改
const
areaList
=
ref
([])
// 地区列表
const
formData
=
ref
({
nextTime
:
undefined
,
mobile
:
undefined
,
...
...
@@ -188,21 +150,10 @@ const formData = ref({
name
:
undefined
,
post
:
undefined
,
qq
:
undefined
,
we
b
chat
:
undefined
,
wechat
:
undefined
,
sex
:
undefined
,
policyMakers
:
undefined
})
const
loading
=
ref
(
true
)
// 列表的加载中
const
total
=
ref
(
0
)
// 列表的总页数
const
list
=
ref
([])
// 列表的数据
const
queryParams
=
reactive
({
pageNo
:
1
,
pageSize
:
10
,
name
:
null
,
mobile
:
null
,
industryId
:
null
,
level
:
null
,
source
:
null
master
:
false
,
areaId
:
undefined
})
const
formRules
=
reactive
({
name
:
[{
required
:
true
,
message
:
'姓名不能为空'
,
trigger
:
'blur'
}],
...
...
@@ -212,56 +163,33 @@ const formRules = reactive({
const
formRef
=
ref
()
// 表单 Ref
const
ownerUserList
=
ref
<
any
[]
>
([])
const
userList
=
ref
<
UserApi
.
UserVO
[]
>
([])
// 用户列表
const
customerList
=
ref
<
CustomerApi
.
CustomerVO
[]
>
([])
// 客户列表
const
allContactList
=
ref
([])
// 所有联系人列表
/** 打开弹窗 */
const
open
=
async
(
type
:
string
,
id
?:
number
)
=>
{
dialogVisible
.
value
=
true
dialogTitle
.
value
=
t
(
'action.'
+
type
)
formType
.
value
=
type
allContactList
.
value
=
await
ContactApi
.
simpleAlllist
()
resetForm
()
allContactList
.
value
=
await
ContactApi
.
simpleAllList
()
userList
.
value
=
await
UserApi
.
getSimpleUserList
()
customerList
.
value
=
await
CustomerApi
.
queryAllList
()
areaList
.
value
=
await
AreaApi
.
getAreaTree
()
// 修改时,设置数据
if
(
id
)
{
formLoading
.
value
=
true
try
{
formData
.
value
=
await
ContactApi
.
getContact
(
id
)
userList
.
value
=
await
UserApi
.
getSimpleUserList
()
await
gotOwnerUser
(
formData
.
value
.
ownerUserId
)
}
finally
{
formLoading
.
value
=
false
}
}
}
defineExpose
({
open
})
// 提供 open 方法,用于打开弹窗
/** 查询列表 */
const
getList
=
async
()
=>
{
loading
.
value
=
true
try
{
const
data
=
await
CustomerApi
.
getCustomerPage
(
queryParams
)
list
.
value
=
data
.
list
total
.
value
=
data
.
total
}
finally
{
loading
.
value
=
false
}
}
const
gotOwnerUser
=
(
owerUserId
:
any
)
=>
{
if
(
owerUserId
!==
null
)
{
owerUserId
.
split
(
','
).
forEach
((
item
:
string
)
=>
{
userList
.
value
.
find
((
user
:
{
id
:
any
})
=>
{
if
(
user
.
id
==
item
)
{
ownerUserList
.
value
.
push
(
user
)
}
})
})
}
}
/** 提交表单 */
const
emit
=
defineEmits
([
'success'
])
// 定义 success 事件,用于操作成功后的回调
const
submitForm
=
async
()
=>
{
owerSelectValue
(
ownerUserList
)
//
owerSelectValue(ownerUserList)
// 校验表单
if
(
!
formRef
)
return
const
valid
=
await
formRef
.
value
.
validate
()
...
...
@@ -302,52 +230,11 @@ const resetForm = () => {
name
:
undefined
,
post
:
undefined
,
qq
:
undefined
,
we
b
chat
:
undefined
,
wechat
:
undefined
,
sex
:
undefined
,
policyMakers
:
undefined
master
:
false
}
formRef
.
value
?.
resetFields
()
ownerUserList
.
value
=
[]
}
/** 添加/修改操作 */
// TODO @zyna:owner?拼写要注意哈;
const
owerRef
=
ref
()
const
openOwerForm
=
(
type
:
string
)
=>
{
owerRef
.
value
.
open
(
type
,
ownerUserList
.
value
)
}
const
owerSelectValue
=
(
value
)
=>
{
ownerUserList
.
value
=
value
.
value
formData
.
value
.
ownerUserId
=
undefined
value
.
value
.
forEach
((
item
,
index
)
=>
{
if
(
index
!=
0
)
{
formData
.
value
.
ownerUserId
=
formData
.
value
.
ownerUserId
+
','
+
item
.
id
}
else
{
formData
.
value
.
ownerUserId
=
item
.
id
}
})
}
// 选择客户
const
showCustomer
=
ref
(
false
)
const
openCustomerSelect
=
()
=>
{
showCustomer
.
value
=
!
showCustomer
.
value
queryParams
.
pageNo
=
1
getList
()
}
const
multipleTableRef
=
ref
<
InstanceType
<
typeof
ElTable
>>
()
const
multipleSelection
=
ref
()
const
handleSelectionChange
=
({},
row
)
=>
{
multipleSelection
.
value
=
row
multipleTableRef
.
value
!
.
clearSelection
()
multipleTableRef
.
value
!
.
toggleRowSelection
(
row
,
undefined
)
}
const
selectCustomer
=
()
=>
{
formData
.
value
.
customerId
=
multipleSelection
.
value
.
id
formData
.
value
.
customerName
=
multipleSelection
.
value
.
name
showCustomer
.
value
=
!
showCustomer
.
value
}
const
allContactList
=
ref
([])
// 所有联系人列表
onMounted
(
async
()
=>
{
allContactList
.
value
=
await
ContactApi
.
simpleAlllist
()
})
</
script
>
src/views/crm/contact/detail/ContactDetails.vue
View file @
e106fb73
<!--
* @Author: zyna
* @Date: 2023-11-26 10:39:46
* @LastEditTime: 2023-11-26 20:43:43
* @FilePath: \yudao-ui-admin-vue3\src\views\crm\contact\detail\ContactDetails.vue
* @Description:
-->
<
template
>
<el-collapse
v-model=
"activeNames"
>
<el-collapse-item
name=
"basicInfo"
>
...
...
@@ -24,14 +31,17 @@
{{ contact.qq }}
</el-descriptions-item>
<el-descriptions-item
label=
"微信"
>
{{ contact.webchat }}
</el-descriptions-item>
<el-descriptions-item
label=
"详细地址"
>
{{ contact.address }}
{{ contact.wechat }}
</el-descriptions-item>
<el-descriptions-item
label=
"下次联系时间"
>
{{ contact.nextTime ? formatDate(contact.nextTime) : '空' }}
</el-descriptions-item>
<el-descriptions-item
label=
"所在地"
>
{{ contact.areaName }}
</el-descriptions-item>
<el-descriptions-item
label=
"详细地址"
>
{{ contact.address }}
</el-descriptions-item>
<el-descriptions-item
label=
"性别"
>
<dict-tag
:type=
"DICT_TYPE.SYSTEM_USER_SEX"
:value=
"contact.sex"
/>
</el-descriptions-item>
...
...
@@ -46,7 +56,7 @@
</
template
>
<el-descriptions
:column=
"2"
>
<el-descriptions-item
label=
"负责人"
>
{{
gotOwnerUser(contact.ownerUserId)
}}
{{
contact.ownerUserName
}}
</el-descriptions-item>
<el-descriptions-item
label=
"创建人"
>
{{ contact.creatorName }}
...
...
@@ -66,29 +76,9 @@
import
*
as
ContactApi
from
'@/api/crm/contact'
import
{
DICT_TYPE
}
from
'@/utils/dict'
import
{
formatDate
}
from
'@/utils/formatTime'
import
*
as
UserApi
from
'@/api/system/user'
const
{
contact
}
=
defineProps
<
{
contact
:
ContactApi
.
ContactVO
}
>
()
// 展示的折叠面板
const
activeNames
=
ref
([
'basicInfo'
,
'systemInfo'
])
const
gotOwnerUser
=
(
owerUserId
:
string
)
=>
{
let
ownerName
=
''
if
(
owerUserId
!==
null
&&
owerUserId
!=
undefined
)
{
owerUserId
.
split
(
','
).
forEach
((
item
:
string
,
index
:
number
)
=>
{
if
(
index
!=
0
)
{
ownerName
=
ownerName
+
','
+
userList
.
value
.
find
((
user
:
{
id
:
any
})
=>
user
.
id
==
item
)?.
nickname
}
else
{
ownerName
=
userList
.
value
.
find
((
user
:
{
id
:
any
})
=>
user
.
id
==
item
)?.
nickname
||
''
}
})
}
return
ownerName
}
const
userList
=
ref
<
UserApi
.
UserVO
[]
>
([])
// 用户列表
/** 初始化 **/
onMounted
(
async
()
=>
{
userList
.
value
=
await
UserApi
.
getSimpleUserList
()
})
</
script
>
<
style
scoped
lang=
"scss"
></
style
>
src/views/crm/contact/detail/index.vue
View file @
e106fb73
...
...
@@ -63,33 +63,18 @@
<!-- TODO wanwan:这个 tab 拉满哈,可以更好看; -->
<el-col
:span=
"18"
>
<el-tabs>
<el-tab-pane
label=
"
详细资料
"
>
<el-tab-pane
label=
"
基本信息
"
>
<!-- TODO wanwan:这个 ml-2 是不是可以优化下,不要整个左移,而是里面的内容有个几 px 的偏移,不顶在框里 -->
<ContactDetails
class=
"ml-2"
:contact=
"contact"
/>
</el-tab-pane>
<el-tab-pane
label=
"活动"
lazy
>
活动
</el-tab-pane>
<el-tab-pane
label=
"邮件"
lazy
>
邮件
</el-tab-pane>
<el-tab-pane
label=
"工商信息"
lazy
>
工商信息
</el-tab-pane>
<!-- TODO wanwan 以下标签上的数量需要接口统计返回 -->
<el-tab-pane
label=
"客户"
lazy
>
<template
#
label
>
客户
<el-badge
:value=
"12"
class=
"item"
type=
"primary"
/>
</
template
>
客户
</el-tab-pane>
<el-tab-pane
label=
"团队成员"
lazy
>
<
template
#
label
>
团队成员
<el-badge
:value=
"2"
class=
"item"
type=
"primary"
/>
</
template
>
团队成员
</el-tab-pane>
<el-tab-pane
label=
"跟进记录"
lazy
>
跟进记录
</el-tab-pane>
<el-tab-pane
label=
"商机"
lazy
>
商机
</el-tab-pane>
<el-tab-pane
label=
"合同"
lazy
>
<
template
#
label
>
合同
<el-badge
:value=
"3"
class=
"item"
type=
"primary"
/>
</
template
>
合同
</el-tab-pane>
<el-tab-pane
label=
"回款"
lazy
>
<
template
#
label
>
回款
<el-badge
:value=
"4"
class=
"item"
type=
"primary"
/>
</
template
>
回款
<el-tab-pane
label=
"附件"
lazy
>
附件
</el-tab-pane>
<!-- TODO wanwan 以下标签上的数量需要接口统计返回 -->
<el-tab-pane
label=
"操作记录"
lazy
>
<template
#
label
>
操作记录
<el-badge
:value=
"12"
class=
"item"
type=
"primary"
/>
</
template
>
操作记录
</el-tab-pane>
<el-tab-pane
label=
"回访"
lazy
>
回访
</el-tab-pane>
<el-tab-pane
label=
"发票"
lazy
>
发票
</el-tab-pane>
</el-tabs>
</el-col>
...
...
src/views/crm/contact/index.vue
View file @
e106fb73
...
...
@@ -55,9 +55,9 @@
class=
"!w-240px"
/>
</el-form-item>
<el-form-item
label=
"微信"
prop=
"we
b
chat"
>
<el-form-item
label=
"微信"
prop=
"wechat"
>
<el-input
v-model=
"queryParams.we
b
chat"
v-model=
"queryParams.wechat"
placeholder=
"请输入微信"
clearable
@
keyup
.
enter=
"handleQuery"
...
...
@@ -109,20 +109,16 @@
</
template
>
</el-table-column>
<el-table-column
label=
"职位"
align=
"center"
prop=
"post"
/>
<el-table-column
label=
"是否关键决策人"
align=
"center"
prop=
"
policyMakers
"
>
<el-table-column
label=
"是否关键决策人"
align=
"center"
prop=
"
master
"
>
<
template
#
default=
"scope"
>
<dict-tag
:type=
"DICT_TYPE.INFRA_BOOLEAN_STRING"
:value=
"scope.row.policyMakers"
/>
</
template
>
</el-table-column>
<el-table-column
label=
"直属上级"
align=
"center"
prop=
"parentId"
>
<
template
#
default=
"scope"
>
{{
allContactList
.
find
((
contact
)
=>
contact
.
id
===
scope
.
row
.
parentId
)?.
name
}}
<dict-tag
:type=
"DICT_TYPE.INFRA_BOOLEAN_STRING"
:value=
"scope.row.master"
/>
</
template
>
</el-table-column>
<el-table-column
label=
"直属上级"
align=
"center"
prop=
"parentName"
/>
<el-table-column
label=
"手机号"
align=
"center"
prop=
"mobile"
/>
<el-table-column
label=
"座机"
align=
"center"
prop=
"telephone"
/>
<el-table-column
label=
"QQ"
align=
"center"
prop=
"qq"
/>
<el-table-column
label=
"微信"
align=
"center"
prop=
"we
b
chat"
/>
<el-table-column
label=
"微信"
align=
"center"
prop=
"wechat"
/>
<el-table-column
label=
"邮箱"
align=
"center"
prop=
"email"
/>
<el-table-column
label=
"地址"
align=
"center"
prop=
"address"
/>
<el-table-column
...
...
@@ -142,7 +138,7 @@
/>
<el-table-column
label=
"负责人"
align=
"center"
prop=
"ownerUserId"
>
<
template
#
default=
"scope"
>
{{
gotOwnerUser
(
scope
.
row
.
ownerUserId
)
}}
{{
scope
.
row
.
ownerUserName
}}
</
template
>
</el-table-column>
<!-- <el-table-column label="所属部门" align="center" prop="ownerUserId" /> -->
...
...
@@ -239,13 +235,12 @@ const queryParams = reactive({
name
:
null
,
post
:
null
,
qq
:
null
,
we
b
chat
:
null
,
wechat
:
null
,
sex
:
null
,
policyMakers
:
null
})
const
queryFormRef
=
ref
()
// 搜索的表单
const
exportLoading
=
ref
(
false
)
// 导出的加载中
const
userList
=
ref
<
UserApi
.
UserVO
[]
>
([])
// 用户列表
/** 查询列表 */
const
getList
=
async
()
=>
{
...
...
@@ -305,35 +300,15 @@ const handleExport = async () => {
}
}
// TODO @zyna:这个负责人的读取,放在后端好点
const
gotOwnerUser
=
(
owerUserId
:
string
)
=>
{
let
ownerName
=
''
if
(
owerUserId
!==
null
)
{
owerUserId
.
split
(
','
).
forEach
((
item
:
string
,
index
:
number
)
=>
{
if
(
index
!=
0
)
{
ownerName
=
ownerName
+
','
+
userList
.
value
.
find
((
user
:
{
id
:
any
})
=>
user
.
id
==
item
)?.
nickname
}
else
{
ownerName
=
userList
.
value
.
find
((
user
:
{
id
:
any
})
=>
user
.
id
==
item
)?.
nickname
||
''
}
})
}
return
ownerName
}
/** 打开客户详情 */
const
{
push
}
=
useRouter
()
const
openDetail
=
(
id
:
number
)
=>
{
push
({
name
:
'CrmContactDetail'
,
params
:
{
id
}
})
}
// TODO @zyna:这个上级的读取,放在后端读取,更合适;因为可能数据量比较大
const
allContactList
=
ref
([])
//所有联系人列表
const
allCustomerList
=
ref
([])
//客户列表
/** 初始化 **/
onMounted
(
async
()
=>
{
await
getList
()
userList
.
value
=
await
UserApi
.
getSimpleUserList
()
allContactList
.
value
=
await
ContactApi
.
simpleAlllist
()
})
</
script
>
src/views/crm/customer/detail/index.vue
View file @
e106fb73
...
...
@@ -8,7 +8,7 @@
</div>
<div>
<!-- 右上:按钮 -->
<el-button
@
click=
"openForm('update', customer.id)"
v-hasPermi=
"['crm:customer:update']
"
>
<el-button
v-hasPermi=
"['crm:customer:update']"
@
click=
"openForm('update', customer.id)
"
>
编辑
</el-button>
<el-button>
更改成交状态
</el-button>
...
...
@@ -16,31 +16,31 @@
</div>
<el-row
class=
"mt-10px"
>
<el-button>
<Icon
icon=
"ph:calendar-fill"
class=
"mr-5px
"
/>
<Icon
class=
"mr-5px"
icon=
"ph:calendar-fill
"
/>
创建任务
</el-button>
<el-button>
<Icon
icon=
"carbon:email"
class=
"mr-5px
"
/>
<Icon
class=
"mr-5px"
icon=
"carbon:email
"
/>
发送邮件
</el-button>
<el-button>
<Icon
icon=
"system-uicons:contacts"
class=
"mr-5px
"
/>
<Icon
class=
"mr-5px"
icon=
"system-uicons:contacts
"
/>
创建联系人
</el-button>
<el-button>
<Icon
icon=
"ep:opportunity"
class=
"mr-5px
"
/>
<Icon
class=
"mr-5px"
icon=
"ep:opportunity
"
/>
创建商机
</el-button>
<el-button>
<Icon
icon=
"clarity:contract-line"
class=
"mr-5px
"
/>
<Icon
class=
"mr-5px"
icon=
"clarity:contract-line
"
/>
创建合同
</el-button>
<el-button>
<Icon
icon=
"icon-park:income-one"
class=
"mr-5px
"
/>
<Icon
class=
"mr-5px"
icon=
"icon-park:income-one
"
/>
创建回款
</el-button>
<el-button>
<Icon
icon=
"fluent:people-team-add-20-filled"
class=
"mr-5px
"
/>
<Icon
class=
"mr-5px"
icon=
"fluent:people-team-add-20-filled
"
/>
添加团队成员
</el-button>
</el-row>
...
...
@@ -75,20 +75,32 @@
<el-tab-pane
label=
"客户关系"
lazy
>
客户关系
</el-tab-pane>
<!-- TODO wanwan 以下标签上的数量需要接口统计返回 -->
<el-tab-pane
label=
"联系人"
lazy
>
<template
#
label
>
联系人
<el-badge
class=
"item"
type=
"primary"
/>
</
template
>
<template
#
label
>
联系人
<el-badge
class=
"item"
type=
"primary"
/>
</
template
>
联系人
</el-tab-pane>
<el-tab-pane
label=
"团队成员"
lazy
>
<
template
#
label
>
团队成员
<el-badge
class=
"item"
type=
"primary"
/>
</
template
>
团队成员
<
template
#
label
>
团队成员
<el-badge
class=
"item"
type=
"primary"
/>
</
template
>
<CrmPermissionList
:biz-id=
"customer.id"
:biz-type=
"CrmBizTypeEnum.CRM_CUSTOMER"
/>
</el-tab-pane>
<el-tab-pane
label=
"商机"
lazy
>
商机
</el-tab-pane>
<el-tab-pane
label=
"合同"
lazy
>
<
template
#
label
>
合同
<el-badge
class=
"item"
type=
"primary"
/>
</
template
>
<
template
#
label
>
合同
<el-badge
class=
"item"
type=
"primary"
/>
</
template
>
合同
</el-tab-pane>
<el-tab-pane
label=
"回款"
lazy
>
<
template
#
label
>
回款
<el-badge
class=
"item"
type=
"primary"
/>
</
template
>
<
template
#
label
>
回款
<el-badge
class=
"item"
type=
"primary"
/>
</
template
>
回款
</el-tab-pane>
<el-tab-pane
label=
"回访"
lazy
>
回访
</el-tab-pane>
...
...
@@ -100,7 +112,7 @@
<CustomerForm
ref=
"formRef"
@
success=
"getCustomerData(id)"
/>
</template>
<
script
setup
lang=
"ts"
>
<
script
lang=
"ts"
setup
>
import
{
ElMessage
}
from
'element-plus'
import
{
useTagsViewStore
}
from
'@/store/modules/tagsView'
import
*
as
CustomerApi
from
'@/api/crm/customer'
...
...
@@ -108,6 +120,7 @@ import CustomerBasicInfo from '@/views/crm/customer/detail/CustomerBasicInfo.vue
import
{
DICT_TYPE
}
from
'@/utils/dict'
import
CustomerDetails
from
'@/views/crm/customer/detail/CustomerDetails.vue'
import
CustomerForm
from
'@/views/crm/customer/CustomerForm.vue'
import
{
CrmBizTypeEnum
,
CrmPermissionList
}
from
'@/views/crm/components'
defineOptions
({
name
:
'CustomerDetail'
})
...
...
src/views/crm/customer/index.vue
View file @
e106fb73
...
...
@@ -121,7 +121,7 @@
<el-table-column
align=
"center"
label=
"手机"
prop=
"mobile"
width=
"120"
/>
<el-table-column
align=
"center"
label=
"详细地址"
prop=
"detailAddress"
width=
"200"
/>
<el-table-column
align=
"center"
label=
"负责人"
prop=
"ownerUserName"
/>
<el-table-column
align=
"center"
label=
"所属部门"
prop=
"ownerUserDept"
/>
<el-table-column
align=
"center"
label=
"所属部门"
prop=
"ownerUserDept
Name
"
/>
<el-table-column
align=
"center"
label=
"创建人"
prop=
"creatorName"
/>
<el-table-column
:formatter=
"dateFormatter"
...
...
@@ -185,8 +185,6 @@
@
pagination=
"getList"
/>
</ContentWrap>
<!-- TODO 方便查看效果 TODO 芋艿:先注释了,避免演示环境报错 -->
<!-- <CrmTeam :biz-id="1" :biz-type="CrmBizTypeEnum.CRM_CUSTOMER" />-->
<!-- 表单弹窗:添加/修改 -->
<CustomerForm
ref=
"formRef"
@
success=
"getList"
/>
...
...
@@ -198,7 +196,6 @@ import { dateFormatter } from '@/utils/formatTime'
import
download
from
'@/utils/download'
import
*
as
CustomerApi
from
'@/api/crm/customer'
import
CustomerForm
from
'./CustomerForm.vue'
import
{
CrmBizTypeEnum
,
CrmTeam
}
from
'@/views/crm/components'
defineOptions
({
name
:
'CrmCustomer'
})
...
...
@@ -211,11 +208,12 @@ const list = ref([]) // 列表的数据
const
queryParams
=
reactive
({
pageNo
:
1
,
pageSize
:
10
,
name
:
null
,
mobile
:
null
,
industryId
:
null
,
level
:
null
,
source
:
null
pool
:
false
,
name
:
''
,
mobile
:
''
,
industryId
:
undefined
,
level
:
undefined
,
source
:
undefined
})
const
queryFormRef
=
ref
()
// 搜索的表单
const
exportLoading
=
ref
(
false
)
// 导出的加载中
...
...
@@ -241,6 +239,7 @@ const handleQuery = () => {
/** 重置按钮操作 */
const
resetQuery
=
()
=>
{
queryFormRef
.
value
.
resetFields
()
queryParams
.
pool
=
false
handleQuery
()
}
...
...
src/views/infra/webSocket/index.vue
View file @
e106fb73
<
template
>
<div
class=
"flex"
>
<!-- 左侧:建立连接、发送消息 -->
<el-card
:gutter=
"12"
class=
"w-1/2"
shadow=
"always"
>
<template
#
header
>
<div
class=
"card-header"
>
...
...
@@ -11,28 +12,38 @@
<el-tag
:color=
"getTagColor"
>
{{ status }}
</el-tag>
</div>
<hr
class=
"my-4"
/>
<div
class=
"flex"
>
<el-input
v-model=
"server"
disabled
>
<
template
#
prepend
>
服务地址
</
template
>
<
template
#
prepend
>
服务地址
</
template
>
</el-input>
<el-button
:type=
"getIsOpen ? 'danger' : 'primary'"
@
click=
"toggle"
>
<el-button
:type=
"getIsOpen ? 'danger' : 'primary'"
@
click=
"toggle
ConnectStatus
"
>
{{ getIsOpen ? '关闭连接' : '开启连接' }}
</el-button>
</div>
<p
class=
"mt-4 text-lg font-medium"
>
设置
</p>
<p
class=
"mt-4 text-lg font-medium"
>
消息输入框
</p>
<hr
class=
"my-4"
/>
<el-input
v-model=
"send
Value
"
v-model=
"send
Text
"
:autosize=
"{ minRows: 2, maxRows: 4 }"
:disabled=
"!getIsOpen"
clearable
type=
"textarea"
placeholder=
"请输入你要发送的消息"
/>
<el-button
:disabled=
"!getIsOpen"
block
class=
"mt-4"
type=
"primary"
@
click=
"handlerSend"
>
<el-select
v-model=
"sendUserId"
class=
"mt-4"
placeholder=
"请选择发送人"
>
<el-option
key=
""
label=
"所有人"
value=
""
/>
<el-option
v-for=
"user in userList"
:key=
"user.id"
:label=
"user.nickname"
:value=
"user.id"
/>
</el-select>
<el-button
:disabled=
"!getIsOpen"
block
class=
"ml-2 mt-4"
type=
"primary"
@
click=
"handlerSend"
>
发送
</el-button>
</el-card>
<!-- 右侧:消息记录 -->
<el-card
:gutter=
"12"
class=
"w-1/2"
shadow=
"always"
>
<
template
#
header
>
<div
class=
"card-header"
>
...
...
@@ -41,13 +52,13 @@
</
template
>
<div
class=
"max-h-80 overflow-auto"
>
<ul>
<li
v-for=
"
item in getList"
:key=
"item
.time"
class=
"mt-2"
>
<li
v-for=
"
msg in messageList.reverse()"
:key=
"msg
.time"
class=
"mt-2"
>
<div
class=
"flex items-center"
>
<span
class=
"text-primary mr-2 font-medium"
>
收到消息:
</span>
<span>
{{ formatDate(
item
.time) }}
</span>
<span>
{{ formatDate(
msg
.time) }}
</span>
</div>
<div>
{{
item.res
}}
{{
msg.text
}}
</div>
</li>
</ul>
...
...
@@ -57,62 +68,113 @@
</template>
<
script
lang=
"ts"
setup
>
import
{
formatDate
}
from
'@/utils/formatTime'
import
{
useUserStore
}
from
'@/store/modules/user'
import
{
useWebSocket
}
from
'@vueuse/core'
import
{
getAccessToken
}
from
'@/utils/auth'
import
*
as
UserApi
from
'@/api/system/user'
defineOptions
({
name
:
'InfraWebSocket'
})
const
userStore
=
useUserStore
()
const
sendValue
=
ref
(
''
)
const
message
=
useMessage
()
// 消息弹窗
const
server
=
ref
(
(
import
.
meta
.
env
.
VITE_BASE_URL
+
'/websocket/message'
).
replace
(
'http'
,
'ws'
)
+
'?userId='
+
userStore
.
getUser
.
id
)
const
state
=
reactive
({
recordList
:
[]
as
{
id
:
number
;
time
:
number
;
res
:
string
}[]
})
(
import
.
meta
.
env
.
VITE_BASE_URL
+
'/infra/ws'
).
replace
(
'http'
,
'ws'
)
+
'?token='
+
getAccessToken
()
)
// WebSocket 服务地址
const
getIsOpen
=
computed
(()
=>
status
.
value
===
'OPEN'
)
// WebSocket 连接是否打开
const
getTagColor
=
computed
(()
=>
(
getIsOpen
.
value
?
'success'
:
'red'
))
// WebSocket 连接的展示颜色
/** 发起 WebSocket 连接 */
const
{
status
,
data
,
send
,
close
,
open
}
=
useWebSocket
(
server
.
value
,
{
autoReconnect
:
false
,
heartbeat
:
true
})
/** 监听接收到的数据 */
const
messageList
=
ref
([]
as
{
time
:
number
;
text
:
string
}[])
// 消息列表
watchEffect
(()
=>
{
if
(
data
.
value
)
{
try
{
const
res
=
JSON
.
parse
(
data
.
value
)
state
.
recordList
.
push
(
res
)
}
catch
(
error
)
{
state
.
recordList
.
push
({
res
:
data
.
value
,
id
:
Math
.
ceil
(
Math
.
random
()
*
1000
),
if
(
!
data
.
value
)
{
return
}
try
{
// 1. 收到心跳
if
(
data
.
value
===
'pong'
)
{
// state.recordList.push({
// text: '【心跳】',
// time: new Date().getTime()
// })
return
}
// 2.1 解析 type 消息类型
const
jsonMessage
=
JSON
.
parse
(
data
.
value
)
const
type
=
jsonMessage
.
type
const
content
=
JSON
.
parse
(
jsonMessage
.
content
)
if
(
!
type
)
{
message
.
error
(
'未知的消息类型:'
+
data
.
value
)
return
}
// 2.2 消息类型:demo-message-receive
if
(
type
===
'demo-message-receive'
)
{
const
single
=
content
.
single
if
(
single
)
{
messageList
.
value
.
push
({
text
:
`【单发】用户编号(
${
content
.
fromUserId
}
):
${
content
.
text
}
`
,
time
:
new
Date
().
getTime
()
})
}
else
{
messageList
.
value
.
push
({
text
:
`【群发】用户编号(
${
content
.
fromUserId
}
):
${
content
.
text
}
`
,
time
:
new
Date
().
getTime
()
})
}
return
}
// 2.3 消息类型:notice-push
if
(
type
===
'notice-push'
)
{
messageList
.
value
.
push
({
text
:
`【系统通知】:
${
content
.
title
}
`
,
time
:
new
Date
().
getTime
()
})
return
}
message
.
error
(
'未处理消息:'
+
data
.
value
)
}
catch
(
error
)
{
message
.
error
(
'处理消息发生异常:'
+
data
.
value
)
console
.
error
(
error
)
}
})
const
getIsOpen
=
computed
(()
=>
status
.
value
===
'OPEN'
)
const
getTagColor
=
computed
(()
=>
(
getIsOpen
.
value
?
'success'
:
'red'
))
const
getList
=
computed
(()
=>
{
return
[...
state
.
recordList
].
reverse
()
})
function
handlerSend
()
{
send
(
sendValue
.
value
)
sendValue
.
value
=
''
/** 发送消息 */
const
sendText
=
ref
(
''
)
// 发送内容
const
sendUserId
=
ref
(
''
)
// 发送人
const
handlerSend
=
()
=>
{
// 1.1 先 JSON 化 message 消息内容
const
messageContent
=
JSON
.
stringify
({
text
:
sendText
.
value
,
toUserId
:
sendUserId
.
value
})
// 1.2 再 JSON 化整个消息
const
jsonMessage
=
JSON
.
stringify
({
type
:
'demo-message-send'
,
content
:
messageContent
})
// 2. 最后发送消息
send
(
jsonMessage
)
sendText
.
value
=
''
}
function
toggle
()
{
/** 切换 websocket 连接状态 */
const
toggleConnectStatus
=
()
=>
{
if
(
getIsOpen
.
value
)
{
close
()
}
else
{
open
()
}
}
/** 初始化 **/
const
userList
=
ref
<
any
[]
>
([])
// 用户列表
onMounted
(
async
()
=>
{
// 获取用户列表
userList
.
value
=
await
UserApi
.
getSimpleUserList
()
})
</
script
>
src/views/mall/product/comment/CommentForm.vue
View file @
e106fb73
...
...
@@ -8,14 +8,7 @@
v-loading=
"formLoading"
>
<el-form-item
label=
"商品"
prop=
"spuId"
>
<div
@
click=
"handleSelectSpu"
class=
"h-60px w-60px"
>
<div
v-if=
"spuData && spuData.picUrl"
>
<el-image
:src=
"spuData.picUrl"
/>
</div>
<div
v-else
class=
"select-box"
>
<Icon
icon=
"ep:plus"
/>
</div>
</div>
<SpuShowcase
v-model=
"formData.spuId"
:limit=
"1"
/>
</el-form-item>
<el-form-item
label=
"商品规格"
prop=
"skuId"
v-if=
"formData.spuId"
>
<div
@
click=
"handleSelectSku"
class=
"h-60px w-60px"
>
...
...
@@ -51,12 +44,11 @@
<el-button
@
click=
"dialogVisible = false"
>
取 消
</el-button>
</
template
>
</Dialog>
<SpuTableSelect
ref=
"spuTableSelectRef"
@
change=
"handleSpuChange"
/>
<SkuTableSelect
ref=
"skuTableSelectRef"
@
change=
"handleSkuChange"
:spu-id=
"spuData.id"
/>
<SkuTableSelect
ref=
"skuTableSelectRef"
@
change=
"handleSkuChange"
:spu-id=
"formData.spuId"
/>
</template>
<
script
setup
lang=
"ts"
>
import
*
as
CommentApi
from
'@/api/mall/product/comment'
import
Spu
TableSelect
from
'@/views/mall/product/spu/components/SpuTableSelect
.vue'
import
Spu
Showcase
from
'@/views/mall/product/spu/components/SpuShowcase
.vue'
import
*
as
ProductSpuApi
from
'@/api/mall/product/spu'
import
SkuTableSelect
from
'@/views/mall/product/spu/components/SkuTableSelect.vue'
...
...
@@ -72,8 +64,7 @@ const formData = ref({
userId
:
undefined
,
userNickname
:
undefined
,
userAvatar
:
undefined
,
spuId
:
undefined
,
spuName
:
undefined
,
spuId
:
0
,
skuId
:
undefined
,
descriptionScores
:
5
,
benefitScores
:
5
,
...
...
@@ -90,7 +81,6 @@ const formRules = reactive({
benefitScores
:
[{
required
:
true
,
message
:
'服务星级不能为空'
,
trigger
:
'blur'
}]
})
const
formRef
=
ref
()
// 表单 Ref
const
spuData
=
ref
<
ProductSpuApi
.
Spu
>
({})
const
skuData
=
ref
({
id
:
-
1
,
name
:
''
,
...
...
@@ -149,8 +139,7 @@ const resetForm = () => {
userId
:
undefined
,
userNickname
:
undefined
,
userAvatar
:
undefined
,
spuId
:
undefined
,
spuName
:
undefined
,
spuId
:
0
,
skuId
:
undefined
,
descriptionScores
:
5
,
benefitScores
:
5
,
...
...
@@ -160,16 +149,6 @@ const resetForm = () => {
formRef
.
value
?.
resetFields
()
}
/** SPU 表格选择 */
const
spuTableSelectRef
=
ref
()
const
handleSelectSpu
=
()
=>
{
spuTableSelectRef
.
value
.
open
()
}
const
handleSpuChange
=
(
spu
:
ProductSpuApi
.
Spu
)
=>
{
spuData
.
value
=
spu
formData
.
value
.
spuId
=
spu
.
id
}
/** SKU 表格选择 */
const
skuTableSelectRef
=
ref
()
const
handleSelectSku
=
()
=>
{
...
...
src/views/mall/product/comment/index.vue
View file @
e106fb73
...
...
@@ -59,7 +59,7 @@
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading=
"loading"
:data=
"list"
:stripe=
"true"
:show-overflow-tooltip=
"false"
>
<el-table-column
label=
"评论编号"
align=
"center"
prop=
"id"
min-width=
"
5
0"
/>
<el-table-column
label=
"评论编号"
align=
"center"
prop=
"id"
min-width=
"
8
0"
/>
<el-table-column
label=
"商品信息"
align=
"center"
min-width=
"400"
>
<template
#
default=
"scope"
>
<div
class=
"row flex items-center gap-x-4px"
>
...
...
src/views/mall/product/spu/components/SpuShowcase.vue
View file @
e106fb73
...
...
@@ -13,54 +13,64 @@
</div>
</el-tooltip>
</div>
<el-tooltip
content=
"选择商品"
>
<div
v-show=
"!disabled"
v-if=
"!limit || limit
<
=
productSpus
.
length
"
class=
"select-box"
@
click=
"openSpuTableSelect"
>
<el-tooltip
content=
"选择商品"
v-if=
"canAdd"
>
<div
class=
"select-box"
@
click=
"openSpuTableSelect"
>
<Icon
icon=
"ep:plus"
/>
</div>
</el-tooltip>
</div>
<!-- 商品选择对话框(表格形式) -->
<SpuTableSelect
ref=
"spuTableSelectRef"
multiple
@
change=
"handleSpuSelected"
/>
<SpuTableSelect
ref=
"spuTableSelectRef"
:multiple=
"limit != 1"
@
change=
"handleSpuSelected"
/>
</
template
>
<
script
lang=
"ts"
setup
>
import
*
as
ProductSpuApi
from
'@/api/mall/product/spu'
import
SpuTableSelect
from
'@/views/mall/product/spu/components/SpuTableSelect.vue'
import
{
propTypes
}
from
'@/utils/propTypes'
import
{
array
}
from
'vue-types'
import
{
oneOfType
}
from
'vue-types'
import
{
isArray
}
from
'@/utils/is'
// 商品橱窗,一般用于与商品建立关系时使用
// 提供功能:展示商品列表、添加商品、移除商品
defineOptions
({
name
:
'SpuShowcase'
})
const
props
=
defineProps
({
modelValue
:
array
<
number
>
().
def
([
]).
isRequired
,
modelValue
:
oneOfType
<
number
|
Array
<
number
>>
([
Number
,
Array
]).
isRequired
,
// 限制数量:默认不限制
limit
:
propTypes
.
number
.
def
(
0
),
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
productSpus
.
value
.
length
<
props
.
limit
})
// 商品列表
const
productSpus
=
ref
<
ProductSpuApi
.
Spu
[]
>
([])
watch
(
()
=>
props
.
modelValue
,
async
()
=>
{
if
(
props
.
modelValue
.
length
===
0
)
{
const
ids
=
isArray
(
props
.
modelValue
)
?
// 情况一:多选
props
.
modelValue
:
// 情况二:单选
props
.
modelValue
?
[
props
.
modelValue
]
:
[]
// 不需要返显
if
(
ids
.
length
===
0
)
{
productSpus
.
value
=
[]
return
}
// 只有商品发生变化之后,才去查询商品
if
(
productSpus
.
value
.
length
===
0
||
productSpus
.
value
.
some
((
spu
)
=>
!
props
.
modelValue
.
includes
(
spu
.
id
))
)
{
debugger
productSpus
.
value
=
await
ProductSpuApi
.
getSpuDetailList
(
props
.
modelValue
)
if
(
productSpus
.
value
.
length
===
0
||
productSpus
.
value
.
some
((
spu
)
=>
!
ids
.
includes
(
spu
.
id
!
)))
{
productSpus
.
value
=
await
ProductSpuApi
.
getSpuDetailList
(
ids
)
}
},
{
immediate
:
true
}
...
...
@@ -77,8 +87,8 @@ const openSpuTableSelect = () => {
* 选择商品后触发
* @param spus 选中的商品列表
*/
const
handleSpuSelected
=
(
spus
:
ProductSpuApi
.
Spu
[])
=>
{
productSpus
.
value
=
spus
const
handleSpuSelected
=
(
spus
:
ProductSpuApi
.
Spu
|
ProductSpuApi
.
Spu
[])
=>
{
productSpus
.
value
=
isArray
(
spus
)
?
spus
:
[
spus
]
emitSpuChange
()
}
...
...
@@ -92,11 +102,17 @@ const handleRemoveSpu = (index: number) => {
}
const
emit
=
defineEmits
([
'update:modelValue'
,
'change'
])
const
emitSpuChange
=
()
=>
{
emit
(
'update:modelValue'
,
productSpus
.
value
.
map
((
spu
)
=>
spu
.
id
)
)
emit
(
'change'
,
productSpus
.
value
)
if
(
props
.
limit
===
1
)
{
const
spu
=
productSpus
.
value
.
length
>
0
?
productSpus
.
value
[
0
]
:
null
emit
(
'update:modelValue'
,
spu
?.
id
||
0
)
emit
(
'change'
,
spu
)
}
else
{
emit
(
'update:modelValue'
,
productSpus
.
value
.
map
((
spu
)
=>
spu
.
id
)
)
emit
(
'change'
,
productSpus
.
value
)
}
}
</
script
>
...
...
src/views/mall/product/spu/components/SpuTableSelect.vue
View file @
e106fb73
<
template
>
<Dialog
v-model=
"dialogVisible"
:appendToBody=
"true"
title=
"选择商品"
width=
"70%"
>
<ContentWrap>
<el-row
:gutter=
"20"
class=
"mb-10px"
>
<el-col
:span=
"6"
>
<el-form
ref=
"queryFormRef"
:inline=
"true"
:model=
"queryParams"
class=
"-mb-15px"
label-width=
"68px"
>
<el-form-item
label=
"商品名称"
prop=
"name"
>
<el-input
v-model=
"queryParams.name"
class=
"!w-240px"
...
...
@@ -10,19 +16,19 @@
placeholder=
"请输入商品名称"
@
keyup
.
enter=
"handleQuery"
/>
</el-
col
>
<el-
col
:span=
"6
"
>
</el-
form-item
>
<el-
form-item
label=
"商品分类"
prop=
"categoryId
"
>
<el-tree-select
v-model=
"queryParams.categoryId"
:data=
"categoryTreeList"
:props=
"defaultProps"
check-strictly
class=
"
w-1/1
"
class=
"
!w-240px
"
node-key=
"id"
placeholder=
"请选择商品分类"
/>
</el-
col
>
<el-
col
:span=
"6
"
>
</el-
form-item
>
<el-
form-item
label=
"创建时间"
prop=
"createTime
"
>
<el-date-picker
v-model=
"queryParams.createTime"
:default-time=
"[new Date('1 00:00:00'), new Date('1 23:59:59')]"
...
...
@@ -32,8 +38,8 @@
type=
"daterange"
value-format=
"YYYY-MM-DD HH:mm:ss"
/>
</el-
col
>
<el-
col
:span=
"6"
>
</el-
form-item
>
<el-
form-item
>
<el-button
@
click=
"handleQuery"
>
<Icon
class=
"mr-5px"
icon=
"ep:search"
/>
搜索
...
...
@@ -42,30 +48,32 @@
<Icon
class=
"mr-5px"
icon=
"ep:refresh"
/>
重置
</el-button>
</el-
col
>
</el-
row
>
</el-
form-item
>
</el-
form
>
<el-table
v-loading=
"loading"
:data=
"list"
show-overflow-tooltip
>
<!--
多选模式
-->
<el-table-column
key=
"2"
type=
"selection"
width=
"55"
v-if=
"multiple"
>
<!--
1. 多选模式(不能使用type="selection",Element会忽略Header插槽)
-->
<el-table-column
width=
"55"
v-if=
"multiple"
>
<template
#
header
>
<el-checkbox
:value=
"allChecked && checkedPageNos.indexOf(queryParams.pageNo) > -1"
v-model=
"isCheckAll"
:indeterminate=
"isIndeterminate"
@
change=
"handleCheckAll"
/>
</
template
>
<
template
#
default=
"{ row }"
>
<el-checkbox
:value=
"checkedSpuIds.indexOf(row.id) > -1
"
@
change=
"(checked: boolean) => handleCheckOne(checked, row)"
v-model=
"checkedStatus[row.id]
"
@
change=
"(checked: boolean) => handleCheckOne(checked, row
, true
)"
/>
</
template
>
</el-table-column>
<!-- 单选模式 -->
<!--
2.
单选模式 -->
<el-table-column
label=
"#"
width=
"55"
v-else
>
<
template
#
default=
"{ row }"
>
<el-radio
:label=
"row.id"
v-model=
"selectedSpuId"
@
change=
"handleSingleSelected(row)"
>
</el-radio
>
<el-radio
:label=
"row.id"
v-model=
"selectedSpuId"
@
change=
"handleSingleSelected(row)"
>
<!-- 空格不能省略,是为了让单选框不显示label,如果不指定label不会有选中的效果 -->
</el-radio>
</
template
>
</el-table-column>
<el-table-column
key=
"id"
align=
"center"
label=
"商品编号"
prop=
"id"
min-width=
"60"
/>
...
...
@@ -102,54 +110,71 @@
</template>
<
script
lang=
"ts"
setup
>
import
{
ElTable
}
from
'element-plus'
import
{
defaultProps
,
handleTree
}
from
'@/utils/tree'
import
*
as
ProductCategoryApi
from
'@/api/mall/product/category'
import
*
as
ProductSpuApi
from
'@/api/mall/product/spu'
import
{
propTypes
}
from
'@/utils/propTypes'
import
{
CHANGE_EVENT
}
from
'element-plus'
type
Spu
=
Required
<
ProductSpuApi
.
Spu
>
/**
* 商品表格选择对话框
* 1. 单选模式:
* 1.1 点击表格左侧的单选框时,结束选择,并关闭对话框
* 1.2 再次打开时,保持选中状态
* 2. 多选模式:
* 2.1 点击表格左侧的多选框时,记录选中的商品
* 2.2 切换分页时,保持商品的选中的状态
* 2.3 点击右下角的确定按钮时,结束选择,关闭对话框
* 2.4 再次打开时,保持选中状态
*/
defineOptions
({
name
:
'SpuTableSelect'
})
const
props
=
defineProps
({
// 多选
defineProps
({
// 多选
模式
multiple
:
propTypes
.
bool
.
def
(
false
)
})
const
total
=
ref
(
0
)
// 列表的总页数
const
list
=
ref
<
Spu
[]
>
([])
// 列表的数据
const
loading
=
ref
(
false
)
// 列表的加载中
const
dialogVisible
=
ref
(
false
)
// 弹窗的是否展示
// 列表的总页数
const
total
=
ref
(
0
)
// 列表的数据
const
list
=
ref
<
Spu
[]
>
([])
// 列表的加载中
const
loading
=
ref
(
false
)
// 弹窗的是否展示
const
dialogVisible
=
ref
(
false
)
// 查询参数
const
queryParams
=
ref
({
pageNo
:
1
,
pageSize
:
10
,
tabType
:
0
,
// 默认获取上架的商品
// 默认获取上架的商品
tabType
:
0
,
name
:
''
,
categoryId
:
null
,
createTime
:
[]
})
// 查询参数
const
selectedSpuId
=
ref
()
// 选中的商品 spuId
})
/** 打开弹窗 */
const
open
=
(
spus
?:
Spu
[])
=>
{
if
(
spus
&&
spus
.
length
>
0
)
{
// todo check-box不显示选中?
checkedSpus
.
value
=
[...
spus
]
checkedSpuIds
.
value
=
spus
.
map
((
spu
)
=>
spu
.
id
)
}
else
{
checkedSpus
.
value
=
[]
checkedSpuIds
.
value
=
[]
const
open
=
(
spuList
?:
Spu
[])
=>
{
// 重置
checkedSpus
.
value
=
[]
checkedStatus
.
value
=
{}
isCheckAll
.
value
=
false
isIndeterminate
.
value
=
false
// 处理已选中
if
(
spuList
&&
spuList
.
length
>
0
)
{
checkedSpus
.
value
=
[...
spuList
]
checkedStatus
.
value
=
Object
.
fromEntries
(
spuList
.
map
((
spu
)
=>
[
spu
.
id
,
true
]))
}
allChecked
.
value
=
false
checkedPageNos
.
value
=
[]
dialogVisible
.
value
=
true
resetQuery
()
}
defineExpose
({
open
})
// 提供 open 方法,用于打开弹窗
// 提供 open 方法,用于打开弹窗
defineExpose
({
open
})
/** 查询列表 */
const
getList
=
async
()
=>
{
...
...
@@ -158,6 +183,12 @@ const getList = async () => {
const
data
=
await
ProductSpuApi
.
getSpuPage
(
queryParams
.
value
)
list
.
value
=
data
.
list
total
.
value
=
data
.
total
// checkbox绑定undefined会有问题,需要给一个bool值
list
.
value
.
forEach
(
(
spu
)
=>
(
checkedStatus
.
value
[
spu
.
id
]
=
checkedStatus
.
value
[
spu
.
id
]
||
false
)
)
// 计算全选框状态
calculateIsCheckAll
()
}
finally
{
loading
.
value
=
false
}
...
...
@@ -174,7 +205,8 @@ const resetQuery = () => {
queryParams
.
value
=
{
pageNo
:
1
,
pageSize
:
10
,
tabType
:
0
,
// 默认获取上架的商品
// 默认获取上架的商品
tabType
:
0
,
name
:
''
,
categoryId
:
null
,
createTime
:
[]
...
...
@@ -182,65 +214,85 @@ const resetQuery = () => {
getList
()
}
const
allChecked
=
ref
(
false
)
//是否全选
const
checkedPageNos
=
ref
<
number
[]
>
([])
//选中的页码
const
checkedSpuIds
=
ref
<
number
[]
>
([])
//选中的商品ID
const
checkedSpus
=
ref
<
Spu
[]
>
([])
//选中的商品
// 是否全选
const
isCheckAll
=
ref
(
false
)
// 全选框是否处于中间状态:不是全部选中 && 任意一个选中
const
isIndeterminate
=
ref
(
false
)
// 选中的商品
const
checkedSpus
=
ref
<
Spu
[]
>
([])
// 选中状态:key为商品ID,value为是否选中
const
checkedStatus
=
ref
<
Record
<
string
,
boolean
>>
({})
// 选中的商品 spuId
const
selectedSpuId
=
ref
()
/** 单选中时触发 */
const
handleSingleSelected
=
(
row
:
Spu
)
=>
{
emits
(
'change'
,
row
)
const
handleSingleSelected
=
(
spu
:
Spu
)
=>
{
emits
(
CHANGE_EVENT
,
spu
)
// 关闭弹窗
dialogVisible
.
value
=
false
// 记住上次选择的ID
selectedSpuId
.
value
=
row
.
id
selectedSpuId
.
value
=
spu
.
id
}
/** 多选完成 */
const
handleEmitChange
=
()
=>
{
// 关闭弹窗
dialogVisible
.
value
=
false
emits
(
'change'
,
[...
checkedSpus
.
value
])
emits
(
CHANGE_EVENT
,
[...
checkedSpus
.
value
])
}
/** 确认选择时的触发事件 */
const
emits
=
defineEmits
<
{
(
e
:
'change'
,
spu
:
Spu
|
Spu
[]
|
any
):
void
change
:
[
spu
:
Spu
|
Spu
[]
|
any
]
}
>
()
/** 全选 */
/** 全选
/全不选
*/
const
handleCheckAll
=
(
checked
:
boolean
)
=>
{
debugger
console
.
log
(
'checkAll'
,
checked
)
allChecked
.
value
=
checked
const
index
=
checkedPageNos
.
value
.
indexOf
(
queryParams
.
value
.
pageNo
)
checkedPageNos
.
value
.
push
(
queryParams
.
value
.
pageNo
)
if
(
index
>
-
1
)
{
checkedPageNos
.
value
.
splice
(
index
,
1
)
}
isCheckAll
.
value
=
checked
isIndeterminate
.
value
=
false
list
.
value
.
forEach
((
item
)
=>
handleCheckOne
(
checked
,
item
))
list
.
value
.
forEach
((
spu
)
=>
handleCheckOne
(
checked
,
spu
,
false
))
}
/** 选中一行 */
const
handleCheckOne
=
(
checked
:
boolean
,
spu
:
Spu
)
=>
{
/**
* 选中一行
* @param checked 是否选中
* @param spu 商品
* @param isCalcCheckAll 是否计算全选
*/
const
handleCheckOne
=
(
checked
:
boolean
,
spu
:
Spu
,
isCalcCheckAll
:
boolean
)
=>
{
if
(
checked
)
{
const
index
=
checkedSpuIds
.
value
.
indexOf
(
spu
.
id
)
if
(
index
===
-
1
)
{
checkedSpuIds
.
value
.
push
(
spu
.
id
)
checkedSpus
.
value
.
push
(
spu
)
}
checkedSpus
.
value
.
push
(
spu
)
checkedStatus
.
value
[
spu
.
id
]
=
true
}
else
{
const
index
=
checkedSpuIds
.
value
.
indexOf
(
spu
.
id
)
const
index
=
findCheckedIndex
(
spu
)
if
(
index
>
-
1
)
{
checkedSpuIds
.
value
.
splice
(
index
,
1
)
checkedSpus
.
value
.
splice
(
index
,
1
)
checkedStatus
.
value
[
spu
.
id
]
=
false
isCheckAll
.
value
=
false
}
}
// 计算全选框状态
if
(
isCalcCheckAll
)
{
calculateIsCheckAll
()
}
}
// 查找商品在已选中商品列表中的索引
const
findCheckedIndex
=
(
spu
:
Spu
)
=>
checkedSpus
.
value
.
findIndex
((
item
)
=>
item
.
id
===
spu
.
id
)
// 计算全选框状态
const
calculateIsCheckAll
=
()
=>
{
isCheckAll
.
value
=
list
.
value
.
every
((
spu
)
=>
checkedStatus
.
value
[
spu
.
id
])
// 计算中间状态:不是全部选中 && 任意一个选中
isIndeterminate
.
value
=
!
isCheckAll
.
value
&&
list
.
value
.
some
((
spu
)
=>
checkedStatus
.
value
[
spu
.
id
])
}
const
categoryList
=
ref
()
// 分类列表
const
categoryTreeList
=
ref
()
// 分类树
// 分类列表
const
categoryList
=
ref
()
// 分类树
const
categoryTreeList
=
ref
()
/** 初始化 **/
onMounted
(
async
()
=>
{
await
getList
()
...
...
src/views/mall/promotion/coupon/components/CouponSelect.vue
View file @
e106fb73
...
...
@@ -150,15 +150,14 @@ import {
}
from
'@/views/mall/promotion/coupon/formatter'
import
{
dateFormatter
}
from
'@/utils/formatTime'
import
*
as
CouponTemplateApi
from
'@/api/mall/promotion/coupon/couponTemplate'
import
type
{
GiveCouponTemplate
}
from
'@/api/mall/product/spu'
defineOptions
({
name
:
'CouponSelect'
})
defineProps
<
{
multipleSelection
:
GiveCouponTemplate
[]
multipleSelection
:
CouponTemplateApi
.
CouponTemplateVO
[]
}
>
()
const
emit
=
defineEmits
<
{
(
e
:
'update:multipleSelection'
,
v
:
GiveCouponTemplate
[])
(
e
:
'update:multipleSelection'
,
v
:
CouponTemplateApi
.
CouponTemplateVO
[])
}
>
()
const
dialogVisible
=
ref
(
false
)
// 弹窗的是否展示
const
dialogTitle
=
ref
(
'选择优惠卷'
)
// 弹窗的标题
...
...
@@ -210,10 +209,7 @@ const open = async () => {
defineExpose
({
open
})
// 提供 open 方法,用于打开弹窗
const
handleSelectionChange
=
(
val
:
CouponTemplateApi
.
CouponTemplateVO
[])
=>
{
emit
(
'update:multipleSelection'
,
val
.
map
((
item
)
=>
({
id
:
item
.
id
,
name
:
item
.
name
}))
)
emit
(
'update:multipleSelection'
,
val
)
}
const
submitForm
=
()
=>
{
...
...
src/views/mall/promotion/coupon/template/CouponTemplateForm.vue
View file @
e106fb73
...
...
@@ -26,15 +26,7 @@
label=
"商品"
prop=
"productSpuIds"
>
<div
class=
"flex flex-wrap items-center gap-1"
>
<div
v-for=
"(spu, index) in productSpus"
:key=
"spu.id"
class=
"select-box spu-pic"
>
<el-image
:src=
"spu.picUrl"
/>
<Icon
class=
"del-icon"
icon=
"ep:circle-close-filled"
@
click=
"handleRemoveSpu(index)"
/>
</div>
<div
class=
"select-box"
@
click=
"openSpuTableSelect"
>
<Icon
icon=
"ep:plus"
/>
</div>
</div>
<SpuShowcase
v-model=
"formData.productSpuIds"
/>
</el-form-item>
<el-form-item
v-if=
"formData.productScope === PromotionProductScopeEnum.CATEGORY.scope"
...
...
@@ -186,18 +178,16 @@
<el-button
@
click=
"dialogVisible = false"
>
取 消
</el-button>
</
template
>
</Dialog>
<SpuTableSelect
ref=
"spuTableSelectRef"
multiple
@
change=
"handleSpuSelected"
/>
</template>
<
script
lang=
"ts"
setup
>
import
{
DICT_TYPE
,
getIntDictOptions
}
from
'@/utils/dict'
import
*
as
CouponTemplateApi
from
'@/api/mall/promotion/coupon/couponTemplate'
import
*
as
ProductSpuApi
from
'@/api/mall/product/spu'
import
{
CouponTemplateValidityTypeEnum
,
PromotionDiscountTypeEnum
,
PromotionProductScopeEnum
}
from
'@/utils/constants'
import
Spu
TableSelect
from
'@/views/mall/product/spu/components/SpuTableSelect
.vue'
import
Spu
Showcase
from
'@/views/mall/product/spu/components/SpuShowcase
.vue'
import
ProductCategorySelect
from
'@/views/mall/product/category/components/ProductCategorySelect.vue'
import
{
convertToInteger
,
formatToFraction
}
from
'@/utils'
...
...
@@ -251,7 +241,6 @@ const formRules = reactive({
productCategoryIds
:
[{
required
:
true
,
message
:
'分类不能为空'
,
trigger
:
'blur'
}]
})
const
formRef
=
ref
()
// 表单 Ref
const
productSpus
=
ref
<
ProductSpuApi
.
Spu
[]
>
([])
// 商品列表
/** 打开弹窗 */
const
open
=
async
(
type
:
string
,
id
?:
number
)
=>
{
...
...
@@ -354,7 +343,6 @@ const resetForm = () => {
productCategoryIds
:
[]
}
formRef
.
value
?.
resetFields
()
productSpus
.
value
=
[]
}
/** 获得商品范围 */
...
...
@@ -363,8 +351,6 @@ const getProductScope = async () => {
case
PromotionProductScopeEnum
.
SPU
.
scope
:
// 设置商品编号
formData
.
value
.
productSpuIds
=
formData
.
value
.
productScopeValues
// 获得商品列表
productSpus
.
value
=
await
ProductSpuApi
.
getSpuDetailList
(
formData
.
value
.
productScopeValues
)
break
case
PromotionProductScopeEnum
.
CATEGORY
.
scope
:
await
nextTick
(()
=>
{
...
...
@@ -397,47 +383,6 @@ function setProductScopeValues(data: CouponTemplateApi.CouponTemplateVO) {
break
}
}
/** 活动商品的按钮 */
const
spuTableSelectRef
=
ref
()
const
openSpuTableSelect
=
()
=>
{
spuTableSelectRef
.
value
.
open
(
productSpus
.
value
)
}
/** 选择商品后触发 */
const
handleSpuSelected
=
(
spus
:
ProductSpuApi
.
Spu
[])
=>
{
productSpus
.
value
=
spus
formData
.
value
.
productSpuIds
=
spus
.
map
((
spu
)
=>
spu
.
id
)
as
[]
}
/** 选择商品后触发 */
const
handleRemoveSpu
=
(
index
:
number
)
=>
{
productSpus
.
value
.
splice
(
index
,
1
)
formData
.
value
.
productSpuIds
.
splice
(
index
,
1
)
}
</
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
;
}
.spu-pic
{
position
:
relative
;
}
.del-icon
{
position
:
absolute
;
top
:
-10px
;
right
:
-10px
;
z-index
:
1
;
width
:
20px
!important
;
height
:
20px
!important
;
}
</
style
>
<
style
lang=
"scss"
scoped
></
style
>
src/views/mall/statistics/trade/index.vue
View file @
e106fb73
...
...
@@ -219,6 +219,8 @@ import { TradeSummaryRespVO, TradeTrendSummaryRespVO } from '@/api/mall/statisti
import
{
calculateRelativeRate
,
fenToYuan
}
from
'@/utils'
import
download
from
'@/utils/download'
import
{
CardTitle
}
from
'@/components/Card'
import
*
as
DateUtil
from
'@/utils/formatTime'
import
dayjs
from
'dayjs'
/** 交易统计 */
defineOptions
({
name
:
'TradeStatistics'
})
...
...
@@ -289,6 +291,13 @@ const lineChartOptions = reactive<EChartsOption>({
/** 处理交易状况查询 */
const
getTradeTrendData
=
async
()
=>
{
trendLoading
.
value
=
true
// 1. 处理时间: 开始与截止在同一天的, 折线图出不来, 需要延长一天
const
times
=
shortcutDateRangePicker
.
value
.
times
if
(
DateUtil
.
isSameDay
(
times
[
0
],
times
[
1
]))
{
// 前天
times
[
0
]
=
DateUtil
.
formatDate
(
dayjs
(
times
[
0
]).
subtract
(
1
,
'd'
))
}
// 查询数据
await
Promise
.
all
([
getTradeTrendSummary
(),
getTradeStatisticsList
()])
trendLoading
.
value
=
false
}
...
...
src/views/system/notice/index.vue
View file @
e106fb73
...
...
@@ -87,6 +87,9 @@
>
删除
</el-button>
<el-button
link
@
click=
"handlePush(scope.row.id)"
v-hasPermi=
"['system:notice:update']"
>
推送
</el-button>
</
template
>
</el-table-column>
</el-table>
...
...
@@ -168,6 +171,17 @@ const handleDelete = async (id: number) => {
}
catch
{}
}
/** 推送按钮操作 */
const
handlePush
=
async
(
id
:
number
)
=>
{
try
{
// 推送的二次确认
await
message
.
confirm
(
'是否推送所选中通知?'
)
// 发起推送
await
NoticeApi
.
pushNotice
(
id
)
message
.
success
(
t
(
'推送成功'
))
}
catch
{}
}
/** 初始化 **/
onMounted
(()
=>
{
getList
()
...
...
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