You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

387 line
8.7 KiB

  1. <template>
  2. <view class="page">
  3. <scroll-view
  4. class="scroll"
  5. scroll-y
  6. refresher-enabled
  7. :refresher-triggered="refreshing"
  8. @refresherrefresh="refresh"
  9. @scrolltolower="loadMore"
  10. >
  11. <view class="list">
  12. <view v-for="item in list" :key="item.chat_open_id" class="item" @click="openItem(item)">
  13. <view class="item-head">
  14. <view class="title">{{ item.question_content || 'AI取名' }}</view>
  15. <text class="status" :class="{ pending: Number(item.is_suc) === 0, failed: Number(item.is_suc) < 0 }">
  16. {{ getStatusText(item) }}
  17. </text>
  18. </view>
  19. <view v-if="getNames(item).length" class="name-row">
  20. <text v-for="name in getNames(item)" :key="name" class="name-chip">{{ name }}</text>
  21. </view>
  22. <view class="desc">{{ getSummary(item) }}</view>
  23. <view class="meta">
  24. <text>{{ formatTime(item.add_time) }}</text>
  25. <text class="del" @click.stop="deleteItem(item)">删除</text>
  26. </view>
  27. </view>
  28. </view>
  29. <view v-if="!list.length && !loading" class="empty">暂无历史记录</view>
  30. <view v-if="loading" class="loading">加载中...</view>
  31. </scroll-view>
  32. </view>
  33. </template>
  34. <script setup>
  35. import { ref } from 'vue'
  36. import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
  37. import dayjs from 'dayjs'
  38. import { toolApi } from '@/api/index.js'
  39. import { useApp } from '@/utils/useApp.js'
  40. const { toast } = useApp()
  41. const robotId = ref('')
  42. const list = ref([])
  43. const page = ref(1)
  44. const pageSize = 10
  45. const total = ref(0)
  46. const loading = ref(false)
  47. const refreshing = ref(false)
  48. let fetching = false
  49. let firstShow = true
  50. let timer = 0
  51. onLoad((options) => {
  52. robotId.value = options.robot_id || ''
  53. fetchList({ reset: true })
  54. })
  55. onShow(() => {
  56. if (firstShow) {
  57. firstShow = false
  58. return
  59. }
  60. refreshLoaded(true)
  61. })
  62. onUnload(() => clearTimeout(timer))
  63. function formatTime(value) {
  64. if (!value) return ''
  65. return dayjs(Number(value) * 1000).format('YYYY-MM-DD HH:mm')
  66. }
  67. async function fetchList(options = {}) {
  68. const {
  69. reset = false,
  70. silent = false,
  71. replace = reset,
  72. size = pageSize
  73. } = options
  74. let queryPage = options.pageNo || page.value
  75. if (fetching) {
  76. finishRefresh()
  77. return
  78. }
  79. fetching = true
  80. if (!silent) loading.value = true
  81. if (reset) {
  82. page.value = 1
  83. queryPage = 1
  84. }
  85. try {
  86. const res = await toolApi.getNameList({
  87. page_no: queryPage,
  88. page_size: size,
  89. robot_open_id: robotId.value,
  90. role: 2
  91. })
  92. const rows = res.list || []
  93. total.value = Number((res.data && res.data.total) || rows.length)
  94. list.value = replace ? rows : list.value.concat(rows)
  95. scheduleRefresh()
  96. } catch (e) {
  97. if (!silent) toast(e.msg || '历史记录加载失败')
  98. } finally {
  99. fetching = false
  100. loading.value = false
  101. finishRefresh()
  102. }
  103. }
  104. function scheduleRefresh() {
  105. clearTimeout(timer)
  106. if (list.value.some((item) => Number(item.is_suc) === 0)) {
  107. timer = setTimeout(() => refreshLoaded(true), 2500)
  108. }
  109. }
  110. function finishRefresh() {
  111. refreshing.value = false
  112. uni.stopPullDownRefresh()
  113. }
  114. function refresh() {
  115. refreshing.value = true
  116. fetchList({ reset: true })
  117. }
  118. function refreshLoaded(silent = true) {
  119. const currentPage = Math.max(1, Number(page.value) || 1)
  120. fetchList({
  121. pageNo: 1,
  122. size: currentPage * pageSize,
  123. replace: true,
  124. silent
  125. })
  126. }
  127. function loadMore() {
  128. if (fetching || list.value.length >= total.value) return
  129. page.value += 1
  130. fetchList()
  131. }
  132. function openItem(item) {
  133. uni.navigateTo({
  134. url: `/pages/tool/name-result?id=${item.chat_open_id}&robot_id=${robotId.value}&key=naming`
  135. })
  136. }
  137. function getStatusText(item) {
  138. if (Number(item.is_suc) === 0) return '生成中'
  139. if (Number(item.is_suc) < 0) return '失败'
  140. return '完成'
  141. }
  142. function getParsed(item) {
  143. if (item._parsed) return item._parsed
  144. item._parsed = parseResult(item.content)
  145. return item._parsed
  146. }
  147. function getNames(item) {
  148. const parsed = getParsed(item)
  149. return (parsed.names || []).map((name) => name.name).filter(Boolean).slice(0, 3)
  150. }
  151. function getSummary(item) {
  152. if (Number(item.is_suc) === 0) return '正在生成取名方案,请稍后查看'
  153. if (Number(item.is_suc) < 0) return item.content || '生成失败'
  154. const parsed = getParsed(item)
  155. if (parsed.summary) return parsed.summary
  156. const first = parsed.names && parsed.names[0]
  157. if (first) return [first.meaning, first.reason].filter(Boolean).join(' ')
  158. return '点击查看取名结果'
  159. }
  160. function parseResult(content) {
  161. if (!content) return { summary: '', names: [] }
  162. let text = String(content).trim()
  163. text = text.replace(/^```json\s*/i, '').replace(/^```\s*/i, '').replace(/```$/i, '').trim()
  164. const lineResult = parseJsonLines(text)
  165. if (lineResult.names.length) return lineResult
  166. const markedResult = parseMarkedText(text)
  167. if (markedResult.names.length) return markedResult
  168. const start = text.indexOf('{')
  169. const end = text.lastIndexOf('}')
  170. if (start >= 0 && end > start) text = text.substring(start, end + 1)
  171. try {
  172. const data = JSON.parse(text)
  173. return {
  174. summary: data.summary || '',
  175. names: Array.isArray(data.names) ? data.names : []
  176. }
  177. } catch (e) {
  178. return { summary: '', names: [] }
  179. }
  180. }
  181. function parseMarkedText(text) {
  182. const data = { summary: '', names: [] }
  183. const value = String(text || '')
  184. const summaryMatch = value.match(/整体建议[::]([\s\S]*?)(?=\n\s*【|$)/)
  185. if (summaryMatch) data.summary = summaryMatch[1].trim()
  186. const reg = /【([^】]+)】([\s\S]*?)(?=\n\s*【|$)/g
  187. let match
  188. while ((match = reg.exec(value))) {
  189. const body = match[2] || ''
  190. data.names.push({
  191. name: match[1].trim(),
  192. pinyin: matchField(body, '拼音'),
  193. score: parseInt(matchField(body, '评分')) || 0,
  194. source: matchField(body, '出处'),
  195. meaning: matchField(body, '寓意'),
  196. tone: matchField(body, '音律'),
  197. reason: matchField(body, '推荐')
  198. })
  199. }
  200. data.names = data.names.filter((item) => item.name).slice(0, 5)
  201. return data
  202. }
  203. function matchField(body, label) {
  204. const reg = new RegExp(`${label}[::]([^\\n\\r]*)`)
  205. const match = String(body || '').match(reg)
  206. return match ? match[1].trim() : ''
  207. }
  208. function parseJsonLines(text) {
  209. const data = { summary: '', names: [] }
  210. String(text || '').split(/\r?\n/).forEach((line) => {
  211. line = line.trim()
  212. if (!line) return
  213. try {
  214. const item = JSON.parse(line)
  215. if (item.type === 'summary') data.summary = item.content || ''
  216. if (item.type === 'name' && item.data) data.names.push(item.data)
  217. } catch (e) {}
  218. })
  219. return data
  220. }
  221. function deleteItem(item) {
  222. uni.showModal({
  223. title: '温馨提示',
  224. content: '是否确认删除该内容?',
  225. confirmText: '确认删除',
  226. success: async (res) => {
  227. if (!res.confirm) return
  228. try {
  229. await toolApi.deleteName({ chat_open_id: item.chat_open_id })
  230. toast('删除成功')
  231. refreshLoaded(true)
  232. } catch (e) {
  233. toast(e.msg || '删除失败')
  234. }
  235. }
  236. })
  237. }
  238. </script>
  239. <style lang="scss" scoped>
  240. .page,
  241. .scroll {
  242. height: 100vh;
  243. background: #f4f7fb;
  244. }
  245. .list {
  246. padding: 26rpx;
  247. }
  248. .item {
  249. padding: 28rpx 30rpx;
  250. border-radius: 20rpx;
  251. background: #fff;
  252. margin-bottom: 22rpx;
  253. box-shadow: 0 8rpx 24rpx rgba(35, 55, 90, 0.05);
  254. }
  255. .item-head {
  256. display: flex;
  257. align-items: center;
  258. min-width: 0;
  259. }
  260. .title {
  261. flex: 1;
  262. min-width: 0;
  263. color: #172033;
  264. font-size: 30rpx;
  265. font-weight: 700;
  266. line-height: 1.4;
  267. white-space: nowrap;
  268. overflow: hidden;
  269. text-overflow: ellipsis;
  270. }
  271. .status {
  272. margin-left: 18rpx;
  273. height: 40rpx;
  274. padding: 0 16rpx;
  275. border-radius: 22rpx;
  276. color: #0b8cff;
  277. font-size: 22rpx;
  278. line-height: 40rpx;
  279. background: #eef6ff;
  280. &.pending {
  281. color: #ff8a00;
  282. background: #fff5e9;
  283. }
  284. &.failed {
  285. color: #f04438;
  286. background: #fff1f1;
  287. }
  288. }
  289. .name-row {
  290. margin-top: 18rpx;
  291. display: flex;
  292. flex-wrap: wrap;
  293. gap: 12rpx;
  294. }
  295. .name-chip {
  296. max-width: 190rpx;
  297. height: 46rpx;
  298. padding: 0 18rpx;
  299. border-radius: 24rpx;
  300. color: #0b8cff;
  301. font-size: 25rpx;
  302. font-weight: 700;
  303. line-height: 46rpx;
  304. background: #f0f8ff;
  305. overflow: hidden;
  306. text-overflow: ellipsis;
  307. white-space: nowrap;
  308. }
  309. .desc {
  310. margin-top: 16rpx;
  311. color: #7f8896;
  312. font-size: 26rpx;
  313. line-height: 1.5;
  314. overflow: hidden;
  315. display: -webkit-box;
  316. -webkit-line-clamp: 3;
  317. -webkit-box-orient: vertical;
  318. }
  319. .meta {
  320. margin-top: 20rpx;
  321. display: flex;
  322. align-items: center;
  323. color: #a0aabb;
  324. font-size: 23rpx;
  325. }
  326. .del {
  327. margin-left: auto;
  328. color: #f04438;
  329. padding-left: 28rpx;
  330. }
  331. .empty,
  332. .loading {
  333. padding: 80rpx 0;
  334. color: #7f8896;
  335. font-size: 26rpx;
  336. text-align: center;
  337. }
  338. </style>