index.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. <route lang="json">
  2. {
  3. "style": {
  4. "navigationBarTitleText": "全部设计师",
  5. "navigationBarBackgroundColor": "#fff"
  6. }
  7. }
  8. </route>
  9. <script setup lang="ts">
  10. import Card from '@/components/card.vue'
  11. import DataForm from '@/components/data-form.vue'
  12. import PageHelperEvo from '@/components/page-helper-evo.vue'
  13. import { createFollowUp, focusOrCancel, getDesigners } from '../../../core/libs/agent-requests'
  14. import { filterIcon } from '@designer-hub/assets/src/svgs'
  15. import { Designer } from '@designer-hub/app/src/core/libs/models'
  16. import { requestToast } from '@designer-hub/app/src/core/utils/common'
  17. import { ComponentExposed } from 'vue-component-type-helpers'
  18. import { useFollowUp } from '../../../composables/followUp'
  19. import SectionHeading from '@designer-hub/app/src/components/section-heading.vue'
  20. import { useMemberLevelsStore } from '../../../store/member-levles'
  21. import { storeToRefs } from 'pinia'
  22. import { useUserStore } from '../../../store'
  23. import link from '@designer-hub/assets/src/libs/assets/link'
  24. import { beforeNow } from '@/utils/date-util'
  25. import { NetImages } from '@/core/libs/enums'
  26. import AMapWX from '@/pages/common/amap-wx.130'
  27. import { addUnit } from 'wot-design-uni/components/common/util'
  28. const action = ref(`${import.meta.env.VITE_SERVER_BASEURL}/app-api/infra/file/upload`)
  29. const userStore = useUserStore()
  30. const { userInfo } = storeToRefs(userStore)
  31. const searchText = ref('')
  32. const publishState = ref(false)
  33. const filterState = ref(false)
  34. const fileList = ref<string[]>([])
  35. const pageHelperRef = ref<ComponentExposed<typeof PageHelperEvo>>()
  36. const schemaTypeOnlineRef = ref<ComponentExposed<typeof DataForm>>()
  37. const { schema, rules, schemaTypeOnline } = useFollowUp()
  38. const memberLevelsStore = useMemberLevelsStore()
  39. const { memberLevels } = storeToRefs(memberLevelsStore)
  40. const { getMemberLevelLogo } = memberLevelsStore
  41. const followUpForm = ref({
  42. stylistId: '',
  43. followType: '1',
  44. followTime: new Date().getTime(),
  45. address: {
  46. latitude: 0,
  47. longitude: 0,
  48. address: '',
  49. },
  50. imgUrl: '',
  51. })
  52. const followUpFormRef = ref({})
  53. const currentAddress = ref<any>([])
  54. // 地图实例化
  55. const AmapFun = new AMapWX.AMapWX({ key: 'efde483f8801a09d3c5db032556e6593' })
  56. const wxGetAddress = (longitude: number, latitude: number) => {
  57. return new Promise((resolve, reject) => {
  58. AmapFun.getRegeo({
  59. location: `${longitude},${latitude}`,
  60. success: (res: any) => {
  61. resolve(res)
  62. },
  63. fail: (err: any) => {
  64. reject(err)
  65. },
  66. })
  67. })
  68. }
  69. const getCurrentLocation = () => {
  70. console.log('点击地址')
  71. uni.getLocation({
  72. type: 'gcj02',
  73. success: async (success: any) => {
  74. currentAddress.value = await wxGetAddress(success?.longitude, success?.latitude)
  75. const { name, latitude, longitude } = currentAddress.value[0]
  76. followUpForm.value.address.address = name
  77. followUpForm.value.address.latitude = latitude
  78. followUpForm.value.address.longitude = longitude
  79. console.log('提交信息:::', followUpForm.value)
  80. },
  81. fail: (err) => {
  82. console.log('获取地址失败', err)
  83. if (err.errCode === 1005 || err.errCode === 10001) {
  84. console.log('位置权限未授权')
  85. uni.authorize({
  86. scope: 'scope.userLocation',
  87. success: () => {
  88. uni.getLocation({
  89. type: 'gcj02',
  90. success: async (success) => {
  91. console.log('授权后,获取地址', success)
  92. currentAddress.value = await wxGetAddress(success?.longitude, success?.latitude)
  93. const { name, latitude, longitude } = currentAddress.value[0]
  94. followUpForm.value.address.address = name
  95. followUpForm.value.address.latitude = latitude
  96. followUpForm.value.address.longitude = longitude
  97. console.log('提交信息:::', followUpForm.value)
  98. },
  99. fail: function (err) {
  100. console.log('获取位置失败:', err)
  101. },
  102. })
  103. },
  104. fail: function () {
  105. console.log('用户拒绝授权,不再提示')
  106. // 用户拒绝授权,可以选择记录下来,不再提示
  107. uni.showToast({
  108. title: '您拒绝了位置授权',
  109. icon: 'none',
  110. duration: 2000,
  111. })
  112. // 可以引导用户去设置中授权
  113. uni.showModal({
  114. title: '提示',
  115. content: '请在系统设置中打开定位服务权限',
  116. success: function (modalRes) {
  117. if (modalRes.confirm) {
  118. uni.openSetting()
  119. }
  120. },
  121. })
  122. },
  123. })
  124. }
  125. },
  126. })
  127. }
  128. const filterQuery = ref<{
  129. tags: any[]
  130. levels: any[]
  131. retryStatus: any[]
  132. minPoints?: string
  133. maxPoints?: string
  134. brokerId?: string
  135. recommend?: boolean
  136. name?: string
  137. }>({
  138. tags: [],
  139. levels: [],
  140. retryStatus: [],
  141. brokerId: String(userInfo.value.userId),
  142. })
  143. const query = ref({})
  144. const searchFocus = async () => {
  145. console.log('focus')
  146. }
  147. const searchBlur = async () => {
  148. console.log('Blur')
  149. query.value = {
  150. ...filterQuery.value,
  151. tags: filterQuery.value.tags.join(','),
  152. levels: filterQuery.value.levels.join(','),
  153. }
  154. await pageHelperRef.value?.refresh()
  155. }
  156. const search = async () => {
  157. console.log('search')
  158. query.value = {
  159. ...filterQuery.value,
  160. tags: filterQuery.value.tags.join(','),
  161. levels: filterQuery.value.levels.join(','),
  162. }
  163. await pageHelperRef.value?.refresh()
  164. }
  165. const cancelSearch = () => {
  166. console.log('cancel')
  167. }
  168. const searchChange = (e: any) => {
  169. console.log(e)
  170. }
  171. const toDetail = async (designer: any) => {
  172. await uni.navigateTo({ url: '/pages/agent/designer/detail' + '?id=' + designer.id })
  173. }
  174. const callPhone = (phoneNumber: string) => {
  175. uni.makePhoneCall({
  176. phoneNumber,
  177. })
  178. }
  179. const filterData = () => {
  180. filterState.value = true
  181. }
  182. const handleImportant = async (designer: Designer) => {
  183. const { code } = await requestToast(
  184. () =>
  185. focusOrCancel({
  186. brokerId: Number(userInfo.value.userId),
  187. userId: Number(designer.id),
  188. }),
  189. {
  190. success: true,
  191. successTitle: '操作成功',
  192. },
  193. )
  194. if (code === 0) {
  195. await pageHelperRef.value?.refresh()
  196. }
  197. }
  198. const saveFollowUp = (item: any) => {
  199. console.log(item)
  200. schema.value.stylistId.props.columns = [{ value: item.id, label: item.name }]
  201. followUpForm.value.stylistId = item.id
  202. followUpForm.value.address.address = ''
  203. followUpForm.value.address.latitude = 0
  204. followUpForm.value.address.longitude = 0
  205. followUpForm.value.imgUrl = ''
  206. publishState.value = true
  207. }
  208. const createFollowUpSubmit = async () => {
  209. const { valid } = await followUpFormRef.value.validate()
  210. if (!valid) {
  211. return
  212. }
  213. if (fileList.value.length) {
  214. const temp: string[] = []
  215. fileList.value.forEach((each: any) => {
  216. temp.push(JSON.parse(each.response).data)
  217. })
  218. followUpForm.value.imgUrl = temp.join(',')
  219. }
  220. if (!followUpForm.value.imgUrl) {
  221. uni.showToast({ icon: 'none', title: '请上传图片' })
  222. return false
  223. }
  224. if (!followUpForm.value.address && followUpForm.value.followType === '1') {
  225. uni.showToast({ icon: 'none', title: '请刷新定位' })
  226. return false
  227. }
  228. const { code } = await requestToast(() => createFollowUp(followUpForm.value), {
  229. success: true,
  230. successTitle: '跟进成功',
  231. })
  232. if (code === 0) {
  233. publishState.value = false
  234. }
  235. }
  236. const handleChange = ({ fileList: files }) => {
  237. fileList.value = files
  238. console.log(fileList.value)
  239. }
  240. const handleSubmit = () => {
  241. query.value = {
  242. ...filterQuery.value,
  243. tags: filterQuery.value.tags ? filterQuery.value.tags.join(',') : '',
  244. levels: filterQuery.value.levels ? filterQuery.value.levels.join(',') : '',
  245. retryStatus: filterQuery.value.retryStatus ? filterQuery.value.retryStatus.join(',') : '',
  246. }
  247. filterState.value = false
  248. }
  249. const handleReset = () => {
  250. filterQuery.value = { tags: [], levels: [], brokerId: String(userInfo.value.userId) }
  251. query.value = {}
  252. }
  253. onLoad(async (params: { title?: string; filter?: string; tags?: string }) => {
  254. if (params.title) {
  255. uni.setNavigationBarTitle({ title: params.title })
  256. }
  257. if (params.filter) {
  258. const filter = JSON.parse(params.filter) as { tags: '' }
  259. }
  260. if (params.tags) {
  261. filterQuery.value.tags = params.tags.split(',')
  262. }
  263. query.value = {
  264. ...filterQuery.value,
  265. tags: filterQuery.value.tags.join(','),
  266. levels: filterQuery.value.levels.join(','),
  267. }
  268. })
  269. </script>
  270. <template>
  271. <view class="flex-grow">
  272. <PageHelperEvo ref="pageHelperRef" :request="getDesigners" :query="query">
  273. <template #top>
  274. <div class="flex items-center justify-between bg-white pr-3.5">
  275. <div class="flex-1">
  276. <wd-search
  277. v-model="filterQuery.name"
  278. placeholder="输入设计师姓名模糊搜索"
  279. @focus="searchFocus"
  280. @blur="searchBlur"
  281. @search="search"
  282. @cancel="cancelSearch"
  283. @change="searchChange"
  284. hide-cancel
  285. />
  286. </div>
  287. <wd-img :src="filterIcon" width="22px" height="22px" @click="filterData"></wd-img>
  288. </div>
  289. </template>
  290. <template #default="{ source }">
  291. <div class="p-3.5 gap-3.5 flex flex-col">
  292. <template v-for="(it, i) in source?.list" :key="i">
  293. <Card>
  294. <div class="items-center" @click="toDetail(it)">
  295. <div class="">
  296. <div class="flex items-center">
  297. <div
  298. class="w-[55px] h-[55px] bg-neutral-100 rounded-full mr-2 flex items-center justify-center relative"
  299. >
  300. <wd-img
  301. width="100%"
  302. height="100%"
  303. round
  304. :src="it.avatar || NetImages.DefaultAvatar"
  305. ></wd-img>
  306. <div v-if="it.retryStatus === 1" class="absolute right-0 bottom--1">
  307. <wd-img width="14" height="14" :src="link"></wd-img>
  308. </div>
  309. </div>
  310. <div class="flex flex-col flex-1">
  311. <div class="flex-row flex items-center justify-between w-full">
  312. <div class="flex-row flex items-center">
  313. <div class="text-black/90 text-base font-normal font-['PingFang_SC']">
  314. <!-- 苏小萌 -->
  315. {{ it.name }}
  316. </div>
  317. <div
  318. class="h-4 rounded-[20px] justify-start items-center inline-flex flex-row ml-[9px]"
  319. >
  320. <div
  321. v-if="it.recommend"
  322. class="text-[10px] bg-[#fff3e4] px-[4px] py-[4px] c-[#f2a64f] rounded-[3px]"
  323. >
  324. 推荐设计师
  325. </div>
  326. <wd-img
  327. v-if="it.levelId"
  328. width="63"
  329. height="18.6"
  330. custom-class="ml-[9px]"
  331. :src="getMemberLevelLogo(Number(it.levelId))"
  332. ></wd-img>
  333. </div>
  334. </div>
  335. <!-- <div-->
  336. <!-- class="text-black/60 text-xs font-normal font-['PingFang_SC'] leading-snug flex items-center"-->
  337. <!-- @click.stop="toHomePage(it.id)"-->
  338. <!-- >-->
  339. <!-- <div>个人主页</div>-->
  340. <!-- <wd-img width="13" height="13" :src="rightArrowIcon"></wd-img>-->
  341. <!-- </div>-->
  342. </div>
  343. <div class="flex items-center gap-2 mt-[18px]">
  344. <div
  345. class="text-black/30 text-xs font-normal font-['PingFang_SC'] leading-none"
  346. >
  347. {{ it.accessTime ? `${beforeNow(new Date(it.accessTime))}跟进` : '' }}
  348. </div>
  349. <div class="bg-[#eeeeee] w-[2px] h-[10px]"></div>
  350. <div
  351. class="text-black/30 text-xs font-normal font-['PingFang_SC'] leading-none"
  352. >
  353. 积分:{{ it.points || 0 }}
  354. </div>
  355. </div>
  356. </div>
  357. </div>
  358. </div>
  359. <div class="row-start-2 col-start-2 col-end-4">
  360. <div
  361. class="flex items-center w-full mt-[20px]"
  362. style="justify-content: flex-start"
  363. >
  364. <div v-if="!it.followUp30Days" class="flex items-center justify-center w-[30%]">
  365. <div class="w-2 h-2 bg-[#89f4e2] rounded-full mr-[7px]"></div>
  366. <div
  367. class="text-black/90 text-xs font-normal font-['PingFang_SC'] leading-snug"
  368. >
  369. 30天未跟进
  370. </div>
  371. </div>
  372. <div
  373. class="flex items-center justify-center w-[35%]"
  374. v-if="!it.generatePoints60Days"
  375. >
  376. <div class="w-2 h-2 bg-[#ffb96a] rounded-full mr-[7px]"></div>
  377. <div
  378. class="text-black/90 text-xs font-normal font-['PingFang_SC'] leading-snug"
  379. >
  380. 60天未产生积分
  381. </div>
  382. </div>
  383. <div
  384. class="flex items-center justify-center w-[35%]"
  385. v-if="!it.expendPoints60Days"
  386. >
  387. <div class="w-2 h-2 bg-[#c493ff] rounded-full mr-[7px]"></div>
  388. <div
  389. class="text-black/90 text-xs font-normal font-['PingFang_SC'] leading-snug"
  390. >
  391. 60天未消耗积分
  392. </div>
  393. </div>
  394. </div>
  395. </div>
  396. <div
  397. class="row-start-5 col-start-2 col-end-4 flex items-center mt-[26px] justify-around"
  398. >
  399. <div
  400. v-if="!it.focus"
  401. class="px-3 py-1.5 rounded-[30px] border border-solid border-[#ff2d2d] justify-center items-center gap-1 flex"
  402. @click.stop="handleImportant(it)"
  403. >
  404. <!-- <span style="color: #ff2d2d" class="flex items-center">+</span> -->
  405. <wd-icon name="add" color="#ff2d2d" size="10"></wd-icon>
  406. <div
  407. class="text-[#ff2d2d] text-xs font-normal font-['PingFang_SC'] leading-none"
  408. >
  409. 重点跟进
  410. </div>
  411. </div>
  412. <div
  413. v-else
  414. class="px-3 py-1.5 rounded-[30px] border border-solid border-[#dcdcdc] justify-center items-center gap-1 inline-flex"
  415. @click.stop="handleImportant(it)"
  416. >
  417. <!-- 对号图标 -->
  418. <wd-icon name="check-bold" color="#8b8b8b" size="14"></wd-icon>
  419. <div
  420. class="text-[#8b8b8b] text-xs font-normal font-['PingFang_SC'] leading-none"
  421. >
  422. 已重点跟进
  423. </div>
  424. </div>
  425. <div
  426. class="px-5 py-1 bg-[#e1ecff] rounded-[30px] border border-[#2357e9] justify-center items-center gap-1 inline-flex"
  427. @click.stop="callPhone(it.mobile)"
  428. >
  429. <div
  430. class="text-center text-[#2357e9] text-sm font-normal font-['PingFang_SC'] leading-normal"
  431. >
  432. 打电话
  433. </div>
  434. </div>
  435. <div
  436. class="px-5 py-1 bg-[#0052d9] rounded-[30px] justify-center items-center gap-1 inline-flex"
  437. >
  438. <div
  439. class="text-center text-white text-sm font-normal font-['PingFang_SC'] leading-normal"
  440. @click.stop="saveFollowUp(it)"
  441. >
  442. 写跟进
  443. </div>
  444. </div>
  445. </div>
  446. </div>
  447. </Card>
  448. </template>
  449. </div>
  450. </template>
  451. </PageHelperEvo>
  452. </view>
  453. <wd-action-sheet v-model="publishState" title="创建跟进" @close="publishState = false">
  454. <view class="flex flex-col p-4 overflow-y-auto h-[calc(75vh)]">
  455. <div>
  456. <DataForm
  457. ref="followUpFormRef"
  458. :schema="schema"
  459. v-model="followUpForm"
  460. :rules="rules"
  461. direction="horizontal"
  462. ></DataForm>
  463. <!-- 根据 followType 值 区分 线下 线上 -->
  464. <template v-if="followUpForm.followType === '1'">
  465. <div
  466. class="grid mb-4 items-start"
  467. :style="{ 'grid-template-columns': `${addUnit(64)} auto` }"
  468. >
  469. <label class="text-sm font-normal leading-relaxed text-black/60 h-10 flex items-center">
  470. <span
  471. class="text-[#ef4343] text-base font-normal font-['PingFang_SC'] leading-normal visible"
  472. >
  473. *
  474. </span>
  475. 地址
  476. </label>
  477. <div class="wd-input h-[40px] lh-[40px] flex justify-between px-[20px]">
  478. <div>
  479. {{
  480. !followUpForm.address.address ? '点击获取当前位置' : followUpForm.address.address
  481. }}
  482. </div>
  483. <wd-icon name="refresh" size="14px" @click="getCurrentLocation"></wd-icon>
  484. </div>
  485. </div>
  486. </template>
  487. <div
  488. class="grid mb-4 items-start"
  489. :style="{ 'grid-template-columns': `${addUnit(64)} auto` }"
  490. >
  491. <label class="text-sm font-normal leading-relaxed text-black/60 h-10 flex items-center">
  492. <span
  493. class="text-[#ef4343] text-base font-normal font-['PingFang_SC'] leading-normal visible"
  494. >
  495. *
  496. </span>
  497. 图片
  498. </label>
  499. <wd-upload
  500. :file-list="fileList"
  501. image-mode="aspectFill"
  502. accept="media"
  503. :action="action"
  504. :multiple="true"
  505. :limit="9"
  506. @change="handleChange"
  507. ></wd-upload>
  508. </div>
  509. </div>
  510. <div><wd-button block :round="false" @click="createFollowUpSubmit">提交</wd-button></div>
  511. </view>
  512. </wd-action-sheet>
  513. <!-- 筛选action-sheet -->
  514. <wd-action-sheet v-model="filterState" title="筛选" @close="filterState = false">
  515. <view class="flex flex-col p-4 overflow-y-auto h-[calc(75vh)]">
  516. <SectionHeading title="标签"></SectionHeading>
  517. <wd-checkbox-group shape="button" v-model="filterQuery.tags">
  518. <template
  519. v-for="(tag, index) in [
  520. // { label: '全部', value: '' },
  521. { label: '重点跟进', value: '1' },
  522. { label: '本月新增', value: '2' },
  523. { label: '超过30天未跟进', value: '3' },
  524. { label: '超过60天未产生积分', value: '4' },
  525. { label: '超过60天未消耗积分', value: '5' },
  526. { label: '未成交过', value: '6' },
  527. ]"
  528. :key="index"
  529. >
  530. <wd-checkbox custom-class="w-50%!" :model-value="tag.value">
  531. {{ tag.label }}
  532. </wd-checkbox>
  533. </template>
  534. </wd-checkbox-group>
  535. <SectionHeading title="会员等级"></SectionHeading>
  536. <wd-checkbox-group shape="button" v-model="filterQuery.levels">
  537. <template
  538. v-for="(tag, index) in memberLevels.map((it) => ({
  539. label: it.memberLevelName,
  540. value: it.id,
  541. }))"
  542. :key="index"
  543. >
  544. <wd-checkbox custom-class="w-50%!" :model-value="tag.value">{{ tag.label }}</wd-checkbox>
  545. </template>
  546. </wd-checkbox-group>
  547. <SectionHeading title="积分区间"></SectionHeading>
  548. <div class="flex items-center justify-between py-4">
  549. <wd-input
  550. v-model="filterQuery.minPoints"
  551. custom-class="h-10 bg-[#f5f7f9]!"
  552. no-border
  553. ></wd-input>
  554. <div class="w-4 h-.25 bg-black/35"></div>
  555. <wd-input
  556. v-model="filterQuery.maxPoints"
  557. custom-class="h-10 bg-[#f5f7f9]!"
  558. no-border
  559. ></wd-input>
  560. </div>
  561. <SectionHeading title="绑定关系"></SectionHeading>
  562. <wd-checkbox-group shape="button" v-model="filterQuery.retryStatus">
  563. <template
  564. v-for="(tag, index) in [
  565. // { label: '全部', value: '' },
  566. { label: '弱绑定', value: '0' },
  567. { label: '强绑定', value: '1' },
  568. ]"
  569. :key="index"
  570. >
  571. <wd-checkbox custom-class="w-50%!" :model-value="tag.value">
  572. {{ tag.label }}
  573. </wd-checkbox>
  574. </template>
  575. </wd-checkbox-group>
  576. <SectionHeading title="推荐设计师"></SectionHeading>
  577. <wd-checkbox-group shape="button" v-model="filterQuery.recommend">
  578. <template
  579. v-for="(tag, index) in [
  580. // { label: '全部', value: '' },
  581. { label: '否', value: false },
  582. { label: '是', value: true },
  583. ]"
  584. :key="index"
  585. >
  586. <wd-checkbox custom-class="w-50%!" :model-value="tag.value">
  587. {{ tag.label }}
  588. </wd-checkbox>
  589. </template>
  590. </wd-checkbox-group>
  591. <div class="flex gap-4 pt-[10px]">
  592. <div class="flex-1">
  593. <wd-button block :round="false" @click="handleReset">重置</wd-button>
  594. </div>
  595. <div class="flex-1">
  596. <wd-button block :round="false" @click="handleSubmit">提交</wd-button>
  597. </div>
  598. </div>
  599. </view>
  600. </wd-action-sheet>
  601. </template>
  602. <style scoped lang="scss">
  603. .filter-item {
  604. @apply w-[168px] h-10 bg-[#f5f7f9] rounded-lg flex items-center justify-center;
  605. &-text {
  606. @apply text-black/90 text-sm font-normal font-['PingFang_SC'] leading-none;
  607. }
  608. }
  609. </style>