【招生小程序】 新增# 1、对接电销的添加跟进、跟进记录列表;2、对接招生的添加进展;3、修复:刷新页面时底部导航栏显示首页路径错误;4、修复:点击底部导航栏中的总结模板后,再点击首页后,页面无反应的问题

master
kaeery 2025-02-27 19:40:00 +08:00
parent 2de7edf2d4
commit 30d61360fa
37 changed files with 515 additions and 425 deletions

View File

@ -6,7 +6,7 @@
# @FilePath: \chargingpile-uniapp\.env.development
###
# 请求域名
# VITE_APP_BASE_URL= 'http://120.77.216.5:8084'
VITE_APP_BASE_URL="http://192.168.111.98:8084"
# VITE_APP_BASE_URL="https://wechat.szcxj2024.com"
# VITE_APP_BASE_URL= 'http://192.168.111.5:8086'
VITE_APP_BASE_URL="http://192.168.111.98:8086"
# VITE_APP_BASE_URL="https://124.220.209.120:8086"
# VITE_APP_SOCKET_URL = 'wss://front.yuegoodlife.com'

View File

@ -13,7 +13,7 @@ import { getAllDict } from '@/hooks/useDictOptions'
const appStore = useAppStore()
const userStore = useUserStore()
// getAllDict()
getAllDict()
onLaunch(async () => {
appStore.getSystemInfoFn()
@ -24,28 +24,9 @@ onLaunch(async () => {
onShow(async () => {
const token = userStore.token
if (token) {
await appStore.getConfig()
// await appStore.getConfig()
appStore.updateLocation()
} else appStore.closeTimer()
})
// function genDates() {
// const weekdays = ['', '', '', '', '', '', '']
// const currentDate = new Date()
// const currentDayOfWeek = currentDate.getDay()
// let preDateOfWeek = currentDate.getDate() - 1
// const daysInRange = []
// for (let i = currentDayOfWeek; i <= weekdays.length - 1; i++) {
// preDateOfWeek = preDateOfWeek + 1
// daysInRange.push(preDateOfWeek)
// }
// let currentDateOfWeek = currentDate.getDate()
// for (let i = 0; i <= weekdays.length - daysInRange.length; i++) {
// currentDateOfWeek = currentDateOfWeek - 1
// daysInRange.unshift(currentDateOfWeek)
// }
// console.log(daysInRange)
// }
// genDates()
</script>
<style lang="scss"></style>

34
src/api/clue.ts 100644
View File

@ -0,0 +1,34 @@
import request from '@/utils/request'
// 新增线索
export function apiAddCluse(params: any) {
return request.post({ url: '/clue/add', data: params })
}
// 线索列表
export function apiCluseList(params: any) {
return request.get({ url: '/clue/list', data: params })
}
// 领取线索
export function apiGetCluse(params: any) {
return request.post({ url: '/clue/grabTheOrder', data: params })
}
// 线索详情
export function apiCluseDetail(params: any) {
return request.get({ url: '/clue/detail', data: params })
}
// 添加进展
export function apiAddCluseProgress(params: any) {
return request.post({ url: '/clue/addProgress', data: params })
}
// 转化完成
export function apiCompleteCluse(params: any) {
return request.post({ url: '/clue/conversionCompleted', data: params })
}
// 修改备注
export function apiEditRemark(params: any) {
return request.post({ url: '/clue/modifyRemarks', data: params })
}
// 修改跟进
export function apiEditClue(params: any) {
return request.post({ url: '/clue/edit', data: params })
}

View File

@ -2,10 +2,13 @@ import request from '@/utils/request'
// 所有字典以及字典下的所有数据
export function dictAllDataList() {
return request.get({
url: '/setting/dict/type/allDataList',
data: {
pageSize: 60
}
})
return request.get(
{
url: '/setting/dict/type/allDataList',
data: {
pageSize: 60
}
},
{ isAuth: true }
)
}

View File

@ -193,7 +193,7 @@ async function handleLogin(e) {
scene: LoginTypeEnum.MNP,
iv: e.detail.iv,
encryptedData: e.detail.encryptedData,
channel: ChannelEnum.STAFF_PLATFORM
channel: ChannelEnum.USER_PLATFORM
})
//
loginData.value = data
@ -228,14 +228,14 @@ async function loginHandle(data: any) {
//
await userStore.getUser()
//
await appStore.getConfig()
// await appStore.getConfig()
//
uni.$u.toast('登录成功')
//
uni.hideLoading()
appStore.setFirstLogin(true)
// appStore.setFirstLogin(true)
const { userInfo } = userStore
if (userInfo.roles?.length > 1) {
uni.redirectTo({

View File

@ -2,9 +2,9 @@
<view class="bg-[#FAFAFE]">
<view class="bg-white px-[32rpx]">
<TForm ref="tForm" :model="form" :rules="rules" errorType="toast">
<TFormItem prop="publishName">
<TFormItem prop="recruitTeacherName">
<TInputField
v-model="form.publishName"
v-model="form.recruitTeacherName"
label="发布人"
placeholder=""
inputAlign="left"
@ -13,9 +13,9 @@
:labelWidth="100"
/>
</TFormItem>
<TFormItem prop="clientName">
<TFormItem prop="studentName">
<TInputField
v-model="form.clientName"
v-model="form.studentName"
label="客户姓名"
placeholder=""
inputAlign="left"
@ -24,9 +24,9 @@
:labelWidth="100"
/>
</TFormItem>
<TFormItem prop="mobile">
<TFormItem prop="phone">
<TInputField
v-model="form.mobile"
v-model="form.phone"
label="电话"
placeholder=""
inputAlign="left"
@ -35,8 +35,9 @@
:labelWidth="100"
/>
</TFormItem>
<TFormItem prop="desc">
<TFormItem prop="basicInformation">
<TTextareaField
v-model="form.basicInformation"
label="基本情况"
placeholder=""
autoHeight
@ -45,9 +46,9 @@
:labelWidth="100"
/>
</TFormItem>
<TFormItem prop="status">
<TFormItem prop="state">
<TMultiSelect
v-model="form.status"
v-model="form.state"
label="状态"
:groupList="statusList"
:labelWidth="100"
@ -56,50 +57,84 @@
</TForm>
</view>
<view class="px-[60rpx]">
<u-button class="btn" color="#0E66FB" shape="circle" @click="handleSubmit">
提交
<u-button
class="btn"
color="#0E66FB"
shape="circle"
:loading="loading"
@click="handleSubmit"
>
提交{{ form.state }}
</u-button>
</view>
</view>
</template>
<script setup lang="ts">
import { apiAddCluseProgress, apiCluseEdit } from '@/api/clue'
import { stateEnum } from '@/enums'
import { useClueDetail } from '@/hooks/useCommon'
import { toast } from '@/utils/util'
import { onLoad } from '@dcloudio/uni-app'
import { shallowRef } from 'vue'
import { ref } from 'vue'
const { fetchClueDetail, clueDetailInfo } = useClueDetail()
const id = ref('')
const tForm = ref()
const form = ref({
publishName: '',
clientName: '张三',
mobile: '',
desc: '',
status: ''
id: '',
recruitTeacherName: '',
studentName: '',
phone: '',
basicInformation: '',
state: null
})
const rules = {
status: [{ required: true, message: '请选择状态' }]
state: [{ required: true, message: '请选择状态' }]
}
const statusList = shallowRef([
{ label: '账号已添加', value: 1 },
{ label: '账号不存在', value: 2 },
{ label: '账号未通过', value: 3 }
{ label: '账号已添加', value: stateEnum.ADD_RELATION },
{ label: '账号不存在', value: stateEnum.NO_EXIST },
{ label: '账号未通过', value: stateEnum.UN_PASS }
])
onLoad(option => {
onLoad(async option => {
if (option?.id) {
id.value = option.id
await fetchClueDetail(id.value)
setFormData()
}
})
const loading = ref(false)
const handleSubmit = () => {
tForm.value
.validate()
.then(valid => {
.then(async valid => {
if (valid) {
console.log('校验通过')
loading.value = true
try {
const data = {
id: form.value.id,
state: form.value.state
}
await apiAddCluseProgress(data)
toast('添加进展成功')
uni.navigateBack()
uni.$emit('refreshPage')
} catch (error) {}
}
})
.catch(() => {})
loading.value = false
}
const setFormData = () => {
for (const key in clueDetailInfo.value) {
if (Object.prototype.hasOwnProperty.call(form.value, key)) {
form.value[key] = clueDetailInfo.value[key]
}
}
}
</script>

View File

@ -17,7 +17,6 @@
flex-direction: column;
.wrapper {
flex: 1;
background-color: #fafafe;
@apply flex flex-col;
}
}

View File

@ -120,6 +120,13 @@ export default defineComponent({
})
})
}
const resetFields = () => {
instance.proxy.children.map(child => {
const prop = child.prop
const value = uni.$u.getProperty(originalModel.value, prop)
uni.$u.setProperty(props.model, prop, value)
})
}
watch(
() => props.rules,
newVal => {
@ -139,7 +146,8 @@ export default defineComponent({
)
return {
children: [],
validate
validate,
resetFields
}
}
})

View File

@ -21,6 +21,7 @@
:inputAlign="inputAlign"
:readonly="readonly"
:maxlength="maxlength"
placeholderStyle="color: '#7c7e82'"
></u-input>
</view>
</template>

View File

@ -58,7 +58,6 @@ const innerValue = ref(props.modelValue)
const handleSelectedItem = (val: number) => {
emit('update:modelValue', val)
innerValue.value = val
console.log(val)
}
</script>

View File

@ -58,7 +58,7 @@ const props = defineProps({
default: false
}
})
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'onInput'])
const innerValue = ref('')
@ -96,6 +96,7 @@ const isShowClear = computed(() => {
const onInput = e => {
innerValue.value = e.detail.value
emit('update:modelValue', innerValue.value)
emit('onInput')
}
const onClear = () => {
innerValue.value = ''

View File

@ -47,8 +47,8 @@ const { roles } = useRoleData()
const emit = defineEmits(['update:modelValue', 'close'])
const activeRole = ref(props.modelValue)
const visible = ref(false)
let currentRoutes = getCurrentPages() //
let currentRoute = currentRoutes[currentRoutes.length - 1].route //
const currentRoutes = getCurrentPages() //
const currentRoute = currentRoutes[currentRoutes.length - 1].route //
const handleActiveRole = (val: number) => {
activeRole.value = val
}

View File

@ -39,6 +39,7 @@ const props = withDefaults(defineProps<TabBarProps>(), {
const tabBarStore = useTabBarStore()
const { tabBarList, activeTabBar } = storeToRefs(tabBarStore)
const handleTabbar = (index: number) => {
tabBarStore.setActiveTabBar(index)
navigateTo(unref(tabBarList)[index], 'reLaunch')

View File

@ -8,12 +8,7 @@
<view class="px-[24rpx] py-[16rpx] flex flex-col gap-[16rpx]">
<slot name="content" />
</view>
<view
class="px-[32rpx] border-t border-solid border-[#F3F3F3] flex justify-end py-[12rpx]"
v-if="isSlotAction"
>
<slot name="action" />
</view>
<slot name="action" v-if="isSlotAction" />
</view>
</template>

View File

@ -14,7 +14,7 @@ import { computed } from 'vue'
const props = defineProps({
curDate: {
type: String,
type: Number,
default: new Date().getTime()
},
type: {

View File

@ -3,9 +3,9 @@
<template #title>
<view class="flex justify-between w-full py-[10rpx]">
<view class="flex">
<text>韩梅梅</text>
<text>{{ item.studentName }}</text>
<view class="flex ml-[48rpx] gap-[4rpx] items-center">
<text class="text-primary">18138952909</text>
<text class="text-primary">{{ item.phone }}</text>
<u-copy content="uview-plus is great !">
<TIcon name="icon-copy" color="#0E66FB" />
</u-copy>
@ -15,7 +15,10 @@
</template>
<template #content>
<view class="flex flex-col gap-[16rpx]">
<view class="flex gap-[8rpx] items-center">
<view
class="flex gap-[8rpx] items-center"
v-if="item.situation == converStatusEnum.EXCEPTION"
>
<TIcon name="icon-warning" color="#F5222D" />
<text class="text-error">待电销老师重新跟进</text>
</view>
@ -23,22 +26,33 @@
<text class="text-muted w-[128rpx]">基本情况</text>
<view class="flex gap-[4rpx] flex-1 items-end">
<text>
学生爸爸接电话学生高三毕业300多分家长不清楚学生收到录取通知家长说学生不读书了我让家长先问问学生对未来的规划先和学生沟通一下家长同意我们加他微信发专业资料给他看看可以在微信上问问学生具体情况推荐3+2给家长发一下学校简介和专业资料
{{ item.basicInformation }}
</text>
<u-copy content="uview-plus is great !">
<TIcon name="icon-copy" color="#0E66FB" />
</u-copy>
</view>
</view>
<view class="flex gap-[20rpx]">
<view class="flex gap-[20rpx]" v-if="item.telemarketingTeacherId !== null">
<text class="text-muted w-[128rpx]">电销老师</text>
<text class="flex-1">王五</text>
<text class="flex-1">{{ item.telemarketingTeacherName }}</text>
</view>
<view class="flex gap-[20rpx]">
<view
class="flex gap-[20rpx]"
v-if="
item.situation == converStatusEnum.CONVERTED_PROCESS && item.state !== null
"
>
<text class="text-muted w-[128rpx]">状态</text>
<text class="flex-1 text-error">账号不存在</text>
<text class="flex-1 text-error">{{ parseStateText }}</text>
</view>
<view class="flex gap-[20rpx]">
<view
class="flex gap-[20rpx]"
v-if="
item.situation == converStatusEnum.CONVERTED ||
item.situation == converStatusEnum.FAILED
"
>
<text class="text-muted w-[128rpx]">备注</text>
<view class="flex gap-[12rpx]">
<text class="flex-1 text-error">已交一部分定位金</text>
@ -51,19 +65,42 @@
</view>
</view>
</view>
<view class="flex gap-[20rpx]">
<view class="flex gap-[20rpx]" v-if="item.situation == converStatusEnum.CONVERTED">
<text class="text-muted w-[128rpx]">成交时间</text>
<text class="flex-1">2025-02-10 16:04:00</text>
</view>
</view>
</template>
<template #action>
<view class="flex justify-end gap-[16rpx]">
<u-button color="#0E66FB" shape="circle" @click="handleGet"></u-button>
<u-button color="#0E66FB" shape="circle" @click="handleAddProgress">
添加进展
</u-button>
<u-button color="#0E66FB" shape="circle" @click="handleComplete"></u-button>
<view
class="px-[32rpx] border-t border-solid border-[#F3F3F3] flex justify-end py-[12rpx]"
>
<view class="flex justify-end gap-[16rpx]">
<u-button
color="#0E66FB"
shape="circle"
v-if="item.situation == converStatusEnum.UN_RECEIVED"
@click="handleGet"
>
领取
</u-button>
<u-button
color="#0E66FB"
shape="circle"
v-if="item.situation == converStatusEnum.CONVERTED_PROCESS"
@click="handleAddProgress"
>
添加进展
</u-button>
<u-button
color="#0E66FB"
shape="circle"
v-if="item.situation == converStatusEnum.ADD_RELATION"
@click="handleComplete"
>
转化完成
</u-button>
</view>
</view>
</template>
</w-card>
@ -71,18 +108,36 @@
<script setup lang="ts">
import { toast } from '@/utils/util'
import { PropType } from 'vue'
import { IClue } from '../telesale/clue-card.vue'
import { converStatusEnum, stateEnum } from '@/enums'
import { apiGetCluse } from '@/api/clue'
import { computed } from 'vue'
const props = defineProps({
item: {
type: Object,
type: Object as PropType<IClue>,
default: () => ({})
}
})
const emit = defineEmits(['handleUpdateRemark'])
const emit = defineEmits(['handleUpdateRemark', 'refreshPage'])
const stateMap: Record<stateEnum, string> = {
[stateEnum.ADD_RELATION]: '账号已添加',
[stateEnum.NO_EXIST]: '账号不存在',
[stateEnum.UN_PASS]: '账号未通过'
}
const parseStateText = computed(() => stateMap[props.item.state])
//
const handleGet = () => {
const { item } = props
toast('领取成功')
const handleGet = async () => {
const {
item: { id }
} = props
try {
await apiGetCluse({ id })
toast('领取成功')
emit('refreshPage')
} catch (error) {}
}
//
const handleAddProgress = () => {

View File

@ -3,15 +3,17 @@
<template #title>
<view class="flex justify-between w-full py-[10rpx]">
<view class="flex">
<text>韩梅梅</text>
<text>{{ item.studentName }}</text>
<view class="flex ml-[48rpx] gap-[4rpx] items-center">
<text class="text-primary">18138952909</text>
<text class="text-primary">{{ item.phone }}</text>
<u-copy content="uview-plus is great !">
<TIcon name="icon-copy" color="#0E66FB" />
</u-copy>
</view>
</view>
<text>待领取</text>
<text v-if="item.situation == converStatusEnum.UN_RECEIVED" class="text-[#ED6D41]">
待领取
</text>
</view>
</template>
<template #content>
@ -23,34 +25,62 @@
</view>
<view class="flex gap-[20rpx] mb-[16rpx]">
<text class="text-muted w-[128rpx]">跟进时间</text>
<text class="flex-1">2025-02-08 11:11:30</text>
<text class="flex-1">{{ item.createTime }}</text>
</view>
<view class="flex gap-[20rpx] mb-[16rpx]">
<view class="flex gap-[20rpx] mb-[16rpx]" v-if="item.recruitTeacherId !== null">
<text class="text-muted w-[128rpx]">招生老师</text>
<text class="flex-1">王五</text>
<text class="flex-1">{{ item.recruitTeacherName }}</text>
</view>
<view class="flex gap-[20rpx]">
<view class="flex gap-[20rpx]" v-if="item.state !== null">
<text class="text-muted w-[128rpx]">状态</text>
<text class="flex-1">账号不存在</text>
</view>
</template>
<template #action>
<u-button color="#0E66FB" shape="circle" @click="handleEditFollow"></u-button>
<view
class="px-[32rpx] border-t border-solid border-[#F3F3F3] flex justify-end py-[12rpx]"
v-if="item.situation == converStatusEnum.EXCEPTION"
>
<u-button color="#0E66FB" shape="circle" @click="handleEditFollow">
修改跟进
</u-button>
</view>
</template>
</w-card>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
import { computed } from 'vue'
import { converStatusEnum, stateEnum } from '@/enums'
export interface IClue {
studentName: string
phone: string
id: number
basicInformation: string
remark: string
isConversion: number
listSource: number //线
state: stateEnum //
situation: number //
createTime: string //
telemarketingTeacherName: string //
recruitTeacherName: string //
recruitTeacherId: number
telemarketingTeacherId: number
}
const props = defineProps({
item: {
type: Object,
type: Object as PropType<IClue>,
default: () => ({})
}
})
const ellipsisDesc = computed(
() => item => item.desc?.length >= 14 ? item.desc?.slice(0, 14) + '...' : item.desc
() => (item: IClue) =>
item.basicInformation?.length >= 14
? item.basicInformation?.slice(0, 14) + '...'
: item.basicInformation
)
const handleEditFollow = () => {
const { item } = props

View File

@ -2,57 +2,86 @@
<view class="mt-[32rpx] p-[32rpx] bg-white">
<view class="flex justify-between items-center mb-[20rpx]">
<text class="text-[44rpx] font-bold">跟进信息</text>
<text class="text-[28rpx]">全部清</text>
<text class="text-[28rpx]" @click="handleClear"></text>
</view>
<TForm ref="tForm" :model="form" :rules="rules" errorType="toast">
<TFormItem prop="clientName">
<TFormItem prop="studentName">
<TInputField
v-model="form.clientName"
v-model="form.studentName"
label="客户姓名"
placeholder="请填写客户姓名(必填)"
inputAlign="left"
/>
</TFormItem>
<TFormItem prop="mobile">
<TFormItem prop="phone">
<TInputField
v-model="form.mobile"
v-model="form.phone"
label="电话"
placeholder="请填写电话(必填)"
inputAlign="left"
/>
</TFormItem>
<TFormItem prop="desc">
<TTextareaField label="基本情况" placeholder="请填写基本情况(必填)" autoHeight />
<TFormItem prop="basicInformation">
<TTextareaField
v-model="form.basicInformation"
label="基本情况"
placeholder="请填写基本情况(必填)"
autoHeight
/>
</TFormItem>
</TForm>
<u-button class="btn" color="#0E66FB" shape="circle" @click="handleConfirm"></u-button>
<u-button
class="btn"
color="#0E66FB"
shape="circle"
:loading="loading"
@click="handleConfirm"
>
确认
</u-button>
</view>
</template>
<script setup lang="ts">
import { apiAddCluse } from '@/api/clue'
import { toast, validate } from '@/utils/util'
import { validateContact } from '@/utils/validate'
import { ref } from 'vue'
const tForm = ref()
const form = ref({
clientName: '',
mobile: '',
desc: ''
studentName: '',
phone: '',
basicInformation: ''
})
const loading = ref(false)
const rules = {
clientName: [{ required: true, message: '请填写客户姓名', trigger: 'blur' }],
mobile: [{ required: true, message: '请填写电话', trigger: 'blur' }],
desc: [{ required: true, message: '请填写基本情况', trigger: 'blur' }]
studentName: [{ required: true, message: '请填写客户姓名', trigger: 'blur' }],
phone: [
{ required: true, message: '请填写电话', trigger: 'blur' },
{ validator: validateContact, trigger: 'blur' }
],
basicInformation: [{ required: true, message: '请填写基本情况', trigger: 'blur' }]
}
const handleConfirm = () => {
tForm.value
.validate()
.then(valid => {
.then(async valid => {
if (valid) {
console.log('校验通过')
loading.value = true
try {
await apiAddCluse(form.value)
toast('添加成功')
handleClear()
} catch (error) {}
loading.value = false
}
})
.catch(() => {})
}
const handleClear = () => {
tForm.value.resetFields()
}
</script>
<style scoped lang="scss">

View File

@ -1,46 +1,47 @@
<template>
<view class="flex-1 h-full flex flex-col">
<TSearch
v-model="searchValue"
v-model="queryParams.likeWork"
placeholder="搜索客户姓名/手机号码"
backgroundColor="#F5F5F5"
@on-input="searchChange"
/>
<view class="flex-1 mt-3 px-2 overflow-auto bg-[#FAFAFE]">
<!-- <z-paging
<z-paging
ref="paging"
v-model="dataList"
@query="queryList"
:fixed="false"
height="100%"
> -->
<clue-card
v-for="(item, index) in dataList"
:key="`${index} + 'unique'`"
:item="item"
/>
<!-- </z-paging> -->
>
<clue-card
v-for="(item, index) in dataList"
:key="`${index} + 'unique'`"
:item="item"
/>
</z-paging>
</view>
</view>
</template>
<script setup lang="ts">
import { apiOverhaulPagelist } from '@/api/overhaul'
import { useZPaging } from '@/hooks/useZPaging'
import { ref } from 'vue'
import clueCard from './clue-card.vue'
import { apiCluseList } from '@/api/clue'
import { debounce } from 'lodash-es'
const searchValue = ref('')
const queryParams = ref({})
const dataList = ref([
{
id: 1
}
])
const queryParams = ref({
likeWork: ''
})
const dataList = ref([])
const { paging, queryList, refresh, changeApi, setParams } = useZPaging(
queryParams.value,
apiOverhaulPagelist,
apiCluseList,
() => {}
)
const searchChange = debounce(() => {
refresh()
}, 300)
</script>
<style scoped></style>

View File

@ -6,8 +6,8 @@ export enum ThemeEnum {
DARK = 'dark'
}
export enum ChannelEnum {
USER_PLATFORM = 0,
STAFF_PLATFORM = 1
USER_PLATFORM = 0, // 用户端
STAFF_PLATFORM = 1 //师傅端
}
// 客户端
export enum ClientEnum {

19
src/enums/index.ts 100644
View File

@ -0,0 +1,19 @@
export enum teleSaleEnum {
ADD_FOLLOW = 0,
FOLLOW_RECORD = 1
}
export enum converStatusEnum {
INTENTION = 0, //有意向
UN_RECEIVED = 1, //待领取
CONVERTED_PROCESS = 2, //转化中
ADD_RELATION = 3, //已添加
EXCEPTION = 4, //异常待处理
CONVERTED = 5, //已成交
FAILED = 6 //已战败
}
export enum stateEnum {
ADD_RELATION = 0, //账号已添加
NO_EXIST = 1, //账号不存在
UN_PASS = 2 //账号未通过
}

View File

@ -0,0 +1,21 @@
import { apiCluseDetail } from '@/api/clue'
import { ref } from 'vue'
// 获取线索详情
export function useClueDetail() {
const clueDetailInfo = ref()
const fetchClueDetail = async (id: number | string) => {
uni.showLoading({
title: '加载中'
})
try {
const result = await apiCluseDetail({ id })
clueDetailInfo.value = result
} catch (error) {}
uni.hideLoading()
}
return {
clueDetailInfo,
fetchClueDetail
}
}

View File

@ -23,110 +23,50 @@ export interface RoleItem {
export function useRoleData() {
const roles: RoleItem[] = [
{
name: '业务员',
ids: [8, 9],
name: '电销老师',
ids: [5],
tabBarList: [
{
text: '客户库',
text: '首页',
path: '/pages/telesale/home/index',
inactiveIcon: clientInActive,
activeIcon: clientActive,
path: '/pages/salesman/client/index'
activeIcon: clientActive
},
{
text: '合同管理',
path: '/pages/salesman/contract/index',
text: '总结模板',
path: '/pages/telesale/summary/index',
inactiveIcon: contractInActive,
activeIcon: contractActive
},
{
text: '个人中心',
path: '/pages/salesman/profile/index',
path: '/pages/telesale/my/index',
inactiveIcon: profileInActive,
activeIcon: profileActive
}
]
},
{
name: '检修员',
name: '招生老师',
ids: [6],
tabBarList: [
{
text: '工单池',
text: '首页',
inactiveIcon: order,
activeIcon: orderActive,
path: '/pages/overhaul/pool/index'
path: '/pages/recruitsale/home/index'
},
{
text: '我的任务',
text: '总结模板',
path: '/pages/recruitsale/summary/index',
inactiveIcon: task,
activeIcon: taskActive,
path: '/pages/overhaul/task/index'
activeIcon: taskActive
},
{
text: '个人中心',
path: '/pages/recruitsale/my/index',
inactiveIcon: profileInActive,
activeIcon: profileActive,
path: '/pages/overhaul/my/index'
}
]
},
{
name: '客户',
ids: [1, 2, 11],
tabBarList: [
{
text: '工单记录',
inactiveIcon: order,
activeIcon: orderActive,
path: '/pages/client/workOrder/index'
},
{
text: '个人中心',
inactiveIcon: profileInActive,
activeIcon: profileActive,
path: '/pages/client/my/index'
}
]
},
{
name: '维修员',
ids: [7],
tabBarList: [
// {
// text: '工单池',
// inactiveIcon: order,
// activeIcon: orderActive,
// path: '/pages/repair/pool/index'
// },
{
text: '工单列表',
inactiveIcon: task,
activeIcon: taskActive,
path: '/pages/repair/task/index'
},
{
text: '个人中心',
inactiveIcon: profileInActive,
activeIcon: profileActive,
path: '/pages/repair/my/index'
}
]
},
{
name: '维修主管',
ids: [10],
tabBarList: [
{
text: '工单列表',
inactiveIcon: task,
activeIcon: taskActive,
path: '/pages/charge/order/index'
},
{
text: '个人中心',
inactiveIcon: profileInActive,
activeIcon: profileActive,
path: '/pages/charge/my/index'
activeIcon: profileActive
}
]
}

View File

@ -1,29 +1,56 @@
{
"pages": [
{
"path": "pages/recruitsale/home/index",
"path": "pages/index/index",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/recruitsale/summary/index",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/telesale/summary/index",
"style": {
"navigationBarTitleText": ""
"navigationBarTitleText": "首页",
"navigationStyle": "custom"
}
},
{
"path": "pages/telesale/home/index",
"style": {
"navigationBarTitleText": ""
},
"auth": true
},
{
"path": "pages/recruitsale/home/index",
"style": {
"navigationBarTitleText": ""
},
"auth": true
},
{
"path": "pages/telesale/summary/index",
"style": {
"navigationBarTitleText": ""
},
"auth": true
},
{
"path": "pages/recruitsale/summary/index",
"style": {
"navigationBarTitleText": ""
},
"auth": true
},
{
"path": "pages/recruitsale/my/index",
"style": {
"navigationBarTitleText": "个人中心",
"navigationStyle": "custom"
},
"auth": true
},
{
"path": "pages/telesale/my/index",
"style": {
"navigationBarTitleText": "个人中心",
"navigationStyle": "custom"
}
},
{
"path": "pages/salesman/contract/index",
"style": {
@ -44,21 +71,6 @@
},
"auth": true
},
{
"path": "pages/client/my/index",
"style": {
"navigationBarTitleText": "个人中心",
"navigationStyle": "custom"
},
"auth": true
},
{
"path": "pages/salesman/profile/index",
"style": {
"navigationBarTitleText": "个人中心",
"navigationStyle": "custom"
}
},
{
"path": "pages/overhaul/pool/index",
"style": {

View File

@ -1,86 +1,19 @@
<!--
* @Author: micky
* @Date: 2024-08-10 15:24:06
* @LastEditors: micky
* @LastEditTime: 2024-08-28 18:39:12
* @FilePath: \chargingpile-uniapp\src\pages\index\index.vue
-->
<template>
<view class="wrapper">
<swiper class="swiper" @change="swiperChange">
<swiper-item v-for="(item, index) in list" :key="index" class="item">
<image :src="item.image"></image>
<view class="title">{{ item.title }}</view>
<view class="desc">{{ item.desc }}</view>
</swiper-item>
</swiper>
<view class="rowDot">
<view v-for="(item, index) in list" :key="index" class="dots">
<view :class="['dot', index === swiperCurrent ? 'active' : '']"></view>
</view>
</view>
<u-button
type="primary"
class="login-btn"
shape="circle"
v-if="swiperCurrent == 3"
@click="toPage"
>
立即登录
</u-button>
</view>
<view></view>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import Step1 from '@/static/images/step_1.png'
import Step2 from '@/static/images/step_2.png'
import Step3 from '@/static/images/step_3.png'
import Step4 from '@/static/images/step_4.png'
import { onLoad } from '@dcloudio/uni-app'
import { useUserStore } from '@/stores/user'
import { ROLEINDEX } from '@/enums/cacheEnums'
import { useRoleData } from '@/hooks/useRoleData'
import { useTabBarStore } from '@/stores/tabbar'
import { useUserStore } from '@/stores/user'
import cache from '@/utils/cache'
import { ROLEINDEX } from '@/enums/cacheEnums'
import { onLoad } from '@dcloudio/uni-app'
const userStore = useUserStore()
const { roles } = useRoleData()
const tabBarStore = useTabBarStore()
const swiperCurrent = ref(0)
const list = reactive([
{
image: Step1,
title: '用户报修',
desc: '用户可以通过在线报修、电话报修或扫码报修等多种方式提交维修请求。'
},
{
image: Step2,
title: '桩点检修',
desc: '检修员接单后将上门对客户上报的桩点进行现场维修。'
},
{
image: Step3,
title: '上门巡检',
desc: '检修员在巡检过程中发现设备异常时需提交检修单。'
},
{
image: Step4,
title: '模块维修',
desc: '维修员通过扫描模块条形码来创建维修工单。'
}
])
const swiperChange = e => {
swiperCurrent.value = e.detail.current
}
const toPage = () => {
uni.redirectTo({
url: '/bundle/pages/login/login'
})
}
const setNextRoute = () => {
const ind = cache.get(ROLEINDEX) || 0
const { userInfo } = userStore
@ -100,51 +33,4 @@ onLoad(() => {
}
})
</script>
<style lang="scss" scoped>
.wrapper {
@apply h-full relative z-10;
.swiper {
@apply px-[30px] h-[400px] pt-[150px];
.item {
@apply flex flex-col items-center;
}
image {
@apply w-[100vw] h-[310px] mb-[48rpx];
line-height: 90rpx;
}
.title {
@apply text-[24px] text-[#2F2F52] mb-[15px];
}
.desc {
@apply text-[#334155];
}
}
.rowDot {
@apply flex justify-center mt-[66px];
.dots {
flex-direction: row;
justify-content: center;
align-items: center;
align-content: center;
.dot {
margin-right: 8rpx;
width: 20rpx;
height: 8rpx;
opacity: 1;
border-radius: 6rpx;
background: #efefef;
}
.dot.active {
width: 48rpx;
background: #1a66ff;
}
}
}
:deep(.login-btn) {
button {
@apply w-[200px] h-[40px] -mt-[20px];
}
}
}
</style>
<style scoped></style>

View File

@ -1,7 +1,7 @@
<template>
<view class="flex-1 h-screen flex flex-col">
<TContainer>
<TSearch
v-model="queryParams.searchValue"
v-model="queryParams.likeWork"
placeholder="搜索客户姓名/手机号码"
backgroundColor="#F5F5F5"
showBorder
@ -13,27 +13,26 @@
:activeStyle="{ color: '#0E66FB' }"
lineWidth="49"
lineColor="#0E66FB"
:current="activeTab"
@change="handleChangeTab"
></u-tabs>
<view class="flex-1 pt-[24rpx] px-[24rpx] overflow-auto bg-[#F8F8F8]">
<!-- <z-paging
<z-paging
ref="paging"
v-model="dataList"
@query="queryList"
:fixed="false"
height="100%"
> -->
<clue-card
v-for="(item, index) in dataList"
:key="`${index} + 'unique'`"
:item="item"
@handle-update-remark="handleUpdateRemark"
/>
<!-- </z-paging> -->
>
<clue-card
v-for="(item, index) in dataList"
:key="`${index} + 'unique'`"
:item="item"
@handle-update-remark="handleUpdateRemark"
@refresh-page="refresh"
/>
</z-paging>
</view>
</view>
</TContainer>
<w-confirm-popup v-model="popupShow" @confirm="handleConfirm">
<template #content>
<TTextareaField v-model="contentText" border="surround" :required="false" />
@ -48,40 +47,33 @@ import { ref } from 'vue'
import clueCard from '@/components/widgets/recruitsale/clue-card.vue'
import { shallowRef } from 'vue'
import { computed } from 'vue'
import { apiCluseList } from '@/api/clue'
import { converStatusEnum } from '@/enums'
import { onLoad } from '@dcloudio/uni-app'
const tabs = shallowRef([
{ name: '待领取', value: 0 },
{ name: '转化中', value: 1 },
{ name: '已成交', value: 2 },
{ name: '已战败', value: 3 }
{ name: '待领取', value: converStatusEnum.UN_RECEIVED },
{ name: '转化中', value: converStatusEnum.CONVERTED_PROCESS },
{ name: '已成交', value: converStatusEnum.CONVERTED },
{ name: '已战败', value: converStatusEnum.FAILED }
])
const activeTab = ref(0)
const activeTab = ref(converStatusEnum.UN_RECEIVED)
const queryParams = computed(() => {
const payload = {
status: activeTab.value,
searchValue: ''
situation: activeTab.value,
likeWork: ''
}
return payload
})
const dataList = ref([
{
id: 1
},
{
id: 2
},
{
id: 3
}
])
const dataList = ref([])
const { paging, queryList, refresh, changeApi, setParams } = useZPaging(
queryParams.value,
apiOverhaulPagelist,
apiCluseList,
() => {}
)
const handleChangeTab = item => {
activeTab.value = item.value
// refresh(queryParams.value)
refresh(queryParams.value)
}
const popupShow = ref(false)
const contentText = ref('')
@ -91,5 +83,10 @@ const handleUpdateRemark = item => {
contentText.value = remark
}
const handleConfirm = () => {}
onLoad(() => {
uni.$on('refreshPage', () => {
refresh(queryParams.value)
})
})
</script>
<style scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<TProfile :isShow="false"></TProfile>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss"></style>

View File

@ -1,9 +1,15 @@
<template>
<view class="pb-[24rpx]">
<w-date-more :curDate="curDate" type="recruitsale" />
<date-strip v-model="curDate" @change="handleDateChange" height="160rpx" />
<w-summary-form v-model="templateItems" @handle-confirm="handleConfirm" :readonly="type" />
</view>
<TContainer>
<view class="pb-[24rpx]">
<w-date-more :curDate="curDate" type="recruitsale" />
<date-strip v-model="curDate" @change="handleDateChange" height="160rpx" />
<w-summary-form
v-model="templateItems"
@handle-confirm="handleConfirm"
:readonly="type"
/>
</view>
</TContainer>
</template>
<script setup lang="ts">

View File

@ -1,5 +1,5 @@
<template>
<view class="flex flex-col h-screen">
<TContainer>
<u-tabs
:list="tabs"
:scrollable="false"
@ -11,10 +11,10 @@
@change="handleChangeTab"
></u-tabs>
<view class="flex-1 bg-gray3">
<follow-form v-if="activeTab === 0" />
<follow-record v-else-if="activeTab === 1" />
<follow-form v-if="activeTab === teleSaleEnum.ADD_FOLLOW" />
<follow-record v-else-if="activeTab === teleSaleEnum.FOLLOW_RECORD" />
</view>
</view>
</TContainer>
</template>
<script setup lang="ts">
@ -22,11 +22,12 @@ import { shallowRef } from 'vue'
import { ref } from 'vue'
import followForm from '@/components/widgets/telesale/follow-form.vue'
import followRecord from '@/components/widgets/telesale/follow-record.vue'
import { teleSaleEnum } from '@/enums'
const activeTab = ref(1)
const activeTab = ref(teleSaleEnum.ADD_FOLLOW)
const tabs = shallowRef([
{ name: '新增跟进', value: 0 },
{ name: '跟进动态', value: 1 }
{ name: '新增跟进', value: teleSaleEnum.ADD_FOLLOW },
{ name: '跟进动态', value: teleSaleEnum.FOLLOW_RECORD }
])
const handleChangeTab = item => {
activeTab.value = item.value

View File

@ -0,0 +1,7 @@
<template>
<TProfile :isShow="false"></TProfile>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss"></style>

View File

@ -3,6 +3,7 @@
<w-date-more :curDate="curDate" type="telesale" />
<date-strip v-model="value" @change="handleDateChange" height="160rpx" />
<w-summary-form v-model="templateItems" @handle-confirm="handleConfirm" />
<tabbar />
</view>
</template>

View File

@ -1,8 +1,8 @@
@font-face {
font-family: 'iconfont'; /* Project id 4837700 */
src: url('//at.alicdn.com/t/c/font_4837700_wjnzp8mer6o.woff2?t=1740542409608') format('woff2'),
url('//at.alicdn.com/t/c/font_4837700_wjnzp8mer6o.woff?t=1740542409608') format('woff'),
url('//at.alicdn.com/t/c/font_4837700_wjnzp8mer6o.ttf?t=1740542409608') format('truetype');
src: url('//at.alicdn.com/t/c/font_4837700_mvrnof6x61s.woff2?t=1740644911077') format('woff2'),
url('//at.alicdn.com/t/c/font_4837700_mvrnof6x61s.woff?t=1740644911077') format('woff'),
url('//at.alicdn.com/t/c/font_4837700_mvrnof6x61s.ttf?t=1740644911077') format('truetype');
}
.iconfont {
@ -13,6 +13,14 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-clear:before {
content: '\e601';
}
.icon-search:before {
content: '\e67d';
}
.icon-left:before {
content: '\e83d';
}

View File

@ -60,11 +60,11 @@ export const useAppStore = defineStore({
return url ? `${this.config.domain}${url}` : ''
// return url || ''
},
async getConfig() {
const data = await apiConfig()
this.config = data
cache.set(CONFIG, data)
},
// async getConfig() {
// const data = await apiConfig()
// this.config = data
// cache.set(CONFIG, data)
// },
// setCityInfo(city: City) {
// this.cityInfo = city

View File

@ -5,7 +5,7 @@
* @LastEditTime: 2024-10-27 18:19:43
* @FilePath: \chargingpile-uniapp\src\stores\user.ts
*/
import { apiUserInfo, getUserCenter } from '@/api/user'
import { apiUserInfo } from '@/api/user'
import { TOKEN_KEY, USER_INFO, CONFIG, ROLEINDEX } from '@/enums/cacheEnums'
import cache from '@/utils/cache'
import { defineStore } from 'pinia'
@ -48,14 +48,6 @@ export const useUserStore = defineStore({
const data = await apiUserInfo({
token: this.token || this.temToken
})
let roleArr = []
for (const key in data.roleMap) {
roleArr.push({
name: data.roleMap[key],
id: Number(key)
})
}
data.roles = roleArr
this.userInfo = data
cache.set(USER_INFO, data)
}

View File

@ -288,3 +288,8 @@ page {
}
}
}
.u-tabbar {
.u-tabbar__content {
z-index: 99999 !important;
}
}

View File

@ -0,0 +1,15 @@
const mobileReg =
/^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[189]))\d{8}$/
/**手机号码 */
export function validateContact(rule: any, value: any, callback: any) {
if (value) {
if (!mobileReg.test(value)) {
callback(new Error('请输入正确的电话'))
} else {
callback()
}
} else {
callback()
}
}

View File

@ -31,7 +31,8 @@ module.exports = {
lightblack: '#3D3D3D',
border: '#F1F1F1',
border2: '#F3F3F3',
blue2: '#EEF6FF'
blue2: '#EEF6FF',
orage: '#ED6D41'
},
fontSize: {
xs: '24rpx',