!326 CRM 客户最新的代码

Merge pull request !326 from 芋道源码/dev
This commit is contained in:
芋道源码 2023-11-18 16:30:42 +00:00 committed by Gitee
commit 4423e9b804
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
46 changed files with 2932 additions and 189 deletions

View File

@ -60,18 +60,11 @@ export function createVitePlugins() {
}
}),
Components({
// 要搜索组件的目录的相对路径
dirs: ['src/components'],
// 组件的有效文件扩展名
extensions: ['vue', 'md'],
// 搜索子目录
deep: true,
include: [/\.vue$/, /\.vue\?vue/],
// 生成自定义 `auto-components.d.ts` 全局声明
dts: 'src/types/auto-components.d.ts',
// 自定义组件的解析器
resolvers: [ElementPlusResolver()],
exclude: [/[\\/]node_modules[\\/]/]
globs: ["src/components/**/**.{vue, md}", '!src/components/DiyEditor/components/mobile/**']
}),
EslintPlugin({
cache: false,

View File

@ -0,0 +1,35 @@
import request from '@/config/axios'
export interface CustomerLimitConfigVO {
id?: number
type?: number
userIds?: string
deptIds?: string
maxCount?: number
dealCountEnabled?: boolean
}
// 查询客户限制配置列表
export const getCustomerLimitConfigPage = async (params) => {
return await request.get({ url: `/crm/customer-limit-config/page`, params })
}
// 查询客户限制配置详情
export const getCustomerLimitConfig = async (id: number) => {
return await request.get({ url: `/crm/customer-limit-config/get?id=` + id })
}
// 新增客户限制配置
export const createCustomerLimitConfig = async (data: CustomerLimitConfigVO) => {
return await request.post({ url: `/crm/customer-limit-config/create`, data })
}
// 修改客户限制配置
export const updateCustomerLimitConfig = async (data: CustomerLimitConfigVO) => {
return await request.put({ url: `/crm/customer-limit-config/update`, data })
}
// 删除客户限制配置
export const deleteCustomerLimitConfig = async (id: number) => {
return await request.delete({ url: `/crm/customer-limit-config/delete?id=` + id })
}

View File

@ -0,0 +1,19 @@
import request from '@/config/axios'
export interface CustomerPoolConfigVO {
enabled?: boolean
contactExpireDays?: number
dealExpireDays?: number
notifyEnabled?: boolean
notifyDays: number
}
// 获取客户公海规则设置
export const getCustomerPoolConfig = async () => {
return await request.get({ url: `/crm/customer-pool-config/get` })
}
// 更新客户公海规则设置
export const updateCustomerPoolConfig = async (data: ConfigVO) => {
return await request.put({ url: `/crm/customer-pool-config/update`, data })
}

View File

@ -0,0 +1,48 @@
import request from '@/config/axios'
export interface PermissionVO {
id?: number // 数据权限编号
userId: number | undefined // 用户编号
bizType: number | undefined // Crm 类型
bizId: number | undefined // Crm 类型数据编号
level: number | undefined // 权限级别
deptName?: string // 部门名称 // 岗位名称数组 TODO @puhui999数组
nickname?: string // 用户昵称
postNames?: string // 岗位名称数组 TODO @puhui999数组
createTime?: Date
}
// 查询团队成员列表
export const getPermissionList = async (params) => {
return await request.get({ url: `/crm/permission/list`, params })
}
// 新增团队成员
export const createPermission = async (data: PermissionVO) => {
return await request.post({ url: `/crm/permission/add`, data })
}
// 修改团队成员权限级别
export const updatePermission = async (data) => {
return await request.put({ url: `/crm/permission/update`, data })
}
// 删除团队成员
export const deletePermission = async (params) => {
return await request.delete({ url: '/crm/permission/delete', params })
}
// 退出团队
export const quitTeam = async (id) => {
return await request.delete({ url: '/crm/permission/quit-team?id=' + id })
}
// 领取公海数据
export const receive = async (data: { bizType: number; bizId: number }) => {
return await request.put({ url: `/crm/permission/receive`, data })
}
// 数据放入公海
export const putPool = async (data: { bizType: number; bizId: number }) => {
return await request.put({ url: `/crm/permission/put-pool`, data })
}

View File

@ -22,6 +22,11 @@ export const getUserPage = (params: PageParam) => {
return request.get({ url: '/system/user/page', params })
}
// 查询所有用户列表
export const getAllUser = () => {
return request.get({ url: '/system/user/all' })
}
// 查询用户详情
export const getUser = (id: number) => {
return request.get({ url: '/system/user/get?id=' + id })

View File

@ -1,38 +1,18 @@
<template>
<el-input v-model="color">
<template #prepend>
<el-color-picker v-model="color" :predefine="COLORS" />
<el-color-picker v-model="color" :predefine="PREDEFINE_COLORS" />
</template>
</el-input>
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes'
import { PREDEFINE_COLORS } from '@/utils/color'
//
defineOptions({ name: 'ColorInput' })
//
const COLORS = [
'#ff4500',
'#ff8c00',
'#ffd700',
'#90ee90',
'#00ced1',
'#1e90ff',
'#c71585',
'#409EFF',
'#909399',
'#C0C4CC',
'#b7390b',
'#ff7800',
'#fad400',
'#5b8c5f',
'#00babd',
'#1f73c3',
'#711f57'
]
const props = defineProps({
modelValue: propTypes.string.def('')
})

View File

@ -0,0 +1,48 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 广告魔方属性 */
export interface MagicCubeProperty {
// 上圆角
borderRadiusTop: number
// 下圆角
borderRadiusBottom: number
// 间隔
space: number
// 导航菜单列表
list: MagicCubeItemProperty[]
// 组件样式
style: ComponentStyle
}
/** 广告魔方项目属性 */
export interface MagicCubeItemProperty {
// 图标链接
imgUrl: string
// 链接
url: string
// 宽
width: number
// 高
height: number
// 上
top: number
// 左
left: number
}
// 定义组件
export const component = {
id: 'MagicCube',
name: '广告魔方',
icon: 'bi:columns',
property: {
borderRadiusTop: 0,
borderRadiusBottom: 0,
space: 0,
list: [],
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<MagicCubeProperty>

View File

@ -0,0 +1,73 @@
<template>
<div
class="relative"
:style="{ height: `${rowCount * CUBE_SIZE}px`, width: `${4 * CUBE_SIZE}px` }"
>
<div
v-for="(item, index) in property.list"
:key="index"
class="absolute"
:style="{
width: `${item.width * CUBE_SIZE - property.space * 2}px`,
height: `${item.height * CUBE_SIZE - property.space * 2}px`,
margin: `${property.space}px`,
top: `${item.top * CUBE_SIZE}px`,
left: `${item.left * CUBE_SIZE}px`
}"
>
<el-image
class="h-full w-full"
fit="cover"
:src="item.imgUrl"
:style="{
borderTopLeftRadius: `${property.borderRadiusTop}px`,
borderTopRightRadius: `${property.borderRadiusTop}px`,
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
borderBottomRightRadius: `${property.borderRadiusBottom}px`
}"
>
<template #error>
<div class="image-slot">
<div
class="flex items-center justify-center"
:style="{
width: `${item.width * CUBE_SIZE}px`,
height: `${item.height * CUBE_SIZE}px`
}"
>
<Icon icon="ep-picture" color="gray" :size="CUBE_SIZE" />
</div>
</div>
</template>
</el-image>
</div>
</div>
</template>
<script setup lang="ts">
import { MagicCubeProperty } from './config'
/** 广告魔方 */
defineOptions({ name: 'MagicCube' })
const props = defineProps<{ property: MagicCubeProperty }>()
//
const CUBE_SIZE = 93.75
/**
* 计算方块的行数
* 行数用于计算魔方的总体高度存在以下情况
* 1. 没有数据时默认就只显示一行的高度
* 2. 底部的空白不算高度例如只有第一行有数据那么就只显示一行的高度
* 3. 顶部及中间的空白算高度例如一共有四行只有最后一行有数据那么也显示四行的高度
*/
const rowCount = computed(() => {
let count = 0
if (props.property.list.length > 0) {
//
count = Math.max(...props.property.list.map((item) => item.bottom))
}
// 0 1
return count + 1
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,76 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<!-- 表单 -->
<el-form label-width="80px" :model="formData" class="m-t-8px">
<el-text tag="p"> 魔方设置 </el-text>
<el-text type="info" size="small"> 每格尺寸187 * 187 </el-text>
<MagicCubeEditor
class="m-y-16px"
v-model="formData.list"
:rows="4"
:cols="4"
@hot-area-selected="handleHotAreaSelected"
/>
<template v-for="(hotArea, index) in formData.list" :key="index">
<template v-if="selectedHotAreaIndex === index">
<el-form-item label="上传图片" :prop="`list[${index}].imgUrl`">
<UploadImg v-model="hotArea.imgUrl" height="80px" width="80px" />
</el-form-item>
<el-form-item label="链接" :prop="`list[${index}].url`">
<el-input v-model="hotArea.url" placeholder="请输入链接" />
</el-form-item>
</template>
</template>
<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-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import { usePropertyForm } from '@/components/DiyEditor/util'
import { MagicCubeProperty } from '@/components/DiyEditor/components/mobile/MagicCube/config'
/** 广告魔方属性面板 */
defineOptions({ name: 'MagicCubeProperty' })
const props = defineProps<{ modelValue: MagicCubeProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
//
const selectedHotAreaIndex = ref(-1)
const handleHotAreaSelected = (_: any, index: number) => {
selectedHotAreaIndex.value = index
}
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,78 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
import { cloneDeep } from 'lodash-es'
/** 宫格导航属性 */
export interface MenuGridProperty {
// 列数
column: number
// 导航菜单列表
list: MenuGridItemProperty[]
// 组件样式
style: ComponentStyle
}
/** 宫格导航项目属性 */
export interface MenuGridItemProperty {
// 图标链接
iconUrl: string
// 标题
title: string
// 标题颜色
titleColor: string
// 副标题
subtitle: string
// 副标题颜色
subtitleColor: string
// 链接
url: string
// 角标
badge: {
// 是否显示
show: boolean
// 角标文字
text: string
// 角标文字颜色
textColor: string
// 角标背景颜色
bgColor: string
}
}
export const EMPTY_MENU_GRID_ITEM_PROPERTY = {
title: '标题',
titleColor: '#333',
subtitle: '副标题',
subtitleColor: '#bbb',
badge: {
show: false,
textColor: '#fff',
bgColor: '#FF6000'
}
} as MenuGridItemProperty
// 定义组件
export const component = {
id: 'MenuGrid',
name: '宫格导航',
icon: 'bi:grid-3x3-gap',
property: {
column: 3,
list: [cloneDeep(EMPTY_MENU_GRID_ITEM_PROPERTY)],
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8,
marginLeft: 8,
marginRight: 8,
padding: 8,
paddingTop: 8,
paddingRight: 8,
paddingBottom: 8,
paddingLeft: 8,
borderRadius: 8,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderBottomRightRadius: 8,
borderBottomLeftRadius: 8
} as ComponentStyle
}
} as DiyComponent<MenuGridProperty>

View File

@ -0,0 +1,35 @@
<template>
<div class="flex flex-row flex-wrap">
<div
v-for="(item, index) in property.list"
:key="index"
class="relative flex flex-col items-center p-b-14px p-t-20px"
:style="{ width: `${100 * (1 / property.column)}%` }"
>
<!-- 右上角角标 -->
<span
v-if="item.badge?.show"
class="absolute left-50% top-10px z-1 h-20px rounded-50% p-x-6px text-center text-12px leading-20px"
:style="{ color: item.badge.textColor, backgroundColor: item.badge.bgColor }"
>
{{ item.badge.text }}
</span>
<el-image v-if="item.iconUrl" class="h-28px w-28px" :src="item.iconUrl" />
<span class="m-t-8px h-16px text-12px leading-16px" :style="{ color: item.titleColor }">
{{ item.title }}
</span>
<span class="m-t-6px h-12px text-10px leading-12px" :style="{ color: item.subtitleColor }">
{{ item.subtitle }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { MenuGridProperty } from './config'
/** 宫格导航 */
defineOptions({ name: 'MenuGrid' })
defineProps<{ property: MenuGridProperty }>()
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,96 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<!-- 表单 -->
<el-form label-width="80px" :model="formData" class="m-t-8px">
<el-form-item label="每行数量" prop="column">
<el-radio-group v-model="formData.column">
<el-radio :label="3">3</el-radio>
<el-radio :label="4">4</el-radio>
</el-radio-group>
</el-form-item>
<el-text tag="p"> 菜单设置 </el-text>
<el-text type="info" size="small"> 拖动左侧的小圆点可以调整顺序 </el-text>
<template v-if="formData.list.length">
<VueDraggable
class="m-t-8px"
:list="formData.list"
item-key="index"
handle=".drag-icon"
:forceFallback="true"
:animation="200"
>
<template #item="{ element, index }">
<div class="mb-4px flex flex-col gap-4px rounded bg-gray-100 p-8px">
<div class="flex flex-row justify-between">
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
<Icon icon="ep:delete" class="text-red-500" @click="handleDeleteMenu(index)" />
</div>
<el-form-item label="图标" prop="iconUrl">
<UploadImg v-model="element.iconUrl" height="80px" width="80px">
<template #tip> 建议尺寸44 * 44 </template>
</UploadImg>
</el-form-item>
<el-form-item label="标题" prop="title">
<InputWithColor v-model="element.title" v-model:color="element.titleColor" />
</el-form-item>
<el-form-item label="副标题" prop="subtitle">
<InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
</el-form-item>
<el-form-item label="链接" prop="url">
<el-input v-model="element.url" />
</el-form-item>
<el-form-item label="显示角标" prop="badge.show">
<el-switch v-model="element.badge.show" />
</el-form-item>
<template v-if="element.badge.show">
<el-form-item label="角标内容" prop="badge.text">
<InputWithColor
v-model="element.badge.text"
v-model:color="element.badge.textColor"
/>
</el-form-item>
<el-form-item label="背景颜色" prop="badge.bgColor">
<ColorInput v-model="element.badge.bgColor" />
</el-form-item>
</template>
</div>
</template>
</VueDraggable>
</template>
<el-form-item label-width="0">
<el-button @click="handleAddMenu" type="primary" plain class="m-t-8px w-full">
<Icon icon="ep:plus" class="mr-5px" /> 添加菜单
</el-button>
</el-form-item>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import VueDraggable from 'vuedraggable'
import { usePropertyForm } from '@/components/DiyEditor/util'
import {
EMPTY_MENU_GRID_ITEM_PROPERTY,
MenuGridProperty
} from '@/components/DiyEditor/components/mobile/MenuGrid/config'
import { cloneDeep } from 'lodash-es'
/** 宫格导航属性面板 */
defineOptions({ name: 'MenuGridProperty' })
const props = defineProps<{ modelValue: MenuGridProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
/* 添加菜单 */
const handleAddMenu = () => {
formData.value.list.push(cloneDeep(EMPTY_MENU_GRID_ITEM_PROPERTY))
}
/* 删除菜单 */
const handleDeleteMenu = (index: number) => {
formData.value.list.splice(index, 1)
}
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,47 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
import { cloneDeep } from 'lodash-es'
/** 列表导航属性 */
export interface MenuListProperty {
// 导航菜单列表
list: MenuListItemProperty[]
// 组件样式
style: ComponentStyle
}
/** 列表导航项目属性 */
export interface MenuListItemProperty {
// 图标链接
iconUrl: string
// 标题
title: string
// 标题颜色
titleColor: string
// 副标题
subtitle: string
// 副标题颜色
subtitleColor: string
// 链接
url: string
}
export const EMPTY_MENU_LIST_ITEM_PROPERTY = {
title: '标题',
titleColor: '#333',
subtitle: '副标题',
subtitleColor: '#bbb'
}
// 定义组件
export const component = {
id: 'MenuList',
name: '列表导航',
icon: 'fa-solid:list',
property: {
list: [cloneDeep(EMPTY_MENU_LIST_ITEM_PROPERTY)],
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<MenuListProperty>

View File

@ -0,0 +1,31 @@
<template>
<div class="min-h-42px flex flex-col">
<div
v-for="(item, index) in property.list"
:key="index"
class="item h-42px flex flex-row items-center justify-between gap-4px p-x-12px"
>
<div class="flex flex-1 flex-row items-center gap-8px">
<el-image v-if="item.iconUrl" class="h-16px w-16px" :src="item.iconUrl" />
<span class="text-16px" :style="{ color: item.titleColor }">{{ item.title }}</span>
</div>
<div class="item-center flex flex-row justify-center gap-4px">
<span class="text-12px" :style="{ color: item.subtitleColor }">{{ item.subtitle }}</span>
<Icon icon="ep-arrow-right" color="#000" :size="16" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { MenuListProperty } from './config'
/** 列表导航 */
defineOptions({ name: 'MenuList' })
defineProps<{ property: MenuListProperty }>()
</script>
<style scoped lang="scss">
.item + .item {
border-top: 1px solid #eee;
}
</style>

View File

@ -0,0 +1,75 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<el-text tag="p"> 菜单设置 </el-text>
<el-text type="info" size="small"> 拖动左侧的小圆点可以调整顺序 </el-text>
<!-- 表单 -->
<el-form label-width="60px" :model="formData" class="m-t-8px">
<div v-if="formData.list.length">
<VueDraggable
:list="formData.list"
item-key="index"
handle=".drag-icon"
:forceFallback="true"
:animation="200"
>
<template #item="{ element, index }">
<div class="mb-4px flex flex-col gap-4px rounded bg-gray-100 p-8px">
<div class="flex flex-row justify-between">
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
<Icon icon="ep:delete" class="text-red-500" @click="handleDeleteMenu(index)" />
</div>
<el-form-item label="图标" prop="iconUrl">
<UploadImg v-model="element.iconUrl" height="80px" width="80px">
<template #tip> 建议尺寸44 * 44 </template>
</UploadImg>
</el-form-item>
<el-form-item label="标题" prop="title">
<InputWithColor v-model="element.title" v-model:color="element.titleColor" />
</el-form-item>
<el-form-item label="副标题" prop="subtitle">
<InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
</el-form-item>
<el-form-item label="链接" prop="url">
<el-input v-model="element.url" />
</el-form-item>
</div>
</template>
</VueDraggable>
</div>
<el-form-item label-width="0">
<el-button @click="handleAddMenu" type="primary" plain class="m-t-8px w-full">
<Icon icon="ep:plus" class="mr-5px" /> 添加菜单
</el-button>
</el-form-item>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import VueDraggable from 'vuedraggable'
import { usePropertyForm } from '@/components/DiyEditor/util'
import {
EMPTY_MENU_LIST_ITEM_PROPERTY,
MenuListProperty
} from '@/components/DiyEditor/components/mobile/MenuList/config'
import { cloneDeep } from 'lodash-es'
/** 列表导航属性面板 */
defineOptions({ name: 'MenuListProperty' })
const props = defineProps<{ modelValue: MenuListProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
/* 添加菜单 */
const handleAddMenu = () => {
formData.value.list.push(cloneDeep(EMPTY_MENU_LIST_ITEM_PROPERTY))
}
/* 删除菜单 */
const handleDeleteMenu = (index: number) => {
formData.value.list.splice(index, 1)
}
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,66 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
import { cloneDeep } from 'lodash-es'
/** 菜单导航属性 */
export interface MenuSwiperProperty {
// 布局: 图标+文字 | 图标
layout: 'iconText' | 'icon'
// 行数
row: number
// 列数
column: number
// 导航菜单列表
list: MenuSwiperItemProperty[]
// 组件样式
style: ComponentStyle
}
/** 菜单导航项目属性 */
export interface MenuSwiperItemProperty {
// 图标链接
iconUrl: string
// 标题
title: string
// 标题颜色
titleColor: string
// 链接
url: string
// 角标
badge: {
// 是否显示
show: boolean
// 角标文字
text: string
// 角标文字颜色
textColor: string
// 角标背景颜色
bgColor: string
}
}
export const EMPTY_MENU_SWIPER_ITEM_PROPERTY = {
title: '标题',
titleColor: '#333',
badge: {
show: false,
textColor: '#fff',
bgColor: '#FF6000'
}
} as MenuSwiperItemProperty
// 定义组件
export const component = {
id: 'MenuSwiper',
name: '菜单导航',
icon: 'bi:grid-3x2-gap',
property: {
layout: 'iconText',
row: 1,
column: 3,
list: [cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY)],
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<MenuSwiperProperty>

View File

@ -0,0 +1,119 @@
<template>
<el-carousel
:height="`${carouselHeight}px`"
:autoplay="false"
arrow="hover"
indicator-position="outside"
>
<el-carousel-item v-for="(page, pageIndex) in pages" :key="pageIndex">
<div class="flex flex-row flex-wrap">
<div
v-for="(item, index) in page"
:key="index"
class="relative flex flex-col items-center justify-center"
:style="{ width: columnWidth, height: `${rowHeight}px` }"
>
<!-- 图标 + 角标 -->
<div class="relative" :class="`h-${ICON_SIZE}px w-${ICON_SIZE}px`">
<!-- 右上角角标 -->
<span
v-if="item.badge?.show"
class="absolute right--10px top--10px z-1 h-20px rounded-10px p-x-6px text-center text-12px leading-20px"
:style="{ color: item.badge.textColor, backgroundColor: item.badge.bgColor }"
>
{{ item.badge.text }}
</span>
<el-image v-if="item.iconUrl" :src="item.iconUrl" class="h-full w-full" />
</div>
<!-- 标题 -->
<span
v-if="property.layout === 'iconText'"
class="text-14px"
:style="{
color: item.titleColor,
height: `${TITLE_HEIGHT}px`,
lineHeight: `${TITLE_HEIGHT}px`
}"
>
{{ item.title }}
</span>
</div>
</div>
</el-carousel-item>
</el-carousel>
</template>
<script setup lang="ts">
import { MenuSwiperProperty, MenuSwiperItemProperty } from './config'
/** 菜单导航 */
defineOptions({ name: 'MenuSwiper' })
const props = defineProps<{ property: MenuSwiperProperty }>()
//
const TITLE_HEIGHT = 20
//
const ICON_SIZE = 50
//
const SPACE_Y = 16
//
const pages = ref<MenuSwiperItemProperty[][]>([])
//
const carouselHeight = ref(0)
//
const rowHeight = ref(0)
//
const columnWidth = ref('')
watch(
() => props.property,
() => {
//
columnWidth.value = `${100 * (1 / props.property.column)}%`
// + 0 + * 2
rowHeight.value =
(props.property.layout === 'iconText' ? ICON_SIZE + TITLE_HEIGHT : ICON_SIZE) + SPACE_Y * 2
// *
carouselHeight.value = props.property.row * rowHeight.value
// *
const pageSize = props.property.row * props.property.column
//
pages.value = []
//
let pageItems: MenuSwiperItemProperty[] = []
for (const item of props.property.list) {
//
if (pageItems.length === pageSize) {
pageItems = []
}
//
if (pageItems.length === 0) {
pages.value.push(pageItems)
}
//
pageItems.push(item)
}
},
{ immediate: true, deep: true }
)
</script>
<style lang="scss">
// APP
:root {
.el-carousel__indicator {
padding-top: 0;
padding-bottom: 0;
.el-carousel__button {
--el-carousel-indicator-height: 6px;
--el-carousel-indicator-width: 6px;
--el-carousel-indicator-out-color: #ff6000;
border-radius: 6px;
}
}
.el-carousel__indicator.is-active {
.el-carousel__button {
--el-carousel-indicator-width: 12px;
}
}
}
</style>

View File

@ -0,0 +1,106 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<!-- 表单 -->
<el-form label-width="80px" :model="formData" class="m-t-8px">
<el-form-item label="布局" prop="layout">
<el-radio-group v-model="formData.layout">
<el-radio label="iconText">图标+文字</el-radio>
<el-radio label="icon">仅图标</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="行数" prop="row">
<el-radio-group v-model="formData.row">
<el-radio :label="1">1</el-radio>
<el-radio :label="2">2</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="列数" prop="column">
<el-radio-group v-model="formData.column">
<el-radio :label="3">3</el-radio>
<el-radio :label="4">4</el-radio>
<el-radio :label="5">5</el-radio>
</el-radio-group>
</el-form-item>
<el-text tag="p"> 菜单设置 </el-text>
<el-text type="info" size="small"> 拖动左侧的小圆点可以调整顺序 </el-text>
<template v-if="formData.list.length">
<VueDraggable
class="m-t-8px"
:list="formData.list"
item-key="index"
handle=".drag-icon"
:forceFallback="true"
:animation="200"
>
<template #item="{ element, index }">
<div class="mb-4px flex flex-col gap-4px rounded bg-gray-100 p-8px">
<div class="flex flex-row justify-between">
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
<Icon icon="ep:delete" class="text-red-500" @click="handleDeleteMenu(index)" />
</div>
<el-form-item label="图标" prop="iconUrl">
<UploadImg v-model="element.iconUrl" height="80px" width="80px">
<template #tip> 建议尺寸98 * 98 </template>
</UploadImg>
</el-form-item>
<el-form-item label="标题" prop="title">
<InputWithColor v-model="element.title" v-model:color="element.titleColor" />
</el-form-item>
<el-form-item label="链接" prop="url">
<el-input v-model="element.url" />
</el-form-item>
<el-form-item label="显示角标" prop="badge.show">
<el-switch v-model="element.badge.show" />
</el-form-item>
<template v-if="element.badge.show">
<el-form-item label="角标内容" prop="badge.text">
<InputWithColor
v-model="element.badge.text"
v-model:color="element.badge.textColor"
/>
</el-form-item>
<el-form-item label="背景颜色" prop="badge.bgColor">
<ColorInput v-model="element.badge.bgColor" />
</el-form-item>
</template>
</div>
</template>
</VueDraggable>
</template>
<el-form-item label-width="0">
<el-button @click="handleAddMenu" type="primary" plain class="m-t-8px w-full">
<Icon icon="ep:plus" class="mr-5px" /> 添加菜单
</el-button>
</el-form-item>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import VueDraggable from 'vuedraggable'
import { usePropertyForm } from '@/components/DiyEditor/util'
import {
EMPTY_MENU_SWIPER_ITEM_PROPERTY,
MenuSwiperProperty
} from '@/components/DiyEditor/components/mobile/MenuSwiper/config'
import { cloneDeep } from 'lodash-es'
/** 菜单导航属性面板 */
defineOptions({ name: 'MenuSwiperProperty' })
const props = defineProps<{ modelValue: MenuSwiperProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
/* 添加菜单 */
const handleAddMenu = () => {
formData.value.list.push(cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY))
}
/* 删除菜单 */
const handleDeleteMenu = (index: number) => {
formData.value.list.splice(index, 1)
}
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,97 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 商品卡片属性 */
export interface ProductCardProperty {
// 布局类型:单列大图 | 单列小图 | 双列
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'
// 商品字段
fields: {
// 商品名称
name: ProductCardFieldProperty
// 商品简介
introduction: ProductCardFieldProperty
// 商品价格
price: ProductCardFieldProperty
// 商品市场价
marketPrice: ProductCardFieldProperty
// 商品销量
salesCount: ProductCardFieldProperty
// 商品库存
stock: ProductCardFieldProperty
}
// 角标
badge: {
// 是否显示
show: boolean
// 角标图片
imgUrl: string
}
// 按钮
btnBuy: {
// 类型:文字 | 图片
type: 'text' | 'img'
// 文字
text: string
// 文字按钮:背景渐变起始颜色
bgBeginColor: string
// 文字按钮:背景渐变结束颜色
bgEndColor: string
// 图片按钮:图片地址
imgUrl: string
}
// 上圆角
borderRadiusTop: number
// 下圆角
borderRadiusBottom: number
// 间距
space: number
// 商品编号列表
spuIds: number[]
// 组件样式
style: ComponentStyle
}
// 商品字段
export interface ProductCardFieldProperty {
// 是否显示
show: boolean
// 颜色
color: string
}
// 定义组件
export const component = {
id: 'ProductCard',
name: '商品卡片',
icon: 'system-uicons:carousel',
property: {
layoutType: 'oneColBigImg',
fields: {
name: { show: true, color: '#000' },
introduction: { show: true, color: '#999' },
price: { show: true, color: '#ff3000' },
marketPrice: { show: true, color: '#c4c4c4' },
salesCount: { show: true, color: '#c4c4c4' },
stock: { show: false, color: '#c4c4c4' }
},
badge: { show: false, imgUrl: '' },
btnBuy: {
type: 'text',
text: '立即购买',
// todo: @owen 根据主题色配置
bgBeginColor: '#FF6000',
bgEndColor: '#FE832A',
imgUrl: ''
},
borderRadiusTop: 8,
borderRadiusBottom: 8,
space: 8,
spuIds: [],
style: {
bgType: 'color',
bgColor: '',
marginLeft: 8,
marginRight: 8,
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<ProductCardProperty>

View File

@ -0,0 +1,165 @@
<template>
<div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef">
<div
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
:style="{
...calculateSpace(index),
...calculateWidth(),
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>
<!-- 商品封面图 -->
<div
:class="[
'h-140px',
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-140px': property.layoutType === 'oneColSmallImg'
}
]"
>
<el-image fit="cover" class="h-full w-full" :src="spu.picUrl" />
</div>
<div
:class="[
' flex flex-col gap-8px p-8px box-border',
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg'
}
]"
>
<!-- 商品名称 -->
<div
v-if="property.fields.name.show"
:class="[
'text-14px ',
{
truncate: property.layoutType !== 'oneColSmallImg',
'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
}
]"
:style="{ color: property.fields.name.color }"
>
{{ spu.name }}
</div>
<!-- 商品简介 -->
<div
v-if="property.fields.introduction.show"
class="truncate text-12px"
:style="{ color: property.fields.introduction.color }"
>
{{ spu.introduction }}
</div>
<div>
<!-- 价格 -->
<span
v-if="property.fields.price.show"
class="text-16px"
:style="{ color: property.fields.price.color }"
>
{{ spu.price }}
</span>
<!-- 市场价 -->
<span
v-if="property.fields.marketPrice.show && spu.marketPrice"
class="ml-4px text-10px line-through"
:style="{ color: property.fields.marketPrice.color }"
>{{ spu.marketPrice }}</span
>
</div>
<div class="text-12px">
<!-- 销量 -->
<span
v-if="property.fields.salesCount.show"
:style="{ color: property.fields.salesCount.color }"
>
已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}
</span>
<!-- 库存 -->
<span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }">
库存{{ spu.stock || 0 }}
</span>
</div>
</div>
<!-- 购买按钮 -->
<div class="absolute bottom-8px right-8px">
<!-- 文字按钮 -->
<span
v-if="property.btnBuy.type === 'text'"
class="rounded-full p-x-12px p-y-4px text-12px text-white"
:style="{
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`
}"
>
{{ property.btnBuy.text }}
</span>
<!-- 图片按钮 -->
<el-image
v-else
class="h-28px w-28px rounded-full"
fit="cover"
:src="property.btnBuy.imgUrl"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ProductCardProperty } from './config'
import * as ProductSpuApi from '@/api/mall/product/spu'
/** 商品卡片 */
defineOptions({ name: 'ProductCard' })
//
const props = defineProps<{ property: ProductCardProperty }>()
//
const spuList = ref<ProductSpuApi.Spu[]>([])
watch(
() => props.property.spuIds,
async () => {
spuList.value = await ProductSpuApi.getSpuDetailList(props.property.spuIds)
},
{
immediate: true,
deep: true
}
)
/**
* 计算商品的间距
* @param index 商品索引
*/
const calculateSpace = (index: number) => {
//
const columns = props.property.layoutType === 'twoCol' ? 2 : 1
//
const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px'
//
const marginTop = index < columns ? '0' : props.property.space + 'px'
return { marginLeft, marginTop }
}
//
const containerRef = ref()
//
const calculateWidth = () => {
let width = '100%'
// - / 2
if (props.property.layoutType === 'twoCol') {
width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`
}
return { width }
}
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,149 @@
<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="oneColBigImg">
<Icon icon="fluent:text-column-one-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="单列小图" placement="bottom">
<el-radio-button label="oneColSmallImg">
<Icon icon="fluent:text-column-two-left-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="双列" placement="bottom">
<el-radio-button label="twoCol">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="商品名称" prop="fields.name.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.name.color" />
<el-checkbox v-model="formData.fields.name.show" />
</div>
</el-form-item>
<el-form-item label="商品简介" prop="fields.introduction.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.introduction.color" />
<el-checkbox v-model="formData.fields.introduction.show" />
</div>
</el-form-item>
<el-form-item label="商品价格" prop="fields.price.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.price.color" />
<el-checkbox v-model="formData.fields.price.show" />
</div>
</el-form-item>
<el-form-item label="市场价" prop="fields.marketPrice.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.marketPrice.color" />
<el-checkbox v-model="formData.fields.marketPrice.show" />
</div>
</el-form-item>
<el-form-item label="商品销量" prop="fields.salesCount.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.salesCount.color" />
<el-checkbox v-model="formData.fields.salesCount.show" />
</div>
</el-form-item>
<el-form-item label="商品库存" prop="fields.stock.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.stock.color" />
<el-checkbox v-model="formData.fields.stock.show" />
</div>
</el-form-item>
</el-card>
<el-card 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="btnBuy.type">
<el-radio-group v-model="formData.btnBuy.type">
<el-radio-button label="text">文字</el-radio-button>
<el-radio-button label="img">图片</el-radio-button>
</el-radio-group>
</el-form-item>
<template v-if="formData.btnBuy.type === 'text'">
<el-form-item label="按钮文字" prop="btnBuy.text">
<el-input v-model="formData.btnBuy.text" />
</el-form-item>
<el-form-item label="左侧背景" prop="btnBuy.bgBeginColor">
<ColorInput v-model="formData.btnBuy.bgBeginColor" />
</el-form-item>
<el-form-item label="右侧背景" prop="btnBuy.bgEndColor">
<ColorInput v-model="formData.btnBuy.bgEndColor" />
</el-form-item>
</template>
<template v-else>
<el-form-item label="图片" prop="btnBuy.imgUrl">
<UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px">
<template #tip> 建议尺寸56 * 56 </template>
</UploadImg>
</el-form-item>
</template>
</el-card>
<el-card 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 { ProductCardProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
//
defineOptions({ name: 'ProductCardProperty' })
const props = defineProps<{ modelValue: ProductCardProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
</script>
<style scoped lang="scss"></style>

View File

@ -100,16 +100,13 @@ export const PAGE_LIBS = [
{
name: '基础组件',
extended: true,
components: [
'SearchBar',
'NoticeBar',
'GridNavigation',
'ListNavigation',
'Divider',
'TitleBar'
]
components: ['SearchBar', 'NoticeBar', 'MenuSwiper', 'MenuGrid', 'MenuList']
},
{
name: '图文组件',
extended: true,
components: ['ImageBar', 'Carousel', 'TitleBar', 'VideoPlayer', 'Divider', 'MagicCube']
},
{ name: '图文组件', extended: true, components: ['ImageBar', 'Carousel', 'VideoPlayer'] },
{ name: '商品组件', extended: true, components: ['ProductCard'] },
{
name: '会员组件',

View File

@ -0,0 +1,59 @@
<template>
<el-input v-model="valueRef" v-bind="$attrs">
<template #append>
<el-color-picker v-model="colorRef" :predefine="PREDEFINE_COLORS" />
</template>
</el-input>
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { PREDEFINE_COLORS } from '@/utils/color'
/**
* 带颜色选择器输入框
*/
defineOptions({ name: 'InputWithColor' })
const props = defineProps({
modelValue: propTypes.string.def('').isRequired,
color: propTypes.string.def('').isRequired
})
watch(
() => props.modelValue,
(val: string) => {
if (val === unref(valueRef)) return
valueRef.value = val
}
)
const emit = defineEmits(['update:modelValue', 'update:color'])
//
const valueRef = ref(props.modelValue)
watch(
() => valueRef.value,
(val: string) => {
emit('update:modelValue', val)
}
)
//
const colorRef = ref(props.color)
watch(
() => colorRef.value,
(val: string) => {
emit('update:color', val)
}
)
</script>
<style scoped lang="scss">
:deep(.el-input-group__append) {
padding: 0;
.el-color-picker__trigger {
padding: 0;
border-left: none;
border-radius: 0 var(--el-input-border-radius) var(--el-input-border-radius) 0;
}
}
</style>

View File

@ -0,0 +1,270 @@
<template>
<div class="relative">
<table class="cube-table">
<!-- 底层魔方矩阵 -->
<tbody>
<tr v-for="(rowCubes, row) in cubes" :key="row">
<td
v-for="(cube, col) in rowCubes"
:key="col"
:class="['cube', { active: cube.active }]"
:style="{
width: `${cubeSize}px`,
height: `${cubeSize}px`
}"
@click="handleCubeClick(row, col)"
@mouseenter="handleCellHover(row, col)"
>
<Icon icon="ep-plus" />
</td>
</tr>
</tbody>
<!-- 顶层热区 -->
<div
v-for="(hotArea, index) in hotAreas"
:key="index"
class="hot-area"
:style="{
top: `${cubeSize * hotArea.top}px`,
left: `${cubeSize * hotArea.left}px`,
height: `${cubeSize * hotArea.height}px`,
width: `${cubeSize * hotArea.width}px`
}"
@click="handleHotAreaSelected(hotArea, index)"
@mouseover="exitHotAreaSelectMode"
>
<!-- 右上角热区删除按钮 -->
<div
v-if="selectedHotAreaIndex === index"
class="btn-delete"
@click="handleDeleteHotArea(index)"
>
<Icon icon="ep:circle-close-filled" />
</div>
{{ `${hotArea.width}×${hotArea.height}` }}
</div>
</table>
</div>
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import * as vueTypes from 'vue-types'
import { Point, Rect, isContains, isOverlap, createRect } from './util'
//
//
// 1.
//
// 1.1
// 1.2
// 1.3
//
// 2.
defineOptions({ name: 'MagicCubeEditor' })
/**
* 方块
* @property active 是否激活
*/
type Cube = Point & { active: boolean }
//
const props = defineProps({
//
modelValue: vueTypes.array<any>().isRequired,
// 4
rows: propTypes.number.def(4),
// 4
cols: propTypes.number.def(4),
// px75px
cubeSize: propTypes.number.def(75)
})
//
const cubes = ref<Cube[][]>([])
//
watch(
() => [props.rows, props.cols],
() => {
//
cubes.value = []
if (!props.rows || !props.cols) return
//
for (let row = 0; row < props.rows; row++) {
cubes.value[row] = []
for (let col = 0; col < props.cols; col++) {
cubes.value[row].push({ x: col, y: row, active: false })
}
}
},
{ immediate: true }
)
//
const hotAreas = ref<Rect[]>([])
//
watch(
() => props.modelValue,
() => (hotAreas.value = props.modelValue || []),
{ immediate: true }
)
//
const hotAreaBeginCube = ref<Cube>()
//
const isHotAreaSelectMode = () => !!hotAreaBeginCube.value
/**
* 处理鼠标点击方块
*
* @param currentRow 当前行号
* @param currentCol 当前列号
*/
const handleCubeClick = (currentRow: number, currentCol: number) => {
const currentCube = cubes.value[currentRow][currentCol]
// 1
if (!isHotAreaSelectMode()) {
hotAreaBeginCube.value = currentCube
hotAreaBeginCube.value.active = true
return
}
// 2
hotAreas.value.push(createRect(hotAreaBeginCube.value!, currentCube))
//
exitHotAreaSelectMode()
//
let hotAreaIndex = hotAreas.value.length - 1
handleHotAreaSelected(hotAreas.value[hotAreaIndex], hotAreaIndex)
//
emitUpdateModelValue()
}
/**
* 处理鼠标经过方块
*
* @param currentRow 当前行号
* @param currentCol 当前列号
*/
const handleCellHover = (currentRow: number, currentCol: number) => {
//
if (!isHotAreaSelectMode()) return
//
const currentSelectedArea = createRect(
hotAreaBeginCube.value!,
cubes.value[currentRow][currentCol]
)
//
for (const hotArea of hotAreas.value) {
//
if (isOverlap(hotArea, currentSelectedArea)) {
//
exitHotAreaSelectMode()
return
}
}
//
eachCube((_, __, cube) => {
cube.active = isContains(currentSelectedArea, cube)
})
}
/**
* 处理热区删除
*
* @param index 热区索引
*/
const handleDeleteHotArea = (index: number) => {
hotAreas.value.splice(index, 1)
//
exitHotAreaSelectMode()
//
emitUpdateModelValue()
}
//
const emit = defineEmits(['update:modelValue', 'hotAreaSelected'])
//
const emitUpdateModelValue = () => emit('update:modelValue', hotAreas)
//
const selectedHotAreaIndex = ref(-1)
const handleHotAreaSelected = (hotArea: Rect, index: number) => {
selectedHotAreaIndex.value = index
emit('hotAreaSelected', hotArea, index)
}
/**
* 结束热区选择模式
*/
function exitHotAreaSelectMode() {
//
eachCube((_, __, cube) => {
if (cube.active) {
cube.active = false
}
})
//
hotAreaBeginCube.value = undefined
}
/**
* 迭代魔方矩阵
* @param callback 回调
*/
const eachCube = (callback: (x: number, y: number, cube: Cube) => void) => {
for (let x = 0; x < cubes.value.length; x++) {
for (let y = 0; y < cubes.value[x].length; y++) {
callback(x, y, cubes.value[x][y])
}
}
}
</script>
<style lang="scss" scoped>
.cube-table {
position: relative;
border-spacing: 0;
border-collapse: collapse;
.cube {
border: 1px solid var(--el-border-color);
text-align: center;
color: var(--el-text-color-secondary);
cursor: pointer;
box-sizing: border-box;
&.active {
background: var(--el-color-primary-light-9);
}
}
.hot-area {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--el-color-primary);
background: var(--el-color-primary-light-8);
color: var(--el-color-primary);
box-sizing: border-box;
border-spacing: 0;
border-collapse: collapse;
cursor: pointer;
.btn-delete {
z-index: 1;
position: absolute;
top: -8px;
right: -8px;
height: 16px;
width: 16px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: #fff;
}
}
}
</style>

View File

@ -0,0 +1,72 @@
// 坐标点
export interface Point {
x: number
y: number
}
// 矩形
export interface Rect {
// 左上角 X 轴坐标
left: number
// 左上角 Y 轴坐标
top: number
// 右下角 X 轴坐标
right: number
// 右下角 Y 轴坐标
bottom: number
// 矩形宽度
width: number
// 矩形高度
height: number
}
/**
*
* @param a A
* @param b B
*/
export const isOverlap = (a: Rect, b: Rect): boolean => {
return (
a.left < b.left + b.width &&
a.left + a.width > b.left &&
a.top < b.top + b.height &&
a.height + a.top > b.top
)
}
/**
*
* @param hotArea
* @param point
*/
export const isContains = (hotArea: Rect, point: Point): boolean => {
return (
point.x >= hotArea.left &&
point.x < hotArea.right &&
point.y >= hotArea.top &&
point.y < hotArea.bottom
)
}
/**
*
*
*
* 1. 1
* 2. X 1
* 3. Y 1
* 4.
*
* @param a
* @param b
*/
export const createRect = (a: Point, b: Point): Rect => {
// 计算矩形的范围
const [left, left2] = [a.x, b.x].sort()
const [top, top2] = [a.y, b.y].sort()
const right = left2 + 1
const bottom = top2 + 1
const height = bottom - top
const width = right - left
return { left, right, top, bottom, height, width }
}

View File

@ -217,6 +217,10 @@ const refreshToken = async () => {
const handleAuthorized = () => {
const { t } = useI18n()
if (!isRelogin.show) {
// 如果已经到重新登录页面则不进行弹窗提示
if (window.location.href.includes('login?redirect=')) {
return
}
isRelogin.show = true
ElMessageBox.confirm(t('sys.api.timeoutMessage'), t('common.confirmTitle'), {
showCancelButton: false,

View File

@ -151,3 +151,24 @@ const subtractLight = (color: string, amount: number) => {
const c = cc < 0 ? 0 : cc
return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`
}
// 预设颜色
export const PREDEFINE_COLORS = [
'#ff4500',
'#ff8c00',
'#ffd700',
'#90ee90',
'#00ced1',
'#1e90ff',
'#c71585',
'#409EFF',
'#909399',
'#C0C4CC',
'#b7390b',
'#ff7800',
'#fad400',
'#5b8c5f',
'#00babd',
'#1f73c3',
'#711f57'
]

View File

@ -0,0 +1,115 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="30%">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item v-if="formType === 'create'" label="选择人员" prop="userId">
<el-select v-model="formData.userId">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</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>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as UserApi from '@/api/system/user'
import * as PermissionApi from '@/api/crm/permission'
import { CrmPermissionLevelEnum } from './index'
defineOptions({ name: 'CrmPermissionForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const userOptions = ref<UserApi.UserVO[]>([]) //
const formData = ref<PermissionApi.PermissionVO & { ids?: number[] }>({
userId: undefined, //
bizType: undefined, // Crm
bizId: undefined, // Crm
level: undefined //
})
const formRules = reactive({
userId: [{ required: true, message: '人员不能为空', trigger: 'blur' }],
level: [{ required: true, message: '权限级别不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: 'create' | 'update', bizType: number, bizId: number, ids?: number[]) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type) + '团队成员'
formType.value = type
resetForm(bizType, bizId)
//
if (ids) {
formData.value.ids = ids
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formData.value
if (formType.value === 'create') {
await PermissionApi.createPermission(unref(data))
message.success(t('common.createSuccess'))
} else {
await PermissionApi.updatePermission(unref(data))
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = (bizType: number, bizId: number) => {
formRef.value?.resetFields()
formData.value = {
userId: undefined, //
bizType, // Crm
bizId, // Crm
level: undefined //
}
}
onMounted(async () => {
//
// TODO
userOptions.value = await UserApi.getSimpleUserList()
})
</script>

View File

@ -0,0 +1,156 @@
<template>
<!-- 操作栏 -->
<el-row justify="end">
<el-button type="primary" @click="handleAdd">
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
<el-button @click="handleEdit">
<Icon class="mr-5px" icon="ep:edit" />
编辑
</el-button>
<el-button @click="handleRemove">
<Icon class="mr-5px" icon="ep:delete" />
移除
</el-button>
<el-button type="danger" @click="handleQuit"> 退出团队</el-button>
</el-row>
<!-- 团队成员展示 -->
<el-table
v-loading="loading"
:data="list"
:show-overflow-tooltip="true"
:stripe="true"
class="mt-20px"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column align="center" label="姓名" prop="nickname" />
<el-table-column align="center" label="部门" prop="deptName" />
<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>
</template>
</el-table-column>
<el-table-column :formatter="dateFormatter" align="center" label="加入时间" prop="createTime" />
</el-table>
<CrmPermissionForm ref="crmPermissionFormRef" />
</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'
defineOptions({ name: 'CrmTeam' })
const message = useMessage() //
const props = defineProps<{
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 getList = async () => {
loading.value = true
try {
const data = await PermissionApi.getPermissionList({
bizType: props.bizType,
bizId: props.bizId
})
list.value = data
} finally {
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 handleSelectionChange = (val: PermissionApi.PermissionVO[]) => {
multipleSelection.value = val
}
// TODO @puhui999 index.vue
const crmPermissionFormRef = ref<InstanceType<typeof CrmPermissionForm>>()
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)
}
const handleRemove = async () => {
if (multipleSelection.value?.length === 0) {
message.warning('请先选择团队成员后操作!')
return
}
await message.delConfirm()
const ids = multipleSelection.value?.map((item) => item.id)
await PermissionApi.deletePermission({
bizType: props.bizType,
bizId: props.bizId,
ids
})
}
const handleAdd = () => {
crmPermissionFormRef.value?.open('create', props.bizType, props.bizId)
}
const userStore = useUserStoreWithOut()
const handleQuit = async () => {
const permission = list.value.find(
(item) => item.userId === userStore.getUser.id && item.level === CrmPermissionLevelEnum.OWNER
)
if (permission) {
message.warning('负责人不能退出团队!')
return
}
const userPermission = list.value.find((item) => item.userId === userStore.getUser.id)
await PermissionApi.quitTeam(userPermission?.id)
}
watch(
() => props.bizId,
() => {
getList()
},
{ immediate: true, deep: true }
)
</script>

View File

@ -0,0 +1,17 @@
import CrmTeam from './CrmTeamList.vue'
enum CrmBizTypeEnum {
CRM_LEADS = 1, // 线索
CRM_CUSTOMER = 2, // 客户
CRM_CONTACTS = 3, // 联系人
CRM_BUSINESS = 5, // 商机
CRM_CONTRACT = 6 // 合同
}
enum CrmPermissionLevelEnum {
OWNER = 1, // 负责人
READ = 2, // 读
WRITE = 3 // 写
}
export { CrmTeam, CrmBizTypeEnum, CrmPermissionLevelEnum }

View File

@ -28,6 +28,7 @@
/>
</el-select>
</el-form-item>
<!-- TODO 芋艿封装成一个组件 -->
<el-form-item label="客户名称" prop="customerName">
<el-popover
placement="bottom"
@ -89,7 +90,6 @@
/>
</el-select>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机号" />
</el-form-item>
@ -212,6 +212,7 @@ const formRules = reactive({
const formRef = ref() // Ref
const ownerUserList = ref<any[]>([])
const userList = ref<UserApi.UserVO[]>([]) //
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
@ -231,6 +232,8 @@ const open = async (type: string, id?: number) => {
}
}
}
defineExpose({ open }) // open
/** 查询列表 */
const getList = async () => {
loading.value = true
@ -242,7 +245,7 @@ const getList = async () => {
loading.value = false
}
}
defineExpose({ open }) // open
const gotOwnerUser = (owerUserId: any) => {
if (owerUserId !== null) {
owerUserId.split(',').forEach((item: string) => {
@ -254,6 +257,7 @@ const gotOwnerUser = (owerUserId: any) => {
})
}
}
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
@ -305,12 +309,13 @@ const resetForm = () => {
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
@ -322,7 +327,7 @@ const owerSelectValue = (value) => {
}
})
}
//
//
const showCustomer = ref(false)
const openCustomerSelect = () => {
showCustomer.value = !showCustomer.value
@ -341,7 +346,7 @@ const selectCustomer = () => {
formData.value.customerName = multipleSelection.value.name
showCustomer.value = !showCustomer.value
}
const allContactList = ref([]) //
const allContactList = ref([]) //
onMounted(async () => {
allContactList.value = await ContactApi.simpleAlllist()
})

View File

@ -18,6 +18,7 @@
</Dialog>
</template>
<script setup lang="ts">
// TODO
import * as UserApi from '@/api/system/user'
import { parseBigInt } from 'jsencrypt/lib/lib/jsbn/jsbn'
const dialogVisible = ref(false) //

View File

@ -3,7 +3,7 @@
* @Date: 2023-11-11 14:50:11
* @LastEditTime: 2023-11-11 14:52:47
* @FilePath: \yudao-ui-admin-vue3\src\views\crm\contact\detail\ContactBasicInfo.vue
* @Description:
* @Description:
-->
<template>
<el-col>
@ -17,6 +17,7 @@
</el-col>
</template>
<script setup lang="ts">
// TODO review
import * as ContactApi from '@/api/crm/contact'
const { contact } = defineProps<{ contact: ContactApi.ContactVO }>()

View File

@ -62,6 +62,7 @@
</el-collapse>
</template>
<script setup lang="ts">
// TODO review
import * as ContactApi from '@/api/crm/contact'
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'

View File

@ -1,4 +1,5 @@
<template>
<!-- TODO 芋艿要不要把 3 62 合并成一个组件 -->
<div v-loading="loading">
<div class="flex items-start justify-between">
<div>
@ -105,6 +106,7 @@ import ContactDetails from '@/views/crm/contact/detail/ContactDetails.vue'
import ContactForm from '@/views/crm/contact/ContactForm.vue'
import { formatDate } from '@/utils/formatTime'
import * as CustomerApi from '@/api/crm/customer'
// TODO review
defineOptions({ name: 'ContactDetail' })
const { delView } = useTagsViewStore() //

View File

@ -1,6 +1,7 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<!-- TODO zyna筛选项按照需求简化下 -->
<el-form
class="-mb-15px"
:model="queryParams"
@ -214,6 +215,7 @@ import * as UserApi from '@/api/system/user'
import * as CustomerApi from '@/api/crm/customer'
defineOptions({ name: 'CrmContact' })
const message = useMessage() //
const { t } = useI18n() //
@ -244,6 +246,7 @@ const queryParams = reactive({
const queryFormRef = ref() //
const exportLoading = ref(false) //
const userList = ref<UserApi.UserVO[]>([]) //
/** 查询列表 */
const getList = async () => {
loading.value = true
@ -301,6 +304,8 @@ const handleExport = async () => {
exportLoading.value = false
}
}
// TODO @zyna
const gotOwnerUser = (owerUserId: string) => {
let ownerName = ''
if (owerUserId !== null) {
@ -315,13 +320,14 @@ const gotOwnerUser = (owerUserId: string) => {
}
return ownerName
}
/** 详情页面 */
/** 打开客户详情 */
const { push } = useRouter()
const openDetail = (id: number) => {
push({ name: 'CrmContactDetail', params: { id } })
}
// TODO @zyna
const allContactList = ref([]) //
const allCustomerList = ref([]) //
/** 初始化 **/

View File

@ -1,83 +1,86 @@
<template>
<el-collapse v-model="activeNames">
<el-collapse-item name="basicInfo">
<template #title>
<span class="text-base font-bold">基本信息</span>
</template>
<el-descriptions :column="4">
<el-descriptions-item label="客户名称">
{{ customer.name }}
</el-descriptions-item>
<el-descriptions-item label="所属行业">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="customer.industryId" />
</el-descriptions-item>
<el-descriptions-item label="客户来源">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="customer.source" />
</el-descriptions-item>
<el-descriptions-item label="客户等级">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="customer.level" />
</el-descriptions-item>
<el-descriptions-item label="手机">
{{ customer.mobile }}
</el-descriptions-item>
<el-descriptions-item label="电话">
{{ customer.telephone }}
</el-descriptions-item>
<el-descriptions-item label="邮箱">
{{ customer.email }}
</el-descriptions-item>
<el-descriptions-item label="QQ">
{{ customer.qq }}
</el-descriptions-item>
<el-descriptions-item label="微信">
{{ customer.wechat }}
</el-descriptions-item>
<el-descriptions-item label="网址">
{{ customer.website }}
</el-descriptions-item>
<el-descriptions-item label="所在地">
{{ customer.areaName }}
</el-descriptions-item>
<el-descriptions-item label="详细地址">
{{ customer.detailAddress }}
</el-descriptions-item>
<el-descriptions-item label="下次联系时间">
{{ customer.contactNextTime ? formatDate(customer.contactNextTime, 'YYYY-MM-DD') : '空' }}
</el-descriptions-item>
</el-descriptions>
<el-descriptions :column="1">
<el-descriptions-item label="客户描述">
{{ customer.description }}
</el-descriptions-item>
<el-descriptions-item label="备注">
{{ customer.remark }}
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
<el-collapse-item name="systemInfo">
<template #title>
<span class="text-base font-bold">系统信息</span>
</template>
<el-descriptions :column="2">
<el-descriptions-item label="负责人">
{{ customer.ownerUserName }}
</el-descriptions-item>
<el-descriptions-item label="创建人">
{{ customer.creatorName }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ customer.createTime ? formatDate(customer.createTime) : '空' }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ customer.updateTime ? formatDate(customer.updateTime) : '空' }}
</el-descriptions-item>
<!-- TODO wanwan要不把最后跟进时间放到下次联系时间后面 -->
<el-descriptions-item label="最后跟进时间">
{{ customer.contactLastTime ? formatDate(customer.contactLastTime) : '空' }}
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
</el-collapse>
<ContentWrap>
<el-collapse class="" v-model="activeNames">
<el-collapse-item name="basicInfo">
<template #title>
<span class="text-base font-bold">基本信息</span>
</template>
<el-descriptions :column="4">
<el-descriptions-item label="客户名称">
{{ customer.name }}
</el-descriptions-item>
<el-descriptions-item label="所属行业">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="customer.industryId" />
</el-descriptions-item>
<el-descriptions-item label="客户来源">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="customer.source" />
</el-descriptions-item>
<el-descriptions-item label="客户等级">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="customer.level" />
</el-descriptions-item>
<el-descriptions-item label="手机">
{{ customer.mobile }}
</el-descriptions-item>
<el-descriptions-item label="电话">
{{ customer.telephone }}
</el-descriptions-item>
<el-descriptions-item label="邮箱">
{{ customer.email }}
</el-descriptions-item>
<el-descriptions-item label="QQ">
{{ customer.qq }}
</el-descriptions-item>
<el-descriptions-item label="微信">
{{ customer.wechat }}
</el-descriptions-item>
<el-descriptions-item label="网址">
{{ customer.website }}
</el-descriptions-item>
<el-descriptions-item label="所在地">
{{ customer.areaName }}
</el-descriptions-item>
<el-descriptions-item label="详细地址">
{{ customer.detailAddress }}
</el-descriptions-item>
<el-descriptions-item label="下次联系时间">
{{
customer.contactNextTime ? formatDate(customer.contactNextTime, 'YYYY-MM-DD') : '空'
}}
</el-descriptions-item>
<el-descriptions-item label="最后跟进时间">
{{ customer.contactLastTime ? formatDate(customer.contactLastTime) : '空' }}
</el-descriptions-item>
</el-descriptions>
<el-descriptions :column="1">
<el-descriptions-item label="客户描述">
{{ customer.description }}
</el-descriptions-item>
<el-descriptions-item label="备注">
{{ customer.remark }}
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
<el-collapse-item name="systemInfo">
<template #title>
<span class="text-base font-bold">系统信息</span>
</template>
<el-descriptions :column="2">
<el-descriptions-item label="负责人">
{{ customer.ownerUserName }}
</el-descriptions-item>
<el-descriptions-item label="创建人">
{{ customer.creatorName }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ customer.createTime ? formatDate(customer.createTime) : '空' }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ customer.updateTime ? formatDate(customer.updateTime) : '空' }}
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
</el-collapse>
</ContentWrap>
</template>
<script setup lang="ts">
import * as CustomerApi from '@/api/crm/customer'

View File

@ -1,4 +1,5 @@
<template>
<!-- TODO @wanwan要不要把上面这一整块搞成一个组件就是把 下面 + Details + BasitcInfo 合并成一个 -->
<div v-loading="loading">
<div class="flex items-start justify-between">
<div>
@ -63,12 +64,10 @@
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
<!-- TODO wanwan这个 tab 拉满哈可以更好看 -->
<el-col :span="18">
<el-col>
<el-tabs>
<el-tab-pane label="详细资料">
<!-- TODO wanwan这个 ml-2 是不是可以优化下不要整个左移而是里面的内容有个几 px 的偏移不顶在框里 -->
<CustomerDetails class="ml-2" :customer="customer" />
<CustomerDetails :customer="customer" />
</el-tab-pane>
<el-tab-pane label="活动" lazy> 活动</el-tab-pane>
<el-tab-pane label="邮件" lazy> 邮件</el-tab-pane>
@ -76,20 +75,20 @@
<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>
<template #label> 联系人<el-badge class="item" type="primary" /> </template>
联系人
</el-tab-pane>
<el-tab-pane label="团队成员" lazy>
<template #label> 团队成员<el-badge :value="2" 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>
<el-tab-pane label="合同" lazy>
<template #label> 合同<el-badge :value="3" 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 :value="4" 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>
@ -118,14 +117,12 @@ const { currentRoute } = useRouter() // 路由
const id = Number(route.params.id)
const loading = ref(true) //
//
const customer = ref<CustomerApi.CustomerVO>({} as CustomerApi.CustomerVO)
/**
* 获取详情
*
* @param id
*/
const customer = ref<CustomerApi.CustomerVO>({} as CustomerApi.CustomerVO) //
const getCustomerData = async (id: number) => {
loading.value = true
try {

View File

@ -2,36 +2,36 @@
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="客户名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入客户名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
clearable
placeholder="请输入客户名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="手机" prop="mobile">
<el-input
v-model="queryParams.mobile"
placeholder="请输入手机"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
clearable
placeholder="请输入手机"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="所属行业" prop="industryId">
<el-select
v-model="queryParams.industryId"
placeholder="请选择所属行业"
clearable
class="!w-240px"
clearable
placeholder="请选择所属行业"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
@ -44,9 +44,9 @@
<el-form-item label="客户等级" prop="level">
<el-select
v-model="queryParams.level"
placeholder="请选择客户等级"
clearable
class="!w-240px"
clearable
placeholder="请选择客户等级"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
@ -59,9 +59,9 @@
<el-form-item label="客户来源" prop="source">
<el-select
v-model="queryParams.source"
placeholder="请选择客户来源"
clearable
class="!w-240px"
clearable
placeholder="请选择客户来源"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
@ -72,19 +72,27 @@
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="primary" @click="openForm('create')" v-hasPermi="['crm:customer:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button v-hasPermi="['crm:customer:create']" type="primary" @click="openForm('create')">
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['crm:customer:export']"
:loading="exportLoading"
plain
type="success"
@click="handleExport"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
<Icon class="mr-5px" icon="ep:download" />
导出
</el-button>
</el-form-item>
</el-form>
@ -92,77 +100,77 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="客户名称" align="center" prop="name" width="160" />
<el-table-column label="所属行业" align="center" prop="industryId" width="120">
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
<el-table-column align="center" label="编号" prop="id" />
<el-table-column align="center" label="客户名称" prop="name" width="160" />
<el-table-column align="center" label="所属行业" prop="industryId" width="120">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
</template>
</el-table-column>
<el-table-column label="客户来源" align="center" prop="source" width="100">
<el-table-column align="center" label="客户来源" 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">
<el-table-column align="center" label="客户等级" 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-column label="手机" align="center" prop="mobile" width="120" />
<el-table-column label="详细地址" align="center" prop="detailAddress" width="200" />
<el-table-column label="负责人" align="center" prop="ownerUserName" />
<el-table-column label="所属部门" align="center" prop="ownerUserDept" />
<el-table-column label="创建人" align="center" prop="creatorName" />
<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="creatorName" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180px"
/>
<el-table-column label="成交状态" align="center" prop="dealStatus">
<el-table-column align="center" label="成交状态" prop="dealStatus">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealStatus" />
</template>
</el-table-column>
<el-table-column
label="下次联系时间"
align="center"
prop="contactNextTime"
:formatter="dateFormatter"
align="center"
label="下次联系时间"
prop="contactNextTime"
width="180px"
/>
<el-table-column
label="最后跟进时间"
align="center"
prop="contactLastTime"
:formatter="dateFormatter"
align="center"
label="最后跟进时间"
prop="contactLastTime"
width="180px"
/>
<el-table-column label="锁定状态" align="center" prop="lockStatus">
<el-table-column align="center" label="锁定状态" prop="lockStatus">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.lockStatus" />
</template>
</el-table-column>
<!-- TODO @wanwan 距进入公海天数 -->
<el-table-column label="操作" align="center" min-width="150" fixed="right">
<el-table-column align="center" fixed="right" label="操作" min-width="150">
<template #default="scope">
<el-button link type="primary" @click="openDetail(scope.row.id)">详情</el-button>
<el-button
v-hasPermi="['crm:customer:update']"
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['crm:customer:update']"
>
编辑
</el-button>
<el-button
v-hasPermi="['crm:customer:delete']"
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['crm:customer:delete']"
>
删除
</el-button>
@ -171,23 +179,26 @@
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<!-- TODO 方便查看效果 TODO 芋艿先注释了避免演示环境报错 -->
<!-- <CrmTeam :biz-id="1" :biz-type="CrmBizTypeEnum.CRM_CUSTOMER" />-->
<!-- 表单弹窗添加/修改 -->
<CustomerForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
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' })

View File

@ -0,0 +1,146 @@
<template>
<el-button type="primary" plain @click="handleQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 刷新
</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['crm:customer-limit-config:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
class="mt-4"
>
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="规则类型" align="center" prop="type" />
<el-table-column label="规则适用人群" align="center" prop="userNames" />
<el-table-column label="规则适用部门" align="center" prop="deptNames" />
<el-table-column
:label="
confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT ? '拥有客户数上限' : '锁定客户数上限'
"
align="center"
prop="maxCount"
/>
<el-table-column
v-if="confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT"
label="成交客户是否占用拥有客户数"
align="center"
prop="dealCountEnabled"
>
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealCountEnabled" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="110" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['crm:customer-limit-config:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['crm:customer-limit-config:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 表单弹窗添加/修改 -->
<CustomerLimitConfigForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import * as CustomerLimitConfigApi from '@/api/crm/customerLimitConfig'
import CustomerLimitConfigForm from '@/views/crm/customerLimitConfig/CustomerLimitConfigForm.vue'
import { LimitConfType } from '@/views/crm/customerLimitConfig/customerLimitConf'
import { DICT_TYPE } from '@/utils/dict'
defineOptions({ name: 'CustomerLimitConfDetails' })
const message = useMessage() //
const { t } = useI18n() //
const { confType } = defineProps<{ confType: LimitConfType }>()
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
type: confType
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await CustomerLimitConfigApi.getCustomerLimitConfigPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id, confType)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await CustomerLimitConfigApi.deleteCustomerLimitConfig(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@ -0,0 +1,224 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="200px"
v-loading="formLoading"
>
<el-form-item label="规则适用人群" prop="userIds">
<el-tree-select
v-model="formData.userIds"
:data="userTree"
:props="defaultProps"
check-on-click-node
multiple
filterable
node-key="id"
placeholder="请选择规则适用人群"
/>
</el-form-item>
<el-form-item label="规则适用部门" prop="deptIds">
<el-tree-select
v-model="formData.deptIds"
:data="deptTree"
:props="defaultProps"
multiple
check-strictly
filterable
node-key="id"
placeholder="请选择规则适用部门"
/>
</el-form-item>
<el-form-item
:label="
formData.type === LimitConfType.CUSTOMER_QUANTITY_LIMIT
? '拥有客户数上限'
: '锁定客户数上限'
"
prop="maxCount"
>
<el-input-number v-model="formData.maxCount" placeholder="请输入数量上限" />
</el-form-item>
<el-form-item
label="成交客户是否占用拥有客户数"
v-if="formData.type === LimitConfType.CUSTOMER_QUANTITY_LIMIT"
prop="dealCountEnabled"
>
<el-switch v-model="formData.dealCountEnabled" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import * as CustomerLimitConfigApi from '@/api/crm/customerLimitConfig'
import { LimitConfType } from '@/views/crm/customerLimitConfig/customerLimitConf'
import * as DeptApi from '@/api/system/dept'
import { defaultProps, handleTree } from '@/utils/tree'
import * as UserApi from '@/api/system/user'
import { cloneDeep } from 'lodash-es'
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
type: undefined,
userIds: undefined,
deptIds: undefined,
maxCount: undefined,
dealCountEnabled: false
})
const formRules = reactive({
type: [{ required: true, message: '规则类型不能为空', trigger: 'change' }],
maxCount: [{ required: true, message: '数量上限不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
// TODO @
const deptTree = ref() //
const userTree = ref() //
/** 打开弹窗 */
const open = async (type: string, id?: number, limitConfType?: LimitConfType) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
await getDeptTree()
//
await getUserTree()
//
if (id) {
formLoading.value = true
try {
formData.value = await CustomerLimitConfigApi.getCustomerLimitConfig(id)
} finally {
formLoading.value = false
}
} else {
formData.value.type = limitConfType
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formData.value as unknown as CustomerLimitConfigApi.CustomerLimitConfigVO
if (formType.value === 'create') {
await CustomerLimitConfigApi.createCustomerLimitConfig(data)
message.success(t('common.createSuccess'))
} else {
await CustomerLimitConfigApi.updateCustomerLimitConfig(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
type: undefined,
userIds: undefined,
deptIds: undefined,
maxCount: undefined,
dealCountEnabled: false
}
formRef.value?.resetFields()
}
/**
* 获取部门树
*/
const getDeptTree = async () => {
const res = await DeptApi.getSimpleDeptList()
deptTree.value = []
deptTree.value.push(...handleTree(res))
}
/**
* 获取用户树
*/
const getUserTree = async () => {
const res = await UserApi.getAllUser()
userTree.value = []
userTree.value = cloneDeep(unref(deptTree))
const deptUserMap = {}
res.forEach((user) => {
if (user.dept) {
if (!deptUserMap[user.deptId]) {
deptUserMap[user.deptId] = []
}
deptUserMap[user.deptId].push(user)
}
})
handleUserData(userTree.value, deptUserMap)
}
// TODO @
/**
* 处理用户树
*
* @param deptTree
* @param deptUserMap
*/
const handleUserData = (deptTree, deptUserMap) => {
for (let i = 0; i < deptTree.length; i++) {
//
if (deptTree[i].isUser) {
continue
}
const users = deptUserMap[deptTree[i].id]
if (users) {
if (!deptTree[i].children) {
deptTree[i].children = []
}
deptTree[i].children.push(
...users.map((user) => {
return {
id: user.id,
name: user.username + '-' + user.nickname,
isUser: true,
//
disabled: user.status === 1
}
})
)
}
if (deptTree[i].children && deptTree[i].children.length !== 0) {
handleUserData(deptTree[i].children, deptUserMap)
}
//
deptTree[i].disabled = true
// id
deptTree[i].id = 'null'
}
}
</script>

View File

@ -0,0 +1,14 @@
// TODO 可以挪到它对应的 api.ts 文件里哈
/**
*
*/
export enum LimitConfType {
/**
*
*/
CUSTOMER_QUANTITY_LIMIT = 1,
/**
*
*/
CUSTOMER_LOCK_LIMIT = 2
}

View File

@ -0,0 +1,20 @@
<template>
<!-- 列表 -->
<ContentWrap>
<el-tabs>
<el-tab-pane label="拥有客户数限制">
<!-- TODO @wanwanCustomerLimitConfigList因为它是列表哈 -->
<CustomerLimitConfDetails :confType="LimitConfType.CUSTOMER_QUANTITY_LIMIT" />
</el-tab-pane>
<el-tab-pane label="锁定客户数限制">
<CustomerLimitConfDetails :confType="LimitConfType.CUSTOMER_LOCK_LIMIT" />
</el-tab-pane>
</el-tabs>
</ContentWrap>
</template>
<script setup lang="ts">
import CustomerLimitConfDetails from '@/views/crm/customerLimitConfig/CustomerLimitConfDetails.vue'
import { LimitConfType } from '@/views/crm/customerLimitConfig/customerLimitConf'
defineOptions({ name: 'CrmCustomerLimitConfig' })
</script>

View File

@ -0,0 +1,135 @@
<template>
<ContentWrap>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="160px"
v-loading="formLoading"
>
<el-card shadow="never">
<!-- 操作 -->
<template #header>
<div class="flex items-center justify-between">
<CardTitle title="客户公海规则设置" />
<el-button
type="primary"
@click="onSubmit"
v-hasPermi="['crm:customer-pool-config:update']"
>
保存
</el-button>
</div>
</template>
<!-- 表单 -->
<el-form-item label="客户公海规则设置" prop="enabled">
<el-radio-group v-model="formData.enabled" class="ml-4">
<el-radio :label="false" size="large">不启用</el-radio>
<el-radio :label="true" size="large">启用</el-radio>
</el-radio-group>
</el-form-item>
<div v-if="formData.enabled">
<el-form-item>
<el-input-number class="mr-2" v-model="formData.contactExpireDays" />
天不跟进或
<el-input-number class="mx-2" v-model="formData.dealExpireDays" />
天未成交
</el-form-item>
<el-form-item label="提前提醒设置" prop="notifyEnabled">
<el-radio-group v-model="formData.notifyEnabled" class="ml-4">
<el-radio :label="false" size="large">不提醒</el-radio>
<el-radio :label="true" size="large">提醒</el-radio>
</el-radio-group>
</el-form-item>
<div v-if="formData.notifyEnabled">
<el-form-item>
提前 <el-input-number class="mx-2" v-model="formData.notifyDays" /> 天提醒
</el-form-item>
</div>
</div>
</el-card>
</el-form>
</ContentWrap>
</template>
<script setup lang="ts">
import * as CustomerPoolConfApi from '@/api/crm/customerPoolConf'
import { CardTitle } from '@/components/Card'
// TODO @wanwanCustomerPoolConf = CustomerPoolConfig crm config customerPoolConfig customerLimitConfig
defineOptions({ name: 'CustomerPoolConf' })
const message = useMessage() //
const { t } = useI18n() //
const formLoading = ref(false)
const formData = ref({
enabled: false,
contactExpireDays: 0,
dealExpireDays: 0,
notifyEnabled: false,
notifyDays: 0
})
const formRules = reactive({
enabled: [{ required: true, message: '是否启用客户公海不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 获取配置 */
const getConfig = async () => {
try {
formLoading.value = true
const data = await CustomerPoolConfApi.getCustomerPoolConfig()
if (data === null) {
return
}
formData.value = data
} finally {
formLoading.value = false
}
}
/** 提交配置 */
const onSubmit = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formData.value as unknown as CustomerPoolConfApi.CustomerPoolConfigVO
await CustomerPoolConfApi.updateCustomerPoolConfig(data)
message.success(t('common.updateSuccess'))
await getConfig()
formLoading.value = false
} finally {
formLoading.value = false
}
}
// TODO @wanwanel-radio-group
watch(
() => formData.value.enabled,
(val: boolean) => {
if (!val) {
formData.value.contactExpireDays = undefined
formData.value.dealExpireDays = undefined
formData.value.notifyEnabled = false
formData.value.notifyDays = undefined
}
}
)
// TODO @wanwanel-radio-group
watch(
() => formData.value.notifyEnabled,
(val: boolean) => {
if (!val) {
formData.value.notifyDays = undefined
}
}
)
onMounted(() => {
getConfig()
})
</script>

View File

@ -0,0 +1,126 @@
<template>
<div class="flex flex-wrap items-center gap-8px">
<div v-for="(spu, index) in productSpus" :key="spu.id" class="select-box spu-pic">
<el-tooltip :content="spu.name">
<div class="relative h-full w-full">
<el-image :src="spu.picUrl" class="h-full w-full" />
<Icon
v-show="!disabled"
class="del-icon"
icon="ep:circle-close-filled"
@click="handleRemoveSpu(index)"
/>
</div>
</el-tooltip>
</div>
<el-tooltip content="选择商品">
<div
v-show="!disabled"
v-if="!limit || limit <= productSpus.length"
class="select-box"
@click="openSpuTableSelect"
>
<Icon icon="ep:plus" />
</div>
</el-tooltip>
</div>
<!-- 商品选择对话框表格形式 -->
<SpuTableSelect ref="spuTableSelectRef" multiple @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'
// 使
//
defineOptions({ name: 'SpuShowcase' })
const props = defineProps({
modelValue: array<number>().def([]).isRequired,
//
limit: propTypes.number.def(0),
disabled: propTypes.bool.def(false)
})
//
const productSpus = ref<ProductSpuApi.Spu[]>([])
watch(
() => props.modelValue,
async () => {
if (props.modelValue.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)
}
},
{ immediate: true }
)
/** 商品表格选择对话框 */
const spuTableSelectRef = ref()
//
const openSpuTableSelect = () => {
spuTableSelectRef.value.open(productSpus.value)
}
/**
* 选择商品后触发
* @param spus 选中的商品列表
*/
const handleSpuSelected = (spus: ProductSpuApi.Spu[]) => {
productSpus.value = spus
emitSpuChange()
}
/**
* 删除商品
* @param index 商品索引
*/
const handleRemoveSpu = (index: number) => {
productSpus.value.splice(index, 1)
emitSpuChange()
}
const emit = defineEmits(['update:modelValue', 'change'])
const emitSpuChange = () => {
emit(
'update:modelValue',
productSpus.value.map((spu) => spu.id)
)
emit('change', productSpus.value)
}
</script>
<style lang="scss" scoped>
.select-box {
display: flex;
width: 60px;
height: 60px;
border: 1px dashed var(--el-border-color-darker);
border-radius: 8px;
align-items: center;
justify-content: center;
}
.spu-pic {
position: relative;
}
.del-icon {
position: absolute;
top: -10px;
right: -10px;
z-index: 1;
width: 20px !important;
height: 20px !important;
}
</style>

View File

@ -78,7 +78,6 @@ const handleTemplateItemChange = () => {
currentFormData.value = formData.value!.pages.find(
(page: DiyPageApi.DiyPageVO) => page.name === templateItems[selectedTemplateItem.value].name
)
console.log(currentFormData.value)
}
//