Merge remote-tracking branch 'yudao/master' into dhb52_master

This commit is contained in:
dhb52 2023-11-28 21:53:20 +08:00
commit e106fb73b6
41 changed files with 1428 additions and 793 deletions

View File

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

View File

@ -1,7 +1,7 @@
/*
* @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
webchat: 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 simpleAlllist = async () => {
return await request.get({ url: `/crm/contact/simpleAlllist` })
export const simpleAllList = async () => {
return await request.get({ url: `/crm/contact/simple-all-list` })
}

View File

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

View File

@ -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 })
}
// 修改团队成员权限级别

View File

@ -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({

View File

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

View File

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

View File

@ -0,0 +1,78 @@
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>
}
})

View File

@ -0,0 +1,47 @@
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>

View File

@ -0,0 +1,142 @@
<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"
>
<!-- 布局11-->
<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>
<!-- 布局22-->
<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>
<!-- 布局33-->
<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>

View File

@ -0,0 +1,104 @@
<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>

View File

@ -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: {

View File

@ -0,0 +1,64 @@
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>

View File

@ -0,0 +1,131 @@
<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>

View File

@ -0,0 +1,99 @@
<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>

View File

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

View File

@ -1,5 +1,5 @@
<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

View File

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

View File

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

View File

@ -437,5 +437,6 @@ export default {
btn_zoom_in: '放大',
btn_zoom_out: '缩小',
preview: '预览'
}
},
'OAuth 2.0': 'OAuth 2.0' // 避免菜单名是 OAuth 2.0 时,一直 warn 报错
}

View File

@ -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, // 消息图标

View File

@ -1,8 +1,8 @@
/**
*
*/
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', // 终端
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',
// ========== 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',
// ========== 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',
// ========== 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', // 转账订单状态
// ========== 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', // 消息类型
// ========== 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 - 会员模块 ==========
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 - 商品模块 ==========
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 - 交易模块 ==========
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 定位
// ========== 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_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' // 用户数据权限类型
}

View File

@ -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')
]
}

View File

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

View File

@ -1,7 +1,7 @@
<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="handleRemove">
<el-button @click="handleDelete">
<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: 'CrmTeam' })
defineOptions({ name: 'CrmPermissionList' })
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)
crmPermissionFormRef.value?.open('update', props.bizType, props.bizId, ids)
permissionFormRef.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

View File

@ -1,4 +1,4 @@
import CrmTeam from './CrmTeamList.vue'
import CrmPermissionList from './CrmPermissionList.vue'
enum CrmBizTypeEnum {
CRM_LEADS = 1, // 线索
@ -9,9 +9,7 @@ enum CrmBizTypeEnum {
}
enum CrmPermissionLevelEnum {
OWNER = 1, // 负责人
READ = 2, // 读
WRITE = 3 // 写
OWNER = 1 // 负责人
}
export { CrmTeam, CrmBizTypeEnum, CrmPermissionLevelEnum }
export { CrmPermissionList, CrmBizTypeEnum, CrmPermissionLevelEnum }

View File

@ -1,170 +1,131 @@
<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) // 12
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,
webchat: 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,
webchat: undefined,
wechat: undefined,
sex: undefined,
policyMakers: undefined
master: false
}
formRef.value?.resetFields()
ownerUserList.value = []
}
/** 添加/修改操作 */
// TODO @zynaowner
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>

View File

@ -1,3 +1,10 @@
<!--
* @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>

View File

@ -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 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="4" 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>

View File

@ -55,9 +55,9 @@
class="!w-240px"
/>
</el-form-item>
<el-form-item label="微信" prop="webchat">
<el-form-item label="微信" prop="wechat">
<el-input
v-model="queryParams.webchat"
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="webchat" />
<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,
webchat: 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>

View File

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

View File

@ -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="ownerUserDeptName" />
<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()
}

View File

@ -1,5 +1,6 @@
<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="toggleConnectStatus">
{{ 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="sendValue"
v-model="sendText"
: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>

View File

@ -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 SpuTableSelect from '@/views/mall/product/spu/components/SpuTableSelect.vue'
import SpuShowcase 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 = () => {

View File

@ -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="50" />
<el-table-column label="评论编号" align="center" prop="id" min-width="80" />
<el-table-column label="商品信息" align="center" min-width="400">
<template #default="scope">
<div class="row flex items-center gap-x-4px">

View File

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

View File

@ -1,8 +1,14 @@
<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)"
>&nbsp;</el-radio
>
<el-radio :label="row.id" v-model="selectedSpuId" @change="handleSingleSelected(row)">
<!-- 空格不能省略是为了让单选框不显示label如果不指定label不会有选中的效果 -->
&nbsp;
</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
// checkboxundefinedbool
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[]>([])
// keyIDvalue
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 categoryList = ref() //
const categoryTreeList = ref() //
//
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()
/** 初始化 **/
onMounted(async () => {
await getList()

View File

@ -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 = () => {

View File

@ -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 SpuTableSelect from '@/views/mall/product/spu/components/SpuTableSelect.vue'
import SpuShowcase 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>

View File

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

View File

@ -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()