index.vue 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940
  1. <template>
  2. <view class="page-chart">
  3. <!-- Top operation bar -->
  4. <view class="top-bar">
  5. <view class="spacer"></view>
  6. <view v-if="userStore.isVip" class="export-btn" @click="exportPdf">
  7. 导出PDF
  8. </view>
  9. </view>
  10. <!-- Triangle chart -->
  11. <view class="chart-section">
  12. <TriangleChart
  13. :chartData="store.currentChart"
  14. :birthYear="store.currentBirthYear"
  15. :birthMonth="store.currentBirthMonth"
  16. :birthDay="store.currentBirthDay"
  17. :recordId="store.currentRecordId"
  18. :annotations="annotations"
  19. :tags="allTags"
  20. @annotation-add="onAnnotationAdd"
  21. @annotation-delete="onAnnotationDelete"
  22. @tag-create="onTagCreate"
  23. />
  24. </view>
  25. <!-- Menu button (regenerate + share info) -->
  26. <view class="menu-bar">
  27. <text class="menu-btn" @click="showMenu">···</text>
  28. </view>
  29. <!-- US-3.1: AI Interpretation section -->
  30. <view class="interpret-bar">
  31. <button class="interpret-btn" :disabled="interpreting" @click="fetchInterpret">
  32. {{ interpreting ? '解读生成中...' : '🔮 查看AI解读' }}
  33. </button>
  34. </view>
  35. <!-- US-3.1: Interpretation panel -->
  36. <view v-if="interpretData" id="interpret-section" class="interpret-panel">
  37. <view
  38. v-for="(section, key) in interpretData"
  39. :key="key"
  40. class="interpret-card"
  41. :class="{ expanded: interpretExpanded[key] }"
  42. @click="toggleInterpretSection(key)"
  43. >
  44. <view class="interpret-card-header">
  45. <text class="interpret-num-badge">{{ section.number }}</text>
  46. <text class="interpret-card-title">{{ section.title }}</text>
  47. <text class="interpret-toggle">{{ interpretExpanded[key] ? '▾' : '▸' }}</text>
  48. </view>
  49. <view v-show="interpretExpanded[key]" class="interpret-card-body">
  50. <text class="interpret-content">{{ section.content }}</text>
  51. </view>
  52. </view>
  53. <!-- Disclaimer -->
  54. <view class="interpret-disclaimer">
  55. <text class="disclaimer-text">本解读由AI生成,仅供参考</text>
  56. </view>
  57. </view>
  58. <!-- Loading skeleton -->
  59. <view v-if="interpreting" class="interpret-skeleton">
  60. <view v-for="n in 4" :key="n" class="skeleton-card">
  61. <view class="skeleton-header"></view>
  62. <view v-if="n === 1" class="skeleton-body">
  63. <view class="skeleton-line"></view>
  64. <view class="skeleton-line short"></view>
  65. <view class="skeleton-line"></view>
  66. </view>
  67. </view>
  68. </view>
  69. <!-- US-9.1: Academic orientation button -->
  70. <view class="academic-bar">
  71. <button class="academic-btn" :disabled="academicLoading" @click="fetchAcademicOrientation">
  72. {{ academicLoading ? '分析生成中...' : '🎓 学业方向分析' }}
  73. </button>
  74. <text v-if="!userStore.isVipActive && maxAcademic > 0" class="academic-quota">
  75. 今日 {{ usedAcademic }}/{{ maxAcademic }} 次
  76. </text>
  77. </view>
  78. <!-- US-2.1: Share bar -->
  79. <view class="share-bar">
  80. <button class="share-btn" :disabled="shareGenerating" @click="onShareTap">
  81. {{ shareGenerating ? '生成中...' : '📸 分享命盘' }}
  82. </button>
  83. <text v-if="!userStore.isVipActive" class="share-quota">
  84. 今日剩余 {{ shareRemaining }} 次
  85. </text>
  86. </view>
  87. <!-- US-9.1: Academic orientation panel -->
  88. <view v-if="academicData" id="academic-section" class="academic-panel">
  89. <view class="academic-section-title">🎯 学业方向深度分析</view>
  90. <view
  91. v-for="(section, key) in academicData"
  92. :key="key"
  93. class="academic-card"
  94. :class="{ expanded: academicExpanded[key], talent: key==='talentTendency', direction: key==='suitableDirection', learning: key==='learningStyle', parenting: key==='parentingAdvice', periods: key==='keyPeriods' }"
  95. @click="toggleAcademicSection(key)"
  96. >
  97. <view class="academic-card-header">
  98. <text class="academic-card-title">{{ section.title }}</text>
  99. <text class="academic-toggle">{{ academicExpanded[key] ? '▾' : '▸' }}</text>
  100. </view>
  101. <view v-show="academicExpanded[key]" class="academic-card-body">
  102. <text class="academic-content">{{ section.content }}</text>
  103. <view v-if="section.matchPercentages" class="match-bars">
  104. <view v-for="(pct, dir) in section.matchPercentages" :key="dir" class="match-row">
  105. <text class="match-label">{{ dir }}</text>
  106. <view class="match-track">
  107. <view class="match-fill" :style="{ width: pct + '%' }" :class="'pct-' + pct"></view>
  108. </view>
  109. <text class="match-value">{{ pct }}%</text>
  110. </view>
  111. </view>
  112. </view>
  113. </view>
  114. <view class="academic-disclaimer">
  115. <text class="disclaimer-text">💡 以上分析由AI生成,如需人工深度方案,可申请能量师出方案</text>
  116. </view>
  117. </view>
  118. <!-- US-9.1: Loading skeleton -->
  119. <view v-if="academicLoading" class="academic-skeleton">
  120. <view v-for="n in 3" :key="n" class="skeleton-card">
  121. <view class="skeleton-header"></view>
  122. <view v-if="n === 1" class="skeleton-body">
  123. <view class="skeleton-line"></view><view class="skeleton-line short"></view><view class="skeleton-line"></view>
  124. </view>
  125. </view>
  126. </view>
  127. <!-- US-9.1: Academic quota exceeded upgrade card -->
  128. <view v-if="quotaAcademicExceeded && !academicData && !academicLoading" class="academic-upgrade-card">
  129. <text class="academic-upgrade-title">🎓 学业方向分析</text>
  130. <text class="academic-upgrade-desc">今日学业分析次数已用完,开通会员享无限次</text>
  131. <button class="academic-upgrade-btn" @click="goSubscribeC">开通会员 ¥131/年</button>
  132. </view>
  133. <!-- AI Chat -->
  134. <view class="chat-section">
  135. <view class="section-title">
  136. <text class="title-icon">◈</text>
  137. AI 解读
  138. <text v-if="!userStore.isVipActive && maxChats > 0" class="quota-badge">
  139. 今日 {{ usedChats }}/{{ maxChats }} 轮
  140. </text>
  141. <text v-if="userStore.isVipActive" class="quota-badge vip">无限次</text>
  142. </view>
  143. <scroll-view class="chat-box" scroll-y :scroll-top="scrollTop">
  144. <view v-for="(msg, idx) in messages" :key="idx">
  145. <!-- System messages: centered italic text, no bubble -->
  146. <view v-if="msg.senderType === 'system'" class="msg-system" @longpress.stop="onLongPressMsg(msg)">
  147. <text>{{ msg.content }}</text>
  148. </view>
  149. <!-- Practitioner messages: green bubble with label -->
  150. <view v-else-if="msg.senderType === 'practitioner'" class="msg-row practitioner" @longpress.stop="onLongPressMsg(msg)">
  151. <view class="msg-practitioner-label">👤 能量师</view>
  152. <view class="msg-bubble msg-practitioner-bubble">
  153. <text>{{ msg.content }}</text>
  154. </view>
  155. </view>
  156. <!-- Default (AI/User): existing rendering -->
  157. <view v-else class="msg-row" :class="msg.role">
  158. <view class="msg-bubble">
  159. <text>{{ msg.content }}</text>
  160. </view>
  161. </view>
  162. </view>
  163. </scroll-view>
  164. <!-- Quota exceeded prompt for non-VIP -->
  165. <view v-if="quotaExceeded" class="upgrade-prompt">
  166. <text class="upgrade-text">今日 AI 解读次数已用完</text>
  167. <text class="upgrade-desc">选择适合的会员方案,解锁无限次解读</text>
  168. <view class="upgrade-actions">
  169. <button class="upgrade-btn secondary" @click="goSubscribeC">C端会员 ¥131/年</button>
  170. <button class="upgrade-btn" @click="goSubscribe">能量师 ¥1,314/年</button>
  171. </view>
  172. </view>
  173. <!-- Share hint (non-intrusive) -->
  174. <view v-if="shareRemaining > 0 && !userStore.isVipActive" class="share-hint">
  175. <text class="share-hint-text">分享好友可解锁更多次数 · 今日剩余 {{ shareRemaining }} 次</text>
  176. </view>
  177. <!-- Input area — always visible, shows upgrade prompt on 4th click -->
  178. <view class="input-bar">
  179. <input
  180. class="chat-input"
  181. v-model="chatInput"
  182. :placeholder="quotaExceeded ? '今日次数已用完,点击发送开通会员' : '输入您的问题...'"
  183. @confirm="sendQuestion(chatInput)"
  184. :disabled="loading"
  185. />
  186. <button class="send-btn" @click="sendQuestion(chatInput)" :disabled="loading">
  187. {{ loading ? '···' : '发送' }}
  188. </button>
  189. </view>
  190. <!-- US-6.6: Subscription card for free users / 已开通 badge for paid -->
  191. <view v-if="userStore.isLoggedIn" class="subscribe-section">
  192. <!-- Annual paid user — always show badge -->
  193. <view v-if="userStore.vipType === 'annual'" class="vip-badge-bar">
  194. <text class="vip-badge-text">✅ 已开通AI解读</text>
  195. <text class="vip-badge-expiry" v-if="userStore.vipEndTime">有效期至 {{ userStore.vipEndTime.slice(0, 10) }}</text>
  196. <text class="vip-poster-link" @click="goPoster">📱 生成推广海报</text>
  197. </view>
  198. <!-- Practitioner paid user — always show badge -->
  199. <view v-else-if="userStore.vipType === 'practitioner'" class="vip-badge-bar pro">
  200. <text class="vip-badge-text">✅ 已开通能量师</text>
  201. <text class="vip-badge-expiry" v-if="userStore.vipEndTime">有效期至 {{ userStore.vipEndTime.slice(0, 10) }}</text>
  202. </view>
  203. <!-- Free user — subscription card only when quota exceeded -->
  204. <view v-else-if="quotaExceeded" class="subscribe-card">
  205. <text class="subscribe-title">🔮 AI解读会员</text>
  206. <text class="subscribe-desc">¥131/年 · 无限AI解读 · 个性化报告 · 推广赚佣金</text>
  207. <view class="subscribe-actions">
  208. <button class="subscribe-btn" @click="goSubscribeC">立即开通 ¥131</button>
  209. <text class="subscribe-link" @click="goSubscribe">了解能量师专业版</text>
  210. </view>
  211. </view>
  212. </view>
  213. </view>
  214. <!-- US-9.4: Proposal card for negotiating state -->
  215. <view v-if="planRequest && planRequest.status === 'negotiating'" class="proposal-section">
  216. <view class="proposal-title">📋 人工方案</view>
  217. <view class="proposal-card">
  218. <view class="proposal-price">¥{{ planRequest.price }}</view>
  219. <view v-if="proposalMessage" class="proposal-message">
  220. <text class="proposal-msg-label">能量师留言:</text>
  221. <text class="proposal-msg-content">{{ proposalMessage }}</text>
  222. </view>
  223. <view v-if="logs.length > 0" class="negotiation-history">
  224. <view class="history-title">协商历史</view>
  225. <view class="history-timeline">
  226. <view v-for="(log, idx) in logs" :key="log.id" class="history-item" :class="log.action">
  227. <view class="timeline-dot" :class="log.action"></view>
  228. <view class="timeline-line" v-if="idx < logs.length - 1"></view>
  229. <view class="timeline-content">
  230. <text v-if="log.action === 'proposal'">能量师提交方案: ¥{{ log.newPrice }}</text>
  231. <text v-else-if="log.action === 'counter' && log.operatorId === currentUserId">您还价: ¥{{ log.newPrice }}</text>
  232. <text v-else-if="log.action === 'counter'">能量师还价: ¥{{ log.newPrice }}</text>
  233. <text v-else-if="log.action === 'accept'">✓ 已接受方案</text>
  234. <text v-else>{{ log.action }}</text>
  235. </view>
  236. </view>
  237. </view>
  238. </view>
  239. <view class="proposal-actions">
  240. <button class="proposal-btn btn-accept" :disabled="acceptingOffer" @click="acceptOffer">接受方案</button>
  241. <button class="proposal-btn btn-counter" @click="showCounterInput = !showCounterInput">还价</button>
  242. <button class="proposal-btn btn-reject" :disabled="rejecting" @click="rejectOffer">拒绝</button>
  243. </view>
  244. <view v-if="showCounterInput" class="counter-input-wrap">
  245. <view class="counter-input-row">
  246. <input class="counter-input" v-model="counterPrice" type="number" placeholder="输入您的报价..." />
  247. <button class="counter-submit" :disabled="countering || !counterPrice" @click="submitCounter">提交还价</button>
  248. </view>
  249. <text class="counter-hint">双方最多协商5轮</text>
  250. </view>
  251. </view>
  252. </view>
  253. <!-- US-9.5: Pending payment — show pay button -->
  254. <view v-if="planRequest && planRequest.status === 'pending_payment' && planOrder" class="proposal-section">
  255. <view class="proposal-title">📋 方案已确认</view>
  256. <view class="proposal-card pending-payment-card">
  257. <view class="proposal-price">¥{{ planRequest.price }}</view>
  258. <text class="payment-hint">请在24小时内完成支付</text>
  259. <button class="payment-btn" @click="goPay">去支付 ¥{{ planRequest.price }}</button>
  260. </view>
  261. </view>
  262. <!-- US-9.5: Paid — show delivery -->
  263. <view v-else-if="planRequest && planRequest.status === 'paid'" class="proposal-section">
  264. <view class="proposal-card paid-card">
  265. <view v-if="deliveryData && deliveryData.length > 0" class="delivery-content">
  266. <view class="delivery-title">📋 方案交付</view>
  267. <view v-for="d in deliveryData" :key="d.id" class="delivery-item">
  268. <view class="delivery-header">
  269. <text class="delivery-type">{{ d.deliveryType === 'text' ? '文字方案' : d.deliveryType === 'file' ? '文件方案' : '综合方案' }}</text>
  270. <text class="delivery-time">{{ d.createdAt?.slice(0, 10) }}</text>
  271. </view>
  272. <text v-if="d.textContent" class="delivery-text">{{ d.textContent }}</text>
  273. </view>
  274. </view>
  275. <view v-else class="paid-waiting">
  276. <text class="paid-waiting-text">方案已确认,等待能量师交付...</text>
  277. </view>
  278. </view>
  279. </view>
  280. <!-- EPIC 9: Apply for practitioner plan -->
  281. <view class="plan-apply-bar">
  282. <button class="plan-apply-btn" @click="goPlanApply">
  283. 💼 申请人工深度方案
  284. </button>
  285. </view>
  286. </view>
  287. <!-- Share canvas (hidden off-screen) -->
  288. <ChartShareCanvas
  289. ref="shareCanvas"
  290. :chart="store.currentChart || {}"
  291. :name="store.currentName"
  292. :birthday="store.currentBirthYear && store.currentBirthMonth && store.currentBirthDay ?
  293. `${store.currentBirthYear}-${String(store.currentBirthMonth).padStart(2,'0')}-${String(store.currentBirthDay).padStart(2,'0')}` : ''"
  294. @ready="onCanvasReady"
  295. />
  296. </template>
  297. <script>
  298. // Page lifecycle hooks (needed for WeChat share)
  299. export default {
  300. onShareAppMessage() {
  301. const pages = getCurrentPages()
  302. const page = pages[pages.length - 1]
  303. const recordId = page?.options?.recordId || ''
  304. const store = this.$scope?.store
  305. const title = store?.currentChart?.O
  306. ? `我的命盘主性格数字是${store.currentChart.O},快来测测你的!`
  307. : '数字能量命盘分析'
  308. return {
  309. title,
  310. path: `/pages/chart/index?recordId=${recordId}`,
  311. imageUrl: '/static/share-card.png'
  312. }
  313. },
  314. onShareTimeline() {
  315. const pages = getCurrentPages()
  316. const page = pages[pages.length - 1]
  317. const recordId = page?.options?.recordId || ''
  318. return {
  319. title: '数字能量命盘分析',
  320. query: `recordId=${recordId}`,
  321. imageUrl: '/static/share-card.png'
  322. }
  323. }
  324. }
  325. </script>
  326. <script setup>
  327. import { ref, computed, onMounted, onUnmounted } from 'vue'
  328. import { useChartStore } from '@/stores/chart'
  329. import { useUserStore } from '@/stores/user'
  330. import TriangleChart from '@/components/TriangleChart.vue'
  331. import ChartShareCanvas from '@/components/ChartShareCanvas.vue'
  332. import { chartApi, chatApi, consultationApi, profileApi, annotationApi, exportApi, interventionApi, planApi, proposalApi, deliveryApi, payApi } from '@/utils/api'
  333. const store = useChartStore()
  334. const userStore = useUserStore()
  335. const chatInput = ref('')
  336. const scrollTop = ref(0)
  337. const messages = ref([])
  338. const loading = ref(false)
  339. const usedChats = ref(0)
  340. const maxChats = ref(0)
  341. const usedAcademic = ref(0)
  342. const maxAcademic = ref(0)
  343. // US-9.3: Intervention state
  344. const hasActiveIntervention = ref(false)
  345. const activeInterventionId = ref(null)
  346. // US-9.4: Plan proposal state
  347. const planRequest = ref(null)
  348. const logs = ref([])
  349. const counterPrice = ref(null)
  350. const showCounterInput = ref(false)
  351. const acceptingOffer = ref(false)
  352. const countering = ref(false)
  353. const rejecting = ref(false)
  354. // US-9.5: Payment and delivery state
  355. const planOrder = ref(null)
  356. const deliveryData = ref([])
  357. // Share canvas ref
  358. const shareCanvas = ref(null)
  359. const shareGenerating = ref(false)
  360. // Annotation / Tag state (US-7.1 / US-7.2)
  361. const annotations = ref([])
  362. const allTags = ref([])
  363. // US-3.1: AI interpretation state
  364. const interpretData = ref(null)
  365. const interpreting = ref(false)
  366. const interpretExpanded = ref({})
  367. // US-9.1: Academic orientation state
  368. const academicData = ref(null)
  369. const academicLoading = ref(false)
  370. const academicExpanded = ref({})
  371. // US-2.1: Share state (shareGenerating declared above with shareCanvas)
  372. const quotaExceeded = computed(() => {
  373. if (userStore.isVipActive) return false
  374. return maxChats.value > 0 && usedChats.value >= maxChats.value
  375. })
  376. const shareRemaining = computed(() => {
  377. if (userStore.isVipActive) return -1
  378. const limit = userStore.shareLimit || 1
  379. const count = userStore.shareCount || 0
  380. return limit > 0 ? Math.max(0, limit - count) : 0
  381. })
  382. const quotaAcademicExceeded = computed(() => {
  383. if (userStore.isVipActive) return false
  384. return maxAcademic.value > 0 && usedAcademic.value >= maxAcademic.value
  385. })
  386. // US-9.4: Computed proposal message from logs
  387. const proposalMessage = computed(() => {
  388. const proposalLog = logs.value.find(l => l.action === 'proposal')
  389. return proposalLog?.message || ''
  390. })
  391. // US-9.4: Current user ID for comparison in negotiation history
  392. const currentUserId = computed(() => userStore.user?.id || userStore.userId || '')
  393. onMounted(async () => {
  394. try {
  395. await userStore.fetchQuota()
  396. } catch (e) { /* ignore */ }
  397. // URL shareability — load from recordId if no store state (US-1.1 §14)
  398. const pages = getCurrentPages()
  399. const currentPage = pages[pages.length - 1]
  400. const recordId = currentPage?.options?.recordId
  401. if (!store.currentChart && recordId) {
  402. try {
  403. await store.loadChart(Number(recordId))
  404. } catch (e) {
  405. // fall through to "no data" message
  406. }
  407. }
  408. if (store.currentChart) {
  409. maxChats.value = userStore.maxChats || 3
  410. usedChats.value = userStore.usedChats || 0
  411. usedAcademic.value = userStore.usedAcademic || 0
  412. maxAcademic.value = userStore.maxAcademic || 1
  413. // Load annotations and tags for this record (US-7.1/7.2)
  414. if (store.currentRecordId) {
  415. loadAnnotations()
  416. }
  417. // US-9.4: Load plan request for this chart
  418. loadPlanRequest()
  419. // Use consultationMessages from store (set during startConsultation) if available
  420. if (store.consultationMessages && store.consultationMessages.length > 0) {
  421. messages.value = store.consultationMessages.map(m => ({
  422. role: m.role === 'ai' ? 'ai' : 'user',
  423. content: m.content,
  424. senderType: m.senderType || null,
  425. interventionId: m.interventionId || null
  426. }))
  427. } else if (store.currentRecordId) {
  428. try {
  429. const history = await chatApi.history(store.currentRecordId)
  430. messages.value = history.map(m => ({
  431. role: m.role === 'ai' ? 'ai' : 'user',
  432. content: m.content,
  433. senderType: m.senderType || null,
  434. interventionId: m.interventionId || null
  435. }))
  436. const userMsgCount = history.filter(m => m.role === 'user').length
  437. usedChats.value = Math.max(usedChats.value, userMsgCount)
  438. } catch (e) {
  439. // fallback to generic greeting
  440. }
  441. }
  442. // US-9.3: Check intervention status from loaded messages
  443. checkInterventionStatus(messages.value)
  444. // Ensure at least a greeting message if still empty
  445. if (messages.value.length === 0) {
  446. messages.value = [{
  447. role: 'ai',
  448. content: `已为您生成命盘。主性格数字为${store.currentChart.O}。请点击下方问题或输入您想了解的内容。`
  449. }]
  450. }
  451. } else {
  452. messages.value = [{
  453. role: 'ai',
  454. content: '暂无命盘数据,请先返回首页输入出生信息。'
  455. }]
  456. }
  457. })
  458. // US-9.3: Check if intervention is active from message history
  459. function checkInterventionStatus(messages) {
  460. let active = false
  461. let lastId = null
  462. for (const msg of messages) {
  463. if (msg.senderType === 'system') {
  464. if (msg.content && msg.content.includes('能量师') && msg.content.includes('进入')) {
  465. active = true
  466. lastId = msg.interventionId
  467. }
  468. if (msg.content && (msg.content.includes('退出') || msg.content.includes('结束'))) {
  469. active = false
  470. }
  471. }
  472. }
  473. hasActiveIntervention.value = active
  474. activeInterventionId.value = lastId
  475. }
  476. // US-9.3: Long-press handler for practitioner / entry system messages
  477. function onLongPressMsg(msg) {
  478. // Only show action if intervention is active
  479. if (!hasActiveIntervention.value) return
  480. // Only on practitioner messages or entry system messages
  481. if (msg.senderType === 'practitioner' || (msg.senderType === 'system' && msg.content && msg.content.includes('能量师'))) {
  482. uni.showActionSheet({
  483. itemList: ['结束能量师介入'],
  484. success: async (res) => {
  485. if (res.tapIndex === 0) {
  486. await endIntervention()
  487. }
  488. }
  489. })
  490. }
  491. }
  492. // US-9.3: End intervention by user's request
  493. async function endIntervention() {
  494. uni.showLoading({ title: '处理中...' })
  495. try {
  496. await interventionApi.endByUser(activeInterventionId.value)
  497. uni.hideLoading()
  498. uni.showToast({ title: '已结束能量师介入', icon: 'success' })
  499. hasActiveIntervention.value = false
  500. activeInterventionId.value = null
  501. } catch (e) {
  502. uni.hideLoading()
  503. uni.showToast({ title: e.message || '操作失败', icon: 'none' })
  504. }
  505. }
  506. // US-3.1: Fetch AI structured interpretation
  507. async function fetchInterpret() {
  508. if (!store.currentRecordId) {
  509. uni.showToast({ title: '暂无命盘数据', icon: 'none' })
  510. return
  511. }
  512. // If already loaded, just scroll to it
  513. if (interpretData.value) {
  514. scrollToInterpret()
  515. return
  516. }
  517. interpreting.value = true
  518. try {
  519. const data = await chartApi.interpret(store.currentRecordId)
  520. interpretData.value = data
  521. // Auto-expand the main character section
  522. interpretExpanded.value = { mainCharacter: true }
  523. uni.showToast({ title: '解读已生成', icon: 'success' })
  524. scrollToInterpret()
  525. } catch (e) {
  526. uni.showToast({ title: e.message || '解读生成失败', icon: 'none' })
  527. } finally {
  528. interpreting.value = false
  529. }
  530. }
  531. function scrollToInterpret() {
  532. // Scroll to the interpret section after rendering
  533. uni.createSelectorQuery().select('#interpret-section').boundingClientRect(res => {
  534. if (res) {
  535. uni.pageScrollTo({ scrollTop: res.top - 20, duration: 300 })
  536. }
  537. }).exec()
  538. }
  539. function toggleInterpretSection(key) {
  540. interpretExpanded.value[key] = !interpretExpanded.value[key]
  541. }
  542. // US-9.1: Fetch academic orientation
  543. async function fetchAcademicOrientation() {
  544. if (!store.currentRecordId) {
  545. uni.showToast({ title: '暂无命盘数据', icon: 'none' })
  546. return
  547. }
  548. if (academicData.value) {
  549. scrollToAcademic()
  550. return
  551. }
  552. if (quotaAcademicExceeded.value) {
  553. uni.showToast({ title: '今日学业分析次数已用完', icon: 'none' })
  554. return
  555. }
  556. academicLoading.value = true
  557. try {
  558. const data = await chartApi.academicOrientation(store.currentRecordId)
  559. academicData.value = data
  560. academicExpanded.value = { talentTendency: true }
  561. usedAcademic.value++
  562. userStore.usedAcademic = usedAcademic.value
  563. uni.showToast({ title: '分析已生成', icon: 'success' })
  564. scrollToAcademic()
  565. } catch (e) {
  566. if (e.code === 1008) {
  567. usedAcademic.value = maxAcademic.value
  568. userStore.usedAcademic = maxAcademic.value
  569. }
  570. uni.showToast({ title: e.message || '分析生成失败', icon: 'none' })
  571. } finally {
  572. academicLoading.value = false
  573. }
  574. }
  575. function scrollToAcademic() {
  576. uni.createSelectorQuery().select('#academic-section').boundingClientRect(res => {
  577. if (res) uni.pageScrollTo({ scrollTop: res.top - 20, duration: 300 })
  578. }).exec()
  579. }
  580. function toggleAcademicSection(key) {
  581. academicExpanded.value[key] = !academicExpanded.value[key]
  582. }
  583. async function sendQuestion(q) {
  584. if (!q.trim() || loading.value) return
  585. if (quotaExceeded.value) {
  586. // Show upgrade prompt instead of sending
  587. uni.showModal({
  588. title: '今日解读次数已用完',
  589. content: '开通 VIP 会员享无限次 AI 解读,或分享好友获取额外次数',
  590. confirmText: '开通会员',
  591. cancelText: '分享好友',
  592. success: (res) => {
  593. if (res.confirm) {
  594. goSubscribeC()
  595. } else {
  596. triggerShare()
  597. }
  598. }
  599. })
  600. return
  601. }
  602. messages.value.push({ role: 'user', content: q })
  603. chatInput.value = ''
  604. loading.value = true
  605. try {
  606. if (store.currentRecordId) {
  607. const res = await chatApi.send({ chartRecordId: store.currentRecordId, query: q })
  608. messages.value.push({ role: 'ai', content: res.reply })
  609. usedChats.value++
  610. } else {
  611. messages.value.push({
  612. role: 'ai',
  613. content: `数字能量分析显示,${q}方面您的能量组合表明有良好的发展潜力,请结合自身情况理性决策。`
  614. })
  615. }
  616. } catch (e) {
  617. if (e.code === 1008) {
  618. usedChats.value = maxChats.value
  619. messages.value.push({
  620. role: 'ai',
  621. content: '今日解读次数已用完,开通会员可享无限次解读。'
  622. })
  623. } else {
  624. messages.value.push({
  625. role: 'ai',
  626. content: e.message || '解读服务暂时不可用,请稍后再试。'
  627. })
  628. }
  629. }
  630. loading.value = false
  631. scrollTop.value = 99999
  632. }
  633. // US-3.3: Regenerate interpretation via action sheet menu
  634. function showMenu() {
  635. const items = ['🔄 重新生成解读']
  636. // US-2.2: PDF export for paid users only
  637. if (userStore.isVipActive) {
  638. items.push('📄 导出 PDF')
  639. }
  640. if (!userStore.isVipActive && shareRemaining.value > 0) {
  641. items.push('📤 分享给好友')
  642. }
  643. items.push('❌ 取消')
  644. uni.showActionSheet({
  645. itemList: items,
  646. success: async (res) => {
  647. if (res.tapIndex === 0) {
  648. await confirmRegenerate()
  649. } else if (res.tapIndex === 1) {
  650. if (items[1] === '📄 导出 PDF') {
  651. await exportPdf()
  652. } else if (items[1] === '📤 分享给好友') {
  653. await triggerShare()
  654. }
  655. } else if (res.tapIndex === 2 && items[2] === '📤 分享给好友') {
  656. await triggerShare()
  657. }
  658. }
  659. })
  660. }
  661. async function confirmRegenerate() {
  662. uni.showModal({
  663. title: '重新生成解读',
  664. content: '将清除当前对话记录并生成全新的AI解读,确定吗?',
  665. success: async (confirm) => {
  666. if (!confirm.confirm) return
  667. if (!store.currentRecordId) {
  668. uni.showToast({ title: '暂无命盘数据', icon: 'none' })
  669. return
  670. }
  671. try {
  672. uni.showLoading({ title: '重新生成中...' })
  673. const newMessages = await consultationApi.regenerate(store.currentRecordId)
  674. messages.value = (newMessages || []).map(m => ({
  675. role: m.role === 'ai' ? 'ai' : 'user',
  676. content: m.content
  677. }))
  678. // Reset chat counters
  679. usedChats.value = 0
  680. uni.hideLoading()
  681. uni.showToast({ title: '解读已重新生成', icon: 'success' })
  682. scrollTop.value = 99999
  683. } catch (e) {
  684. uni.hideLoading()
  685. uni.showToast({ title: '重新生成失败,请重试', icon: 'none' })
  686. }
  687. }
  688. })
  689. }
  690. // US-2.1: Track share and update quota
  691. async function triggerShare() {
  692. uni.showShareMenu({
  693. withShareTicket: true,
  694. menus: ['shareAppMessage', 'shareTimeline'],
  695. success: async () => {
  696. // Call backend to track share
  697. try {
  698. const result = await profileApi.trackShare()
  699. shareCount.value = result.dailyShareCount || shareCount.value + 1
  700. } catch (e) { /* best-effort */ }
  701. }
  702. })
  703. }
  704. function goSubscribe() {
  705. uni.navigateTo({ url: '/pages/subscribe/index' })
  706. }
  707. function goSubscribeC() {
  708. uni.navigateTo({ url: '/pages/payment/index?product=annual' })
  709. }
  710. function goPoster() {
  711. uni.navigateTo({ url: '/pages/subscribe/index' })
  712. }
  713. // EPIC 9: Navigate to plan apply page
  714. function goPlanApply() {
  715. if (!userStore.isLoggedIn) {
  716. uni.navigateTo({ url: '/pages/login/index' })
  717. return
  718. }
  719. if (!store.currentRecordId) {
  720. uni.showToast({ title: '暂无命盘数据', icon: 'none' })
  721. return
  722. }
  723. uni.navigateTo({ url: `/pages/plan/apply?chartRecordId=${store.currentRecordId}` })
  724. }
  725. // US-2.1: Share chart image
  726. async function onShareTap() {
  727. if (shareGenerating.value) return
  728. // Check quota for non-VIP
  729. if (!userStore.isVipActive && shareRemaining.value <= 0) {
  730. uni.showToast({ title: '今日分享次数已用完', icon: 'none' })
  731. return
  732. }
  733. // Show action sheet
  734. uni.showActionSheet({
  735. itemList: ['保存到相册', '分享给微信好友', '分享到朋友圈'],
  736. success: async (res) => {
  737. await performShare(res.tapIndex)
  738. },
  739. fail: (err) => {
  740. if (err.errMsg && err.errMsg.includes('cancel')) return
  741. uni.showToast({ title: '操作取消', icon: 'none' })
  742. }
  743. })
  744. }
  745. async function performShare(actionIndex) {
  746. shareGenerating.value = true
  747. try {
  748. // Consume quota if not VIP
  749. if (!userStore.isVipActive) {
  750. try {
  751. await profileApi.trackShare()
  752. // Optimistically update local store
  753. userStore.shareCount++
  754. } catch (e) {
  755. uni.showToast({ title: '分享次数不足', icon: 'none' })
  756. shareGenerating.value = false
  757. return
  758. }
  759. }
  760. // Generate image via canvas
  761. if (!shareCanvas.value) throw new Error('Canvas未就绪')
  762. const filePath = await shareCanvas.value.generate()
  763. if (actionIndex === 0) {
  764. // Save to album
  765. await uni.saveImageToPhotosAlbum({ filePath })
  766. uni.showToast({ title: '已保存到相册', icon: 'success' })
  767. } else {
  768. // Share via WeChat
  769. const scene = actionIndex === 1 ? 'WXSceneSession' : 'WXSceneTimeline'
  770. const title = store.currentChart?.O
  771. ? `我的命盘主性格数字是${store.currentChart.O},快来测测你的!`
  772. : '数字能量命盘分析'
  773. await uni.share({
  774. provider: 'weixin',
  775. scene,
  776. type: 'image',
  777. imageUrl: filePath,
  778. title
  779. })
  780. }
  781. } catch (e) {
  782. uni.showToast({ title: e.message || '分享失败', icon: 'none' })
  783. } finally {
  784. shareGenerating.value = false
  785. }
  786. }
  787. // US-2.2: Export PDF (paid users only)
  788. async function exportPdf() {
  789. if (!store.currentRecordId) {
  790. uni.showToast({ title: '暂无命盘记录', icon: 'none' })
  791. return
  792. }
  793. uni.showLoading({ title: '生成中...' })
  794. try {
  795. const filePath = await exportApi.pdf(store.currentRecordId)
  796. uni.hideLoading()
  797. // Open PDF document
  798. uni.openDocument({
  799. filePath,
  800. success: () => {
  801. uni.showToast({ title: 'PDF 已生成', icon: 'success' })
  802. },
  803. fail: () => {
  804. uni.showToast({ title: '打开文件失败', icon: 'none' })
  805. }
  806. })
  807. } catch (e) {
  808. uni.hideLoading()
  809. uni.showToast({ title: e.message || '导出失败', icon: 'none' })
  810. }
  811. }
  812. // === Annotation / Tag handlers (US-7.1 / US-7.2) ===
  813. async function loadAnnotations() {
  814. if (!store.currentRecordId) return
  815. try {
  816. const [annoList, tagList] = await Promise.all([
  817. annotationApi.list(store.currentRecordId),
  818. annotationApi.listTags(store.currentRecordId)
  819. ])
  820. annotations.value = annoList || []
  821. allTags.value = tagList || []
  822. } catch (e) { /* best-effort */ }
  823. }
  824. async function onAnnotationAdd(data) {
  825. try {
  826. const saved = await annotationApi.add(data)
  827. // Reload annotations to include the new one with server-generated ID
  828. if (store.currentRecordId) {
  829. const annoList = await annotationApi.list(store.currentRecordId)
  830. annotations.value = annoList || []
  831. }
  832. uni.showToast({ title: '批注已保存', icon: 'success' })
  833. } catch (e) {
  834. uni.showToast({ title: '保存失败', icon: 'none' })
  835. }
  836. }
  837. async function onAnnotationDelete(annoId) {
  838. try {
  839. await annotationApi.delete(annoId)
  840. annotations.value = annotations.value.filter(a => a.id !== annoId)
  841. uni.showToast({ title: '批注已删除', icon: 'success' })
  842. } catch (e) {
  843. uni.showToast({ title: '删除失败', icon: 'none' })
  844. }
  845. }
  846. async function onTagCreate(tagData) {
  847. try {
  848. const saved = await annotationApi.addTag({ ...tagData, recordId: store.currentRecordId })
  849. if (store.currentRecordId) {
  850. const tagList = await annotationApi.listTags(store.currentRecordId)
  851. allTags.value = tagList || []
  852. }
  853. uni.showToast({ title: '标签已创建', icon: 'success' })
  854. } catch (e) {
  855. uni.showToast({ title: '创建失败', icon: 'none' })
  856. }
  857. }
  858. // US-9.4: Load plan request for current chart
  859. async function loadPlanRequest() {
  860. if (!store.currentRecordId) return
  861. try {
  862. const requests = await planApi.myRequests()
  863. const match = requests.find(r => r.chartRecordId === store.currentRecordId && (r.status === 'negotiating' || r.status === 'pending_payment' || r.status === 'paid'))
  864. if (match) {
  865. planRequest.value = match
  866. const logList = await proposalApi.logs(match.id)
  867. logs.value = logList || []
  868. // US-9.5: Load delivery data when status is paid
  869. if (match.status === 'paid') {
  870. try {
  871. const deliveries = await deliveryApi.list(match.id)
  872. deliveryData.value = deliveries || []
  873. } catch (e) { /* best-effort */ }
  874. }
  875. }
  876. } catch (e) {
  877. // No plan request found — that's fine
  878. }
  879. }
  880. // US-9.4: Accept offer
  881. async function acceptOffer() {
  882. uni.showModal({
  883. title: '接受方案',
  884. content: `确定以 ¥${planRequest.value.price} 接受此方案吗?`,
  885. success: async (res) => {
  886. if (!res.confirm) return
  887. acceptingOffer.value = true
  888. try {
  889. const result = await proposalApi.accept({ requestId: planRequest.value.id })
  890. planRequest.value = result.planRequest
  891. planOrder.value = result.order
  892. const logList = await proposalApi.logs(planRequest.value.id)
  893. logs.value = logList || []
  894. uni.showToast({ title: '已接受方案,请完成支付', icon: 'success' })
  895. } catch (e) {
  896. uni.showToast({ title: e.message || '操作失败', icon: 'none' })
  897. } finally {
  898. acceptingOffer.value = false
  899. }
  900. }
  901. })
  902. }
  903. // US-9.5: Navigate to payment page
  904. function goPay() {
  905. if (!planOrder.value || !planRequest.value) return
  906. uni.navigateTo({
  907. url: `/pages/payment/index?product=practitioner_plan&totalFee=${planRequest.value.price}&outTradeNo=${planOrder.value.outTradeNo}`
  908. })
  909. }
  910. // US-9.4: Reject offer
  911. async function rejectOffer() {
  912. uni.showModal({
  913. title: '拒绝方案',
  914. content: '确定拒绝此方案吗?',
  915. success: async (res) => {
  916. if (!res.confirm) return
  917. rejecting.value = true
  918. try {
  919. await proposalApi.reject({ requestId: planRequest.value.id })
  920. planRequest.value.status = 'rejected'
  921. uni.showToast({ title: '已拒绝', icon: 'success' })
  922. } catch (e) {
  923. uni.showToast({ title: e.message || '操作失败', icon: 'none' })
  924. } finally {
  925. rejecting.value = false
  926. }
  927. }
  928. })
  929. }
  930. // US-9.4: Submit counter-offer
  931. async function submitCounter() {
  932. const price = Number(counterPrice.value)
  933. if (!price || price <= 0) {
  934. uni.showToast({ title: '请输入有效价格', icon: 'none' })
  935. return
  936. }
  937. countering.value = true
  938. try {
  939. await proposalApi.counter({ requestId: planRequest.value.id, newPrice: price })
  940. const logList = await proposalApi.logs(planRequest.value.id)
  941. logs.value = logList || []
  942. showCounterInput.value = false
  943. counterPrice.value = null
  944. uni.showToast({ title: '已提交还价', icon: 'success' })
  945. } catch (e) {
  946. uni.showToast({ title: e.message || '还价失败', icon: 'none' })
  947. } finally {
  948. countering.value = false
  949. }
  950. }
  951. </script>
  952. <style scoped lang="scss">
  953. .page-chart {
  954. height: 100vh;
  955. display: flex;
  956. flex-direction: column;
  957. overflow: hidden;
  958. background: linear-gradient(180deg, #0c0a1a 0%, #120d28 50%, #0f0c1e 100%);
  959. }
  960. /* Top operation bar */
  961. .top-bar {
  962. display: flex;
  963. justify-content: flex-end;
  964. align-items: center;
  965. padding: 8px 12px;
  966. background: rgba(0,0,0,0.15);
  967. }
  968. .spacer {
  969. flex: 1;
  970. }
  971. .export-btn {
  972. font-size: 13px;
  973. color: #fbbf24;
  974. padding: 6px 12px;
  975. border: 1px solid rgba(251,191,36,0.3);
  976. border-radius: 8px;
  977. }
  978. .chart-section {
  979. flex-shrink: 0;
  980. margin: 0 14px;
  981. }
  982. /* Menu bar — three-dot button */
  983. .menu-bar {
  984. display: flex;
  985. justify-content: flex-end;
  986. padding: 0 14px;
  987. margin: -8px 0 0;
  988. }
  989. .menu-btn {
  990. width: 32px;
  991. height: 28px;
  992. display: flex;
  993. align-items: center;
  994. justify-content: center;
  995. font-size: 20px;
  996. font-weight: 700;
  997. color: rgba(255, 255, 255, 0.4);
  998. letter-spacing: 2px;
  999. border-radius: 6px;
  1000. background: rgba(255, 255, 255, 0.04);
  1001. }
  1002. .menu-btn:active {
  1003. background: rgba(255, 255, 255, 0.08);
  1004. }
  1005. /* US-3.1: Interpret button bar */
  1006. .interpret-bar {
  1007. flex-shrink: 0;
  1008. margin: 0 14px 10px;
  1009. }
  1010. .interpret-btn {
  1011. width: 100%;
  1012. height: 44px;
  1013. background: linear-gradient(135deg, #7c3aed, #6366f1);
  1014. color: #fff;
  1015. border: none;
  1016. border-radius: 12px;
  1017. line-height: 44px;
  1018. font-size: 15px;
  1019. font-weight: 600;
  1020. letter-spacing: 1px;
  1021. }
  1022. .interpret-btn[disabled] {
  1023. opacity: 0.5;
  1024. }
  1025. /* Interpretation panel */
  1026. .interpret-panel {
  1027. flex-shrink: 0;
  1028. margin: 0 14px 16px;
  1029. }
  1030. .interpret-card {
  1031. background: rgba(124, 58, 237, 0.06);
  1032. border: 1px solid rgba(124, 58, 237, 0.12);
  1033. border-radius: 12px;
  1034. margin-bottom: 10px;
  1035. overflow: hidden;
  1036. transition: border-color 0.2s;
  1037. }
  1038. .interpret-card.expanded {
  1039. border-color: rgba(124, 58, 237, 0.25);
  1040. background: rgba(124, 58, 237, 0.08);
  1041. }
  1042. .interpret-card-header {
  1043. display: flex;
  1044. align-items: center;
  1045. padding: 12px 14px;
  1046. gap: 10px;
  1047. }
  1048. .interpret-num-badge {
  1049. width: 28px;
  1050. height: 28px;
  1051. border-radius: 50%;
  1052. background: linear-gradient(135deg, #7c3aed, #6366f1);
  1053. color: #fff;
  1054. font-size: 13px;
  1055. font-weight: 700;
  1056. line-height: 28px;
  1057. text-align: center;
  1058. flex-shrink: 0;
  1059. }
  1060. .interpret-card-title {
  1061. flex: 1;
  1062. font-size: 14px;
  1063. font-weight: 600;
  1064. color: rgba(255, 255, 255, 0.85);
  1065. }
  1066. .interpret-toggle {
  1067. font-size: 12px;
  1068. color: rgba(255, 255, 255, 0.35);
  1069. }
  1070. .interpret-card-body {
  1071. padding: 0 14px 14px 52px;
  1072. }
  1073. .interpret-content {
  1074. display: block;
  1075. font-size: 13px;
  1076. line-height: 1.7;
  1077. color: rgba(255, 255, 255, 0.65);
  1078. white-space: pre-wrap;
  1079. }
  1080. /* Disclaimer */
  1081. .interpret-disclaimer {
  1082. text-align: center;
  1083. padding: 6px 0 2px;
  1084. }
  1085. .disclaimer-text {
  1086. font-size: 11px;
  1087. color: rgba(255, 255, 255, 0.2);
  1088. }
  1089. /* Loading skeleton */
  1090. .interpret-skeleton {
  1091. flex-shrink: 0;
  1092. margin: 0 14px 16px;
  1093. }
  1094. .skeleton-card {
  1095. background: rgba(255, 255, 255, 0.03);
  1096. border: 1px solid rgba(255, 255, 255, 0.05);
  1097. border-radius: 12px;
  1098. margin-bottom: 10px;
  1099. overflow: hidden;
  1100. }
  1101. .skeleton-header {
  1102. height: 24px;
  1103. margin: 12px 14px;
  1104. background: rgba(255, 255, 255, 0.06);
  1105. border-radius: 6px;
  1106. width: 60%;
  1107. }
  1108. .skeleton-body {
  1109. padding: 0 14px 14px;
  1110. }
  1111. .skeleton-line {
  1112. height: 12px;
  1113. background: rgba(255, 255, 255, 0.04);
  1114. border-radius: 4px;
  1115. margin-bottom: 8px;
  1116. }
  1117. .skeleton-line.short {
  1118. width: 70%;
  1119. }
  1120. /* US-9.1: Academic bar */
  1121. .academic-bar {
  1122. flex-shrink: 0;
  1123. margin: 0 14px 10px;
  1124. display: flex;
  1125. gap: 8px;
  1126. align-items: center;
  1127. }
  1128. .academic-btn {
  1129. flex: 1;
  1130. height: 44px;
  1131. background: linear-gradient(135deg, #0891b2, #06b6d4);
  1132. color: #fff;
  1133. border: none;
  1134. border-radius: 12px;
  1135. line-height: 44px;
  1136. font-size: 15px;
  1137. font-weight: 600;
  1138. letter-spacing: 1px;
  1139. }
  1140. .academic-btn[disabled] { opacity: 0.5; }
  1141. .academic-quota {
  1142. font-size: 11px;
  1143. color: #22d3ee;
  1144. background: rgba(34, 211, 238, 0.1);
  1145. padding: 3px 10px;
  1146. border-radius: 10px;
  1147. border: 1px solid rgba(34, 211, 238, 0.15);
  1148. white-space: nowrap;
  1149. }
  1150. /* US-9.1: Academic panel */
  1151. .academic-panel {
  1152. flex-shrink: 0;
  1153. margin: 0 14px 16px;
  1154. }
  1155. .academic-section-title {
  1156. font-size: 16px;
  1157. font-weight: 700;
  1158. color: #22d3ee;
  1159. margin-bottom: 12px;
  1160. padding-left: 2px;
  1161. }
  1162. .academic-card {
  1163. border-radius: 12px;
  1164. margin-bottom: 10px;
  1165. overflow: hidden;
  1166. transition: border-color 0.2s;
  1167. }
  1168. .academic-card.talent { background: rgba(8, 145, 178, 0.06); border: 1px solid rgba(8, 145, 178, 0.12); }
  1169. .academic-card.talent.expanded { border-color: rgba(8, 145, 178, 0.25); background: rgba(8, 145, 178, 0.08); }
  1170. .academic-card.direction { background: rgba(251, 191, 36, 0.06); border: 1px solid rgba(251, 191, 36, 0.12); }
  1171. .academic-card.direction.expanded { border-color: rgba(251, 191, 36, 0.25); background: rgba(251, 191, 36, 0.08); }
  1172. .academic-card.learning { background: rgba(52, 211, 153, 0.06); border: 1px solid rgba(52, 211, 153, 0.12); }
  1173. .academic-card.learning.expanded { border-color: rgba(52, 211, 153, 0.25); background: rgba(52, 211, 153, 0.08); }
  1174. .academic-card.parenting { background: rgba(244, 114, 182, 0.06); border: 1px solid rgba(244, 114, 182, 0.12); }
  1175. .academic-card.parenting.expanded { border-color: rgba(244, 114, 182, 0.25); background: rgba(244, 114, 182, 0.08); }
  1176. .academic-card.periods { background: rgba(167, 139, 250, 0.06); border: 1px solid rgba(167, 139, 250, 0.12); }
  1177. .academic-card.periods.expanded { border-color: rgba(167, 139, 250, 0.25); background: rgba(167, 139, 250, 0.08); }
  1178. .academic-card-header {
  1179. display: flex;
  1180. align-items: center;
  1181. padding: 12px 14px;
  1182. gap: 10px;
  1183. }
  1184. .academic-card-title {
  1185. flex: 1;
  1186. font-size: 14px;
  1187. font-weight: 600;
  1188. color: rgba(255,255,255,0.85);
  1189. }
  1190. .academic-toggle {
  1191. font-size: 12px;
  1192. color: rgba(255,255,255,0.35);
  1193. }
  1194. .academic-card-body {
  1195. padding: 0 14px 14px;
  1196. }
  1197. .academic-content {
  1198. display: block;
  1199. font-size: 13px;
  1200. line-height: 1.7;
  1201. color: rgba(255,255,255,0.65);
  1202. white-space: pre-wrap;
  1203. }
  1204. /* Match percentage bars */
  1205. .match-bars {
  1206. margin-top: 10px;
  1207. }
  1208. .match-row {
  1209. display: flex;
  1210. align-items: center;
  1211. gap: 8px;
  1212. margin-bottom: 8px;
  1213. }
  1214. .match-label {
  1215. width: 32px;
  1216. font-size: 12px;
  1217. color: rgba(255,255,255,0.55);
  1218. text-align: right;
  1219. }
  1220. .match-track {
  1221. flex: 1;
  1222. height: 8px;
  1223. background: rgba(255,255,255,0.06);
  1224. border-radius: 4px;
  1225. overflow: hidden;
  1226. }
  1227. .match-fill {
  1228. height: 100%;
  1229. border-radius: 4px;
  1230. transition: width 0.4s ease;
  1231. }
  1232. .match-fill.pct-80, .match-fill.pct-85, .match-fill.pct-90, .match-fill.pct-95 { background: linear-gradient(90deg, #22d3ee, #06b6d4); }
  1233. .match-fill.pct-60, .match-fill.pct-65, .match-fill.pct-70, .match-fill.pct-75 { background: linear-gradient(90deg, #fbbf24, #f59e0b); }
  1234. .match-fill.pct-40, .match-fill.pct-45, .match-fill.pct-50, .match-fill.pct-55 { background: linear-gradient(90deg, #f87171, #ef4444); }
  1235. .match-value {
  1236. width: 36px;
  1237. font-size: 12px;
  1238. font-weight: 600;
  1239. color: rgba(255,255,255,0.6);
  1240. text-align: right;
  1241. }
  1242. /* Academic disclaimer */
  1243. .academic-disclaimer {
  1244. text-align: center;
  1245. padding: 6px 0 2px;
  1246. }
  1247. .academic-disclaimer .disclaimer-text {
  1248. font-size: 11px;
  1249. color: rgba(255,255,255,0.2);
  1250. }
  1251. /* Academic loading skeleton */
  1252. .academic-skeleton {
  1253. flex-shrink: 0;
  1254. margin: 0 14px 16px;
  1255. }
  1256. /* Academic quota exceeded upgrade card */
  1257. .academic-upgrade-card {
  1258. flex-shrink: 0;
  1259. margin: 0 14px 16px;
  1260. background: linear-gradient(135deg, rgba(8, 145, 178, 0.08), rgba(6, 182, 212, 0.05));
  1261. border: 1px solid rgba(8, 145, 178, 0.15);
  1262. border-radius: 12px;
  1263. padding: 20px;
  1264. text-align: center;
  1265. }
  1266. .academic-upgrade-title {
  1267. display: block;
  1268. font-size: 15px;
  1269. font-weight: 600;
  1270. color: #22d3ee;
  1271. margin-bottom: 6px;
  1272. }
  1273. .academic-upgrade-desc {
  1274. display: block;
  1275. font-size: 12px;
  1276. color: rgba(255,255,255,0.45);
  1277. margin-bottom: 14px;
  1278. }
  1279. .academic-upgrade-btn {
  1280. background: linear-gradient(135deg, #0891b2, #06b6d4);
  1281. color: #fff;
  1282. border: none;
  1283. border-radius: 10px;
  1284. padding: 12px 40px;
  1285. font-size: 15px;
  1286. font-weight: 600;
  1287. }
  1288. .chat-section {
  1289. flex: 1;
  1290. display: flex;
  1291. flex-direction: column;
  1292. overflow: hidden;
  1293. min-height: 0;
  1294. margin: 0 14px;
  1295. }
  1296. .section-title {
  1297. font-size: 17px;
  1298. font-weight: 600;
  1299. color: rgba(255, 255, 255, 0.9);
  1300. margin: 12px 0 14px;
  1301. display: flex;
  1302. align-items: center;
  1303. gap: 8px;
  1304. }
  1305. .title-icon {
  1306. font-size: 14px;
  1307. color: #fbbf24;
  1308. opacity: 0.7;
  1309. }
  1310. .quota-badge {
  1311. font-size: 11px;
  1312. font-weight: 400;
  1313. color: #fbbf24;
  1314. background: rgba(251, 191, 36, 0.12);
  1315. padding: 2px 10px;
  1316. border-radius: 10px;
  1317. border: 1px solid rgba(251, 191, 36, 0.15);
  1318. }
  1319. .quota-badge.vip {
  1320. color: #34d399;
  1321. background: rgba(52, 211, 153, 0.1);
  1322. border-color: rgba(52, 211, 153, 0.15);
  1323. }
  1324. .chat-box {
  1325. flex: 1;
  1326. height: auto;
  1327. overflow-y: auto;
  1328. background: rgba(255, 255, 255, 0.04);
  1329. border: 1px solid rgba(255, 255, 255, 0.06);
  1330. border-radius: 14px;
  1331. padding: 14px;
  1332. margin-bottom: 10px;
  1333. }
  1334. .msg-row {
  1335. margin-bottom: 14px;
  1336. display: flex;
  1337. }
  1338. .msg-row.user {
  1339. justify-content: flex-end;
  1340. }
  1341. .msg-bubble {
  1342. max-width: 80%;
  1343. padding: 10px 16px;
  1344. border-radius: 14px;
  1345. font-size: 14px;
  1346. line-height: 1.5;
  1347. color: rgba(255, 255, 255, 0.85);
  1348. background: rgba(255, 255, 255, 0.06);
  1349. border: 1px solid rgba(255, 255, 255, 0.06);
  1350. }
  1351. .msg-row.user .msg-bubble {
  1352. background: rgba(251, 191, 36, 0.12);
  1353. border-color: rgba(251, 191, 36, 0.15);
  1354. color: #fbbf24;
  1355. }
  1356. /* US-9.3: Practitioner message bubble */
  1357. .msg-row.practitioner {
  1358. justify-content: flex-start;
  1359. flex-direction: column;
  1360. align-items: flex-start;
  1361. }
  1362. .msg-practitioner-label {
  1363. font-size: 12px;
  1364. color: #10b981;
  1365. margin-bottom: 4px;
  1366. font-weight: 500;
  1367. }
  1368. .msg-practitioner-bubble {
  1369. background: rgba(16, 185, 129, 0.15);
  1370. border: 1px solid rgba(16, 185, 129, 0.3);
  1371. color: rgba(255, 255, 255, 0.85);
  1372. }
  1373. /* US-9.3: System message — centered italic text */
  1374. .msg-system {
  1375. text-align: center;
  1376. padding: 8px 0;
  1377. color: rgba(255, 255, 255, 0.4);
  1378. font-style: italic;
  1379. font-size: 12px;
  1380. line-height: 1.6;
  1381. }
  1382. .input-bar {
  1383. display: flex;
  1384. gap: 8px;
  1385. }
  1386. .chat-input {
  1387. flex: 1;
  1388. height: 42px;
  1389. border: 1px solid rgba(255, 255, 255, 0.1);
  1390. border-radius: 10px;
  1391. padding: 0 14px;
  1392. font-size: 14px;
  1393. color: rgba(255, 255, 255, 0.85);
  1394. background: rgba(255, 255, 255, 0.04);
  1395. }
  1396. .send-btn {
  1397. height: 42px;
  1398. padding: 0 24px;
  1399. background: linear-gradient(135deg, #f59e0b, #d97706);
  1400. color: #fff;
  1401. border: none;
  1402. border-radius: 10px;
  1403. line-height: 42px;
  1404. font-size: 14px;
  1405. font-weight: 500;
  1406. }
  1407. .send-btn[disabled] {
  1408. opacity: 0.4;
  1409. }
  1410. /* Share hint */
  1411. .share-hint {
  1412. padding: 8px 0;
  1413. text-align: center;
  1414. }
  1415. .share-hint-text {
  1416. font-size: 11px;
  1417. color: rgba(255, 255, 255, 0.3);
  1418. }
  1419. /* Upgrade prompt */
  1420. .upgrade-prompt {
  1421. background: linear-gradient(135deg, rgba(251, 191, 36, 0.08), rgba(217, 119, 6, 0.06));
  1422. border: 1px solid rgba(251, 191, 36, 0.12);
  1423. border-radius: 14px;
  1424. padding: 22px;
  1425. text-align: center;
  1426. margin-bottom: 12px;
  1427. }
  1428. .upgrade-text {
  1429. display: block;
  1430. font-size: 16px;
  1431. font-weight: 600;
  1432. color: #fbbf24;
  1433. }
  1434. .upgrade-desc {
  1435. display: block;
  1436. font-size: 13px;
  1437. color: rgba(255, 255, 255, 0.45);
  1438. margin: 8px 0 16px;
  1439. }
  1440. .upgrade-actions {
  1441. display: flex;
  1442. gap: 10px;
  1443. justify-content: center;
  1444. }
  1445. .upgrade-btn {
  1446. flex: 1;
  1447. background: linear-gradient(135deg, #f59e0b, #d97706);
  1448. color: #fff;
  1449. border: none;
  1450. border-radius: 10px;
  1451. padding: 10px 0;
  1452. font-size: 14px;
  1453. font-weight: 500;
  1454. line-height: 1.4;
  1455. }
  1456. .upgrade-btn.secondary {
  1457. background: rgba(255, 255, 255, 0.06);
  1458. border: 1px solid rgba(251, 191, 36, 0.2);
  1459. color: #fbbf24;
  1460. }
  1461. /* US-6.6: Subscribe section */
  1462. .subscribe-section {
  1463. margin-top: 10px;
  1464. }
  1465. .vip-badge-bar {
  1466. background: rgba(52, 211, 153, 0.06);
  1467. border: 1px solid rgba(52, 211, 153, 0.15);
  1468. border-radius: 10px;
  1469. padding: 12px 14px;
  1470. display: flex;
  1471. flex-wrap: wrap;
  1472. align-items: center;
  1473. gap: 6px;
  1474. }
  1475. .vip-badge-bar.pro {
  1476. background: rgba(167, 139, 250, 0.06);
  1477. border-color: rgba(167, 139, 250, 0.15);
  1478. }
  1479. .vip-badge-text {
  1480. font-size: 13px;
  1481. font-weight: 600;
  1482. color: #34d399;
  1483. }
  1484. .vip-badge-bar.pro .vip-badge-text {
  1485. color: #a78bfa;
  1486. }
  1487. .vip-badge-expiry {
  1488. font-size: 11px;
  1489. color: rgba(255,255,255,0.4);
  1490. }
  1491. .vip-poster-link {
  1492. font-size: 12px;
  1493. color: #fbbf24;
  1494. margin-left: auto;
  1495. }
  1496. .subscribe-card {
  1497. background: linear-gradient(135deg, rgba(251, 191, 36, 0.08), rgba(217, 119, 6, 0.05));
  1498. border: 1px solid rgba(251, 191, 36, 0.15);
  1499. border-radius: 12px;
  1500. padding: 16px;
  1501. text-align: center;
  1502. }
  1503. .subscribe-title {
  1504. display: block;
  1505. font-size: 15px;
  1506. font-weight: 600;
  1507. color: #fbbf24;
  1508. margin-bottom: 6px;
  1509. }
  1510. .subscribe-desc {
  1511. display: block;
  1512. font-size: 12px;
  1513. color: rgba(255,255,255,0.45);
  1514. margin-bottom: 14px;
  1515. line-height: 1.5;
  1516. }
  1517. .subscribe-actions {
  1518. display: flex;
  1519. flex-direction: column;
  1520. gap: 8px;
  1521. align-items: center;
  1522. }
  1523. .subscribe-btn {
  1524. width: 100%;
  1525. background: linear-gradient(135deg, #f59e0b, #d97706);
  1526. color: #fff;
  1527. border: none;
  1528. border-radius: 10px;
  1529. padding: 12px 0;
  1530. font-size: 15px;
  1531. font-weight: 600;
  1532. }
  1533. .subscribe-btn:active {
  1534. opacity: 0.85;
  1535. }
  1536. .subscribe-link {
  1537. font-size: 12px;
  1538. color: rgba(255,255,255,0.35);
  1539. }
  1540. /* US-9.4: Proposal section */
  1541. .proposal-section {
  1542. flex-shrink: 0;
  1543. margin: 0 14px 10px;
  1544. }
  1545. .proposal-title {
  1546. font-size: 16px;
  1547. font-weight: 700;
  1548. color: #a78bfa;
  1549. margin-bottom: 10px;
  1550. padding-left: 2px;
  1551. }
  1552. .proposal-card {
  1553. background: rgba(124,58,237,0.06);
  1554. border: 1px solid rgba(124,58,237,0.12);
  1555. border-radius: 14px;
  1556. padding: 16px;
  1557. }
  1558. .proposal-price {
  1559. text-align: center;
  1560. font-size: 24px;
  1561. font-weight: 700;
  1562. color: #fbbf24;
  1563. margin-bottom: 12px;
  1564. }
  1565. .proposal-message {
  1566. background: rgba(255,255,255,0.03);
  1567. border-radius: 8px;
  1568. padding: 10px 12px;
  1569. margin-bottom: 12px;
  1570. }
  1571. .proposal-msg-label {
  1572. display: block;
  1573. font-size: 12px;
  1574. color: rgba(255,255,255,0.4);
  1575. margin-bottom: 4px;
  1576. }
  1577. .proposal-msg-content {
  1578. display: block;
  1579. font-size: 13px;
  1580. color: rgba(255,255,255,0.75);
  1581. line-height: 1.5;
  1582. }
  1583. .negotiation-history {
  1584. margin-bottom: 14px;
  1585. }
  1586. .history-title {
  1587. font-size: 13px;
  1588. font-weight: 600;
  1589. color: rgba(255,255,255,0.5);
  1590. margin-bottom: 10px;
  1591. }
  1592. .history-timeline {
  1593. padding-left: 6px;
  1594. }
  1595. .history-item {
  1596. display: flex;
  1597. align-items: flex-start;
  1598. position: relative;
  1599. padding-bottom: 6px;
  1600. }
  1601. .timeline-dot {
  1602. width: 8px;
  1603. height: 8px;
  1604. border-radius: 50%;
  1605. background: rgba(255,255,255,0.2);
  1606. flex-shrink: 0;
  1607. margin-top: 4px;
  1608. margin-right: 10px;
  1609. }
  1610. .timeline-dot.accept {
  1611. background: #10b981;
  1612. }
  1613. .timeline-line {
  1614. position: absolute;
  1615. left: 3px;
  1616. top: 12px;
  1617. width: 2px;
  1618. height: calc(100% - 6px);
  1619. background: rgba(255,255,255,0.08);
  1620. }
  1621. .timeline-content {
  1622. flex: 1;
  1623. font-size: 13px;
  1624. color: rgba(255,255,255,0.6);
  1625. line-height: 1.5;
  1626. }
  1627. .proposal-actions {
  1628. display: flex;
  1629. gap: 10px;
  1630. }
  1631. .proposal-btn {
  1632. flex: 1;
  1633. height: 40px;
  1634. border: none;
  1635. border-radius: 10px;
  1636. font-size: 14px;
  1637. font-weight: 600;
  1638. line-height: 40px;
  1639. text-align: center;
  1640. }
  1641. .proposal-btn.btn-accept {
  1642. background: linear-gradient(135deg, #10b981, #059669);
  1643. color: #fff;
  1644. }
  1645. .proposal-btn.btn-accept[disabled] {
  1646. opacity: 0.5;
  1647. }
  1648. .proposal-btn.btn-counter {
  1649. background: transparent;
  1650. border: 1px solid #3b82f6;
  1651. color: #3b82f6;
  1652. }
  1653. .proposal-btn.btn-reject {
  1654. background: transparent;
  1655. border: 1px solid #ef4444;
  1656. color: #ef4444;
  1657. }
  1658. .proposal-btn.btn-reject[disabled] {
  1659. opacity: 0.5;
  1660. }
  1661. .counter-input-wrap {
  1662. margin-top: 12px;
  1663. padding-top: 12px;
  1664. border-top: 1px solid rgba(255,255,255,0.06);
  1665. }
  1666. .counter-input-row {
  1667. display: flex;
  1668. gap: 8px;
  1669. }
  1670. .counter-input {
  1671. flex: 1;
  1672. height: 42px;
  1673. border: 1px solid rgba(255,255,255,0.1);
  1674. border-radius: 10px;
  1675. padding: 0 14px;
  1676. font-size: 14px;
  1677. color: rgba(255,255,255,0.85);
  1678. background: rgba(255,255,255,0.04);
  1679. }
  1680. .counter-submit {
  1681. height: 42px;
  1682. padding: 0 20px;
  1683. background: linear-gradient(135deg, #3b82f6, #2563eb);
  1684. color: #fff;
  1685. border: none;
  1686. border-radius: 10px;
  1687. line-height: 42px;
  1688. font-size: 14px;
  1689. font-weight: 500;
  1690. }
  1691. .counter-submit[disabled] {
  1692. opacity: 0.4;
  1693. }
  1694. .counter-hint {
  1695. display: block;
  1696. font-size: 11px;
  1697. color: rgba(255,255,255,0.3);
  1698. margin-top: 6px;
  1699. text-align: center;
  1700. }
  1701. .proposal-card.paid-card {
  1702. text-align: center;
  1703. padding: 16px;
  1704. }
  1705. .paid-text {
  1706. font-size: 14px;
  1707. color: #10b981;
  1708. font-weight: 600;
  1709. }
  1710. /* EPIC 9: Plan apply button bar */
  1711. .plan-apply-bar {
  1712. flex-shrink: 0;
  1713. margin: 10px 14px calc(env(safe-area-inset-bottom) + 16px);
  1714. }
  1715. .plan-apply-btn {
  1716. width: 100%;
  1717. height: 48px;
  1718. background: linear-gradient(135deg, #a78bfa, #7c3aed);
  1719. color: #fff;
  1720. border: none;
  1721. border-radius: 14px;
  1722. line-height: 48px;
  1723. font-size: 16px;
  1724. font-weight: 700;
  1725. letter-spacing: 1px;
  1726. box-shadow: 0 4px 16px rgba(124, 58, 237, 0.25);
  1727. }
  1728. .plan-apply-btn:active {
  1729. opacity: 0.85;
  1730. }
  1731. /* US-9.5: Pending payment card */
  1732. .pending-payment-card {
  1733. background: linear-gradient(135deg, rgba(167, 139, 250, 0.08), rgba(124, 58, 237, 0.05));
  1734. border: 1px solid rgba(167, 139, 250, 0.2);
  1735. border-radius: 14px;
  1736. padding: 20px;
  1737. text-align: center;
  1738. }
  1739. .pending-payment-card .proposal-price {
  1740. font-size: 28px;
  1741. font-weight: 700;
  1742. color: #fbbf24;
  1743. margin-bottom: 8px;
  1744. }
  1745. .payment-hint {
  1746. font-size: 12px;
  1747. color: rgba(255, 255, 255, 0.4);
  1748. display: block;
  1749. margin-bottom: 14px;
  1750. }
  1751. .payment-btn {
  1752. width: 100%;
  1753. height: 46px;
  1754. background: linear-gradient(135deg, #10b981, #059669);
  1755. color: #fff;
  1756. border: none;
  1757. border-radius: 12px;
  1758. line-height: 46px;
  1759. font-size: 16px;
  1760. font-weight: 600;
  1761. box-shadow: 0 4px 12px rgba(16, 185, 129, 0.25);
  1762. }
  1763. /* US-9.5: Delivery content display */
  1764. .delivery-content {
  1765. padding: 0;
  1766. }
  1767. .delivery-title {
  1768. font-size: 15px;
  1769. font-weight: 600;
  1770. color: rgba(255, 255, 255, 0.85);
  1771. margin-bottom: 12px;
  1772. }
  1773. .delivery-item {
  1774. background: rgba(255, 255, 255, 0.03);
  1775. border-radius: 10px;
  1776. padding: 14px;
  1777. margin-bottom: 10px;
  1778. }
  1779. .delivery-header {
  1780. display: flex;
  1781. justify-content: space-between;
  1782. align-items: center;
  1783. margin-bottom: 8px;
  1784. }
  1785. .delivery-type {
  1786. font-size: 13px;
  1787. font-weight: 600;
  1788. color: #10b981;
  1789. }
  1790. .delivery-time {
  1791. font-size: 11px;
  1792. color: rgba(255, 255, 255, 0.3);
  1793. }
  1794. .delivery-text {
  1795. display: block;
  1796. font-size: 13px;
  1797. color: rgba(255, 255, 255, 0.65);
  1798. line-height: 1.6;
  1799. white-space: pre-wrap;
  1800. }
  1801. /* US-9.5: Paid waiting state */
  1802. .paid-waiting {
  1803. text-align: center;
  1804. padding: 10px 0;
  1805. }
  1806. .paid-waiting-text {
  1807. font-size: 14px;
  1808. color: rgba(255, 255, 255, 0.4);
  1809. }
  1810. </style>