monitor.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878
  1. <template>
  2. <view class="page-monitor">
  3. <!-- Header: subordinate user info -->
  4. <view class="monitor-header">
  5. <view class="header-info">
  6. <text class="header-user">用户 {{ userId }}</text>
  7. <text class="header-subtitle">AI 咨询记录</text>
  8. </view>
  9. </view>
  10. <!-- Loading skeleton -->
  11. <view v-if="loading" class="loading-msgs">
  12. <view class="skeleton-msg left-msg">
  13. <view class="skeleton-label"></view>
  14. <view class="skeleton-bubble w-60"></view>
  15. </view>
  16. <view class="skeleton-msg right-msg">
  17. <view class="skeleton-bubble w-40"></view>
  18. </view>
  19. <view class="skeleton-msg left-msg">
  20. <view class="skeleton-label"></view>
  21. <view class="skeleton-bubble w-75"></view>
  22. </view>
  23. <view class="skeleton-msg right-msg">
  24. <view class="skeleton-bubble w-55"></view>
  25. </view>
  26. <view class="skeleton-msg left-msg">
  27. <view class="skeleton-label"></view>
  28. <view class="skeleton-bubble w-65"></view>
  29. </view>
  30. <view class="skeleton-msg right-msg">
  31. <view class="skeleton-bubble w-45"></view>
  32. </view>
  33. </view>
  34. <template v-else>
  35. <!-- Chat display area -->
  36. <scroll-view class="chat-box" scroll-y :scroll-top="scrollTop">
  37. <view v-for="(msg, idx) in messages" :key="msg.id || idx" class="msg-wrapper">
  38. <!-- System message: centered, italic, no bubble -->
  39. <view v-if="msg.senderType === 'system'" class="msg-system">
  40. <text class="msg-system-text">{{ msg.content }}</text>
  41. </view>
  42. <!-- Practitioner message: green bubble left-aligned with label -->
  43. <view v-else-if="msg.senderType === 'practitioner'" class="msg-row practitioner">
  44. <view class="msg-label label-practitioner">👤 能量师</view>
  45. <view class="msg-bubble bubble-practitioner">
  46. <text>{{ msg.content }}</text>
  47. </view>
  48. </view>
  49. <!-- User message: dark blue bubble right-aligned -->
  50. <view v-else-if="msg.senderType === 'user'" class="msg-row user">
  51. <view class="msg-bubble bubble-user">
  52. <text>{{ msg.content }}</text>
  53. </view>
  54. </view>
  55. <!-- AI message (default): gray bubble left-aligned with label -->
  56. <view v-else class="msg-row ai">
  57. <view class="msg-label label-ai">AI</view>
  58. <view class="msg-bubble bubble-ai">
  59. <text>{{ msg.content }}</text>
  60. </view>
  61. </view>
  62. </view>
  63. <!-- Empty state -->
  64. <view v-if="messages.length === 0" class="empty-chat">
  65. <text class="empty-chat-text">暂无聊天记录</text>
  66. </view>
  67. </scroll-view>
  68. <!-- Negotiation logs -->
  69. <view v-if="negotiationLogs.length > 0" class="negotiation-section">
  70. <view class="negotiation-title">📋 协商记录</view>
  71. <view v-for="log in negotiationLogs" :key="log.id" class="negotiation-entry">
  72. <view class="negotiation-entry-header">
  73. <text class="negotiation-entry-action">{{ getActionLabel(log.action) }}</text>
  74. <text class="negotiation-entry-time">{{ formatTime(log.createdAt) }}</text>
  75. </view>
  76. <view v-if="log.oldPrice != null || log.newPrice != null" class="negotiation-entry-price">
  77. <text class="price-old" v-if="log.oldPrice">¥{{ log.oldPrice }}</text>
  78. <text class="price-arrow" v-if="log.oldPrice != null && log.newPrice != null"> → </text>
  79. <text class="price-new" v-if="log.newPrice">¥{{ log.newPrice }}</text>
  80. </view>
  81. <text v-if="log.message" class="negotiation-entry-msg">{{ log.message }}</text>
  82. </view>
  83. </view>
  84. <!-- Bottom action area -->
  85. <view class="bottom-bar">
  86. <!-- Proposal: status is "accepted" and not yet submitted -->
  87. <template v-if="requestStatus === 'accepted' && !submitted">
  88. <view class="proposal-form-title">📋 提交方案</view>
  89. <view class="proposal-price-row">
  90. <text class="proposal-label">方案价格</text>
  91. <input
  92. class="proposal-input proposal-price-input"
  93. v-model="proposalPrice"
  94. type="digit"
  95. placeholder="输入方案价格..."
  96. />
  97. <text class="proposal-unit">元</text>
  98. </view>
  99. <textarea
  100. class="proposal-textarea"
  101. v-model="proposalMessage"
  102. placeholder="描述方案内容..."
  103. />
  104. <button
  105. class="action-btn proposal-submit-btn"
  106. @click="submitProposal"
  107. :disabled="submitting"
  108. >
  109. {{ submitting ? '提交中...' : '提交方案' }}
  110. </button>
  111. </template>
  112. <!-- Proposal submitted or negotiating -->
  113. <template v-else-if="submitted || requestStatus === 'negotiating'">
  114. <view class="status-bar-submitted">
  115. <text class="status-icon">✅</text>
  116. <text class="status-text">方案已提交,等待用户回应</text>
  117. </view>
  118. </template>
  119. <!-- Pending payment: waiting for user to pay -->
  120. <template v-else-if="requestStatus === 'pending_payment'">
  121. <view class="status-bar-submitted">
  122. <text class="status-icon">⏳</text>
  123. <text class="status-text">等待用户支付</text>
  124. </view>
  125. </template>
  126. <!-- Paid: delivery form for practitioners -->
  127. <template v-else-if="requestStatus === 'paid'">
  128. <template v-if="deliverySubmitted">
  129. <view class="status-bar-delivered">
  130. <text class="status-icon">✅</text>
  131. <text class="status-text">方案已交付</text>
  132. </view>
  133. </template>
  134. <template v-else>
  135. <view class="delivery-form-title">📦 交付方案</view>
  136. <textarea
  137. class="delivery-textarea"
  138. v-model="deliveryContent"
  139. placeholder="输入方案交付内容..."
  140. />
  141. <button
  142. class="action-btn delivery-submit-btn"
  143. @click="submitDelivery"
  144. :disabled="delivering || !deliveryContent.trim()"
  145. >
  146. {{ delivering ? '提交中...' : '提交交付' }}
  147. </button>
  148. </template>
  149. </template>
  150. <!-- Completed: show completed status -->
  151. <template v-else-if="requestStatus === 'completed'">
  152. <view class="status-bar-delivered">
  153. <text class="status-icon">✅</text>
  154. <text class="status-text">已完成</text>
  155. </view>
  156. </template>
  157. <!-- Default: existing intervention UI -->
  158. <template v-else>
  159. <button
  160. v-if="!interventionMode"
  161. class="action-btn intervene-btn"
  162. :disabled="intervening"
  163. @click="startIntervention"
  164. >
  165. {{ intervening ? '介入中...' : '介入会话' }}
  166. </button>
  167. <template v-else>
  168. <view class="input-row">
  169. <input
  170. class="chat-input"
  171. v-model="chatInput"
  172. placeholder="输入消息..."
  173. @confirm="sendMessage"
  174. :disabled="sending"
  175. />
  176. <button
  177. class="action-btn send-btn"
  178. @click="sendMessage"
  179. :disabled="sending || !chatInput.trim()"
  180. >
  181. {{ sending ? '发送中...' : '发送' }}
  182. </button>
  183. </view>
  184. <button class="action-btn exit-btn" @click="confirmExit">退出介入</button>
  185. </template>
  186. </template>
  187. </view>
  188. </template>
  189. </view>
  190. </template>
  191. <script>
  192. // Page lifecycle for WeChat share compatibility
  193. export default {}
  194. </script>
  195. <script setup>
  196. import { ref, computed, nextTick } from 'vue'
  197. import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
  198. import { interventionApi, proposalApi, deliveryApi } from '@/utils/api'
  199. // ── Query params ──
  200. const chartRecordId = ref('')
  201. const userId = ref('')
  202. const requestId = ref('')
  203. const requestStatus = ref('')
  204. const practitionerId = ref('')
  205. // ── State ──
  206. const loading = ref(true)
  207. const messages = ref([])
  208. const scrollTop = ref(0)
  209. const interventionMode = ref(false)
  210. const interventionId = ref(null)
  211. const intervening = ref(false)
  212. const sending = ref(false)
  213. const chatInput = ref('')
  214. // ── Proposal state ──
  215. const proposalPrice = ref('')
  216. const proposalMessage = ref('')
  217. const submitting = ref(false)
  218. const submitted = ref(false)
  219. const negotiationLogs = ref([])
  220. // ── Delivery state ──
  221. const deliveryContent = ref('')
  222. const deliverySubmitted = ref(false)
  223. const delivering = ref(false)
  224. const deliveryData = ref(null)
  225. // ── Lifecycle ──
  226. onLoad((options) => {
  227. chartRecordId.value = options.chartRecordId || ''
  228. userId.value = options.userId || ''
  229. requestId.value = options.requestId || ''
  230. requestStatus.value = options.status || ''
  231. practitionerId.value = options.practitionerId || ''
  232. fetchHistory()
  233. // Fetch negotiation logs if status indicates ongoing negotiation
  234. if (requestStatus.value === 'negotiating' || requestStatus.value === 'accepted') {
  235. fetchLogs()
  236. }
  237. // Fetch delivery data if status is paid or completed
  238. if (requestStatus.value === 'paid' || requestStatus.value === 'completed') {
  239. loadDelivery()
  240. fetchLogs()
  241. }
  242. })
  243. onPullDownRefresh(async () => {
  244. await fetchHistory()
  245. uni.stopPullDownRefresh()
  246. })
  247. // ── Data fetching ──
  248. async function fetchHistory() {
  249. if (!chartRecordId.value) {
  250. loading.value = false
  251. return
  252. }
  253. loading.value = true
  254. try {
  255. const data = await interventionApi.history(chartRecordId.value)
  256. // Sort by timestamp or id for chronological order
  257. const msgs = data || []
  258. msgs.sort((a, b) => (a.createdAt || a.id || 0) - (b.createdAt || b.id || 0))
  259. messages.value = msgs
  260. // Scroll to bottom after rendering
  261. nextTick(() => {
  262. scrollTop.value = 99999
  263. })
  264. } catch (e) {
  265. uni.showToast({ title: '加载聊天记录失败', icon: 'none' })
  266. } finally {
  267. loading.value = false
  268. }
  269. }
  270. // ── Intervention actions ──
  271. async function startIntervention() {
  272. intervening.value = true
  273. try {
  274. const result = await interventionApi.start(chartRecordId.value)
  275. interventionId.value = result.interventionId || result.id || result
  276. interventionMode.value = true
  277. uni.showToast({ title: '已介入会话', icon: 'success' })
  278. // Append a system message to indicate intervention started
  279. messages.value.push({
  280. id: Date.now(),
  281. senderType: 'system',
  282. content: '能量师已介入会话'
  283. })
  284. nextTick(() => {
  285. scrollTop.value = 99999
  286. })
  287. } catch (e) {
  288. uni.showToast({ title: e.message || '介入失败', icon: 'none' })
  289. } finally {
  290. intervening.value = false
  291. }
  292. }
  293. async function sendMessage() {
  294. const content = chatInput.value.trim()
  295. if (!content || sending.value) return
  296. // Optimistically add practitioner message to local list
  297. const localMsg = {
  298. id: Date.now(),
  299. senderType: 'practitioner',
  300. content,
  301. createdAt: Date.now()
  302. }
  303. messages.value.push(localMsg)
  304. chatInput.value = ''
  305. sending.value = true
  306. try {
  307. await interventionApi.send({
  308. chartRecordId: chartRecordId.value,
  309. content
  310. })
  311. // Scroll to bottom
  312. nextTick(() => {
  313. scrollTop.value = 99999
  314. })
  315. } catch (e) {
  316. // Remove the optimistically added message on failure
  317. messages.value = messages.value.filter(m => m.id !== localMsg.id)
  318. uni.showToast({ title: e.message || '发送失败', icon: 'none' })
  319. } finally {
  320. sending.value = false
  321. }
  322. }
  323. function confirmExit() {
  324. uni.showModal({
  325. title: '退出介入',
  326. content: '确定要退出当前会话介入吗?',
  327. success: async (res) => {
  328. if (res.confirm) {
  329. try {
  330. await interventionApi.end(interventionId.value)
  331. interventionMode.value = false
  332. interventionId.value = null
  333. uni.showToast({ title: '已退出介入', icon: 'success' })
  334. // Append system message
  335. messages.value.push({
  336. id: Date.now(),
  337. senderType: 'system',
  338. content: '能量师已退出介入'
  339. })
  340. nextTick(() => {
  341. scrollTop.value = 99999
  342. })
  343. } catch (e) {
  344. uni.showToast({ title: e.message || '退出失败', icon: 'none' })
  345. }
  346. }
  347. }
  348. })
  349. }
  350. // ── Proposal actions ──
  351. async function fetchLogs() {
  352. if (!requestId.value) return
  353. try {
  354. const logs = await proposalApi.logs(requestId.value)
  355. negotiationLogs.value = logs || []
  356. } catch (e) {
  357. console.error('获取协商记录失败', e)
  358. }
  359. }
  360. async function submitProposal() {
  361. const price = Number(proposalPrice.value)
  362. if (!price || price <= 0) {
  363. uni.showToast({ title: '请输入有效的方案价格', icon: 'none' })
  364. return
  365. }
  366. if (!proposalMessage.value.trim()) {
  367. uni.showToast({ title: '请描述方案内容', icon: 'none' })
  368. return
  369. }
  370. submitting.value = true
  371. try {
  372. await proposalApi.create({
  373. requestId: requestId.value,
  374. price,
  375. message: proposalMessage.value.trim()
  376. })
  377. submitted.value = true
  378. uni.showToast({ title: '方案已提交', icon: 'success' })
  379. } catch (e) {
  380. uni.showToast({ title: e.message || '提交失败', icon: 'none' })
  381. } finally {
  382. submitting.value = false
  383. }
  384. }
  385. function getActionLabel(action) {
  386. const labels = {
  387. create: '出价',
  388. counter: '还价',
  389. accept: '接受',
  390. reject: '拒绝'
  391. }
  392. return labels[action] || action
  393. }
  394. function formatTime(timestamp) {
  395. if (!timestamp) return ''
  396. const d = new Date(timestamp)
  397. const pad = (n) => String(n).padStart(2, '0')
  398. return `${d.getMonth() + 1}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
  399. }
  400. // ── Delivery actions ──
  401. async function loadDelivery() {
  402. if (!requestId.value) return
  403. try {
  404. const data = await deliveryApi.list(requestId.value)
  405. if (data && data.length > 0) {
  406. deliveryData.value = data[0]
  407. deliveryContent.value = data[0].textContent || ''
  408. deliverySubmitted.value = true
  409. }
  410. } catch (e) {
  411. // Delivery may not exist yet — that's fine
  412. }
  413. }
  414. async function submitDelivery() {
  415. const content = deliveryContent.value.trim()
  416. if (!content || delivering.value) return
  417. delivering.value = true
  418. try {
  419. await deliveryApi.create({
  420. planRequestId: requestId.value,
  421. textContent: content
  422. })
  423. deliverySubmitted.value = true
  424. uni.showToast({ title: '交付成功', icon: 'success' })
  425. } catch (e) {
  426. uni.showToast({ title: e.message || '交付失败', icon: 'none' })
  427. } finally {
  428. delivering.value = false
  429. }
  430. }
  431. </script>
  432. <style scoped lang="scss">
  433. .page-monitor {
  434. display: flex;
  435. flex-direction: column;
  436. height: 100vh;
  437. overflow: hidden;
  438. background: linear-gradient(180deg, #0c0a1a 0%, #1a1232 100%);
  439. }
  440. /* ── Header ── */
  441. .monitor-header {
  442. flex-shrink: 0;
  443. padding: 16px 16px 12px;
  444. }
  445. .header-info {
  446. display: flex;
  447. flex-direction: column;
  448. gap: 4px;
  449. }
  450. .header-user {
  451. font-size: 17px;
  452. font-weight: 700;
  453. color: rgba(255, 255, 255, 0.9);
  454. }
  455. .header-subtitle {
  456. font-size: 12px;
  457. color: rgba(255, 255, 255, 0.4);
  458. }
  459. /* ── Loading Skeleton ── */
  460. .loading-msgs {
  461. flex: 1;
  462. overflow-y: auto;
  463. padding: 0 16px;
  464. }
  465. .skeleton-msg {
  466. display: flex;
  467. flex-direction: column;
  468. margin-bottom: 18px;
  469. }
  470. .skeleton-msg.right-msg {
  471. align-items: flex-end;
  472. }
  473. .skeleton-label {
  474. width: 32px;
  475. height: 12px;
  476. background: rgba(255, 255, 255, 0.06);
  477. border-radius: 4px;
  478. margin-bottom: 6px;
  479. animation: pulse 1.5s ease-in-out infinite;
  480. }
  481. .skeleton-bubble {
  482. height: 40px;
  483. background: rgba(255, 255, 255, 0.04);
  484. border-radius: 12px;
  485. animation: pulse 1.5s ease-in-out infinite;
  486. }
  487. .w-40 { width: 40%; }
  488. .w-45 { width: 45%; }
  489. .w-55 { width: 55%; }
  490. .w-60 { width: 60%; }
  491. .w-65 { width: 65%; }
  492. .w-75 { width: 75%; }
  493. @keyframes pulse {
  494. 0%, 100% { opacity: 0.3; }
  495. 50% { opacity: 0.6; }
  496. }
  497. /* ── Chat Box ── */
  498. .chat-box {
  499. flex: 1;
  500. overflow-y: auto;
  501. margin: 0 16px;
  502. background: rgba(255, 255, 255, 0.02);
  503. border-radius: 16px;
  504. padding: 14px;
  505. }
  506. /* ── Message Row ── */
  507. .msg-wrapper {
  508. margin-bottom: 16px;
  509. }
  510. /* System message */
  511. .msg-system {
  512. display: flex;
  513. justify-content: center;
  514. padding: 6px 0;
  515. }
  516. .msg-system-text {
  517. font-size: 12px;
  518. color: rgba(255, 255, 255, 0.4);
  519. font-style: italic;
  520. text-align: center;
  521. }
  522. /* Regular message (left by default) */
  523. .msg-row {
  524. display: flex;
  525. flex-direction: column;
  526. max-width: 85%;
  527. }
  528. .msg-row.user {
  529. align-items: flex-end;
  530. margin-left: auto;
  531. }
  532. .msg-row.practitioner,
  533. .msg-row.ai {
  534. align-items: flex-start;
  535. margin-right: auto;
  536. }
  537. /* Message label */
  538. .msg-label {
  539. font-size: 11px;
  540. margin-bottom: 4px;
  541. padding-left: 4px;
  542. }
  543. .label-ai {
  544. color: rgba(255, 255, 255, 0.35);
  545. }
  546. .label-practitioner {
  547. color: rgba(16, 185, 129, 0.7);
  548. }
  549. /* Message bubble */
  550. .msg-bubble {
  551. padding: 10px 14px;
  552. border-radius: 14px;
  553. font-size: 14px;
  554. line-height: 1.5;
  555. word-break: break-word;
  556. }
  557. /* AI bubble: gray, left-aligned */
  558. .bubble-ai {
  559. background: rgba(255, 255, 255, 0.08);
  560. color: rgba(255, 255, 255, 0.85);
  561. }
  562. /* User bubble: dark blue, right-aligned */
  563. .bubble-user {
  564. background: #1E3A5F;
  565. color: rgba(255, 255, 255, 0.9);
  566. }
  567. /* Practitioner bubble: green, left-aligned */
  568. .bubble-practitioner {
  569. background: rgba(16, 185, 129, 0.15);
  570. border: 1px solid rgba(16, 185, 129, 0.3);
  571. color: rgba(255, 255, 255, 0.9);
  572. }
  573. /* ── Empty state ── */
  574. .empty-chat {
  575. display: flex;
  576. justify-content: center;
  577. padding: 60px 20px;
  578. }
  579. .empty-chat-text {
  580. font-size: 14px;
  581. color: rgba(255, 255, 255, 0.3);
  582. }
  583. /* ── Bottom bar ── */
  584. .bottom-bar {
  585. flex-shrink: 0;
  586. padding: 12px 16px calc(env(safe-area-inset-bottom) + 12px);
  587. border-top: 1px solid rgba(255, 255, 255, 0.06);
  588. display: flex;
  589. flex-direction: column;
  590. gap: 10px;
  591. }
  592. /* Action buttons base */
  593. .action-btn {
  594. height: 44px;
  595. border: none;
  596. border-radius: 12px;
  597. font-size: 15px;
  598. font-weight: 600;
  599. display: flex;
  600. align-items: center;
  601. justify-content: center;
  602. line-height: 1;
  603. }
  604. /* Intervene button: green gradient */
  605. .intervene-btn {
  606. width: 100%;
  607. background: linear-gradient(135deg, #10b981, #059669);
  608. color: #fff;
  609. }
  610. .intervene-btn[disabled] {
  611. opacity: 0.5;
  612. }
  613. /* Send button */
  614. .send-btn {
  615. flex-shrink: 0;
  616. padding: 0 24px;
  617. background: linear-gradient(135deg, #10b981, #059669);
  618. color: #fff;
  619. }
  620. .send-btn[disabled] {
  621. opacity: 0.4;
  622. }
  623. /* Exit button: red outline */
  624. .exit-btn {
  625. width: 100%;
  626. background: transparent;
  627. border: 1px solid #ef4444;
  628. color: #ef4444;
  629. }
  630. /* Input row */
  631. .input-row {
  632. display: flex;
  633. gap: 8px;
  634. align-items: center;
  635. }
  636. .chat-input {
  637. flex: 1;
  638. height: 42px;
  639. border: 1px solid rgba(255, 255, 255, 0.1);
  640. border-radius: 10px;
  641. padding: 0 14px;
  642. font-size: 14px;
  643. color: rgba(255, 255, 255, 0.85);
  644. background: rgba(255, 255, 255, 0.04);
  645. }
  646. /* ── Negotiation Logs Section ── */
  647. .negotiation-section {
  648. flex-shrink: 0;
  649. margin: 0 16px 8px;
  650. padding: 14px;
  651. background: rgba(255, 255, 255, 0.02);
  652. border-radius: 16px;
  653. max-height: 200px;
  654. overflow-y: auto;
  655. }
  656. .negotiation-title {
  657. font-size: 14px;
  658. font-weight: 600;
  659. color: rgba(255, 255, 255, 0.8);
  660. margin-bottom: 10px;
  661. }
  662. .negotiation-entry {
  663. padding: 10px 0;
  664. border-bottom: 1px solid rgba(255, 255, 255, 0.04);
  665. }
  666. .negotiation-entry:last-child {
  667. border-bottom: none;
  668. }
  669. .negotiation-entry-header {
  670. display: flex;
  671. align-items: center;
  672. gap: 8px;
  673. margin-bottom: 4px;
  674. }
  675. .negotiation-entry-action {
  676. font-size: 13px;
  677. font-weight: 600;
  678. color: rgba(255, 255, 255, 0.7);
  679. }
  680. .negotiation-entry-time {
  681. font-size: 11px;
  682. color: rgba(255, 255, 255, 0.3);
  683. }
  684. .negotiation-entry-price {
  685. margin-bottom: 4px;
  686. }
  687. .price-old {
  688. font-size: 14px;
  689. color: rgba(255, 255, 255, 0.4);
  690. text-decoration: line-through;
  691. }
  692. .price-arrow {
  693. font-size: 14px;
  694. color: rgba(255, 255, 255, 0.3);
  695. }
  696. .price-new {
  697. font-size: 15px;
  698. font-weight: 700;
  699. color: #fbbf24;
  700. }
  701. .negotiation-entry-msg {
  702. font-size: 12px;
  703. color: rgba(255, 255, 255, 0.5);
  704. display: block;
  705. }
  706. /* ── Proposal Form ── */
  707. .proposal-form-title {
  708. font-size: 16px;
  709. font-weight: 700;
  710. color: rgba(255, 255, 255, 0.9);
  711. margin-bottom: 12px;
  712. text-align: center;
  713. }
  714. .proposal-price-row {
  715. display: flex;
  716. align-items: center;
  717. gap: 8px;
  718. margin-bottom: 10px;
  719. }
  720. .proposal-label {
  721. font-size: 14px;
  722. color: rgba(255, 255, 255, 0.6);
  723. flex-shrink: 0;
  724. }
  725. .proposal-input {
  726. flex: 1;
  727. height: 42px;
  728. border: 1px solid rgba(255, 255, 255, 0.1);
  729. border-radius: 10px;
  730. padding: 0 14px;
  731. font-size: 14px;
  732. color: rgba(255, 255, 255, 0.85);
  733. background: rgba(255, 255, 255, 0.04);
  734. }
  735. .proposal-price-input {
  736. text-align: right;
  737. }
  738. .proposal-unit {
  739. font-size: 14px;
  740. color: rgba(255, 255, 255, 0.5);
  741. flex-shrink: 0;
  742. }
  743. .proposal-textarea {
  744. width: 100%;
  745. min-height: 80px;
  746. border: 1px solid rgba(255, 255, 255, 0.1);
  747. border-radius: 10px;
  748. padding: 10px 14px;
  749. font-size: 14px;
  750. color: rgba(255, 255, 255, 0.85);
  751. background: rgba(255, 255, 255, 0.04);
  752. margin-bottom: 10px;
  753. box-sizing: border-box;
  754. }
  755. .proposal-submit-btn {
  756. width: 100%;
  757. background: linear-gradient(135deg, #a78bfa, #7c3aed);
  758. color: #fff;
  759. }
  760. .proposal-submit-btn[disabled] {
  761. opacity: 0.5;
  762. }
  763. /* ── Submitted Status Bar ── */
  764. .status-bar-submitted {
  765. display: flex;
  766. align-items: center;
  767. justify-content: center;
  768. gap: 8px;
  769. padding: 12px 16px;
  770. background: rgba(16, 185, 129, 0.1);
  771. border-radius: 12px;
  772. }
  773. .status-icon {
  774. font-size: 16px;
  775. }
  776. .status-text {
  777. font-size: 14px;
  778. color: #10b981;
  779. font-weight: 500;
  780. }
  781. /* ── Delivered Status Bar ── */
  782. .status-bar-delivered {
  783. display: flex;
  784. align-items: center;
  785. justify-content: center;
  786. gap: 8px;
  787. padding: 12px 16px;
  788. background: rgba(16, 185, 129, 0.1);
  789. border-radius: 12px;
  790. }
  791. /* ── Delivery Form ── */
  792. .delivery-form-title {
  793. font-size: 16px;
  794. font-weight: 700;
  795. color: rgba(255, 255, 255, 0.9);
  796. margin-bottom: 12px;
  797. text-align: center;
  798. }
  799. .delivery-textarea {
  800. width: 100%;
  801. min-height: 100px;
  802. border: 1px solid rgba(255, 255, 255, 0.1);
  803. border-radius: 10px;
  804. padding: 10px 14px;
  805. font-size: 14px;
  806. color: rgba(255, 255, 255, 0.85);
  807. background: rgba(255, 255, 255, 0.04);
  808. margin-bottom: 10px;
  809. box-sizing: border-box;
  810. }
  811. .delivery-submit-btn {
  812. width: 100%;
  813. background: linear-gradient(135deg, #f59e0b, #d97706);
  814. color: #fff;
  815. }
  816. .delivery-submit-btn[disabled] {
  817. opacity: 0.5;
  818. }
  819. </style>