index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. <script lang="ts" setup>
  2. import { addQuestionReportData,REPORTDATA, getQuestionTypeList, pageQuestionReportData,getPerson } from '@/api/questionReqort'
  3. import { AddQuestionReportDataReq, PageQuestionReportDataReq, QuestionListRes, QuestionType, QuestionTypeListRes } from '@/api/questionReqort/types'
  4. import type { ApiResponse, PageResp } from '@/api/types'
  5. import { getCachedWecomVisitorProfile, getWecomInternalUser } from '@/api/wecom'
  6. import { computed, onMounted, ref, watch } from 'vue'
  7. import GridAddress from '@/components/address/gridAddress/index.vue'
  8. import { showNotify } from 'vant'
  9. import dayjs from 'dayjs';
  10. import { onLoad } from '@dcloudio/uni-app'
  11. import useUserState from '@/store/userState'
  12. const active = ref(0)
  13. const userState = useUserState()
  14. interface ReportFormData extends AddQuestionReportDataReq {
  15. location?: string
  16. contactPhone?: string
  17. }
  18. interface GridPerson {
  19. areaName: string
  20. areaCode: string
  21. }
  22. interface AddressOption {
  23. areaName: string
  24. areaCode: string
  25. }
  26. interface AddressSelection {
  27. value: string
  28. tabIndex: number
  29. selectedOptions: AddressOption[]
  30. }
  31. const createInitialFormData = (): ReportFormData => ({
  32. chargerName: '',
  33. chargerCode: '',
  34. contactPerson: '',
  35. contactPersonPhone: '',
  36. contactPhone: '',
  37. questionContent: '',
  38. questionType: '',
  39. addrName: '',
  40. addrDetailName: '',
  41. })
  42. const formData = ref<ReportFormData>(createInitialFormData())
  43. const range = ref<{ text: string; value: string }[]>([])
  44. const rangePerson = ref([]) as any
  45. const rules = ref({
  46. questionType: [{ required: true, message: '请选择类型' }],
  47. // questionTitle: [{ required: true, message: '请输入标题' }],
  48. contactPerson: [{ required: true, message: '请输入联系人' }],
  49. contactPhone: [{ required: true, message: '请输入联系人电话' }],
  50. questionContent: [{ required: true, message: '请输入内容' }],
  51. addrName: [{ required: true, message: '请选择所在地' }],
  52. })
  53. /** 上报问题 */
  54. const personData = ref<GridPerson[]>([])
  55. const addressList = ref<AddressSelection | null>(null)
  56. const handleAddressFinish = async(value: AddressSelection) => {
  57. formData.value.chargerName = ''
  58. formData.value.chargerCode = ''
  59. formData.value.addrName = value.selectedOptions[value.tabIndex]?.areaName || ''
  60. addressList.value = value
  61. const { data } = await getPerson({parentCode:value.value})
  62. personData.value = (data || []) as GridPerson[]
  63. rangePerson.value = personData.value.map((item) => ({
  64. text: item.areaName,
  65. value: item.areaName,
  66. }))
  67. }
  68. /* 获取群主手机号 */
  69. const ownerId = ref(''); // 群主id
  70. onLoad((option) => {
  71. if (option && option.owner) {
  72. ownerId.value = option.owner
  73. }
  74. })
  75. const ownerPhone = ref('')
  76. const visitorPhone = ref('')
  77. const fillVisitorName = () => {
  78. const profile = getCachedWecomVisitorProfile()
  79. if (!profile?.name) return
  80. visitorPhone.value = profile.mobile || ''
  81. formData.value.contactPerson = profile.name
  82. }
  83. const fillVisitorPhone = () => {
  84. const profile = getCachedWecomVisitorProfile()
  85. const mobile = (profile?.mobile || visitorPhone.value || '').trim()
  86. if (!mobile) {
  87. showNotify({ type: 'warning', message: '暂未获取到您的手机号,请手动填写' })
  88. return
  89. }
  90. visitorPhone.value = mobile
  91. formData.value.contactPhone = mobile
  92. formData.value.contactPersonPhone = mobile
  93. }
  94. const getOwnerPhone = async () => {
  95. if (!ownerId.value) return
  96. const res = await getWecomInternalUser(ownerId.value)
  97. ownerPhone.value = res.mobile || ''
  98. }
  99. const submit = async () => {
  100. console.log(`output->formData.value`,formData.value)
  101. if (!formData.value.questionType) {
  102. showNotify('请选择问题类型')
  103. return
  104. }
  105. if (!formData.value.questionContent) {
  106. showNotify('请输入内容')
  107. return
  108. }
  109. if (!formData.value.contactPerson) {
  110. showNotify('请输入联系人姓名')
  111. return
  112. }
  113. if (!formData.value.contactPhone) {
  114. showNotify('请输入联系人电话')
  115. return
  116. }
  117. if (!addressList.value || addressList.value.selectedOptions.length === 0) {
  118. showNotify('请选择所在地')
  119. return
  120. }
  121. formData.value.chargerCode = personData.value.find(
  122. item => item.areaName === formData.value.chargerName
  123. )?.areaCode || '' // 添加兜底处理
  124. formData.value.contactPersonPhone = formData.value.contactPhone || ''
  125. console.log('formData.value',formData.value)
  126. /* 获取群主手机号(不阻塞流程,报错也继续执行) */
  127. try {
  128. await getOwnerPhone();
  129. } catch (e) {
  130. console.error('获取群主手机号失败:', e);
  131. }
  132. const { data } = await addQuestionReportData({
  133. ...formData.value,
  134. ...(ownerPhone.value ? { ownerPhone: ownerPhone.value } : {}),
  135. // personList:formData.value.person1+','+formData.value.person2,
  136. streetCode: addressList.value.selectedOptions[1] ? addressList.value.selectedOptions[1].areaCode : '',
  137. streetName: addressList.value.selectedOptions[1] ? addressList.value.selectedOptions[1].areaName : '',
  138. communityCode: addressList.value.selectedOptions[2] ? addressList.value.selectedOptions[2].areaCode : '',
  139. communityName: addressList.value.selectedOptions[2] ? addressList.value.selectedOptions[2].areaName : '',
  140. grid: addressList.value.selectedOptions[3] ? addressList.value.selectedOptions[3].areaCode : '',
  141. gridName: addressList.value.selectedOptions[3] ? addressList.value.selectedOptions[3].areaName : '',
  142. })
  143. if (data) {
  144. uni.showToast({
  145. title: '上报成功',
  146. icon: 'success',
  147. duration: 1000,
  148. })
  149. formData.value = createInitialFormData()
  150. visitorPhone.value = ''
  151. addressList.value = null
  152. data.districtCode = '330205'
  153. data.districtName = '江北区'
  154. // reportCY(data)
  155. }
  156. }
  157. // 上报到城运
  158. const reportCY = async (params:any) => {
  159. const { data } = await REPORTDATA({
  160. data:{
  161. otherTaskNum:params.uuid.replace(/-/g,''),
  162. // eventTitle:params.questionTitle,
  163. eventDesc:params.questionContent,
  164. eventTypeCode:'109000',
  165. eventTypeName:'民事纠纷',
  166. subTypeCode:'109002',
  167. subTypeName:'邻里纠纷',
  168. reportTime: dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'),
  169. reporter:params.contactPerson,
  170. address:params.districtName+params.streetName+params.communityName,
  171. districtCode:'330205',
  172. districtName:'江北区',
  173. gridCode:params.grid,
  174. gridName:params.gridName,
  175. communityCode:params.communityCode,
  176. communityName:params.communityName,
  177. streetCode:params.streetCode,
  178. streetName:params.streetName,
  179. },
  180. dataId:params.uuid.replace(/-/g,''),
  181. dataType:'main',
  182. timestamp:new Date().getTime(),
  183. })
  184. console.log(data)
  185. if (data.code== '200'){
  186. }
  187. }
  188. /**
  189. * 已上报问题
  190. */
  191. const list = ref<QuestionListRes[]>([])
  192. const pageInfo = ref({
  193. pageNumber: 1,
  194. pageSize: 10,
  195. })
  196. const loading = ref(false)
  197. const finished = ref(false)
  198. const listInitialized = ref(false)
  199. const listRequesting = ref(false)
  200. /** 列表重置代数:reset 后递增,用于丢弃 reset 之前发出的请求的响应,避免乱序写回 */
  201. const listLoadVersion = ref(0)
  202. /** 合并并发 init,避免 watch 与重复进入导致多次 reset/请求交错 */
  203. let listInitPromise: Promise<void> | null = null
  204. /** 列表接口调试:最近一次请求参数与完整响应 */
  205. const showListRawJsonPopup = ref(false)
  206. const showApiFullUrlPopup = ref(false)
  207. const listLastReqParams = ref<PageQuestionReportDataReq | null>(null)
  208. const listLastApiResponse = ref<ApiResponse<PageResp<QuestionListRes>> | null>(null)
  209. const listReqParamsJson = computed(() => {
  210. if (!listLastReqParams.value) return '{}'
  211. return JSON.stringify(listLastReqParams.value, null, 2)
  212. })
  213. const listApiRawJson = computed(() => {
  214. if (!listLastApiResponse.value) return '{}'
  215. return JSON.stringify(listLastApiResponse.value, null, 2)
  216. })
  217. const httpsOrigin = computed(() => {
  218. if (typeof window === 'undefined') return ''
  219. const host = window.location.host || ''
  220. return host ? `https://${host}` : ''
  221. })
  222. const listApiFullUrl = computed(() => `${httpsOrigin.value}/api/questionReport/pageQuestionReportData`)
  223. const detailApiFullUrl = computed(() => `${httpsOrigin.value}/api/questionReport/detailsQuestionReportData/{id}`)
  224. const currentPageUrl = computed(() => {
  225. if (typeof window === 'undefined') return ''
  226. return window.location.href || ''
  227. })
  228. const detailPageUrl = computed(() => `${httpsOrigin.value}/subPages/pages/reportProblems/detail?id={id}`)
  229. const resetQuestionList = () => {
  230. list.value = []
  231. pageInfo.value.pageNumber = 1
  232. finished.value = false
  233. loading.value = false
  234. listRequesting.value = false
  235. listLoadVersion.value += 1
  236. }
  237. const questList = async () => {
  238. if (listRequesting.value || finished.value) return
  239. const requestVersion = listLoadVersion.value
  240. listRequesting.value = true
  241. loading.value = true
  242. try {
  243. const currentPage = pageInfo.value.pageNumber
  244. const reqParams: PageQuestionReportDataReq = {
  245. pageNumber: currentPage,
  246. pageSize: pageInfo.value.pageSize,
  247. contactPerson: formData.value.contactPerson || '',
  248. }
  249. const res = await pageQuestionReportData(reqParams)
  250. if (requestVersion !== listLoadVersion.value) return
  251. listLastReqParams.value = reqParams
  252. listLastApiResponse.value = res
  253. const records = res.data.records || []
  254. list.value = currentPage === 1 ? records : list.value.concat(records)
  255. pageInfo.value.pageNumber = currentPage + 1
  256. finished.value = records.length < pageInfo.value.pageSize
  257. } finally {
  258. listRequesting.value = false
  259. loading.value = false
  260. }
  261. }
  262. const initQuestionList = (): Promise<void> => {
  263. if (listInitPromise) return listInitPromise
  264. listInitPromise = (async () => {
  265. resetQuestionList()
  266. await questList()
  267. listInitialized.value = true
  268. })().finally(() => {
  269. listInitPromise = null
  270. })
  271. return listInitPromise
  272. }
  273. /** 跳转问题详情 */
  274. const handleGoDetail = (item: QuestionListRes) => {
  275. uni.navigateTo({
  276. url: `/subPages/pages/reportProblems/detail?id=${item.uuid}&t=${Date.now()}`,
  277. })
  278. }
  279. const areaShow = ref(false)
  280. const hanleSelectArea = () => {
  281. areaShow.value = true
  282. }
  283. const changeQuestionType = (e: any) => {
  284. const { contactPerson, contactPhone, contactPersonPhone } = formData.value
  285. formData.value = createInitialFormData()
  286. formData.value.questionType = e
  287. formData.value.contactPerson = contactPerson
  288. formData.value.contactPhone = contactPhone
  289. formData.value.contactPersonPhone = contactPersonPhone
  290. addressList.value = null
  291. personData.value = []
  292. rangePerson.value = []
  293. areaShow.value = false
  294. }
  295. onMounted(async () => {
  296. fillVisitorName()
  297. // 问题类型下拉
  298. const { data } = await getQuestionTypeList({
  299. page: 1,
  300. limit: 9999,
  301. category: QuestionType.QUESTION,
  302. })
  303. if (data.records) {
  304. range.value = data.records.map((item) => {
  305. return {
  306. text: item.typeName,
  307. value: item.typeName,
  308. }
  309. })
  310. } else {
  311. range.value = []
  312. }
  313. })
  314. watch(active, async (value) => {
  315. if (value === 1 && !listInitialized.value) {
  316. await initQuestionList()
  317. }
  318. })
  319. </script>
  320. <template>
  321. <van-tabs v-model:active="active" type="card" class="top-tabs">
  322. <van-tab title="问题上报">
  323. <view class="tab-container">
  324. <uni-forms ref="questionReportFormRef" :modelValue="formData" :rules="rules" label-align="right" label-width="80">
  325. <uni-forms-item label="问题类型" required name="questionType">
  326. <uni-data-select v-model="formData.questionType" :localdata="range" @change="changeQuestionType"></uni-data-select>
  327. </uni-forms-item>
  328. <!-- 问题标题 -->
  329. <!-- <uni-forms-item label="问题标题" required name="questionTitle">
  330. <uni-easyinput v-model="formData.questionTitle" placeholder="请输入标题"></uni-easyinput>
  331. </uni-forms-item> -->
  332. <!-- 当事人 -->
  333. <!-- <uni-forms-item v-if="formData.questionType=='矛盾纠纷'" label="当事人1" required name="person1">
  334. <uni-easyinput v-model="formData.person1" placeholder="请输入当事人"></uni-easyinput>
  335. </uni-forms-item>
  336. <uni-forms-item v-if="formData.questionType=='矛盾纠纷'" label="当事人2" required name="person2">
  337. <uni-easyinput v-model="formData.person2" placeholder="请输入当事人"></uni-easyinput>
  338. </uni-forms-item> -->
  339. <!-- 问题内容 -->
  340. <uni-forms-item label="问题描述" required name="questionContent">
  341. <uni-easyinput v-model="formData.questionContent" :maxlength="-1" type="textarea" placeholder="请输入问题描述"></uni-easyinput>
  342. </uni-forms-item>
  343. <!-- 问题所在地 -->
  344. <uni-forms-item label="所在地" required name="addrName">
  345. <uni-easyinput v-model="formData.addrName" placeholder="请选择所在地" @focus="hanleSelectArea"></uni-easyinput>
  346. <GridAddress v-model="areaShow" @finish="handleAddressFinish"></GridAddress>
  347. </uni-forms-item>
  348. <!-- 详细地址-->
  349. <uni-forms-item label="详细位置" name="addrDetailName">
  350. <uni-easyinput v-model="formData.addrDetailName" placeholder="请输入"></uni-easyinput>
  351. </uni-forms-item>
  352. <!-- 负责人下拉 -->
  353. <!-- <uni-forms-item label="负责人" required name="person">
  354. <uni-data-select v-model="formData.chargerName" :localdata="rangePerson"></uni-data-select>
  355. </uni-forms-item> -->
  356. <!-- 联系人姓名 -->
  357. <uni-forms-item label="您的姓名" required name="contactPerson">
  358. <uni-easyinput v-model="formData.contactPerson" placeholder="请输入"></uni-easyinput>
  359. </uni-forms-item>
  360. <uni-forms-item label="手机号" required name="contactPhone">
  361. <view class="contact-phone-field">
  362. <uni-easyinput v-model="formData.contactPhone" placeholder="请输入"></uni-easyinput>
  363. <text class="contact-phone-field__action" @click="fillVisitorPhone">自动填写</text>
  364. </view>
  365. </uni-forms-item>
  366. </uni-forms>
  367. <van-button class="w-full mb-3" type="primary" @click="submit">提交</van-button>
  368. </view>
  369. </van-tab>
  370. <van-tab title="问题答复">
  371. <view class="tab-container">
  372. <view class="mb-[8px]">
  373. <van-button size="small" plain type="primary" @click="showListRawJsonPopup = true">查看接口原始JSON</van-button>
  374. <van-button class="ml-[8px]" size="small" plain type="primary" @click="showApiFullUrlPopup = true">查看完整访问地址</van-button>
  375. </view>
  376. <van-list
  377. v-model:loading="loading"
  378. :finished="finished"
  379. :immediate-check="false"
  380. finished-text="没有更多了"
  381. @load="questList"
  382. >
  383. <view v-for="item in list" :key="item.uuid" class="list-item-style">
  384. <view class="top">
  385. <view class="title">{{ item.questionContent }}</view>
  386. <van-button plain hairline size="mini" type="primary" @click="handleGoDetail(item)">查看详情</van-button>
  387. </view>
  388. <view>所在地:{{ item.addrName }}</view>
  389. <view>详细位置:{{ item.addrDetailName }}</view>
  390. <view class="flex gap-3">
  391. <view class="type">类型</view>
  392. <van-tag
  393. class="max-w-[100px] text-ellipsis overflow-hidden text-nowrap !block !leading-[25px]"
  394. color="#CFFECE"
  395. text-color="black"
  396. >{{ item.questionType }}</van-tag>
  397. <van-tag v-if="item?.status === 0" color="red">未解答</van-tag>
  398. <van-tag v-if="item?.status === 1" color="green">已解答</van-tag>
  399. </view>
  400. </view>
  401. </van-list>
  402. <van-popup
  403. v-model:show="showListRawJsonPopup"
  404. round
  405. position="bottom"
  406. :style="{ height: '75%' }"
  407. >
  408. <view class="json-popup">
  409. <view class="json-title">请求参数</view>
  410. <scroll-view scroll-y class="json-scroll json-scroll--small">
  411. <text class="json-text">{{ listReqParamsJson }}</text>
  412. </scroll-view>
  413. <view class="json-title">列表接口完整JSON</view>
  414. <scroll-view scroll-y class="json-scroll">
  415. <text class="json-text">{{ listApiRawJson }}</text>
  416. </scroll-view>
  417. </view>
  418. </van-popup>
  419. <van-popup
  420. v-model:show="showApiFullUrlPopup"
  421. round
  422. position="bottom"
  423. :style="{ height: '55%' }"
  424. >
  425. <view class="json-popup">
  426. <view class="json-title">当前HTTPS域名</view>
  427. <scroll-view scroll-y class="json-scroll json-scroll--small">
  428. <text class="json-text">{{ httpsOrigin || '当前环境未获取到浏览器域名' }}</text>
  429. </scroll-view>
  430. <view class="json-title">问题答复列表接口</view>
  431. <scroll-view scroll-y class="json-scroll json-scroll--small">
  432. <text class="json-text">{{ listApiFullUrl }}</text>
  433. </scroll-view>
  434. <view class="json-title">详情接口</view>
  435. <scroll-view scroll-y class="json-scroll">
  436. <text class="json-text">{{ detailApiFullUrl }}</text>
  437. </scroll-view>
  438. <view class="json-title">当前页面访问地址</view>
  439. <scroll-view scroll-y class="json-scroll json-scroll--small">
  440. <text class="json-text">{{ currentPageUrl || '当前环境未获取到页面地址' }}</text>
  441. </scroll-view>
  442. <view class="json-title">详情页访问地址</view>
  443. <scroll-view scroll-y class="json-scroll">
  444. <text class="json-text">{{ detailPageUrl }}</text>
  445. </scroll-view>
  446. </view>
  447. </van-popup>
  448. </view>
  449. </van-tab>
  450. </van-tabs>
  451. </template>
  452. <style lang="scss" scoped>
  453. .top-tabs {
  454. :deep(.van-tabs__nav) {
  455. margin: 0;
  456. }
  457. }
  458. .tab-container {
  459. padding: 20px;
  460. padding-bottom: 30px;
  461. min-height: calc(100vh - var(--van-tabs-card-height));
  462. box-sizing: border-box;
  463. display: flex;
  464. flex-direction: column;
  465. > uni-view:first-child {
  466. flex: 1;
  467. }
  468. }
  469. .contact-phone-field {
  470. display: flex;
  471. align-items: center;
  472. gap: 12px;
  473. :deep(.uni-easyinput) {
  474. flex: 1;
  475. }
  476. &__action {
  477. flex-shrink: 0;
  478. color: #1989fa;
  479. font-size: 14px;
  480. line-height: 20px;
  481. }
  482. }
  483. .list-item-style {
  484. padding: 20px;
  485. margin-bottom: 10px;
  486. background-color: white;
  487. > view {
  488. margin-bottom: 10px;
  489. }
  490. .top {
  491. display: flex;
  492. align-items: center;
  493. .title {
  494. font-size: 20px;
  495. font-weight: bold;
  496. flex: 1;
  497. word-break: break-word;
  498. display: -webkit-box; /* 将对象作为弹性伸缩盒子模型显示 */
  499. -webkit-box-orient: vertical; /* 设置或检索伸缩盒对象的子元素的排列方式 */
  500. -webkit-line-clamp: 3; /* 限制在一个块元素显示的文本的行数 */
  501. overflow: hidden; /* 隐藏溢出的内容 */
  502. }
  503. .type {
  504. margin-right: 10px;
  505. }
  506. }
  507. }
  508. .json-popup {
  509. height: 100%;
  510. display: flex;
  511. flex-direction: column;
  512. padding: 12px;
  513. }
  514. .json-title {
  515. font-size: 16px;
  516. font-weight: 500;
  517. margin-bottom: 10px;
  518. }
  519. .json-scroll {
  520. flex: 1;
  521. overflow: hidden;
  522. background: #f7f8fa;
  523. border-radius: 8px;
  524. padding: 12px;
  525. }
  526. .json-scroll--small {
  527. flex: 0 0 auto;
  528. max-height: 120px;
  529. margin-bottom: 10px;
  530. }
  531. .json-text {
  532. white-space: pre-wrap;
  533. word-break: break-all;
  534. font-size: 13px;
  535. line-height: 20px;
  536. }
  537. </style>