Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
owen 2024-01-05 15:26:41 +08:00
commit 1fb7701e6e
64 changed files with 1121 additions and 569 deletions

View File

@ -83,7 +83,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": "explicit"
}, },
"[vue]": { "[vue]": {
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint" "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"

View File

@ -44,7 +44,7 @@ export const exportTask = async (params) => {
// 获取所有可回退的节点 // 获取所有可回退的节点
export const getReturnList = async (params) => { export const getReturnList = async (params) => {
return await request.get({ url: '/bpm/task/get-return-list', params }) return await request.get({ url: '/bpm/task/return-list', params })
} }
// 回退 // 回退

View File

@ -1,3 +1,10 @@
/*
* @Author: zyna
* @Date: 2023-12-02 13:08:56
* @LastEditTime: 2023-12-17 16:28:20
* @FilePath: \yudao-ui-admin-vue3\src\api\crm\business\index.ts
* @Description:
*/
import request from '@/config/axios' import request from '@/config/axios'
export interface BusinessVO { export interface BusinessVO {
@ -55,3 +62,8 @@ export const deleteBusiness = async (id: number) => {
export const exportBusiness = async (params) => { export const exportBusiness = async (params) => {
return await request.download({ url: `/crm/business/export-excel`, params }) return await request.download({ url: `/crm/business/export-excel`, params })
} }
//联系人关联商机列表
export const getBusinessPageByContact = async (params) => {
return await request.get({ url: `/crm/business/page-by-contact`, params })
}

View File

@ -8,7 +8,7 @@ export interface ContactVO {
email: string email: string
post: string post: string
customerId: number customerId: number
address: string detailAddress: string
remark: string remark: string
ownerUserId: string ownerUserId: string
lastTime: Date lastTime: Date
@ -26,6 +26,11 @@ export interface ContactVO {
ownerUserName: string ownerUserName: string
} }
export interface ContactBusinessReqVO {
contactId: number
businessIds: number[]
}
// 查询 CRM 联系人列表 // 查询 CRM 联系人列表
export const getContactPage = async (params) => { export const getContactPage = async (params) => {
return await request.get({ url: `/crm/contact/page`, params }) return await request.get({ url: `/crm/contact/page`, params })
@ -65,3 +70,13 @@ export const exportContact = async (params) => {
export const getSimpleContactList = async () => { export const getSimpleContactList = async () => {
return await request.get({ url: `/crm/contact/simple-all-list` }) return await request.get({ url: `/crm/contact/simple-all-list` })
} }
// 批量新增联系人商机关联
export const createContactBusinessList = async (data: ContactBusinessReqVO) => {
return await request.post({ url: `/crm/contact/create-business-list`, data })
}
// 解除联系人商机关联
export const deleteContactBusinessList = async (data: ContactBusinessReqVO) => {
return await request.delete({ url: `/crm/contact/delete-business-list`, data })
}

View File

@ -1,47 +0,0 @@
import request from '@/config/axios'
export interface ContactBusinessLinkVO {
id: number
contactId: number
businessId: number
}
// 查询联系人商机关联分页
export const getContactBusinessLinkPage = async (params) => {
return await request.get({ url: `/crm/contact-business-link/page`, params })
}
// 查询联系人商机关联详情
export const getContactBusinessLink = async (id: number) => {
return await request.get({ url: `/crm/contact-business-link/get?id=` + id })
}
// 新增联系人商机关联
export const createContactBusinessLink = async (data: ContactBusinessLinkVO) => {
return await request.post({ url: `/crm/contact-business-link/create`, data })
}
// 修改联系人商机关联
export const updateContactBusinessLink = async (data: ContactBusinessLinkVO) => {
return await request.put({ url: `/crm/contact-business-link/update`, data })
}
// 删除联系人商机关联
export const deleteContactBusinessLink = async (data: ContactBusinessLinkVO) => {
return await request.delete({ url: `/crm/contact-business-link/delete-batch`, data })
}
// 导出联系人商机关联 Excel
export const exportContactBusinessLink = async (params) => {
return await request.download({ url: `/crm/contact-business-link/export-excel`, params })
}
//批量新增联系人商机关联
export const createContactBusinessLinkBatch = async (data: ContactBusinessLinkVO[]) => {
return await request.post({ url: `/crm/contact-business-link/create-batch`, data })
}
// 查询联系人关联商机列表
export const getBusinessByContactPage = async (params) => {
return await request.get({ url: `/crm/contact-business-link/page-by-contact`, params })
}
// TODO @zyna根据后端调整下请求

View File

@ -59,7 +59,7 @@ export const deleteCustomer = async (id: number) => {
} }
// 导出客户 Excel // 导出客户 Excel
export const exportCustomer = async (params) => { export const exportCustomer = async (params: any) => {
return await request.download({ url: `/crm/customer/export-excel`, params }) return await request.download({ url: `/crm/customer/export-excel`, params })
} }
@ -67,3 +67,13 @@ export const exportCustomer = async (params) => {
export const queryAllList = async () => { export const queryAllList = async () => {
return await request.get({ url: `/crm/customer/query-all-list` }) return await request.get({ url: `/crm/customer/query-all-list` })
} }
// 查询客户操作日志
export const getOperateLogPage = async (params: any) => {
return await request.get({ url: '/crm/customer/operate-log-page', params })
}
// 锁定/解锁客户
export const lockCustomer = async (id: number, lockStatus: boolean) => {
return await request.put({ url: `/crm/customer/lock`, data: { id, lockStatus } })
}

View File

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

View File

@ -0,0 +1,39 @@
import request from '@/config/axios'
export interface UReportDataVO {
id: number
name: string
status: number
content: string
remark: string
}
// 查询Ureport2报表分页
export const getUReportDataPage = async (params) => {
return await request.get({ url: `/report/ureport-data/page`, params })
}
// 查询Ureport2报表详情
export const getUReportData = async (id: number) => {
return await request.get({ url: `/report/ureport-data/get?id=` + id })
}
// 新增Ureport2报表
export const createUReportData = async (data: UReportDataVO) => {
return await request.post({ url: `/report/ureport-data/create`, data })
}
// 修改Ureport2报表
export const updateUReportData = async (data: UReportDataVO) => {
return await request.put({ url: `/report/ureport-data/update`, data })
}
// 删除Ureport2报表
export const deleteUReportData = async (id: number) => {
return await request.delete({ url: `/report/ureport-data/delete?id=` + id })
}
// 导出Ureport2报表 Excel
export const exportUReportData = async (params) => {
return await request.download({ url: `/report/ureport-data/export-excel`, params })
}

View File

@ -23,6 +23,32 @@ export type OperateLogVO = {
resultData: string resultData: string
} }
export type OperateLogV2VO = {
id: number
userNickname: string
traceId: string
userType: number
userId: number
userName: string
type: string
subType: string
bizId: number
action: string
extra: string
requestMethod: string
requestUrl: string
userIp: string
userAgent: string
creator: string
creatorName: string
createTime: Date
// 数据扩展,渲染时使用
title: string // 操作标题(如果为空则取 name 值)
colSize: number // 变更记录行数
contentStrList: string[]
tagsContentList: string[]
}
// 查询操作日志列表 // 查询操作日志列表
export const getOperateLogPage = (params: PageParam) => { export const getOperateLogPage = (params: PageParam) => {
return request.get({ url: '/system/operate-log/page', params }) return request.get({ url: '/system/operate-log/page', params })

View File

@ -35,8 +35,6 @@ export const enum APP_LINK_TYPE_ENUM {
PRODUCT_DETAIL_NORMAL, PRODUCT_DETAIL_NORMAL,
// 拼团商品详情 // 拼团商品详情
PRODUCT_DETAIL_COMBINATION, PRODUCT_DETAIL_COMBINATION,
// 积分商品详情
PRODUCT_DETAIL_POINT,
// 秒杀商品详情 // 秒杀商品详情
PRODUCT_DETAIL_SECKILL PRODUCT_DETAIL_SECKILL
} }
@ -80,10 +78,6 @@ export const APP_LINK_GROUP_LIST = [
name: '系统设置', name: '系统设置',
path: '/pages/public/setting' path: '/pages/public/setting'
}, },
{
name: '问题反馈',
path: '/pages/public/feedback'
},
{ {
name: '常见问题', name: '常见问题',
path: '/pages/public/faq' path: '/pages/public/faq'
@ -112,11 +106,6 @@ export const APP_LINK_GROUP_LIST = [
name: '秒杀商品详情', name: '秒杀商品详情',
path: '/pages/goods/seckill', path: '/pages/goods/seckill',
type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_SECKILL type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_SECKILL
},
{
name: '积分商品详情',
path: '/pages/goods/score',
type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_POINT
} }
] ]
}, },
@ -145,10 +134,6 @@ export const APP_LINK_GROUP_LIST = [
name: '签到中心', name: '签到中心',
path: '/pages/app/sign' path: '/pages/app/sign'
}, },
{
name: '积分商城',
path: '/pages/app/score-shop'
},
{ {
name: '优惠券中心', name: '优惠券中心',
path: '/pages/coupon/list' path: '/pages/coupon/list'
@ -172,10 +157,6 @@ export const APP_LINK_GROUP_LIST = [
name: '分销中心', name: '分销中心',
path: '/pages/commission/index' path: '/pages/commission/index'
}, },
{
name: '申请分销商',
path: '/pages/commission/apply'
},
{ {
name: '推广商品', name: '推广商品',
path: '/pages/commission/goods' path: '/pages/commission/goods'
@ -184,10 +165,6 @@ export const APP_LINK_GROUP_LIST = [
name: '分销订单', name: '分销订单',
path: '/pages/commission/order' path: '/pages/commission/order'
}, },
{
name: '分享记录',
path: '/pages/commission/share-log'
},
{ {
name: '我的团队', name: '我的团队',
path: '/pages/commission/team' path: '/pages/commission/team'
@ -208,10 +185,6 @@ export const APP_LINK_GROUP_LIST = [
{ {
name: '申请提现', name: '申请提现',
path: '/pages/pay/withdraw' path: '/pages/pay/withdraw'
},
{
name: '提现记录',
path: '/pages/pay/withdraw-log'
} }
] ]
}, },
@ -242,10 +215,6 @@ export const APP_LINK_GROUP_LIST = [
name: '地址管理', name: '地址管理',
path: '/pages/user/address/list' path: '/pages/user/address/list'
}, },
{
name: '发票管理',
path: '/pages/user/invoice/list'
},
{ {
name: '用户佣金', name: '用户佣金',
path: '/pages/user/wallet/commission' path: '/pages/user/wallet/commission'

View File

@ -8,11 +8,11 @@
<component :is="component.id" :property="component.property" /> <component :is="component.id" :property="component.property" />
</div> </div>
<div class="component-wrap"> <div class="component-wrap">
<!-- 左侧组件名 --> <!-- 左侧组件名悬浮的小贴条 -->
<div class="component-name" v-if="component.name"> <div class="component-name" v-if="component.name">
{{ component.name }} {{ component.name }}
</div> </div>
<!-- 组件操作工具栏 --> <!-- 组件操作工具栏 -->
<div class="component-toolbar" v-if="showToolbar && component.name && active"> <div class="component-toolbar" v-if="showToolbar && component.name && active">
<VerticalButtonGroup type="primary"> <VerticalButtonGroup type="primary">
<el-tooltip content="上移" placement="right"> <el-tooltip content="上移" placement="right">
@ -54,7 +54,7 @@ import { propTypes } from '@/utils/propTypes'
import { object } from 'vue-types' import { object } from 'vue-types'
/** /**
* 组件容器 * 组件容器目前在中间部分
* 用于包裹组件为组件提供 背景外边距内边距边框等样式 * 用于包裹组件为组件提供 背景外边距内边距边框等样式
*/ */
defineOptions({ name: 'ComponentContainer' }) defineOptions({ name: 'ComponentContainer' })
@ -100,6 +100,7 @@ const emits = defineEmits<{
(e: 'copy'): void (e: 'copy'): void
(e: 'delete'): void (e: 'delete'): void
}>() }>()
/** /**
* 移动组件 * 移动组件
* @param direction 移动方向 * @param direction 移动方向
@ -107,12 +108,14 @@ const emits = defineEmits<{
const handleMoveComponent = (direction: number) => { const handleMoveComponent = (direction: number) => {
emits('move', direction) emits('move', direction)
} }
/** /**
* 复制组件 * 复制组件
*/ */
const handleCopyComponent = () => { const handleCopyComponent = () => {
emits('copy') emits('copy')
} }
/** /**
* 删除组件 * 删除组件
*/ */

View File

@ -1,8 +1,11 @@
<template> <template>
<el-tabs stretch> <el-tabs stretch>
<!-- 每个组件的自定义内容 -->
<el-tab-pane label="内容" v-if="$slots.default"> <el-tab-pane label="内容" v-if="$slots.default">
<slot></slot> <slot></slot>
</el-tab-pane> </el-tab-pane>
<!-- 每个组件的通用内容 -->
<el-tab-pane label="样式" lazy> <el-tab-pane label="样式" lazy>
<el-card header="组件样式" class="property-group"> <el-card header="组件样式" class="property-group">
<el-form :model="formData" label-width="80px"> <el-form :model="formData" label-width="80px">
@ -51,7 +54,7 @@
import { ComponentStyle, usePropertyForm } from '@/components/DiyEditor/util' import { ComponentStyle, usePropertyForm } from '@/components/DiyEditor/util'
/** /**
* 组件容器属性 * 组件容器属性目前右边部分
* 用于包裹组件为组件提供 背景外边距内边距边框等样式 * 用于包裹组件为组件提供 背景外边距内边距边框等样式
*/ */
defineOptions({ name: 'ComponentContainer' }) defineOptions({ name: 'ComponentContainer' })

View File

@ -41,16 +41,19 @@ import { componentConfigs } from '../components/mobile/index'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { DiyComponent, DiyComponentLibrary } from '@/components/DiyEditor/util' import { DiyComponent, DiyComponentLibrary } from '@/components/DiyEditor/util'
/** 组件库 */ /** 组件库:目前左侧的【基础组件】、【图文组件】部分 */
defineOptions({ name: 'ComponentLibrary' }) defineOptions({ name: 'ComponentLibrary' })
// //
const props = defineProps<{ const props = defineProps<{
list: DiyComponentLibrary[] list: DiyComponentLibrary[]
}>() }>()
//
const groups = reactive<any[]>([]) const groups = reactive<any[]>([])
// //
const extendGroups = reactive<string[]>([]) const extendGroups = reactive<string[]>([])
// list DiyComponentLibrary name
watch( watch(
() => props.list, () => props.list,
() => { () => {

View File

@ -4,6 +4,7 @@ import { floatToFixed2 } from '@/utils'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
// 优惠值 // 优惠值
// TODO @疯狂idea 有告警
export const CouponDiscount = defineComponent({ export const CouponDiscount = defineComponent({
name: 'CouponDiscount', name: 'CouponDiscount',
props: { props: {

View File

@ -24,6 +24,7 @@ export interface CouponCardProperty {
} }
// 定义组件 // 定义组件
// TODO @疯狂idea 有告警
export const component = { export const component = {
id: 'CouponCard', id: 'CouponCard',
name: '优惠券', name: '优惠券',

View File

@ -9,6 +9,7 @@ export interface HotZoneProperty {
// 组件样式 // 组件样式
style: ComponentStyle style: ComponentStyle
} }
/** 热区项目属性 */ /** 热区项目属性 */
export interface HotZoneItemProperty { export interface HotZoneItemProperty {
// 链接的名称 // 链接的名称

View File

@ -13,6 +13,7 @@ export interface MagicCubeProperty {
// 组件样式 // 组件样式
style: ComponentStyle style: ComponentStyle
} }
/** 广告魔方项目属性 */ /** 广告魔方项目属性 */
export interface MagicCubeItemProperty { export interface MagicCubeItemProperty {
// 图标链接 // 图标链接
@ -30,6 +31,7 @@ export interface MagicCubeItemProperty {
} }
// 定义组件 // 定义组件
// TODO @疯狂:有 idea 爆红告警
export const component = { export const component = {
id: 'MagicCube', id: 'MagicCube',
name: '广告魔方', name: '广告魔方',

View File

@ -10,6 +10,7 @@ export interface MenuGridProperty {
// 组件样式 // 组件样式
style: ComponentStyle style: ComponentStyle
} }
/** 宫格导航项目属性 */ /** 宫格导航项目属性 */
export interface MenuGridItemProperty { export interface MenuGridItemProperty {
// 图标链接 // 图标链接

View File

@ -8,6 +8,7 @@ export interface MenuListProperty {
// 组件样式 // 组件样式
style: ComponentStyle style: ComponentStyle
} }
/** 列表导航项目属性 */ /** 列表导航项目属性 */
export interface MenuListItemProperty { export interface MenuListItemProperty {
// 图标链接 // 图标链接

View File

@ -12,7 +12,6 @@
<el-form-item label="文字颜色" prop="文字颜色"> <el-form-item label="文字颜色" prop="文字颜色">
<ColorInput v-model="formData.textColor" /> <ColorInput v-model="formData.textColor" />
</el-form-item> </el-form-item>
<el-card header="公告内容" class="property-group" shadow="never"> <el-card header="公告内容" class="property-group" shadow="never">
<Draggable v-model="formData.contents"> <Draggable v-model="formData.contents">
<template #default="{ element }"> <template #default="{ element }">

View File

@ -59,6 +59,7 @@ export interface ProductCardFieldProperty {
} }
// 定义组件 // 定义组件
// TODO @疯狂idea 有告警
export const component = { export const component = {
id: 'ProductCard', id: 'ProductCard',
name: '商品卡片', name: '商品卡片',

View File

@ -134,6 +134,7 @@ watch(
deep: true deep: true
} }
) )
/** /**
* 计算商品的间距 * 计算商品的间距
* @param index 商品索引 * @param index 商品索引

View File

@ -38,6 +38,7 @@ export interface ProductListFieldProperty {
} }
// 定义组件 // 定义组件
// TODO @疯狂idea 有告警
export const component = { export const component = {
id: 'ProductList', id: 'ProductList',
name: '商品栏', name: '商品栏',

View File

@ -6,6 +6,7 @@ import { PromotionArticleProperty } from './config'
import * as ArticleApi from '@/api/mall/promotion/article/index' import * as ArticleApi from '@/api/mall/promotion/article/index'
/** 营销文章 */ /** 营销文章 */
// TODO @idea
defineOptions({ name: 'PromotionArticle' }) defineOptions({ name: 'PromotionArticle' })
// //
const props = defineProps<{ property: PromotionArticleProperty }>() const props = defineProps<{ property: PromotionArticleProperty }>()

View File

@ -29,6 +29,7 @@ export interface PromotionCombinationProperty {
// 组件样式 // 组件样式
style: ComponentStyle style: ComponentStyle
} }
// 商品字段 // 商品字段
export interface PromotionCombinationFieldProperty { export interface PromotionCombinationFieldProperty {
// 是否显示 // 是否显示
@ -38,6 +39,7 @@ export interface PromotionCombinationFieldProperty {
} }
// 定义组件 // 定义组件
// TODO @疯狂idea 有告警
export const component = { export const component = {
id: 'PromotionCombination', id: 'PromotionCombination',
name: '拼团', name: '拼团',

View File

@ -17,6 +17,7 @@ export interface SearchProperty {
export type PlaceholderPosition = 'left' | 'center' export type PlaceholderPosition = 'left' | 'center'
// 定义组件 // 定义组件
// TODO @疯狂idea 这里爆红可以卡看咋优化下哇is missing the following properties from type DiyComponent<SearchProperty>: uid, position
export const component = { export const component = {
id: 'SearchBar', id: 'SearchBar',
name: '搜索框', name: '搜索框',

View File

@ -19,6 +19,7 @@ export interface VideoPlayerStyle extends ComponentStyle {
} }
// 定义组件 // 定义组件
// TODO @疯狂idea 有告警
export const component = { export const component = {
id: 'VideoPlayer', id: 'VideoPlayer',
name: '视频播放', name: '视频播放',

View File

@ -27,11 +27,12 @@
</el-tooltip> </el-tooltip>
</el-button-group> </el-button-group>
</el-header> </el-header>
<!-- 中心区域 --> <!-- 中心区域 -->
<el-container class="editor-container"> <el-container class="editor-container">
<!-- 左侧组件库 --> <!-- 左侧组件库ComponentLibrary -->
<ComponentLibrary ref="componentLibrary" :list="libs" v-if="libs && libs.length > 0" /> <ComponentLibrary ref="componentLibrary" :list="libs" v-if="libs && libs.length > 0" />
<!-- 中心设计区域 --> <!-- 中心设计区域ComponentContainer -->
<div class="editor-center page-prop-area" @click="handlePageSelected"> <div class="editor-center page-prop-area" @click="handlePageSelected">
<!-- 手机顶部 --> <!-- 手机顶部 -->
<div class="editor-design-top"> <div class="editor-design-top">
@ -132,7 +133,7 @@
</template> </template>
</div> </div>
</div> </div>
<!-- 右侧属性面板 --> <!-- 右侧属性面板ComponentContainerProperty -->
<el-aside class="editor-right" width="350px" v-if="selectedComponent?.property"> <el-aside class="editor-right" width="350px" v-if="selectedComponent?.property">
<el-card <el-card
shadow="never" shadow="never"
@ -160,6 +161,7 @@
</el-aside> </el-aside>
</el-container> </el-container>
</el-container> </el-container>
<!-- 预览弹框 --> <!-- 预览弹框 -->
<Dialog v-model="previewDialogVisible" title="预览" width="700"> <Dialog v-model="previewDialogVisible" title="预览" width="700">
<div class="flex justify-around"> <div class="flex justify-around">
@ -231,6 +233,7 @@ const props = defineProps({
}) })
// //
// pageConfigComponent navigationBarComponentpageComponentstabBarComponent
watch( watch(
() => props.modelValue, () => props.modelValue,
() => { () => {
@ -251,6 +254,7 @@ watch(
immediate: true immediate: true
} }
) )
// //
const handleSave = () => { const handleSave = () => {
const pageConfig = { const pageConfig = {
@ -303,7 +307,7 @@ const handleTabBarSelected = () => {
handleComponentSelected(unref(tabBarComponent)) handleComponentSelected(unref(tabBarComponent))
} }
// //
const handleComponentChange = (dragEvent: any) => { const handleComponentChange = (dragEvent: any) => {
// //
if (dragEvent.added) { if (dragEvent.added) {
@ -327,19 +331,21 @@ const swapComponent = (oldIndex: number, newIndex: number) => {
selectedComponentIndex.value = newIndex selectedComponentIndex.value = newIndex
} }
/** 移动组件 */ /** 移动组件(上移、下移) */
const handleMoveComponent = (index: number, direction: number) => { const handleMoveComponent = (index: number, direction: number) => {
const newIndex = index + direction const newIndex = index + direction
if (newIndex < 0 || newIndex >= pageComponents.value.length) return if (newIndex < 0 || newIndex >= pageComponents.value.length) return
swapComponent(index, newIndex) swapComponent(index, newIndex)
} }
/** 复制组件 */ /** 复制组件 */
const handleCopyComponent = (index: number) => { const handleCopyComponent = (index: number) => {
const component = cloneDeep(pageComponents.value[index]) const component = cloneDeep(pageComponents.value[index])
component.uid = new Date().getTime() component.uid = new Date().getTime()
pageComponents.value.splice(index + 1, 0, component) pageComponents.value.splice(index + 1, 0, component)
} }
/** /**
* 删除组件 * 删除组件
* @param index 当前组件index * @param index 当前组件index
@ -371,6 +377,7 @@ const handleReset = () => {
if (reload) reload() if (reload) reload()
emits('reset') emits('reset')
} }
// //
const previewDialogVisible = ref(false) const previewDialogVisible = ref(false)
const handlePreview = () => { const handlePreview = () => {
@ -388,10 +395,12 @@ const setDefaultSelectedComponent = () => {
selectedComponent.value = unref(tabBarComponent) selectedComponent.value = unref(tabBarComponent)
} }
} }
watch( watch(
() => [props.showPageConfig, props.showNavigationBar, props.showTabBar], () => [props.showPageConfig, props.showNavigationBar, props.showTabBar],
() => setDefaultSelectedComponent() () => setDefaultSelectedComponent()
) )
onMounted(() => setDefaultSelectedComponent()) onMounted(() => setDefaultSelectedComponent())
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -103,6 +103,7 @@ export function usePropertyForm<T>(modelValue: T, emit: Function): { formData: R
} }
) )
// TODO @疯狂:这个 idea 爆红,看看怎么可以解决哈
return { formData } return { formData }
} }

View File

@ -0,0 +1,3 @@
import OperateLogV2 from './src/OperateLogV2.vue'
export { OperateLogV2 }

View File

@ -0,0 +1,175 @@
<template>
<div class="p-20px">
<el-timeline>
<el-timeline-item
v-for="(log, index) in logDataList"
:key="index"
:timestamp="formatDate(log.createTime)"
placement="top"
>
<div class="el-timeline-right-content">
<el-row>
<el-col :span="24" class="mb-10px">
=======================
<el-tag class="mr-10px" type="success">{{ log.userName }}</el-tag>
<span>{{ log.title }}</span>
=======================
</el-col>
<!-- 先处理一下有几行-->
<template v-for="colNum in log.colSize" :key="colNum + 'col'">
<el-col :span="24" class="mb-10px">
<!-- 处理每一行-->
<template
v-for="(tagVal, index2) in log.tagsContentList.slice(
(colNum - 1) * 3,
3 * colNum
)"
:key="index2"
>
<el-tag class="mx-10px"> {{ tagVal }}</el-tag>
<span>{{ log.contentStrList[index2] }}</span>
</template>
</el-col>
</template>
</el-row>
</div>
<template #dot>
<span :style="{ backgroundColor: getUserTypeColor(log.userType) }" class="dot-node-style">
{{ getDictLabel(DICT_TYPE.USER_TYPE, log.userType)[0] }}
</span>
</template>
</el-timeline-item>
</el-timeline>
</div>
</template>
<script lang="ts" setup>
import { OperateLogV2VO } from '@/api/system/operatelog'
import { formatDate } from '@/utils/formatTime'
import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict'
import { ElTag } from 'element-plus'
defineOptions({ name: 'OperateLogV2' })
const props = defineProps<{
logList: OperateLogV2VO[] //
}>()
const logDataList = ref<OperateLogV2VO[]>([]) //
/** 获得 userType 颜色 */
const getUserTypeColor = (type: number) => {
const dict = getDictObj(DICT_TYPE.USER_TYPE, type)
switch (dict?.colorType) {
case 'success':
return '#67C23A'
case 'info':
return '#909399'
case 'warning':
return '#E6A23C'
case 'danger':
return '#F56C6C'
}
return '#409EFF'
}
// tag
const renderTags = (content: string) => {
let newStr = unref(content).slice() //
newStr = newStr.replaceAll('【】', '【空】').replaceAll('', '') //
const regex = /【([^【】]+)】/g
const fg = '|' //
let match: any[] | null
let matchStr: string[] = []
let oldStr: string[] = []
while ((match = regex.exec(newStr)) !== null) {
matchStr.push(match[1]) //
oldStr.push(match[0]) //
}
// while match
oldStr.forEach((item) => {
newStr = newStr.replace(item, fg)
})
return [newStr.split(fg), matchStr]
}
const initLog = () => {
logDataList.value = props.logList.map((logItem) => {
const keyValue = renderTags(logItem.action)
//
logItem.contentStrList = keyValue[0]
if (keyValue[0][0] === '从') {
logItem.title = logItem.subType
} else {
logItem.title = keyValue[0][0]
logItem.contentStrList.splice(0, 1)
}
logItem.colSize = keyValue[0].length / 3 //
logItem.tagsContentList = keyValue[1]
return logItem
})
}
watch(
() => props.logList.length,
(newObj) => {
if (newObj) {
initLog()
console.log(logDataList.value)
}
},
{
immediate: true,
deep: true
}
)
</script>
<style lang="scss" scoped>
// 线
:deep(.el-timeline) {
margin: 10px 0 0 160px;
.el-timeline-item__wrapper {
position: relative;
top: -20px;
.el-timeline-item__timestamp {
position: absolute !important;
top: 10px;
left: -150px;
}
}
.el-timeline-right-content {
display: flex;
align-items: center;
min-height: 30px;
padding: 10px;
background-color: #fff;
&::before {
position: absolute;
top: 10px;
left: 13px; /* 将伪元素水平居中 */
border-color: transparent #fff transparent transparent; /* 尖角颜色,左侧朝向 */
border-style: solid;
border-width: 8px; /* 调整尖角大小 */
content: ''; /* 必须设置 content 属性 */
}
}
}
.dot-node-style {
position: absolute;
left: -5px;
display: flex;
width: 20px;
height: 20px;
font-size: 10px;
color: #fff;
border-radius: 50%;
justify-content: center;
align-items: center;
}
</style>

View File

@ -2,25 +2,27 @@
<div class="upload-file"> <div class="upload-file">
<el-upload <el-upload
ref="uploadRef" ref="uploadRef"
:multiple="props.limit > 1"
name="file"
v-model="valueRef"
v-model:file-list="fileList" v-model:file-list="fileList"
:show-file-list="true"
:auto-upload="autoUpload"
:action="updateUrl" :action="updateUrl"
:auto-upload="autoUpload"
:before-upload="beforeUpload"
:drag="drag"
:headers="uploadHeaders" :headers="uploadHeaders"
:limit="props.limit" :limit="props.limit"
:drag="drag" :multiple="props.limit > 1"
:before-upload="beforeUpload"
:on-exceed="handleExceed"
:on-success="handleFileSuccess"
:on-error="excelUploadError" :on-error="excelUploadError"
:on-remove="handleRemove" :on-exceed="handleExceed"
:on-preview="handlePreview" :on-preview="handlePreview"
:on-remove="handleRemove"
:on-success="handleFileSuccess"
:show-file-list="true"
class="upload-file-uploader" class="upload-file-uploader"
name="file"
> >
<el-button type="primary"><Icon icon="ep:upload-filled" />选取文件</el-button> <el-button type="primary">
<Icon icon="ep:upload-filled" />
选取文件
</el-button>
<template v-if="isShowTip" #tip> <template v-if="isShowTip" #tip>
<div style="font-size: 8px"> <div style="font-size: 8px">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
@ -35,8 +37,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
import { getAccessToken, getTenantId } from '@/utils/auth' import { getAccessToken, getTenantId } from '@/utils/auth'
import type { UploadInstance, UploadUserFile, UploadProps, UploadRawFile } from 'element-plus' import type { UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
import { isArray, isString } from '@/utils/is' import { isString } from '@/utils/is'
defineOptions({ name: 'UploadFile' }) defineOptions({ name: 'UploadFile' })
@ -54,8 +56,8 @@ const props = defineProps({
drag: propTypes.bool.def(false), // drag: propTypes.bool.def(false), //
isShowTip: propTypes.bool.def(true) // isShowTip: propTypes.bool.def(true) //
}) })
// ========== ========== // ========== ==========
const valueRef = ref(props.modelValue)
const uploadRef = ref<UploadInstance>() const uploadRef = ref<UploadInstance>()
const uploadList = ref<UploadUserFile[]>([]) const uploadList = ref<UploadUserFile[]>([])
const fileList = ref<UploadUserFile[]>([]) const fileList = ref<UploadUserFile[]>([])
@ -64,6 +66,7 @@ const uploadHeaders = ref({
Authorization: 'Bearer ' + getAccessToken(), Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId() 'tenant-id': getTenantId()
}) })
// //
const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => { const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
if (fileList.value.length >= props.limit) { if (fileList.value.length >= props.limit) {
@ -97,12 +100,10 @@ const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
// //
const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => { const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
message.success('上传成功') message.success('上传成功')
const fileListNew = fileList.value fileList.value.shift()
fileListNew.pop()
fileList.value = fileListNew
uploadList.value.push({ name: res.data, url: res.data }) uploadList.value.push({ name: res.data, url: res.data })
if (uploadList.value.length == uploadNumber.value) { if (uploadList.value.length == uploadNumber.value) {
fileList.value = fileList.value.concat(uploadList.value) fileList.value.push(...uploadList.value)
uploadList.value = [] uploadList.value = []
uploadNumber.value = 0 uploadNumber.value = 0
emitUpdateModelValue() emitUpdateModelValue()
@ -131,29 +132,25 @@ const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
// //
watch( watch(
() => props.modelValue, () => props.modelValue,
() => { (val: string | string[]) => {
const files: string[] = [] if (!val) {
// 1 fileList.value = [] // fix
if (isString(props.modelValue)) { return
// 1.1
if (props.modelValue.includes(',')) {
files.concat(props.modelValue.split(','))
} else if (props.modelValue.length > 0) {
files.push(props.modelValue)
}
} else if (isArray(props.modelValue)) {
// 2
files.concat(props.modelValue)
} else if (props.modelValue == null) {
// 3undefined
} else {
throw new Error('不支持的 modelValue 类型')
} }
fileList.value = files.map((url: string) => {
return { url, name: url.substring(url.lastIndexOf('/') + 1) } as UploadUserFile fileList.value = [] //
}) // 1
if (isString(val)) {
fileList.value.push(
...val.split(',').map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
)
}
// 2
fileList.value.push(
...(val as string[]).map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
)
}, },
{ immediate: true } { immediate: true, deep: true }
) )
// //
const emitUpdateModelValue = () => { const emitUpdateModelValue = () => {
@ -166,7 +163,7 @@ const emitUpdateModelValue = () => {
emit('update:modelValue', result) emit('update:modelValue', result)
} }
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
.upload-file-uploader { .upload-file-uploader {
margin-bottom: 5px; margin-bottom: 5px;
} }

View File

@ -88,7 +88,7 @@ service.interceptors.request.use(
// response 拦截器 // response 拦截器
service.interceptors.response.use( service.interceptors.response.use(
async (response: AxiosResponse<any>) => { async (response: AxiosResponse<any>) => {
const { data } = response let { data } = response
const config = response.config const config = response.config
if (!data) { if (!data) {
// 返回“[HTTP]请求没有返回值”; // 返回“[HTTP]请求没有返回值”;
@ -96,14 +96,18 @@ service.interceptors.response.use(
} }
const { t } = useI18n() const { t } = useI18n()
// 未设置状态码则默认成功状态 // 未设置状态码则默认成功状态
const code = data.code || result_code // 二进制数据则直接返回,例如说 Excel 导出
// 二进制数据则直接返回
if ( if (
response.request.responseType === 'blob' || response.request.responseType === 'blob' ||
response.request.responseType === 'arraybuffer' response.request.responseType === 'arraybuffer'
) { ) {
return response.data // 注意:如果导出的响应为 json说明可能失败了不直接返回进行下载
if (response.data.type !== 'application/json') {
return response.data
}
data = await new Response(response.data).json()
} }
const code = data.code || result_code
// 获取错误信息 // 获取错误信息
const msg = data.msg || errorCode[code] || errorCode['default'] const msg = data.msg || errorCode[code] || errorCode['default']
if (ignoreMsgs.indexOf(msg) !== -1) { if (ignoreMsgs.indexOf(msg) !== -1) {

View File

@ -472,7 +472,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
meta: { meta: {
title: '模板装修', title: '模板装修',
noCache: true, noCache: true,
hidden: true hidden: true,
// TODO @疯狂:建议 menu 那的 /mall/promotion/diy-template/diy-template 改成 /mall/promotion/diy/template
activeMenu: '/mall/promotion/diy-template/diy-template'
}, },
component: () => import('@/views/mall/promotion/diy/template/decorate.vue') component: () => import('@/views/mall/promotion/diy/template/decorate.vue')
}, },
@ -482,7 +484,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
meta: { meta: {
title: '页面装修', title: '页面装修',
noCache: true, noCache: true,
hidden: true hidden: true,
// TODO @疯狂:建议 menu 那的 /mall/promotion/diy-template/diy-page 改成 /mall/promotion/diy/page
activeMenu: '/mall/promotion/diy-template/diy-page'
}, },
component: () => import('@/views/mall/promotion/diy/page/decorate.vue') component: () => import('@/views/mall/promotion/diy/page/decorate.vue')
} }

View File

@ -1,13 +1,13 @@
<template> <template>
<el-form <el-form
ref="formRef" ref="formRef"
v-loading="formLoading"
:model="formData" :model="formData"
:rules="formRules" :rules="formRules"
label-width="80px" label-width="80px"
v-loading="formLoading"
> >
<el-form-item label="请假类型" prop="type"> <el-form-item label="请假类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择请假类型" clearable> <el-select v-model="formData.type" clearable placeholder="请选择请假类型">
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)" v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
:key="dict.value" :key="dict.value"
@ -18,27 +18,27 @@
</el-form-item> </el-form-item>
<el-form-item label="开始时间" prop="startTime"> <el-form-item label="开始时间" prop="startTime">
<el-date-picker <el-date-picker
clearable
v-model="formData.startTime" v-model="formData.startTime"
clearable
placeholder="请选择开始时间"
type="datetime" type="datetime"
value-format="x" value-format="x"
placeholder="请选择开始时间"
/> />
</el-form-item> </el-form-item>
<el-form-item label="结束时间" prop="endTime"> <el-form-item label="结束时间" prop="endTime">
<el-date-picker <el-date-picker
clearable
v-model="formData.endTime" v-model="formData.endTime"
clearable
placeholder="请选择结束时间"
type="datetime" type="datetime"
value-format="x" value-format="x"
placeholder="请选择结束时间"
/> />
</el-form-item> </el-form-item>
<el-form-item label="原因" prop="reason"> <el-form-item label="原因" prop="reason">
<el-input v-model="formData.reason" type="textarea" placeholder="请输请假原因" /> <el-input v-model="formData.reason" placeholder="请输请假原因" type="textarea" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button> <el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
@ -50,9 +50,6 @@ import { useTagsViewStore } from '@/store/modules/tagsView'
defineOptions({ name: 'BpmOALeaveCreate' }) defineOptions({ name: 'BpmOALeaveCreate' })
const message = useMessage() // const message = useMessage() //
const { delView } = useTagsViewStore() //
const { currentRoute } = useRouter() //
const formLoading = ref(false) // 12 const formLoading = ref(false) // 12
const formData = ref({ const formData = ref({
type: undefined, type: undefined,
@ -67,7 +64,8 @@ const formRules = reactive({
endTime: [{ required: true, message: '请假结束时间不能为空', trigger: 'change' }] endTime: [{ required: true, message: '请假结束时间不能为空', trigger: 'change' }]
}) })
const formRef = ref() // Ref const formRef = ref() // Ref
const { delView } = useTagsViewStore() //
const { push, currentRoute } = useRouter() //
/** 提交表单 */ /** 提交表单 */
const submitForm = async () => { const submitForm = async () => {
// //
@ -82,6 +80,7 @@ const submitForm = async () => {
message.success('发起成功') message.success('发起成功')
// Tab // Tab
delView(unref(currentRoute)) delView(unref(currentRoute))
await push({ name: 'BpmOALeave' })
} finally { } finally {
formLoading.value = false formLoading.value = false
} }

View File

@ -2,18 +2,18 @@
<ContentWrap> <ContentWrap>
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<el-form <el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef" ref="queryFormRef"
:inline="true" :inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px" label-width="68px"
> >
<el-form-item label="请假类型" prop="type"> <el-form-item label="请假类型" prop="type">
<el-select <el-select
v-model="queryParams.type" v-model="queryParams.type"
placeholder="请选择请假类型"
clearable
class="!w-240px" class="!w-240px"
clearable
placeholder="请选择请假类型"
> >
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)" v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
@ -26,16 +26,16 @@
<el-form-item label="申请时间" prop="createTime"> <el-form-item label="申请时间" prop="createTime">
<el-date-picker <el-date-picker
v-model="queryParams.createTime" v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px" class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/> />
</el-form-item> </el-form-item>
<el-form-item label="结果" prop="result"> <el-form-item label="结果" prop="result">
<el-select v-model="queryParams.result" placeholder="请选择结果" clearable class="!w-240px"> <el-select v-model="queryParams.result" class="!w-240px" clearable placeholder="请选择结果">
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)" v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)"
:key="dict.value" :key="dict.value"
@ -47,17 +47,24 @@
<el-form-item label="原因" prop="reason"> <el-form-item label="原因" prop="reason">
<el-input <el-input
v-model="queryParams.reason" v-model="queryParams.reason"
placeholder="请输入原因"
clearable
@keyup.enter="handleQuery"
class="!w-240px" class="!w-240px"
clearable
placeholder="请输入原因"
@keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <el-button @click="handleQuery">
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <Icon class="mr-5px" icon="ep:search" />
<el-button type="primary" plain @click="handleCreate()"> 搜索
<Icon icon="ep:plus" class="mr-5px" /> 发起请假 </el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button plain type="primary" @click="handleCreate()">
<Icon class="mr-5px" icon="ep:plus" />
发起请假
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -66,63 +73,63 @@
<!-- 列表 --> <!-- 列表 -->
<ContentWrap> <ContentWrap>
<el-table v-loading="loading" :data="list"> <el-table v-loading="loading" :data="list">
<el-table-column label="申请编号" align="center" prop="id" /> <el-table-column align="center" label="申请编号" prop="id" />
<el-table-column label="状态" align="center" prop="result"> <el-table-column align="center" label="状态" prop="result">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" /> <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
label="开始时间" :formatter="dateFormatter"
align="center" align="center"
label="开始时间"
prop="startTime" prop="startTime"
width="180" width="180"
:formatter="dateFormatter"
/> />
<el-table-column <el-table-column
label="结束时间" :formatter="dateFormatter"
align="center" align="center"
label="结束时间"
prop="endTime" prop="endTime"
width="180" width="180"
:formatter="dateFormatter"
/> />
<el-table-column label="请假类型" align="center" prop="type"> <el-table-column align="center" label="请假类型" prop="type">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="scope.row.type" /> <dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="scope.row.type" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="原因" align="center" prop="reason" /> <el-table-column align="center" label="原因" prop="reason" />
<el-table-column <el-table-column
label="申请时间" :formatter="dateFormatter"
align="center" align="center"
label="申请时间"
prop="createTime" prop="createTime"
width="180" width="180"
:formatter="dateFormatter"
/> />
<el-table-column label="操作" align="center" width="200"> <el-table-column align="center" label="操作" width="200">
<template #default="scope"> <template #default="scope">
<el-button <el-button
v-hasPermi="['bpm:oa-leave:query']"
link link
type="primary" type="primary"
@click="handleDetail(scope.row)" @click="handleDetail(scope.row)"
v-hasPermi="['bpm:oa-leave:query']"
> >
详情 详情
</el-button> </el-button>
<el-button <el-button
v-hasPermi="['bpm:oa-leave:query']"
link link
type="primary" type="primary"
@click="handleProcessDetail(scope.row)" @click="handleProcessDetail(scope.row)"
v-hasPermi="['bpm:oa-leave:query']"
> >
进度 进度
</el-button> </el-button>
<el-button <el-button
v-if="scope.row.result === 1"
v-hasPermi="['bpm:oa-leave:create']"
link link
type="danger" type="danger"
@click="cancelLeave(scope.row)" @click="cancelLeave(scope.row)"
v-hasPermi="['bpm:oa-leave:create']"
v-if="scope.row.result === 1"
> >
取消 取消
</el-button> </el-button>
@ -131,9 +138,9 @@
</el-table> </el-table>
<!-- 分页 --> <!-- 分页 -->
<Pagination <Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize" v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList" @pagination="getList"
/> />
</ContentWrap> </ContentWrap>
@ -228,6 +235,14 @@ const handleProcessDetail = (row) => {
}) })
} }
// fix:
watch(
() => router.currentRoute.value,
() => {
getList()
}
)
/** 初始化 **/ /** 初始化 **/
onMounted(() => { onMounted(() => {
getList() getList()

View File

@ -62,12 +62,12 @@
</el-row> </el-row>
</el-popover> </el-popover>
</el-form-item> </el-form-item>
<!-- TODO @ljlleoidea 红色的报错可以解决下 -->
<el-form-item label="商机状态类型" prop="statusTypeId"> <el-form-item label="商机状态类型" prop="statusTypeId">
<el-select <el-select
v-model="formData.statusTypeId" v-model="formData.statusTypeId"
placeholder="请选择商机状态类型" placeholder="请选择商机状态类型"
clearable clearable
size="small"
@change="changeBusinessStatusType" @change="changeBusinessStatusType"
> >
<el-option <el-option
@ -79,7 +79,7 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="商机状态" prop="statusId"> <el-form-item label="商机状态" prop="statusId">
<el-select v-model="formData.statusId" placeholder="请选择商机状态" clearable size="small"> <el-select v-model="formData.statusId" placeholder="请选择商机状态" clearable>
<el-option <el-option
v-for="item in businessStatusList" v-for="item in businessStatusList"
:key="item.id" :key="item.id"
@ -242,7 +242,8 @@ const queryParams = reactive({
mobile: null, mobile: null,
industryId: null, industryId: null,
level: null, level: null,
source: null source: null,
pool: false
}) })
// //
const showCustomer = ref(false) const showCustomer = ref(false)
@ -251,12 +252,12 @@ const openCustomerSelect = () => {
queryParams.pageNo = 1 queryParams.pageNo = 1
getCustomerList() getCustomerList()
} }
/** 查询客户列表 */ /** 查询客户列表 */
const getCustomerList = async () => { const getCustomerList = async () => {
loading.value = true loading.value = true
try { try {
const data = await CustomerApi.getCustomerPage(queryParams) const data = await CustomerApi.getCustomerPage(queryParams)
console.log(JSON.stringify(data))
customerList.value = data.list customerList.value = data.list
total.value = data.total total.value = data.total
} finally { } finally {

View File

@ -5,11 +5,32 @@
<Icon class="mr-5px" icon="ep:opportunity" /> <Icon class="mr-5px" icon="ep:opportunity" />
创建商机 创建商机
</el-button> </el-button>
<el-button
@click="openBusinessModal"
v-hasPermi="['crm:contact:create-business']"
v-if="queryParams.contactId"
>
<Icon class="mr-5px" icon="ep:circle-plus" />关联
</el-button>
<el-button
@click="deleteContactBusinessList"
v-hasPermi="['crm:contact:delete-business']"
v-if="queryParams.contactId"
>
<Icon class="mr-5px" icon="ep:remove" />解除关联
</el-button>
</el-row> </el-row>
<!-- 列表 --> <!-- 列表 -->
<ContentWrap class="mt-10px"> <ContentWrap class="mt-10px">
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> <el-table
ref="businessRef"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
>
<el-table-column type="selection" width="55" v-if="queryParams.contactId" />
<el-table-column label="商机名称" fixed="left" align="center" prop="name"> <el-table-column label="商机名称" fixed="left" align="center" prop="name">
<template #default="scope"> <template #default="scope">
<el-link type="primary" :underline="false" @click="openDetail(scope.row.id)"> <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
@ -33,17 +54,28 @@
<!-- 表单弹窗添加 --> <!-- 表单弹窗添加 -->
<BusinessForm ref="formRef" @success="getList" /> <BusinessForm ref="formRef" @success="getList" />
<!-- 关联商机选择弹框 -->
<BusinessListModal
ref="businessModalRef"
:customer-id="props.customerId"
@success="createContactBusinessList"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as BusinessApi from '@/api/crm/business' import * as BusinessApi from '@/api/crm/business'
import * as ContactApi from '@/api/crm/contact'
import BusinessForm from './../BusinessForm.vue' import BusinessForm from './../BusinessForm.vue'
import { BizTypeEnum } from '@/api/crm/permission' import { BizTypeEnum } from '@/api/crm/permission'
import { fenToYuanFormat } from '@/utils/formatter' import { fenToYuanFormat } from '@/utils/formatter'
import BusinessListModal from './BusinessListModal.vue'
const message = useMessage() //
defineOptions({ name: 'CrmBusinessList' }) defineOptions({ name: 'CrmBusinessList' })
const props = defineProps<{ const props = defineProps<{
bizType: number // bizType: number //
bizId: number // bizId: number //
customerId?: number // customerId
}>() }>()
const loading = ref(true) // const loading = ref(true) //
@ -52,7 +84,8 @@ const list = ref([]) // 列表的数据
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
customerId: undefined as unknown // undefined + number customerId: undefined as unknown, // undefined + number
contactId: undefined as unknown // undefined + number
}) })
/** 查询列表 */ /** 查询列表 */
@ -61,6 +94,7 @@ const getList = async () => {
try { try {
// //
queryParams.customerId = undefined queryParams.customerId = undefined
queryParams.contactId = undefined
// //
let data = { list: [], total: 0 } let data = { list: [], total: 0 }
switch (props.bizType) { switch (props.bizType) {
@ -68,6 +102,10 @@ const getList = async () => {
queryParams.customerId = props.bizId queryParams.customerId = props.bizId
data = await BusinessApi.getBusinessPageByCustomer(queryParams) data = await BusinessApi.getBusinessPageByCustomer(queryParams)
break break
case BizTypeEnum.CRM_CONTACT:
queryParams.contactId = props.bizId
data = await BusinessApi.getBusinessPageByContact(queryParams)
break
default: default:
return return
} }
@ -96,6 +134,41 @@ const openDetail = (id: number) => {
push({ name: 'CrmBusinessDetail', params: { id } }) push({ name: 'CrmBusinessDetail', params: { id } })
} }
/** 打开联系人与商机的关联弹窗 */
const businessModalRef = ref()
const openBusinessModal = () => {
businessModalRef.value.open()
}
const createContactBusinessList = async (businessIds: number[]) => {
const data = {
contactId: props.bizId,
businessIds: businessIds
} as ContactApi.ContactBusinessReqVO
businessRef.value.getSelectionRows().forEach((row: BusinessApi.BusinessVO) => {
data.businessIds.push(row.id)
})
await ContactApi.createContactBusinessList(data)
//
message.success('关联商机成功')
handleQuery()
}
/** 解除联系人与商机的关联 */
const businessRef = ref()
const deleteContactBusinessList = async () => {
const data = {
contactId: props.bizId,
businessIds: businessRef.value.getSelectionRows().map((row: BusinessApi.BusinessVO) => row.id)
} as ContactApi.ContactBusinessReqVO
if (data.businessIds.length === 0) {
return message.error('未选择商机')
}
await ContactApi.deleteContactBusinessList(data)
//
message.success('取关商机成功')
handleQuery()
}
/** 监听打开的 bizId + bizType从而加载最新的列表 */ /** 监听打开的 bizId + bizType从而加载最新的列表 */
watch( watch(
() => [props.bizId, props.bizType], () => [props.bizId, props.bizType],

View File

@ -1,5 +1,5 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible"> <Dialog title="关联商机" v-model="dialogVisible">
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<el-form <el-form
@ -29,7 +29,6 @@
</ContentWrap> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
<!-- TODO @zyna字段按照他们对齐下 -->
<ContentWrap class="mt-10px"> <ContentWrap class="mt-10px">
<el-table <el-table
v-loading="loading" v-loading="loading"
@ -75,16 +74,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as BusinessApi from '@/api/crm/business' import * as BusinessApi from '@/api/crm/business'
import BusinessForm from '../../business/BusinessForm.vue' import BusinessForm from '../BusinessForm.vue'
import { fenToYuanFormat } from '@/utils/formatter' import { fenToYuanFormat } from '@/utils/formatter'
// TODO @zyna
import * as ContactbusinesslinkApi from '@/api/crm/contactbusinesslink'
const message = useMessage() //
defineOptions({ name: 'CrmBusinessLinkContactList' }) const message = useMessage() //
const props = defineProps<{
customerId: number
}>()
defineOptions({ name: 'BusinessListModal' })
const dialogVisible = ref(false) // const dialogVisible = ref(false) //
const dialogTitle = ref('') // TODO @zyna
const loading = ref(true) // const loading = ref(true) //
const total = ref(0) // const total = ref(0) //
const list = ref([]) // const list = ref([]) //
@ -93,17 +92,14 @@ const formLoading = ref(false) // 表单的加载中1修改时的数据加
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
// TODO @zyna customerId name: undefined,
name: undefined customerId: props.customerId
}) })
const contactIdProp = ref(0) //
/** 打开弹窗 */ /** 打开弹窗 */
const open = async (contactId: number) => { const open = async () => {
dialogVisible.value = true dialogVisible.value = true
contactIdProp.value = contactId await getList()
// TODO @zyna await idea
getList()
} }
defineExpose({ open }) // open defineExpose({ open }) // open
@ -111,7 +107,7 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
const data = await BusinessApi.getBusinessPage(queryParams) const data = await BusinessApi.getBusinessPageByCustomer(queryParams)
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
} finally { } finally {
@ -141,25 +137,14 @@ const openForm = () => {
const emit = defineEmits(['success']) // success const emit = defineEmits(['success']) // success
const businessRef = ref() const businessRef = ref()
const submitForm = async () => { const submitForm = async () => {
// TODO @zyna if return const businessIds = businessRef.value
if (businessRef.value.getSelectionRows().length === 0) { .getSelectionRows()
message.success('未选择商机') .map((row: BusinessApi.BusinessVO) => row.id)
} else { if (businessIds.length === 0) {
// TODO @zyna postData ref return message.error('未选择商机')
const postData = ref<ContactbusinesslinkApi.ContactBusinessLinkVO[]>([])
businessRef.value.getSelectionRows().forEach((element) => {
// TODO @zyna push data
let data = {
id: undefined,
businessId: element.id,
contactId: contactIdProp.value
}
postData.value.push(data)
})
await ContactbusinesslinkApi.createContactBusinessLinkBatch(postData.value)
dialogVisible.value = false
emit('success')
} }
dialogVisible.value = false
emit('success', businessIds)
} }
/** 打开联系人详情 */ /** 打开联系人详情 */

View File

@ -49,8 +49,8 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12" <el-col :span="12">
><el-form-item label="性别" prop="sex"> <el-form-item label="性别" prop="sex">
<el-select v-model="formData.sex" placeholder="请选择"> <el-select v-model="formData.sex" placeholder="请选择">
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)" v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
@ -58,8 +58,9 @@
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
/> />
</el-select> </el-form-item </el-select>
></el-col> </el-form-item>
</el-col>
</el-row> </el-row>
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
@ -72,8 +73,8 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="座机" prop="telephone"> <el-form-item label="电话" prop="telephone">
<el-input v-model="formData.telephone" placeholder="请输入座机" style="width: 215px" /> <el-input v-model="formData.telephone" placeholder="请输入电话" style="width: 215px" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -104,10 +105,10 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="下次联系时间" prop="nextTime"> <el-form-item label="下次联系时间" prop="contactNextTime">
<el-date-picker <el-date-picker
v-model="formData.nextTime" v-model="formData.contactNextTime"
type="date" type="datetime"
value-format="x" value-format="x"
placeholder="选择下次联系时间" placeholder="选择下次联系时间"
/> />
@ -126,15 +127,17 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="地址" prop="address"> <el-form-item label="地址" prop="detailAddress">
<el-input <el-input
input-style="width:190px;" input-style="width:190px;"
v-model="formData.address" v-model="formData.detailAddress"
placeholder="请输入地址" placeholder="请输入地址"
/> />
</el-form-item> </el-form-item>
</el-col> </el-row </el-col>
><el-row> </el-row>
<!-- TODO @zyna解决下 ide 报错 -->
<el-row>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="直属上级" prop="parentId"> <el-form-item label="直属上级" prop="parentId">
<el-select v-model="formData.parentId" placeholder="请选择"> <el-select v-model="formData.parentId" placeholder="请选择">
@ -152,10 +155,11 @@
<el-form-item label="职位" prop="post"> <el-form-item label="职位" prop="post">
<el-input input-style="width:190px;" v-model="formData.post" placeholder="请输入职位" /> <el-input input-style="width:190px;" v-model="formData.post" placeholder="请输入职位" />
</el-form-item> </el-form-item>
</el-col> </el-row </el-col>
><el-row> </el-row>
<el-col :span="12" <el-row>
><el-form-item label="是否关键决策人" prop="master" style="width: 400px"> <el-col :span="12">
<el-form-item label="关键决策人" prop="master" style="width: 400px">
<el-radio-group v-model="formData.master"> <el-radio-group v-model="formData.master">
<el-radio <el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)" v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
@ -169,8 +173,8 @@
</el-col> </el-col>
</el-row> </el-row>
<el-row> <el-row>
<el-col :span="24" <el-col :span="24">
><el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" /> <el-input v-model="formData.remark" placeholder="请输入备注" />
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -192,19 +196,20 @@ import { defaultProps } from '@/utils/tree'
const { t } = useI18n() // const { t } = useI18n() //
const message = useMessage() // const message = useMessage() //
const dialogVisible = ref(false) // const dialogVisible = ref(false) //
const dialogTitle = ref('') // const dialogTitle = ref('') //
const formLoading = ref(false) // 12 const formLoading = ref(false) // 12
const formType = ref('') // create - update - const formType = ref('') // create - update -
const areaList = ref([]) // const areaList = ref([]) //
const formData = ref({ const formData = ref({
nextTime: undefined, contactNextTime: undefined,
mobile: undefined, mobile: undefined,
telephone: undefined, telephone: undefined,
email: undefined, email: undefined,
customerId: undefined, customerId: undefined,
customerName: undefined, customerName: undefined,
address: undefined, detailAddress: undefined,
remark: undefined, remark: undefined,
ownerUserId: undefined, ownerUserId: undefined,
lastTime: undefined, lastTime: undefined,
@ -279,13 +284,14 @@ const submitForm = async () => {
/** 重置表单 */ /** 重置表单 */
const resetForm = () => { const resetForm = () => {
// TODO zynaide
formData.value = { formData.value = {
nextTime: undefined, contactNextTime: undefined,
mobile: undefined, mobile: undefined,
telephone: undefined, telephone: undefined,
email: undefined, email: undefined,
customerId: undefined, customerId: undefined,
address: undefined, detailAddress: undefined,
remark: undefined, remark: undefined,
ownerUserId: undefined, ownerUserId: undefined,
lastTime: undefined, lastTime: undefined,

View File

@ -1,14 +1,5 @@
<!--
* @Author: zyna
* @Date: 2023-12-02 13:08:57
* @LastEditTime: 2023-12-03 13:47:16
* @FilePath: \yudao-ui-admin-vue3\src\views\crm\contact\detail\ContactDetailsHeader.vue
* @Description:
-->
<!-- TODO @zyna上面这个不加哈 -->
<template> <template>
<!-- TODO @zynaloading 缺了 --> <div>
<div v-loading="loading">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div> <div>
<el-col> <el-col>
@ -48,7 +39,6 @@
import * as ContactApi from '@/api/crm/contact' import * as ContactApi from '@/api/crm/contact'
import ContactForm from '@/views/crm/contact/ContactForm.vue' import ContactForm from '@/views/crm/contact/ContactForm.vue'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
// //
const formRef = ref() const formRef = ref()
const openForm = (type: string, id?: number) => { const openForm = (type: string, id?: number) => {

View File

@ -1,4 +1,5 @@
<template> <template>
<!-- TODO @zyna少了一个外边框 -->
<el-collapse v-model="activeNames"> <el-collapse v-model="activeNames">
<el-collapse-item name="basicInfo"> <el-collapse-item name="basicInfo">
<template #title> <template #title>

View File

@ -10,18 +10,22 @@
<PermissionList :biz-id="contact.id!" :biz-type="BizTypeEnum.CRM_CONTACT" /> <PermissionList :biz-id="contact.id!" :biz-type="BizTypeEnum.CRM_CONTACT" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="商机" lazy> <el-tab-pane label="商机" lazy>
<BusinessList :biz-id="contact.id!" :biz-type="BizTypeEnum.CRM_CONTACT" /> <BusinessList
:biz-id="contact.id!"
:biz-type="BizTypeEnum.CRM_CONTACT"
:customer-id="contact.customerId"
/>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</el-col> </el-col>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus' // TODO @zyna使 hook message
import { useTagsViewStore } from '@/store/modules/tagsView' import { useTagsViewStore } from '@/store/modules/tagsView'
import * as ContactApi from '@/api/crm/contact' import * as ContactApi from '@/api/crm/contact'
import ContactDetailsHeader from '@/views/crm/contact/detail/ContactDetailsHeader.vue' import ContactDetailsHeader from '@/views/crm/contact/detail/ContactDetailsHeader.vue'
import ContactDetailsInfo from '@/views/crm/contact/detail/ContactDetailsInfo.vue' import ContactDetailsInfo from '@/views/crm/contact/detail/ContactDetailsInfo.vue'
import BusinessList from '@/views/crm/contactBusinessLink/components/BusinessListByContact.vue' // import BusinessList from '@/views/crm/business/components/BusinessList.vue' //
import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // import PermissionList from '@/views/crm/permission/components/PermissionList.vue' //
import { BizTypeEnum } from '@/api/crm/permission' import { BizTypeEnum } from '@/api/crm/permission'

View File

@ -11,6 +11,7 @@
<el-form-item label="客户" prop="customerId"> <el-form-item label="客户" prop="customerId">
<el-select <el-select
v-model="queryParams.customerId" v-model="queryParams.customerId"
class="!w-240px"
placeholder="请选择客户" placeholder="请选择客户"
value-key="id" value-key="id"
lable-key="name" lable-key="name"
@ -28,56 +29,55 @@
<el-form-item label="姓名" prop="name"> <el-form-item label="姓名" prop="name">
<el-input <el-input
v-model="queryParams.name" v-model="queryParams.name"
class="!w-240px"
placeholder="请输入姓名" placeholder="请输入姓名"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="手机号" prop="mobile"> <el-form-item label="手机号" prop="mobile">
<el-input <el-input
v-model="queryParams.mobile" v-model="queryParams.mobile"
class="!w-240px"
placeholder="请输入手机号" placeholder="请输入手机号"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="座机" prop="telephone"> <el-form-item label="电话" prop="telephone">
<el-input <el-input
v-model="queryParams.telephone" v-model="queryParams.telephone"
class="!w-240px"
placeholder="请输入电话" placeholder="请输入电话"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="QQ" prop="qq"> <el-form-item label="QQ" prop="qq">
<el-input <el-input
v-model="queryParams.qq" v-model="queryParams.qq"
class="!w-240px"
placeholder="请输入QQ" placeholder="请输入QQ"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="微信" prop="wechat"> <el-form-item label="微信" prop="wechat">
<el-input <el-input
v-model="queryParams.wechat" v-model="queryParams.wechat"
class="!w-240px"
placeholder="请输入微信" placeholder="请输入微信"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="电子邮箱" prop="email"> <el-form-item label="电子邮箱" prop="email">
<el-input <el-input
v-model="queryParams.email" v-model="queryParams.email"
class="!w-240px"
placeholder="请输入电子邮箱" placeholder="请输入电子邮箱"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@ -102,53 +102,57 @@
<!-- 列表 --> <!-- 列表 -->
<ContentWrap> <ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="姓名" fixed="left" align="center" prop="name"> <el-table-column label="姓名" fixed="left" align="center" prop="name" width="140">
<template #default="scope"> <template #default="scope">
<el-link type="primary" :underline="false" @click="openDetail(scope.row.id)"> <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
{{ scope.row.name }} {{ scope.row.name }}
</el-link> </el-link>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="客户" fixed="left" align="center" prop="customerName" /> <el-table-column label="客户名称" fixed="left" align="center" prop="customerName" width="120">
<template #default="scope">
<el-link
type="primary"
:underline="false"
@click="openCustomerDetail(scope.row.customerId)"
>
{{ scope.row.customerName }}
</el-link>
</template>
</el-table-column>
<el-table-column label="手机" align="center" prop="mobile" width="120" />
<el-table-column label="电话" align="center" prop="telephone" width="120" />
<el-table-column label="邮箱" align="center" prop="email" width="120" />
<el-table-column label="职位" align="center" prop="post" width="120" />
<el-table-column label="地址" align="center" prop="detailAddress" width="120" />
<el-table-column
:formatter="dateFormatter"
align="center"
label="下次联系时间"
prop="contactNextTime"
width="180px"
/>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="关键决策人" align="center" prop="master" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
</template>
</el-table-column>
<el-table-column label="直属上级" align="center" prop="parentName" width="140" />
<el-table-column
label="最后跟进时间"
align="center"
prop="contactLastTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="性别" align="center" prop="sex"> <el-table-column label="性别" align="center" prop="sex">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" /> <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="职位" align="center" prop="post" /> <el-table-column label="负责人" align="center" prop="ownerUserName" width="120" />
<el-table-column label="是否关键决策人" align="center" prop="master"> <el-table-column label="创建人" align="center" prop="creatorName" width="120" />
<template #default="scope">
<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="wechat" />
<el-table-column label="邮箱" align="center" prop="email" />
<el-table-column label="地址" align="center" prop="address" />
<el-table-column
label="下次联系时间"
align="center"
prop="nextTime"
width="180px"
:formatter="dateFormatter"
/>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column
label="最后跟进时间"
align="center"
prop="lastTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="负责人" align="center" prop="ownerUserId">
<template #default="scope">
{{ scope.row.ownerUserName }}
</template>
</el-table-column>
<!-- <el-table-column label="所属部门" align="center" prop="ownerUserId" /> -->
<el-table-column <el-table-column
label="更新时间" label="更新时间"
align="center" align="center"
@ -163,21 +167,10 @@
:formatter="dateFormatter" :formatter="dateFormatter"
width="180px" width="180px"
/> />
<!-- <el-table-column
label="创建人"
align="center"
prop="creator"
:formatter="dateFormatter"
width="180px"
>
<template #default="scope">
{{ userList.find((user) => user.id === scope.row.creator)?.nickname }}
</template>
</el-table-column> -->
<el-table-column label="操作" align="center" fixed="right" width="200"> <el-table-column label="操作" align="center" fixed="right" width="200">
<template #default="scope"> <template #default="scope">
<el-button <el-button
plain link
type="primary" type="primary"
@click="openForm('update', scope.row.id)" @click="openForm('update', scope.row.id)"
v-hasPermi="['crm:contact:update']" v-hasPermi="['crm:contact:update']"
@ -185,7 +178,7 @@
编辑 编辑
</el-button> </el-button>
<el-button <el-button
plain link
type="danger" type="danger"
@click="handleDelete(scope.row.id)" @click="handleDelete(scope.row.id)"
v-hasPermi="['crm:contact:delete']" v-hasPermi="['crm:contact:delete']"
@ -228,23 +221,13 @@ const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
nextTime: [], mobile: undefined,
mobile: null, telephone: undefined,
telephone: null, email: undefined,
email: null, customerId: undefined,
customerId: null, name: undefined,
address: null, qq: undefined,
remark: null, wechat: undefined
ownerUserId: null,
createTime: [],
lastTime: [],
parentId: null,
name: null,
post: null,
qq: null,
wechat: null,
sex: null,
policyMakers: null
}) })
const queryFormRef = ref() // const queryFormRef = ref() //
const exportLoading = ref(false) // const exportLoading = ref(false) //
@ -307,11 +290,15 @@ const handleExport = async () => {
} }
} }
/** 打开客户详情 */ /** 打开联系人详情 */
const { push } = useRouter() const { push } = useRouter()
const openDetail = (id: number) => { const openDetail = (id: number) => {
push({ name: 'CrmContactDetail', params: { id } }) push({ name: 'CrmContactDetail', params: { id } })
} }
/** 打开客户详情 */
const openCustomerDetail = (id: number) => {
push({ name: 'CrmCustomerDetail', params: { id } })
}
/** 初始化 **/ /** 初始化 **/
onMounted(async () => { onMounted(async () => {

View File

@ -1,143 +0,0 @@
<template>
<!-- 操作栏 -->
<el-row justify="end">
<el-button @click="openForm">
<Icon class="mr-5px" icon="ep:opportunity" />
创建商机
</el-button>
<el-button @click="openBusinessLink"> 关联 </el-button>
<el-button @click="deleteBusinessLink"> 解除关联 </el-button>
</el-row>
<!-- 列表 -->
<ContentWrap class="mt-10px">
<el-table
v-loading="loading"
ref="businessRef"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
>
<el-table-column type="selection" width="55" />
<el-table-column label="商机名称" fixed="left" align="center" prop="name">
<template #default="scope">
<el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
{{ scope.row.name }}
</el-link>
</template>
</el-table-column>
<el-table-column label="商机金额" align="center" prop="price" :formatter="fenToYuanFormat" />
<el-table-column label="客户名称" align="center" prop="customerName" />
<el-table-column label="商机组" align="center" prop="statusTypeName" />
<el-table-column label="商机阶段" align="center" prop="statusName" />
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加 -->
<BusinessForm ref="formRef" @success="getList" />
<!---->
<BusinessLink ref="businessLinkRef" @success="getList" />
</template>
<script setup lang="ts">
import * as ContactBusinessLinkApi from '@/api/crm/contactbusinesslink'
import BusinessForm from '../../business/BusinessForm.vue'
import { BizTypeEnum } from '@/api/crm/permission'
import { fenToYuanFormat } from '@/utils/formatter'
import BusinessLink from './BusinessLinkContactList.vue'
const message = useMessage() //
// TODO @zyna BusinessList
defineOptions({ name: 'CrmBusinessContactList' })
const props = defineProps<{
bizType: number //
bizId: number //
}>()
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
contactId: undefined as unknown // undefined + number
})
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
//
queryParams.contactId = undefined
//
let data = { list: [], total: 0 }
switch (props.bizType) {
case BizTypeEnum.CRM_CONTACT:
queryParams.contactId = props.bizId
data = await ContactBusinessLinkApi.getBusinessByContactPage(queryParams)
break
default:
return
}
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 添加操作 */
const formRef = ref()
const openForm = () => {
formRef.value.open('create')
}
/** 关联操作 */
const businessLinkRef = ref()
const openBusinessLink = () => {
businessLinkRef.value.open(props.bizId)
}
/**解除关联 */
const businessRef = ref()
const deleteBusinessLink = async () => {
if (businessRef.value.getSelectionRows().length === 0) {
message.success('未选择商机')
} else {
const postData = ref<ContactBusinessLinkApi.ContactBusinessLinkVO[]>([])
businessRef.value.getSelectionRows().forEach((element) => {
let data = {
id: undefined,
businessId: element.id,
contactId: props.bizId
}
postData.value.push(data)
})
await ContactBusinessLinkApi.deleteContactBusinessLink(postData.value)
handleQuery()
}
}
/** 打开联系人详情 */
const { push } = useRouter()
const openDetail = (id: number) => {
push({ name: 'CrmBusinessDetail', params: { id } })
}
/** 监听打开的 bizId + bizType从而加载最新的列表 */
watch(
() => [props.bizId, props.bizType],
() => {
handleQuery()
},
{ immediate: true, deep: true }
)
</script>

View File

@ -1,11 +1,11 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible"> <Dialog v-model="dialogVisible" :title="dialogTitle">
<el-form <el-form
ref="formRef" ref="formRef"
v-loading="formLoading"
:model="formData" :model="formData"
:rules="formRules" :rules="formRules"
label-width="100px" label-width="100px"
v-loading="formLoading"
> >
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
@ -17,7 +17,7 @@
<el-form-item label="所属行业" prop="industryId"> <el-form-item label="所属行业" prop="industryId">
<el-select v-model="formData.industryId" placeholder="请选择所属行业"> <el-select v-model="formData.industryId" placeholder="请选择所属行业">
<el-option <el-option
v-for="dict in getStrDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)" v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
:key="dict.value" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
@ -31,7 +31,7 @@
<el-form-item label="客户来源" prop="source"> <el-form-item label="客户来源" prop="source">
<el-select v-model="formData.source" placeholder="请选择客户来源"> <el-select v-model="formData.source" placeholder="请选择客户来源">
<el-option <el-option
v-for="dict in getStrDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)" v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
:key="dict.value" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
@ -43,7 +43,7 @@
<el-form-item label="客户等级" prop="level"> <el-form-item label="客户等级" prop="level">
<el-select v-model="formData.level" placeholder="请选择客户等级"> <el-select v-model="formData.level" placeholder="请选择客户等级">
<el-option <el-option
v-for="dict in getStrDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)" v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
:key="dict.value" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
@ -120,9 +120,9 @@
<el-form-item label="下次联系时间" prop="contactNextTime"> <el-form-item label="下次联系时间" prop="contactNextTime">
<el-date-picker <el-date-picker
v-model="formData.contactNextTime" v-model="formData.contactNextTime"
type="date"
value-format="x"
placeholder="选择下次联系时间" placeholder="选择下次联系时间"
type="datetime"
value-format="x"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -139,13 +139,13 @@
</el-col> </el-col>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button> <el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false"> </el-button>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script lang="ts" setup>
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as CustomerApi from '@/api/crm/customer' import * as CustomerApi from '@/api/crm/customer'
import * as AreaApi from '@/api/system/area' import * as AreaApi from '@/api/system/area'
import { defaultProps } from '@/utils/tree' import { defaultProps } from '@/utils/tree'

View File

@ -11,10 +11,19 @@
</div> </div>
<div> <div>
<!-- 右上按钮 --> <!-- 右上按钮 -->
<el-button v-hasPermi="['crm:customer:update']" @click="openForm(customer.id)"> <el-button
type="primary"
v-hasPermi="['crm:customer:update']"
@click="openForm(customer.id)"
>
编辑 编辑
</el-button> </el-button>
<!-- TODO @puhui999转移的操作接入 -->
<el-button type="primary" @click="transfer">转移</el-button>
<!-- TODO @puhui999修改成交状态的接入 -->
<el-button>更改成交状态</el-button> <el-button>更改成交状态</el-button>
<el-button v-if="customer.lockStatus" @click="handleUnlock(customer.id!)">解锁</el-button>
<el-button v-else @click="handleLock(customer.id!)">锁定</el-button>
</div> </div>
</div> </div>
</div> </div>
@ -26,26 +35,29 @@
<el-descriptions-item label="成交状态"> <el-descriptions-item label="成交状态">
{{ customer.dealStatus ? '已成交' : '未成交' }} {{ customer.dealStatus ? '已成交' : '未成交' }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="负责人">{{ customer.ownerUserName }} </el-descriptions-item> <el-descriptions-item label="负责人">{{ customer.ownerUserName }}</el-descriptions-item>
<!-- TODO wanwan 首要联系人? --> <!-- TODO wanwan 首要联系人? -->
<el-descriptions-item label="首要联系人" /> <el-descriptions-item label="首要联系人" />
<!-- TODO wanwan 首要联系人电话? --> <!-- TODO wanwan 首要联系人电话? -->
<el-descriptions-item label="首要联系人电话">{{ customer.mobile }} </el-descriptions-item> <el-descriptions-item label="首要联系人电话">{{ customer.mobile }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</ContentWrap> </ContentWrap>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<CustomerForm ref="formRef" @success="emit('refresh')" /> <CustomerForm ref="formRef" @success="emit('refresh')" />
</template> </template>
<script setup lang="ts"> <script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict' import { DICT_TYPE } from '@/utils/dict'
import * as CustomerApi from '@/api/crm/customer' import * as CustomerApi from '@/api/crm/customer'
import CustomerForm from '../CustomerForm.vue' import CustomerForm from '../CustomerForm.vue'
defineOptions({ name: 'CustomerDetailsHeader' })
const { customer, loading } = defineProps<{ const { customer, loading } = defineProps<{
customer: CustomerApi.CustomerVO // customer: CustomerApi.CustomerVO //
loading: boolean // loading: boolean //
}>() }>()
const message = useMessage() //
/** 修改操作 */ /** 修改操作 */
const formRef = ref() const formRef = ref()
@ -53,5 +65,20 @@ const openForm = (id?: number) => {
formRef.value.open('update', id) formRef.value.open('update', id)
} }
/** 锁定操作 */
const handleLock = async (id: number) => {
await CustomerApi.lockCustomer(id, true)
message.success('锁定成功')
emit('refresh')
}
/** 解锁操作 */
const handleUnlock = async (id: number) => {
console.log(customer, '=======')
await CustomerApi.lockCustomer(id, false)
message.success('解锁成功')
emit('refresh')
}
const emit = defineEmits(['refresh']) // success const emit = defineEmits(['refresh']) // success
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<ContentWrap> <ContentWrap>
<el-collapse class="" v-model="activeNames"> <el-collapse v-model="activeNames" class="">
<el-collapse-item name="basicInfo"> <el-collapse-item name="basicInfo">
<template #title> <template #title>
<span class="text-base font-bold">基本信息</span> <span class="text-base font-bold">基本信息</span>
@ -20,11 +20,11 @@
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="手机">{{ customer.mobile }}</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.telephone }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ customer.email }} </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="QQ">{{ customer.qq }}</el-descriptions-item>
<el-descriptions-item label="微信">{{ customer.wechat }} </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.website }}</el-descriptions-item>
<el-descriptions-item label="所在地">{{ customer.areaName }} </el-descriptions-item> <el-descriptions-item label="所在地">{{ customer.areaName }}</el-descriptions-item>
<el-descriptions-item label="详细地址" <el-descriptions-item label="详细地址"
>{{ customer.detailAddress }} >{{ customer.detailAddress }}
</el-descriptions-item> </el-descriptions-item>
@ -38,8 +38,8 @@
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
<el-descriptions :column="1"> <el-descriptions :column="1">
<el-descriptions-item label="客户描述">{{ customer.description }} </el-descriptions-item> <el-descriptions-item label="客户描述">{{ customer.description }}</el-descriptions-item>
<el-descriptions-item label="备注">{{ customer.remark }} </el-descriptions-item> <el-descriptions-item label="备注">{{ customer.remark }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-collapse-item> </el-collapse-item>
<el-collapse-item name="systemInfo"> <el-collapse-item name="systemInfo">
@ -47,8 +47,8 @@
<span class="text-base font-bold">系统信息</span> <span class="text-base font-bold">系统信息</span>
</template> </template>
<el-descriptions :column="2"> <el-descriptions :column="2">
<el-descriptions-item label="负责人">{{ customer.ownerUserName }} </el-descriptions-item> <el-descriptions-item label="负责人">{{ customer.ownerUserName }}</el-descriptions-item>
<el-descriptions-item label="创建人">{{ customer.creatorName }} </el-descriptions-item> <el-descriptions-item label="创建人">{{ customer.creatorName }}</el-descriptions-item>
<el-descriptions-item label="创建时间"> <el-descriptions-item label="创建时间">
{{ customer.createTime ? formatDate(customer.createTime) : '空' }} {{ customer.createTime ? formatDate(customer.createTime) : '空' }}
</el-descriptions-item> </el-descriptions-item>
@ -60,15 +60,16 @@
</el-collapse> </el-collapse>
</ContentWrap> </ContentWrap>
</template> </template>
<script setup lang="ts"> <script lang="ts" setup>
import * as CustomerApi from '@/api/crm/customer' import * as CustomerApi from '@/api/crm/customer'
import { DICT_TYPE } from '@/utils/dict' import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
defineOptions({ name: 'CustomerDetailsInfo' })
const { customer } = defineProps<{ const { customer } = defineProps<{
customer: CustomerApi.CustomerVO // customer: CustomerApi.CustomerVO //
}>() }>()
const activeNames = ref(['basicInfo', 'systemInfo']) // const activeNames = ref(['basicInfo', 'systemInfo']) //
</script> </script>
<style scoped lang="scss"></style> <style lang="scss" scoped></style>

View File

@ -5,7 +5,9 @@
<el-tab-pane label="详细资料"> <el-tab-pane label="详细资料">
<CustomerDetailsInfo :customer="customer" /> <CustomerDetailsInfo :customer="customer" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="操作日志" lazy>TODO 待开发</el-tab-pane> <el-tab-pane label="操作日志">
<OperateLogV2 :log-list="logList" />
</el-tab-pane>
<el-tab-pane label="联系人" lazy> <el-tab-pane label="联系人" lazy>
<ContactList :biz-id="customer.id!" :biz-type="BizTypeEnum.CRM_CUSTOMER" /> <ContactList :biz-id="customer.id!" :biz-type="BizTypeEnum.CRM_CUSTOMER" />
</el-tab-pane> </el-tab-pane>
@ -38,24 +40,39 @@ import ReceivableList from '@/views/crm/receivable/components/ReceivableList.vue
import ReceivablePlanList from '@/views/crm/receivable/plan/components/ReceivablePlanList.vue' // import ReceivablePlanList from '@/views/crm/receivable/plan/components/ReceivablePlanList.vue' //
import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // import PermissionList from '@/views/crm/permission/components/PermissionList.vue' //
import { BizTypeEnum } from '@/api/crm/permission' import { BizTypeEnum } from '@/api/crm/permission'
import { OperateLogV2VO } from '@/api/system/operatelog'
defineOptions({ name: 'CrmCustomerDetail' }) defineOptions({ name: 'CrmCustomerDetail' })
const route = useRoute() const route = useRoute()
const id = Number(route.params.id) // const id = Number(route.params.id) //
const loading = ref(true) // const loading = ref(true) //
/** 获取详情 */ /** 获取详情 */
const customer = ref<CustomerApi.CustomerVO>({} as CustomerApi.CustomerVO) // const customer = ref<CustomerApi.CustomerVO>({} as CustomerApi.CustomerVO) //
const getCustomer = async (id: number) => { const getCustomer = async (id: number) => {
loading.value = true loading.value = true
try { try {
customer.value = await CustomerApi.getCustomer(id) customer.value = await CustomerApi.getCustomer(id)
await getOperateLog(id)
} finally { } finally {
loading.value = false loading.value = false
} }
} }
const logList = ref<OperateLogV2VO[]>([]) //
/**
* 获取操作日志
*/
const getOperateLog = async (customerId: number) => {
if (!customerId) {
return
}
const data = await CustomerApi.getOperateLogPage({
pageNo: 1,
pageSize: 10,
bizId: customerId
})
logList.value = data.list
}
/** 初始化 */ /** 初始化 */
const { delView } = useTagsViewStore() // const { delView } = useTagsViewStore() //
const { currentRoute } = useRouter() // const { currentRoute } = useRouter() //

View File

@ -101,38 +101,24 @@
</el-link> </el-link>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="所属行业" prop="industryId" width="120"> <el-table-column align="center" label="手机" prop="mobile" width="120" />
<template #default="scope"> <el-table-column align="center" label="电话" prop="telephone" width="120" />
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
</template>
</el-table-column>
<el-table-column align="center" label="客户来源" prop="source" width="100"> <el-table-column align="center" label="客户来源" prop="source" width="100">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" /> <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
</template> </template>
</el-table-column> </el-table-column>
<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 align="center" label="客户等级" prop="level" width="120"> <el-table-column align="center" label="客户等级" prop="level" width="120">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" /> <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="手机" prop="mobile" width="120" /> <el-table-column align="center" label="网址" prop="website" width="200" />
<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="ownerUserDeptName" />
<el-table-column align="center" label="创建人" prop="creatorName" />
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180px"
/>
<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 <el-table-column
:formatter="dateFormatter" :formatter="dateFormatter"
align="center" align="center"
@ -140,6 +126,13 @@
prop="contactNextTime" prop="contactNextTime"
width="180px" width="180px"
/> />
<el-table-column align="center" label="备注" prop="remark" width="200" />
<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>
<!-- TODO @puhui999距进入公海天数 -->
<el-table-column <el-table-column
:formatter="dateFormatter" :formatter="dateFormatter"
align="center" align="center"
@ -147,12 +140,23 @@
prop="contactLastTime" prop="contactLastTime"
width="180px" width="180px"
/> />
<el-table-column align="center" label="锁定状态" prop="lockStatus"> <el-table-column
<template #default="scope"> :formatter="dateFormatter"
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.lockStatus" /> align="center"
</template> label="创建时间"
</el-table-column> prop="updateTime"
<!-- TODO @wanwan 距进入公海天数 --> width="180px"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180px"
/>
<el-table-column align="center" label="负责人" prop="ownerUserName" width="100px" />
<el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100px" />
<el-table-column align="center" label="创建人" prop="creatorName" width="100px" />
<el-table-column align="center" fixed="right" label="操作" min-width="150"> <el-table-column align="center" fixed="right" label="操作" min-width="150">
<template #default="scope"> <template #default="scope">
<el-button <el-button

View File

@ -56,12 +56,12 @@
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as CustomerLimitConfigApi from '@/api/crm/customerLimitConfig' import * as CustomerLimitConfigApi from '@/api/crm/customer/limitConfig'
import * as DeptApi from '@/api/system/dept' import * as DeptApi from '@/api/system/dept'
import { defaultProps, handleTree } from '@/utils/tree' import { defaultProps, handleTree } from '@/utils/tree'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { LimitConfType } from '@/api/crm/customerLimitConfig' import { LimitConfType } from '@/api/crm/customer/limitConfig'
const { t } = useI18n() // const { t } = useI18n() //
const message = useMessage() // const message = useMessage() //
@ -72,7 +72,7 @@ const formLoading = ref(false) // 表单的加载中1修改时的数据加
const formType = ref('') // create - update - const formType = ref('') // create - update -
const formData = ref({ const formData = ref({
id: undefined, id: undefined,
type: undefined, type: LimitConfType.CUSTOMER_LOCK_LIMIT, // IDE
userIds: undefined, userIds: undefined,
deptIds: undefined, deptIds: undefined,
maxCount: undefined, maxCount: undefined,
@ -88,15 +88,11 @@ const deptTree = ref() // 部门树形结构
const userTree = ref() // const userTree = ref() //
/** 打开弹窗 */ /** 打开弹窗 */
const open = async (type: string, id?: number, limitConfType?: LimitConfType) => { const open = async (type: string, limitConfType: LimitConfType, id?: number) => {
dialogVisible.value = true dialogVisible.value = true
dialogTitle.value = t('action.' + type) dialogTitle.value = t('action.' + type)
formType.value = type formType.value = type
resetForm() resetForm()
//
await getDeptTree()
//
await getUserTree()
// //
if (id) { if (id) {
formLoading.value = true formLoading.value = true
@ -108,6 +104,10 @@ const open = async (type: string, id?: number, limitConfType?: LimitConfType) =>
} else { } else {
formData.value.type = limitConfType formData.value.type = limitConfType
} }
//
await getDeptTree()
//
await getUserTree()
} }
defineExpose({ open }) // open defineExpose({ open }) // open
@ -141,7 +141,7 @@ const submitForm = async () => {
const resetForm = () => { const resetForm = () => {
formData.value = { formData.value = {
id: undefined, id: undefined,
type: undefined, type: LimitConfType.CUSTOMER_LOCK_LIMIT,
userIds: undefined, userIds: undefined,
deptIds: undefined, deptIds: undefined,
maxCount: undefined, maxCount: undefined,

View File

@ -1,7 +1,5 @@
<template> <template>
<el-button type="primary" plain @click="handleQuery"> <el-button plain @click="handleQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 刷新 </el-button>
<Icon icon="ep:refresh" class="mr-5px" /> 刷新
</el-button>
<el-button <el-button
type="primary" type="primary"
plain plain
@ -87,10 +85,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import * as CustomerLimitConfigApi from '@/api/crm/customerLimitConfig' import * as CustomerLimitConfigApi from '@/api/crm/customer/limitConfig'
import CustomerLimitConfigForm from '@/views/crm/config/customerLimitConfig/CustomerLimitConfigForm.vue' import CustomerLimitConfigForm from './CustomerLimitConfigForm.vue'
import { DICT_TYPE } from '@/utils/dict' import { DICT_TYPE } from '@/utils/dict'
import { LimitConfType } from '@/api/crm/customerLimitConfig' import { LimitConfType } from '@/api/crm/customer/limitConfig'
defineOptions({ name: 'CustomerLimitConfigList' }) defineOptions({ name: 'CustomerLimitConfigList' })
@ -107,8 +105,6 @@ const queryParams = reactive({
pageSize: 10, pageSize: 10,
type: confType type: confType
}) })
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
@ -125,7 +121,7 @@ const getList = async () => {
/** 添加/修改操作 */ /** 添加/修改操作 */
const formRef = ref() const formRef = ref()
const openForm = (type: string, id?: number) => { const openForm = (type: string, id?: number) => {
formRef.value.open(type, id, confType) formRef.value.open(type, confType, id)
} }
/** 删除按钮操作 */ /** 删除按钮操作 */

View File

@ -12,8 +12,8 @@
</ContentWrap> </ContentWrap>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import CustomerLimitConfigList from '@/views/crm/config/customerLimitConfig/CustomerLimitConfigList.vue' import CustomerLimitConfigList from './CustomerLimitConfigList.vue'
import { LimitConfType } from '@/api/crm/customerLimitConfig' import { LimitConfType } from '@/api/crm/customer/limitConfig'
defineOptions({ name: 'CrmCustomerLimitConfig' }) defineOptions({ name: 'CrmCustomerLimitConfig' })
</script> </script>

View File

@ -56,7 +56,7 @@
</ContentWrap> </ContentWrap>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as CustomerPoolConfigApi from '@/api/crm/customerPoolConfig' import * as CustomerPoolConfigApi from '@/api/crm/customer/poolConfig'
import { CardTitle } from '@/components/Card' import { CardTitle } from '@/components/Card'
defineOptions({ name: 'CrmCustomerPoolConfig' }) defineOptions({ name: 'CrmCustomerPoolConfig' })
@ -67,10 +67,10 @@ const { t } = useI18n() // 国际化
const formLoading = ref(false) const formLoading = ref(false)
const formData = ref({ const formData = ref({
enabled: false, enabled: false,
contactExpireDays: 0, contactExpireDays: undefined,
dealExpireDays: 0, dealExpireDays: undefined,
notifyEnabled: false, notifyEnabled: false,
notifyDays: 0 notifyDays: undefined
}) })
const formRules = reactive({ const formRules = reactive({
enabled: [{ required: true, message: '是否启用客户公海不能为空', trigger: 'blur' }] enabled: [{ required: true, message: '是否启用客户公海不能为空', trigger: 'blur' }]
@ -100,7 +100,7 @@ const onSubmit = async () => {
// //
formLoading.value = true formLoading.value = true
try { try {
const data = formData.value as unknown as CustomerPoolConfigApi.CustomerPoolConfigVO const data = formData.value as CustomerPoolConfigApi.CustomerPoolConfigVO
await CustomerPoolConfigApi.saveCustomerPoolConfig(data) await CustomerPoolConfigApi.saveCustomerPoolConfig(data)
message.success(t('common.updateSuccess')) message.success(t('common.updateSuccess'))
await getConfig() await getConfig()

View File

@ -17,6 +17,7 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<!-- TODO @puhui999编辑时level 没带过来 -->
<el-form-item label="权限级别" prop="level"> <el-form-item label="权限级别" prop="level">
<el-radio-group v-model="formData.level"> <el-radio-group v-model="formData.level">
<template <template

View File

@ -24,6 +24,7 @@
:stripe="true" :stripe="true"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
> >
<!-- TODO @puhui999负责人不允许选中 -->
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
<el-table-column align="center" label="姓名" prop="nickname" /> <el-table-column align="center" label="姓名" prop="nickname" />
<el-table-column align="center" label="部门" prop="deptName" /> <el-table-column align="center" label="部门" prop="deptName" />

View File

@ -197,7 +197,7 @@ defineOptions({ name: 'ProductSpuBasicInfoForm' })
const ruleConfig: RuleConfig[] = [ const ruleConfig: RuleConfig[] = [
{ {
name: 'stock', name: 'stock',
rule: (arg) => arg >= 1, rule: (arg) => arg >= 0,
message: '商品库存必须大于等于 1 ' message: '商品库存必须大于等于 1 '
}, },
{ {
@ -213,7 +213,7 @@ const ruleConfig: RuleConfig[] = [
{ {
name: 'costPrice', name: 'costPrice',
rule: (arg) => arg >= 0.01, rule: (arg) => arg >= 0.01,
message: '商品成本价格必须大于等于 0.01 元!!!' message: '商品成本价格必须大于等于 0.00 元!!!'
} }
] ]

View File

@ -30,6 +30,7 @@ const getPageDetail = async (id: any) => {
formLoading.value = false formLoading.value = false
} }
} }
// //
const submitForm = async () => { const submitForm = async () => {
// //

View File

@ -27,10 +27,11 @@
</DiyEditor> </DiyEditor>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// TODO @ decorate index.vue
import * as DiyTemplateApi from '@/api/mall/promotion/diy/template' import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
import * as DiyPageApi from '@/api/mall/promotion/diy/page' import * as DiyPageApi from '@/api/mall/promotion/diy/page'
import { useTagsViewStore } from '@/store/modules/tagsView' import { useTagsViewStore } from '@/store/modules/tagsView'
import { DiyComponentLibrary, PAGE_LIBS } from '@/components/DiyEditor/util' import { DiyComponentLibrary, PAGE_LIBS } from '@/components/DiyEditor/util' // DIY DiyEditor
import { toNumber } from 'lodash-es' import { toNumber } from 'lodash-es'
/** 装修模板表单 */ /** 装修模板表单 */
@ -51,7 +52,7 @@ const formData = ref<DiyTemplateApi.DiyTemplatePropertyVO>()
const formRef = ref() // Ref const formRef = ref() // Ref
// //
const currentFormData = ref<DiyTemplateApi.DiyTemplatePropertyVO | DiyPageApi.DiyPageVO>() const currentFormData = ref<DiyTemplateApi.DiyTemplatePropertyVO | DiyPageApi.DiyPageVO>()
// H5 // H5
const previewUrl = ref('') const previewUrl = ref('')
// //

View File

@ -205,7 +205,7 @@ const handleUse = async (row: DiyTemplateApi.DiyTemplateVO) => {
// 使 // 使
await message.confirm(`是否使用模板“${row.name}”?`) await message.confirm(`是否使用模板“${row.name}”?`)
// //
await DiyTemplateApi.useDiyTemplate(row.id) await DiyTemplateApi.useDiyTemplate(row.id!)
message.success('使用成功') message.success('使用成功')
// //
await getList() await getList()

View File

@ -0,0 +1,113 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="文件名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入文件名称" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="文件内容" prop="content">
<Editor v-model="formData.content" height="150px" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</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 { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import * as UReportDataApi from '@/api/report/ureport'
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,
name: undefined,
status: undefined,
content: undefined,
remark: undefined,
})
const formRules = reactive({
name: [{ required: true, message: '文件名称不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await UReportDataApi.getUReportData(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as UReportDataApi.UReportDataVO
if (formType.value === 'create') {
await UReportDataApi.createUReportData(data)
message.success(t('common.createSuccess'))
} else {
await UReportDataApi.updateUReportData(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
status: undefined,
content: undefined,
remark: undefined,
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,13 @@
<template>
<ContentWrap>
<IFrame :src="src" />
</ContentWrap>
</template>
<script lang="ts" setup>
import { getAccessToken } from '@/utils/auth'
defineOptions({ name: 'UReportData' })
const BASE_URL = import.meta.env.VITE_BASE_URL
const src = ref(BASE_URL + '/ureport/designer?token=' + getAccessToken())
</script>

View File

@ -0,0 +1,220 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="文件名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入文件名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="queryParams.remark"
placeholder="请输入备注"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</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"
plain
@click="openForm('create')"
v-hasPermi="['report:ureport-data:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['report:ureport-data:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="ID" align="center" prop="id" />
<el-table-column label="文件名称" align="center" prop="name" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="文件内容" align="center" prop="content" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['report:ureport-data:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['report:ureport-data:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<UReportDataForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as UReportDataApi from '@/api/report/ureport'
import UReportDataForm from './UReportDataForm.vue'
defineOptions({ name: 'UReportData' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const list = ref([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: null,
status: null,
remark: null,
createTime: [],
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await UReportDataApi.getUReportDataPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await UReportDataApi.deleteUReportData(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await UReportDataApi.exportUReportData(queryParams)
download.excel(data, 'Ureport2报表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>