当前位置: 首页 > news >正文

使用ant-design-vue 写个适用于web端的条件搜索栏组件,可折叠展开

效果图:

我的想法还是和上一个封装的table组件一样,只通过一个配置文件就控制整个搜索栏

这个组件是超过两行会显示折叠按钮

 思路也很简单,就是将不同的筛选字段的形式都封装到一起,包括文本输入框、单选、多选、时间选择器、radio/checkbox、联级选择器等等,此外还允许通过插槽进行自定义格式。

像选择器,他需要list选项,但各个选项所用到的value和label有可能是不一样的字段,这些我想的是将这个需要获取数据的搜索字段都写到一个方法里面去,然后对这些数据进行格式化,全都格式化为统一的数据结构形式,然后将这个方法再传递给组件,这样就可以实现渲染出来的组件是完全功能的组件了。

注意:这个组件封装是使用的ant-design组件库

组件代码:

其中使用的引用会放在下面

<script setup lang="ts">
import PageTitle from '@/components/Public/PageTitle.vue' // 标题组件
import { computed, nextTick, onActivated, onMounted, reactive, ref } from 'vue'
import { compareAndUpdateArraysName, getSearchFieldList, isInputType, queryArea } from '@/utils/utils.js' // 这是在需要扩展字段和自定义展示搜索字段时候用到的
import Tooltip from '@/components/Tooltip/Tooltip.vue' // 这是动态计算并超出指定宽度自动显示省略号的组件,主要是给label用的
import enums from '@/utils/enums.js'
import RangePicker from '@/components/Public/RangePicker.vue' // 时间选择器组件
import FoldSearch from '@/components/Public/FoldSearch.vue' // 折叠和展开组件const props = defineProps({fields: { // 暂时没用到type: Array,default: () => [],},params: { // 外部参数,type: Object,default: () => ({}),},title: { // 标题type: String,default: '',},extendFieldList: { // 扩展字段type: Array,default: () => [],},dateType: { // 暂时不用type: String,default: 'date',},module: { // 自定义搜索字段模块名,一般都用不到type: String,default: '',},fetchParentData: {type: Function,},isResize: {type: Boolean,default: false,},isArea: {type: Boolean,default: false,},isPlaceholder: {type: Boolean,default: false,},labelWidth: {type: Number,default: enums.SEARCH_LABEL}
})
const emits = defineEmits(['search', 'resize'])
const searchFieldList = ref([])
const extendFieldList = computed(() => props?.extendFieldList)const loading = ref(false)
const searchData = reactive(props?.params)
const areaOptions = ref([])function onSearch() {initParams()
}
function inputChange(e) {if (!e.target?.value && e?.type === 'click') {initParams()}
}
function changeExtendDate(dateString, it) {if (!dateString?.length) {searchData[`${it.name}_start`] = nullsearchData[`${it.name}_end`] = null} else {const [created_at_start, created_at_end] = dateStringsearchData[`${it.name}_start`] = created_at_startsearchData[`${it.name}_end`] = created_at_end}initParams()
}
const isSelf = ref(false)
const isDep = ref(false)
function checkSearchSelectDisable(it) {if (it.name === 'salesman_user_id' && isSelf.value) {return true}if (it.name === 'salesman_department_id' && isDep.value) {return true}return false
}
function customFileSelectSearchChange(value, option) {// console.log('value', value)// console.log('option', option)initParams()
}
function handleChangeArea(value) {if (!value) { // 清空searchData.province_code = nullsearchData.city_code = nullsearchData.area_code = null} else {const [province, city, area] = valuesearchData.province_code = provincesearchData.city_code = citysearchData.area_code = area}initParams()
}
function handleChangeCascader(value, selectedOptions) {console.log('value', value)console.log('selectedOptions', selectedOptions)// initParams()
}
function initParams() {// const params = deleteEmptyValue(searchData)emits('search', searchData)
}
const searchRef = ref()
const foldSearchRef = ref()
const containerStyle = ref({})
function foldSearchChange(value) {containerStyle.value = valueif (props?.isResize) {nextTick(() => {emits('resize')})}
}
const staticData = ref([])
async function initPromise(isRefreshFoldSearch = false) {loading.value = trueconst promiseList = []if (props.fetchParentData) {promiseList.push(props.fetchParentData())}if (props?.module) {promiseList.push(getSearchFieldList(props?.module))}if (props?.isArea) {promiseList.push(queryArea())}await Promise.all(promiseList).then((res) => {if (res?.length) {res.forEach((item) => {if (item.name === 'area') {areaOptions.value = item.data}if (item.name === 'cloneSearchFields') {staticData.value = item.data}if (item.name === 'search') {let JSONData = []if (item?.data?.length) {JSONData = JSON.parse(item?.data[0]?.json_data)}const formatJsonData = compareAndUpdateArraysName([...staticData.value, ...extendFieldList.value], JSONData)searchFieldList.value = formatJsonData.length ? formatJsonData : staticData.value}if (!props?.module) {searchFieldList.value = staticData.value}})if (isRefreshFoldSearch) {containerStyle.value = {height: 'auto',overflow: 'visible',}nextTick(() => {foldSearchRef.value.searchChange()})}}}).catch((err) => {console.log('error', err)}).finally(() => {loading.value = false})
}
onMounted(async () => {await initPromise(true)
})
onActivated(async () => {
})
defineExpose({initPromise,
})
</script><template><div class="search-bar"><slot name="header"><page-title :title="props.title" /></slot><a-spin :spinning="loading"><div class="search-box"><div ref="searchRef" class="search-left" :style="containerStyle"><template v-for="it in searchFieldList" :key="it.name"><div v-if="!it?.noShow" class="search-item"><div class="search-label" :style="{ width: `${props.labelWidth}px` }"><div class="search-label-text" :style="{ width: `${props.labelWidth - 14}px` }"><tooltip:is-weight="false":max-width="props.labelWidth - 14":text="it?.label"/></div>:</div><a-inputv-if="isInputType(it)"v-model:value="searchData[it.name]":allow-clear="it?.isClear !== enums.BOOL.NO.v":disabled="it?.searchDisable":style="enums.SEARCH_INPUT_WIDTH":placeholder="props?.isPlaceholder ? `请输入${it.label}` : ''"@press-enter="onSearch"@change="inputChange"/><range-pickerv-if="(it.type === enums.FIELD_TYPE.DATE.v || it.type === enums.FIELD_TYPE.DATETIME.v) && !it?.isCustomEvent"v-model:value="searchData[it.name]"picker="date":it="it":disabled="it?.searchDisable":style="enums.SEARCH_INPUT_WIDTH":placeholder="props?.isPlaceholder ? ['开始日期', '结束日期'] : []"@change="changeExtendDate"/><a-selectv-if="it.type === enums.FIELD_TYPE.SELECTOR.v && !it?.isCustomEvent"v-model:value="searchData[it.name]":allow-clear="it?.isClear !== enums.BOOL.NO.v":placeholder="props?.isPlaceholder ? '请选择' : ''":style="enums.SEARCH_INPUT_WIDTH"show-searchoption-filter-prop="label":disabled="it?.searchDisable"@change="customFileSelectSearchChange"><a-select-optionv-for="item in it?.field_options":key="item.the_key":value="item.the_key":label="item.the_value">{{ item.the_value }}</a-select-option></a-select><a-selectv-if="it.type === enums.FIELD_TYPE.MULTI_SELECTOR.v && !it?.isCustomEvent"v-model:value="searchData[it.name]":allow-clear="it?.isClear !== enums.BOOL.NO.v"mode="multiple":placeholder="props?.isPlaceholder ? '请选择' : ''":style="enums.SEARCH_INPUT_WIDTH"show-searchoption-filter-prop="label":disabled="it?.searchDisable"@change="customFileSelectSearchChange"><a-select-optionv-for="item in it?.field_options":key="item.the_key":value="item.the_key":label="item.the_value">{{ item.the_value }}</a-select-option></a-select><a-cascaderv-if="(it.type === enums.FIELD_TYPE.CASCADER.v && it?.isArea) && !it?.isCustomEvent":allow-clear="it?.isClear !== enums.BOOL.NO.v":style="enums.SEARCH_INPUT_WIDTH":options="areaOptions":placeholder="props?.isPlaceholder ? '请选择' : ''":field-names="{ label: 'name', value: 'code', children: 'children' }":change-on-select="it?.changeOnSelect":disabled="it?.searchDisable"@change="(values) => handleChangeArea(values, it.name)"/><a-cascaderv-if="(it.type === enums.FIELD_TYPE.CASCADER.v && !it?.isArea) && !it?.isCustomEvent":allow-clear="it?.isClear !== enums.BOOL.NO.v":style="enums.SEARCH_INPUT_WIDTH":options="it?.field_options":placeholder="props?.isPlaceholder ? '请选择' : ''":field-names="{ label: 'name', value: 'id', children: 'children' }":change-on-select="it?.changeOnSelect":disabled="it?.searchDisable"@change="handleChangeCascader"/><!--        <RangePicker --><!--          v-if="it.type === enums.FIELD_TYPE.DATETIME.v" --><!--          v-model:value="searchData[it.name]" --><!--          :picker="'dateTime'" --><!--          :format="'YYYY-MM-DD HH:mm:ss'" --><!--          :valueFormat="'YYYY-MM-DD HH:mm:ss'" --><!--          :it="it" --><!--          :placeholder="['开始时间','结束时间']" --><!--          @change="changeExtendDate" --><!--        /> --><template v-if="it?.isCustomEvent"><slot :it="it" :name="it?.name" /></template></div></template></div><div class="search-right"><fold-search ref="foldSearchRef" :search-ref="searchRef" @fold-search-change="foldSearchChange" /></div></div></a-spin></div>
</template><style scoped lang="less">
.search-bar{width: 100%;border-radius: 6px;overflow: hidden;padding: 0 20px;background-color: #fff;.search-box{flex: 1;padding: 20px 0;display: flex;.search-left {flex: 1;display: flex;flex-wrap: wrap;gap: 10px;row-gap: 24px;.search-item {display: flex;align-items: center;.search-label {width: 104px;margin-right: 5px;display: flex;align-items: center;.search-label-text{width: 90px;color: rgba(0, 0, 0, 0.85);text-align: right;display: flex;align-items: center;justify-content: flex-end;}//.tooltip-text {//  display: inline-block;//  max-width: 86px; /* 设置最大宽度以进行测试 *///  white-space: nowrap; /* 不换行 *///  overflow: hidden; /* 隐藏超出部分 *///  text-overflow: ellipsis; /* 使用省略号 *///}}}}.search-right {}}
}
</style>

Search组件 使用样例:

<sqb-search-barref="sqbSearchBarRef"title="客户中心":fields="searchFields":fetch-parent-data="initSearchFields":params="params"@search="initParams"></sqb-search-bar>const params = ref({})// 搜索参数
function initParams(newParams) {// 刷新table表格params.value = newParamsrefreshTable({ reload: true })
}// 初始化筛选条件栏的数据
async function initSearchFields() {// 1. 初始应该是 searchFields 的深拷贝,而不是空数组const cloneSearchFields = cloneDeep(searchFields.value)// , getDep(), formatClueStatus(enums.CLUE_STATUS), formatBool('from_seas'), formatCallStatus(enums.CUSTOMER_CALL_STATUS)const promiseList = [getUserSelector(), getCustomerFromList(), queryStagList()]try {const res = await Promise.all(promiseList)if (res?.length) {res.forEach((item) => {FIELD_CONFIGS.forEach((config) => {updateFieldOptions(cloneSearchFields, item, config.name, config.format)})})}searchFields.value = cloneDeep(cloneSearchFields)return {name: 'cloneSearchFields',data: cloneSearchFields,} // 正确返回数据} catch (err) {console.error('initSearchFields error:', err)throw err // 抛出错误,让调用方处理}
}const FIELD_CONFIGS = [{name: 'sale_user_id',format: { id: 'the_key', card_user_name: 'the_value' },},{name: 'customer_source_id',format: { id: 'the_key', name: 'the_value' },},{name: 'customer_stage_id',format: { id: 'the_key', name: 'the_value' },},
]// 销售顾问选择器
function getUserSelector() {return userSelector({page_no: 1,page_size: 999,}).then(res => ({name: 'sale_user_id',data: updateListData(res?.data?.data, selfUserInfo),})).catch(err => err)
}
// 客户来源
function getCustomerFromList() {return DictApi.list({page_no: 1,page_size: 9999,type: enums.DICT.CUSTOMER_FROM.v,value_sort: 'ascend',}).then(res => ({name: 'customer_source_id',data: res?.data?.data,}))
}
// 客户阶段
async function queryStagList() {return DictApi.list({page_no: 1,page_size: 9999,type: enums.DICT.CLUE_STAGE.v,value_sort: 'ascend',}).then(res => ({name: 'customer_stage_id',data: res?.data?.data,}))
}
searchFields这个就是配置文件:
import enums from '@/utils/enums.js'
import { isAdminRole } from '@/utils/utils.js'export default [{label: '客户名称',name: 'name',type: enums.FIELD_TYPE.TEXT_IPT.v,disable: true,},{label: '客户编号',name: 'code',type: enums.FIELD_TYPE.TEXT_IPT.v,},{label: '手机号码',name: 'mobile',type: enums.FIELD_TYPE.TEXT_IPT.v,},{label: '销售顾问',name: 'sale_user_id',type: enums.FIELD_TYPE.SELECTOR.v,searchDisable: !isAdminRole(),noShow: !isAdminRole(),},// {//   label: '部门',//   name: 'salesman_department_id',//   type: enums.FIELD_TYPE.SELECTOR.v,// },{label: '客户区域',name: 'customer_region',type: enums.FIELD_TYPE.CASCADER.v,isArea: true,changeOnSelect: true,},{label: '客户来源',name: 'customer_source_id',type: enums.FIELD_TYPE.SELECTOR.v,},// {//   label: '跟单状态',//   name: 'clue_status',//   type: enums.FIELD_TYPE.SELECTOR.v,// },{label: '客户阶段',name: 'customer_stage_id',type: enums.FIELD_TYPE.SELECTOR.v,},// {//   label: 'AI标签',//   name: 'ai_analysis_tags',//   type: enums.FIELD_TYPE.SELECTOR.v,//   isCustomEvent: true,// },// {//   label: '领取时间',//   name: 'claim_time',//   type: enums.FIELD_TYPE.DATE.v,// },{label: '最后跟单时间',name: 'last_follow_time',type: enums.FIELD_TYPE.DATE.v,},// {//   label: '公海客户',//   name: 'from_seas',//   type: enums.FIELD_TYPE.SELECTOR.v,// },// {//   label: '拨打状态',//   name: 'call_status',//   type: enums.FIELD_TYPE.SELECTOR.v,// },{label: '创建时间',name: 'created_at',type: enums.FIELD_TYPE.DATE.v,},
]

PageTitle组件:
<script setup>
import { computed } from 'vue'const props = defineProps({title: {type: String,default: '',},
})
const title = computed(() => props?.title)
</script><template><div class="page-title"><div class="title-left"><div class="title">{{ title }}</div><slot name="title_left" /></div><div class="title-right"><slot name="title_right" /></div></div>
</template><style scoped lang="less">
.page-title{width: 100%;height: 60px;box-sizing: border-box;padding: 20px 0;background-color: #fff;display: flex;align-items: center;justify-content: space-between;border-bottom: 1px solid rgb(239, 239, 239);.title-left{display: flex;align-items: center;gap: var(--small-gap);.title{font-size: 18px;font-weight: bold;}}
}
</style>

Tooltip组件:

<script setup>
import { onMounted, ref, watch } from 'vue'const props = defineProps({text: {type: String,required: true,},maxWidth: {type: Number,default: 100, // 默认最大宽度},color: { // 使用指定颜色渲染文本type: String,color: 'rgba(0, 0, 0, 0.65)',},colorEqual: { // 不论是否超出隐藏,文本颜色都是一样的(配合指定颜色使用)type: Boolean,default: false,},isCursor: { // 是否显示选中效果type: Boolean,default: false,},isWeight: {type: Boolean,default: false,},
})const isOverflowing = ref(false)
const textRef = ref(null)
function initPage() {if (textRef.value) {const computedStyle = getComputedStyle(textRef.value)const padding = Number.parseFloat(computedStyle.paddingLeft) + Number.parseFloat(computedStyle.paddingRight)const totalMaxWidth = props.maxWidth - padding // 考虑 padding// 创建临时元素以测量文本宽度const tempElement = document.createElement('span')// 复制样式tempElement.style.font = computedStyle.font // 复制字体tempElement.style.visibility = 'hidden'tempElement.style.whiteSpace = 'nowrap'tempElement.style.position = 'absolute' // 确保不占空间tempElement.textContent = props.text// 将元素添加到文档中document.body.appendChild(tempElement)// 判断文本是否超出最大宽度isOverflowing.value = tempElement.scrollWidth > totalMaxWidth// 移除临时元素document.body.removeChild(tempElement)}
}
onMounted(() => {initPage()
})
// updated(() => {
//   initPage();
// });
watch([() => props.text, () => props.maxWidth], () => {initPage()
})
</script><template><a-tooltip v-if="isOverflowing" :title="text"><spanref="textRef":class="{ 'cursor-class': props.isCursor, 'font-weight': props?.isWeight }"class="tooltip-text":style="{ color: props.color }">{{ text }}</span></a-tooltip><spanv-elseref="textRef":class="{ 'cursor-class': props.isCursor, 'font-weight': props?.isWeight }":style="{ color: props.colorEqual ? props.color : 'rgba(0, 0, 0, 0.88)' }">{{ text }}</span>
</template><style scoped lang="less">
.tooltip-text {display: inline-block; /* 使文本能正确测量 */max-width: 100%;      /* 允许文本最大宽度 */white-space: nowrap;   /* 不换行 */overflow: hidden;      /* 隐藏超出部分 */text-overflow: ellipsis; /* 使用省略号 */cursor: pointer;       /* 鼠标悬停时显示手指光标 */
}
.font-weight{font-weight: 500;
}
</style>

utils方法:

/*** 处理搜索条件配置的--name为键--给扩展字段用的--格式化扩展字段* @param staticData* @param dynamicData* @returns {*[]}*/
export function compareAndUpdateArraysName(staticData, dynamicData) {const updatedArr = []const staticMap = new Map()staticData.forEach((item) => {staticMap.set(item.name, item)})dynamicData.forEach((item) => {if (staticMap.has(item.name)) {const staticItem = staticMap.get(item.name)updatedArr.push({ ...item, ...staticItem })}})return updatedArr
}/*** 获取搜索条件栏的字段* @param module_name* @returns {Promise<unknown>}*/
export async function getSearchFieldList(module_name) {return getWEBFilterApi({module_name,channel: enums.CHANNEL.WEB.v,}).then(res => ({name: 'search',data: res.data,})).catch(() => ({name: 'search',data: [],})).finally(() => {})
}/*** 是否是input形态的字段--用于搜索条件字段的显示* @param it 字段* @returns {*}*/
export function isInputType(it) {if (!it || !it.type || it?.isCustomEvent) return falseconst inputTypes = [enums.FIELD_TYPE.TEXT_IPT.v,enums.FIELD_TYPE.MULTI_TEXT_IPT.v,enums.FIELD_TYPE.EXPRESS_NUMBER.v,]return inputTypes.includes(it?.type)
}
/*** 获取省市县树状结构* @returns {Promise<{name: string, data: *}>}*/
export async function queryArea() {return queryRegionTree({}).then((res) => {return {name: 'area',data: res.data,}})
}
enums.js
export default {ALL: {ALL: {v: null,name: '全部',visitorName: '全部',},},BOOL: {YES: {v: 1,name: '是',visitorName: '客户',},NO: {v: 2,name: '否',visitorName: '微信用户',},},CHANNEL: {WECHAT: {v: 1,name: '小程序端',},WEB: {v: 2,name: 'WEB端',},APP: {v: 3,name: 'APP',},},ERROR_CODE: {LOGIN_INVALID: {code: 1,name: '登录失效',},BUSI: {code: 2,name: '业务异常',},DATA_NOT_EXIST: {code: 3,name: '数据不存在',},},ROUTER_CONFIG: {LOGIN_PATH: {v: '/login',name: '登录地址',},DEFAULT_HOME_PATH: {v: '/customerManage/businessCardVisitor/list',name: '默认首页地址',},WX_LOGIN_PATH: {v: '/wxLogin',name: '微信登录',},Bind_MOBILE_PATH: {v: '/bindMobile',name: '绑手机号',},},MAXLENGTH: 100,// 搜索字段label显示宽度SEARCH_LABEL: 104,// 搜索字段输入框宽度SEARCH_INPUT_WIDTH: 'width: 270px',// 字段类型FIELD_TYPE: {TEXT_IPT: {v: 'text_ipt',name: '单行文本',icon: extendFieldIcon.textImg,},MULTI_TEXT_IPT: {v: 'multi_text_ipt',name: '多行文本',icon: extendFieldIcon.textareaImg,},DATE: {v: 'date',name: '日期',icon: extendFieldIcon.dateImg,},LOCATION: {v: 'location',name: '定位',icon: extendFieldIcon.locationImg,},NUM_IPT: {v: 'num_ipt',name: '数字输入框',icon: extendFieldIcon.locationImg,},SELECTOR: {v: 'selector',name: '单选下拉框',icon: extendFieldIcon.SelectImg,},CASCADER: {v: 'cascader',name: '级联选择',icon: extendFieldIcon.locationImg,},MULTI_SELECTOR: {v: 'multi_selector',name: '多选下拉框',icon: extendFieldIcon.multiSelectorImg,},DATETIME: {v: 'datetime',name: '时间',icon: extendFieldIcon.dataTimeImg,},FILE: {v: 'file',name: '附件',icon: extendFieldIcon.AttachmentImg,},SHOT: {v: 'shot',name: '拍照/摄影',icon: extendFieldIcon.locationImg,},MOBILE: {v: 'mobile',name: '手机号码',icon: extendFieldIcon.locationImg,},EMAIL: {v: 'email',name: '邮箱',icon: extendFieldIcon.locationImg,},EXPRESS_NUMBER: {v: 'express_number',name: '快递单号',icon: extendFieldIcon.expressageImg,},SIGN: {v: 'sign',name: '签字',icon: extendFieldIcon.locationImg,},FORMULA: {v: 'formula',name: '公式',icon: extendFieldIcon.locationImg,},ASSOCIATION_OBJECT: {v: 'association_object',name: '关联对象',icon: extendFieldIcon.locationImg,},ASSOCIATION_ATTR: {v: 'association_attr',name: '关联属性',icon: extendFieldIcon.locationImg,},DOUBLE_INPUT: {v: 'double_input',name: '联级输入框',icon: extendFieldIcon.locationImg,},RADIO: {v: 'radio',name: '单选',icon: extendFieldIcon.locationImg,},},// 模块类型,MODULE_TYPE: {DYNAMIC: {v: 1,name: '动态',},STATIC: {v: 2,name: '静态',},},// 模块名称--扩展字段用的MODULE: {BUSINESS_CARD_VISITOR: 'businessCardVisitor', // 名片访客CLUE_CONTROL: 'clueControl',CLUE: 'clue',FOLLOW_UP: 'followUp',CLUE_OPEN_SEAS: 'clueOpenSeas',ORDER_MANAGEMENT: 'clue_deal_order',PAYMENT_EXPENDITURE: 'payment_expenditure',INVOICE: 'invoice',PRODUCT: 'product',AI_ANALYSIS: 'aiAnalysis',ELITE_RECORDING_LIBRARY: 'eliteRecordingLibrary',GAME_REWARD: 'gameReward',SOP_INSPECT: 'sopInspect', // 话术应用质检VIOLATION: 'violation', // 违规质检EXECUTE: 'execute', // 执行力报表RETRIEVAL: 'retrieval',CUSTOMER_INSIGHT: 'customerInsight',PERFORMANCE_TARGET: 'performanceTarget', // 目标管理-业绩目标FOLLOW_CLUE: 'followClue', // 目标管理-跟单拓客目标RECOMMENDED_OFFICER_LIST: 'recommendedOfficerList', // 推荐官列表RECOMMENDATION_OFFICER_REVIEW: 'recommendationOfficerReview', // 推荐官审核MONEY_DISTRIBUTION: 'moneyDistribution', // 红包发放WITHDRAWAL_SETTLEMENT: 'withdrawalSettlement', // 提现结算REFERRER_BALANCE: 'referrerBalance', // 推荐官余额SUBMISSION_APPROVAL: 'submissionApproval', // 订单审核OUT_IN_APPROVAL: 'outInApproval', // 收支审核ARTICLE: 'article', // 资讯VIDEO: 'video', // 视频BROCHURE: 'brochure', // 宣传册PRODUCT_LIBRARY: 'productLibrary', // 产品POSTER: 'poster', // 海报EMPLOYEE_MANAGE: 'employeeManage', // 组织架构ROLE: 'role', // 角色},// 表名--列配置、搜索配置用的TABLE_NAME: {BUSINESS_CARD_VISITOR: 'businessCardVisitor', // 名片访客CLUE_CONTROL: 'clueControl', // 线索CLUE: 'clue', // 客户中心FOLLOW_UP: 'followUp', // 跟单管理CLUE_OPEN_SEAS: 'clueOpenSeas', // 客户公海ORDER_MANAGEMENT: 'orderManagement', // 订单管理PAYMENT_EXPENDITURE: 'payment_expenditure', // 收支管理INVOICE: 'invoice', // 开票、开票管理PRODUCT: 'product', // 产品管理AI_ANALYSIS: 'aiAnalysis', // 沟通分析ELITE_RECORDING_LIBRARY: 'eliteRecordingLibrary', // AI销售大脑GAME_REWARD: 'gameReward', // 游戏激励SOP_INSPECT: 'sopInspect', // 话术应用质检VIOLATION: 'violation', // 违规质检EXECUTE: 'execute', // 执行力报表RETRIEVAL: 'retrieval', // 客户回捞CUSTOMER_INSIGHT: 'customerInsight', // 客户洞察PERFORMANCE_TARGET: 'performanceTarget', // 目标管理-业绩目标FOLLOW_CLUE: 'followClue', // 目标管理-跟单拓客目标RECOMMENDED_OFFICER_LIST: 'recommendedOfficerList', // 推荐官列表RECOMMENDATION_OFFICER_REVIEW: 'recommendationOfficerReview', // 推荐官审核MONEY_DISTRIBUTION: 'moneyDistribution', // 红包发放WITHDRAWAL_SETTLEMENT: 'withdrawalSettlement', // 提现结算REFERRER_BALANCE: 'referrerBalance', // 推荐官余额SUBMISSION_APPROVAL: 'submissionApproval', // 订单审核OUT_IN_APPROVAL: 'outInApproval', // 收支审核ARTICLE: 'article', // 资讯VIDEO: 'video', // 视频BROCHURE: 'brochure', // 宣传册PRODUCT_LIBRARY: 'productLibrary', // 产品POSTER: 'poster', // 海报EMPLOYEE_MANAGE: 'employeeManage', // 组织架构ROLE: 'role', // 角色},}
RangePicker组件:
<script setup>
import {computed,defineEmits,defineProps,ref,watch,
} from 'vue'
import dayjs from 'dayjs'const props = defineProps({width: {default: 280,},bordered: {type: Boolean,default: true,},placeholder: {type: Array,default: () => ['开始时间', '结束时间'],},value: {type: Array,default: () => [],},format: {type: String,default: 'YYYY-MM-DD',},valueFormat: {type: String,default: 'YYYY-MM-DD',},picker: {type: String,default: 'date',},it: {// 这个是给扩展字段用的},
})
const emits = defineEmits(['update:value', 'change'])
const selectedRange = ref(props.value)
const selectedFormat = computed(() => (props.picker === 'month' ? 'YYYY-MM' : props.format))
const selectedValueFormat = computed(() => (props.picker === 'month' ? 'YYYY-MM' : props.valueFormat))
const today = dayjs()
const yesterday = today.subtract(1, 'day')
const startOfThisWeek = today.startOf('week')
const startOfThisMonth = today.startOf('month')
const startOfLastWeek = startOfThisWeek.subtract(1, 'week')
const startOfLastMonth = startOfThisMonth.subtract(1, 'month')
const startOfLastYear = today.subtract(1, 'year').startOf('year')
const endOfLastYear = startOfLastYear.endOf('year')
const startOfLastQuarter = dayjs().subtract(1, 'quarter').startOf('quarter')
const endOfLastQuarter = dayjs().startOf('quarter').subtract(1, 'day')
const rangePresets = [{label: '今天',value: [today, today],},{label: '昨天',value: [yesterday, yesterday],},{label: '本周',value: [startOfThisWeek, today],},{label: '上周',value: [startOfLastWeek, startOfThisWeek.subtract(1, 'day')],},{label: '本月',value: [startOfThisMonth, today],},{label: '上月',value: [startOfLastMonth, startOfThisMonth.subtract(1, 'day')],},{label: '本季度',value: [dayjs().startOf('quarter'), today],},{label: '上季度',value: [startOfLastQuarter, endOfLastQuarter],},{label: '本年',value: [dayjs().startOf('year'), today],},{label: '上年',value: [startOfLastYear, endOfLastYear],},
]
const computedPresets = computed(() => {if (props.picker === 'month') {return rangePresets.filter(preset => preset.label !== '本周' && preset.label !== '上周')}return rangePresets
})function handleChange(value) {selectedRange.value = valueemits('update:value', value)if (props.it) {emits('change', value, props.it)} else {emits('change', value)}
}watch(() => props.value, (v) => {// console.log('时间范围变动了', v);if (!v) {selectedRange.value = []}
})
</script><template><a-range-pickerv-model:value="selectedRange":bordered="props.bordered":format="selectedFormat":picker="picker":placeholder="props.placeholder":presets="computedPresets":show-time="props.picker === 'dataTime'":style="{ width: `${props.width}px` }":value-format="selectedValueFormat"@change="handleChange"/>
</template>
FoldSearch组件:
<script setup>
import { DownOutlined, UpOutlined } from '@ant-design/icons-vue'
import { computed, ref } from 'vue'const props = defineProps({searchRef: {type: Object,default: () => ({}),},foldHeight: {type: Number,default: 88,},isVisibilityHidden: {type: Boolean,default: true,},
})
const emit = defineEmits(['foldSearchChange'])
const isInit = ref(true)
const showFoldSearch = ref(false)
const searchFold = ref(false)
const containerStyle = computed(() => ({height: searchFold.value ? `${props.foldHeight}px` : 'auto', // 折叠时设置固定高度,展开时自适应内容高度overflow: searchFold.value ? 'hidden' : 'visible', // 折叠时隐藏溢出内容transition: 'height 1s ease', // 为了平滑过渡效果
}))
function changeFold() {searchFold.value = !searchFold.valueemit('foldSearchChange', containerStyle.value)
}
function searchChange(isFold = false) {searchFold.value = falseif (props.searchRef?.clientHeight && props.searchRef?.clientHeight > props.foldHeight) {showFoldSearch.value = trueif (isInit.value || isFold) {searchFold.value = true}emit('foldSearchChange', containerStyle.value)} else {showFoldSearch.value = falseemit('foldSearchChange', containerStyle.value)}isInit.value = false
}defineExpose({ searchChange })
</script><template><div class="fold-search" :style="{ display: isVisibilityHidden || showFoldSearch ? 'block' : 'none' }"><a-button v-if="showFoldSearch" type="primary" @click="changeFold"><div style="display: flex;align-items: center;gap: 2px;">{{ searchFold ? '展开' : '收起' }}筛选<down-outlined v-if="searchFold" /><up-outlined v-else /></div></a-button></div>
</template><style lang="less" scoped>
.fold-search {//width: 100px;
}
</style>

http://www.lqws.cn/news/545563.html

相关文章:

  • 2025Mybatis最新教程(七)
  • 机器学习中为什么要用混合精度训练
  • 2025.6.27总结
  • HTTP协议中Connection: Keep-Alive和Keep-Alive: timeout=60, max=100的作用
  • SpringMVC系列(四)(请求处理的十个实验(下))
  • 多模态融合相机L3CAM
  • 高斯过程动态规划(GPDP)
  • 免费无广告PDFCreator:虚拟打印软件一键转 PDF/PNG/JPG
  • printf和scanf
  • 问卷调查[bell ring]
  • 全志A733、瑞芯微RK3576与联发科MTK8371场景化应用解析在物联网与智能设备快速迭代的今天,芯片作为硬件核心直接决定了设备的性能边界与应用场景。
  • moduo之tcp客户端TcpClient
  • Webpack 自定义插件开发指南:构建流程详解与实战开发全攻略
  • Html5播放器禁止拖动播放器进度条(教学场景)
  • 神经网络的概念和案例
  • FrozenBatchNorm2d 详解
  • 聚铭网络入选嘶吼《中国网络安全细分领域产品名录》“云平台安全管理”与“态势感知”双领域TOP10
  • Linux tcp_info:监控TCP连接的秘密武器
  • CatBoost:征服类别型特征的梯度提升王者
  • 蓝牙工作频段与跳频扩频技术(FHSS)详解:面试高频考点与真题解析
  • System.Threading.Tasks 库简介
  • ubuntu ollama 遇到的若干问题
  • Linux命令行操作基础
  • WPF 3D 开发全攻略:实现3D模型创建、旋转、平移、缩放
  • 记录一个C#/.NET的HTTP工具类
  • Feign 实战指南:从 REST 替代到性能优化与最佳实践
  • 文法、正规式相关习题
  • Linux系统(信号篇)信号的保存
  • WinAppDriver 自动化测试:JavaScript 篇
  • gRPC技术解析与python示例