西藏巴青项目

index.vue 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. <template>
  2. <view :key="layoutKey" :class="pageRootClass" class="tab-page ai-page">
  3. <view class="ai-list-shell">
  4. <up-virtual-list
  5. ref="aiVList"
  6. class="ai-vlist"
  7. :list-data="historyList"
  8. :item-height="slotHeightPx"
  9. :height="scrollAreaPx"
  10. :buffer="8"
  11. key-field="id"
  12. :scroll-top="vScrollTop"
  13. @update:scrollTop="vScrollTop = $event"
  14. >
  15. <template #default="{ item }">
  16. <view class="ai-row-cell">
  17. <view
  18. class="ai-row"
  19. :class="{ 'ai-row--hl': highlightId === item.id }"
  20. role="button"
  21. :style="{ height: rowBodyPx + 'px' }"
  22. @click="openSession(item)"
  23. @longpress.stop="onRowLongPress(item)"
  24. >
  25. <view class="ai-row__left">
  26. <view class="ai-row__icon-circle">
  27. <up-icon name="chat" color="#22C55E" :size="22" />
  28. </view>
  29. </view>
  30. <view class="ai-row__right">
  31. <text class="ai-row__title">{{ item.title }}</text>
  32. <text class="ai-row__hint">{{ item.preview }}</text>
  33. </view>
  34. </view>
  35. </view>
  36. </template>
  37. </up-virtual-list>
  38. </view>
  39. <view class="ai-float">
  40. <view class="ai-float__inner">
  41. <up-button
  42. type="primary"
  43. shape="circle"
  44. :text="$t('aiPage.createChat')"
  45. :custom-style="floatBtnStyle"
  46. @click="createSession"
  47. />
  48. </view>
  49. </view>
  50. </view>
  51. </template>
  52. <script>
  53. import UButton from 'uview-plus/components/u-button/u-button.vue'
  54. import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
  55. import UVirtualList from 'uview-plus/components/u-virtual-list/u-virtual-list.vue'
  56. import tabPage from '@/mixins/tabPage'
  57. import { ensureApiToken } from '@/utils/apiAuth'
  58. import {
  59. resolveSessionId,
  60. normalizeSessionRow,
  61. sessionTitle,
  62. DEFAULT_SESSION_TITLE
  63. } from '@/utils/aiConsult'
  64. import {
  65. listAiConsultSessions,
  66. createAiConsultSession,
  67. hideAiConsultSession
  68. } from '@/api/diseaseTreatment/aiOnlineConsult'
  69. const ASSISTANT_PATH = '/package-a/ai-assistant/index'
  70. /** 单行卡片内容区高度(rpx),不含与下一行的间距 */
  71. const ROW_BODY_RPX = 168
  72. /** 列表项之间的间距(rpx),计入虚拟列表单项高度 */
  73. const ROW_GAP_RPX = 20
  74. export default {
  75. components: {
  76. 'up-button': UButton,
  77. 'up-icon': UIcon,
  78. 'up-virtual-list': UVirtualList
  79. },
  80. mixins: [tabPage],
  81. data() {
  82. return {
  83. navTitleKey: 'nav.ai',
  84. sessions: [],
  85. listLoading: false,
  86. rowBodyPx: 76,
  87. marginPx: 10,
  88. slotHeightPx: 86,
  89. scrollAreaPx: 400,
  90. vScrollTop: 0,
  91. highlightId: '',
  92. suppressRowClick: false,
  93. floatBtnStyle:
  94. 'min-width:280rpx;max-width:92%;height:auto;padding:22rpx 40rpx;display:flex;align-items:center;justify-content:center;'
  95. }
  96. },
  97. computed: {
  98. historyList() {
  99. return this.sessions.map((s, idx) => {
  100. const id = resolveSessionId(s)
  101. const title =
  102. sessionTitle(s, this.$t('aiOnlineConsult.defaultSessionTitle')) ||
  103. this.$t('aiPage.sessionTitle', { n: idx + 1 })
  104. const preview = (s.lastMessagePreview || '').trim() || this.$t('aiPage.openChat')
  105. return { id, n: idx + 1, title, preview, raw: s }
  106. })
  107. }
  108. },
  109. watch: {
  110. historyList() {
  111. this.$nextTick(() => {
  112. this.$refs.aiVList?.measureContainerHeight?.()
  113. })
  114. }
  115. },
  116. onShow() {
  117. if (ensureApiToken(false)) {
  118. this.loadSessions()
  119. }
  120. },
  121. onReady() {
  122. try {
  123. this.marginPx = Math.ceil(uni.upx2px(ROW_GAP_RPX))
  124. this.rowBodyPx = Math.max(64, Math.ceil(uni.upx2px(ROW_BODY_RPX)))
  125. this.slotHeightPx = this.rowBodyPx + this.marginPx
  126. } catch (e) {
  127. this.marginPx = 10
  128. this.rowBodyPx = 76
  129. this.slotHeightPx = 86
  130. }
  131. this.applyScrollLayoutFallback()
  132. this.$nextTick(() => this.measureListShell())
  133. },
  134. methods: {
  135. applyScrollLayoutFallback() {
  136. const sys = uni.getSystemInfoSync()
  137. const winH = sys.windowHeight || 600
  138. this.scrollAreaPx = Math.max(200, winH - 30)
  139. },
  140. measureListShell() {
  141. uni.createSelectorQuery()
  142. .in(this)
  143. .select('.ai-list-shell')
  144. .boundingClientRect((rect) => {
  145. if (rect && rect.height > 0) {
  146. this.scrollAreaPx = Math.max(200, Math.floor(rect.height))
  147. this.$nextTick(() => {
  148. this.$refs.aiVList?.measureContainerHeight?.()
  149. })
  150. }
  151. })
  152. .exec()
  153. },
  154. loadSessions() {
  155. if (!ensureApiToken()) return
  156. this.listLoading = true
  157. listAiConsultSessions({
  158. pageNum: 1,
  159. pageSize: 100
  160. })
  161. .then((res) => {
  162. this.sessions = (res.rows || []).map((row) => normalizeSessionRow(row))
  163. })
  164. .catch(() => {
  165. this.sessions = []
  166. })
  167. .finally(() => {
  168. this.listLoading = false
  169. })
  170. },
  171. openSession(item) {
  172. if (this.suppressRowClick || !item || !item.id) return
  173. let url = `${ASSISTANT_PATH}?sessionId=${encodeURIComponent(item.id)}`
  174. const llm = item.raw && (item.raw.llmSessionId || null)
  175. if (llm) {
  176. url += `&llmSessionId=${encodeURIComponent(llm)}`
  177. }
  178. uni.navigateTo({ url })
  179. },
  180. onRowLongPress(item) {
  181. this.suppressRowClick = true
  182. setTimeout(() => {
  183. this.suppressRowClick = false
  184. }, 450)
  185. this.highlightId = item.id
  186. uni.showActionSheet({
  187. itemList: [this.$t('messagePage.delete')],
  188. success: (res) => {
  189. if (res.tapIndex === 0) {
  190. this.removeSession(item.id)
  191. }
  192. this.highlightId = ''
  193. },
  194. fail: () => {
  195. this.highlightId = ''
  196. }
  197. })
  198. },
  199. removeSession(id) {
  200. if (!id || !ensureApiToken()) return
  201. hideAiConsultSession(id)
  202. .then(() => {
  203. this.sessions = this.sessions.filter((s) => resolveSessionId(s) !== id)
  204. this.highlightId = ''
  205. this.$nextTick(() => {
  206. this.$refs.aiVList?.measureContainerHeight?.()
  207. })
  208. })
  209. .catch(() => {})
  210. },
  211. createSession() {
  212. if (!ensureApiToken()) return
  213. uni.showLoading({ mask: true })
  214. createAiConsultSession({})
  215. .then((res) => {
  216. const data = res.data || {}
  217. const id = resolveSessionId(data)
  218. if (!id) return
  219. const row = normalizeSessionRow({
  220. ...data,
  221. id,
  222. realSessionId: id,
  223. llmSessionId: null
  224. })
  225. const exists = this.sessions.some((s) => resolveSessionId(s) === id)
  226. if (!exists) {
  227. this.sessions.unshift(row)
  228. }
  229. uni.navigateTo({
  230. url: `${ASSISTANT_PATH}?sessionId=${encodeURIComponent(id)}&new=1`
  231. })
  232. })
  233. .finally(() => {
  234. uni.hideLoading()
  235. })
  236. }
  237. }
  238. }
  239. </script>
  240. <style lang="scss" scoped>
  241. @import '@/styles/morandi.scss';
  242. @import '@/styles/tab-page.scss';
  243. .ai-page {
  244. display: flex;
  245. flex-direction: column;
  246. min-width: 0;
  247. min-height: 100%;
  248. padding: 24rpx 16rpx 0;
  249. box-sizing: border-box;
  250. gap: 16rpx;
  251. background: $morandi-bg-page;
  252. }
  253. .ai-list-shell {
  254. flex: 1;
  255. min-height: 0;
  256. height: 0;
  257. min-width: 0;
  258. box-sizing: border-box;
  259. }
  260. .ai-vlist {
  261. width: 100%;
  262. }
  263. .ai-row-cell {
  264. height: 100%;
  265. box-sizing: border-box;
  266. display: flex;
  267. flex-direction: column;
  268. justify-content: flex-start;
  269. }
  270. .ai-row {
  271. display: flex;
  272. flex-direction: row;
  273. align-items: flex-start;
  274. min-width: 0;
  275. overflow: hidden;
  276. padding: 22rpx 20rpx;
  277. gap: 20rpx;
  278. box-sizing: border-box;
  279. border-radius: 16rpx;
  280. background: $morandi-bg-card;
  281. border: 1rpx solid $morandi-border;
  282. flex-shrink: 0;
  283. }
  284. .ai-row--hl {
  285. background: $morandi-highlight;
  286. border-color: $morandi-highlight-border;
  287. }
  288. .ai-row__left {
  289. flex-shrink: 0;
  290. padding-top: 4rpx;
  291. }
  292. .ai-row__icon-circle {
  293. display: flex;
  294. align-items: center;
  295. justify-content: center;
  296. width: 88rpx;
  297. height: 88rpx;
  298. border-radius: 50%;
  299. background: rgba(34, 197, 94, 0.12);
  300. }
  301. .ai-row__right {
  302. display: flex;
  303. flex: 1;
  304. min-width: 0;
  305. width: 0;
  306. flex-direction: column;
  307. justify-content: center;
  308. gap: 8rpx;
  309. overflow: hidden;
  310. }
  311. .ai-row__title {
  312. display: block;
  313. width: 100%;
  314. max-width: 100%;
  315. min-width: 0;
  316. font-size: 32rpx;
  317. line-height: 1.35;
  318. font-weight: 600;
  319. color: $morandi-text;
  320. overflow: hidden;
  321. text-overflow: ellipsis;
  322. white-space: nowrap;
  323. word-break: normal;
  324. overflow-wrap: normal;
  325. flex-shrink: 0;
  326. }
  327. .ai-row__hint {
  328. display: block;
  329. width: 100%;
  330. max-width: 100%;
  331. min-width: 0;
  332. font-size: 24rpx;
  333. line-height: 1.35;
  334. color: $morandi-text-muted;
  335. overflow: hidden;
  336. text-overflow: ellipsis;
  337. white-space: nowrap;
  338. word-break: normal;
  339. overflow-wrap: normal;
  340. flex-shrink: 1;
  341. }
  342. .ai-float {
  343. position: fixed;
  344. left: 0;
  345. right: 0;
  346. z-index: 200;
  347. display: flex;
  348. justify-content: center;
  349. padding: 0 24rpx;
  350. bottom: calc(140rpx + env(safe-area-inset-bottom));
  351. pointer-events: none;
  352. }
  353. .ai-float__inner {
  354. pointer-events: auto;
  355. }
  356. </style>