|
|
@@ -153,10 +153,24 @@
|
|
|
</view>
|
|
|
|
|
|
<scroll-view class="chat-box" scroll-y :scroll-top="scrollTop">
|
|
|
- <view v-for="(msg, idx) in messages" :key="idx" class="msg-row" :class="msg.role">
|
|
|
- <view class="msg-bubble">
|
|
|
+ <view v-for="(msg, idx) in messages" :key="idx">
|
|
|
+ <!-- System messages: centered italic text, no bubble -->
|
|
|
+ <view v-if="msg.senderType === 'system'" class="msg-system" @longpress.stop="onLongPressMsg(msg)">
|
|
|
<text>{{ msg.content }}</text>
|
|
|
</view>
|
|
|
+ <!-- Practitioner messages: green bubble with label -->
|
|
|
+ <view v-else-if="msg.senderType === 'practitioner'" class="msg-row practitioner" @longpress.stop="onLongPressMsg(msg)">
|
|
|
+ <view class="msg-practitioner-label">👤 能量师</view>
|
|
|
+ <view class="msg-bubble msg-practitioner-bubble">
|
|
|
+ <text>{{ msg.content }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <!-- Default (AI/User): existing rendering -->
|
|
|
+ <view v-else class="msg-row" :class="msg.role">
|
|
|
+ <view class="msg-bubble">
|
|
|
+ <text>{{ msg.content }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</scroll-view>
|
|
|
|
|
|
@@ -213,6 +227,86 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
+
|
|
|
+ <!-- US-9.4: Proposal card for negotiating state -->
|
|
|
+ <view v-if="planRequest && planRequest.status === 'negotiating'" class="proposal-section">
|
|
|
+ <view class="proposal-title">📋 人工方案</view>
|
|
|
+ <view class="proposal-card">
|
|
|
+ <view class="proposal-price">¥{{ planRequest.price }}</view>
|
|
|
+
|
|
|
+ <view v-if="proposalMessage" class="proposal-message">
|
|
|
+ <text class="proposal-msg-label">能量师留言:</text>
|
|
|
+ <text class="proposal-msg-content">{{ proposalMessage }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="logs.length > 0" class="negotiation-history">
|
|
|
+ <view class="history-title">协商历史</view>
|
|
|
+ <view class="history-timeline">
|
|
|
+ <view v-for="(log, idx) in logs" :key="log.id" class="history-item" :class="log.action">
|
|
|
+ <view class="timeline-dot" :class="log.action"></view>
|
|
|
+ <view class="timeline-line" v-if="idx < logs.length - 1"></view>
|
|
|
+ <view class="timeline-content">
|
|
|
+ <text v-if="log.action === 'proposal'">能量师提交方案: ¥{{ log.newPrice }}</text>
|
|
|
+ <text v-else-if="log.action === 'counter' && log.operatorId === currentUserId">您还价: ¥{{ log.newPrice }}</text>
|
|
|
+ <text v-else-if="log.action === 'counter'">能量师还价: ¥{{ log.newPrice }}</text>
|
|
|
+ <text v-else-if="log.action === 'accept'">✓ 已接受方案</text>
|
|
|
+ <text v-else>{{ log.action }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="proposal-actions">
|
|
|
+ <button class="proposal-btn btn-accept" :disabled="acceptingOffer" @click="acceptOffer">接受方案</button>
|
|
|
+ <button class="proposal-btn btn-counter" @click="showCounterInput = !showCounterInput">还价</button>
|
|
|
+ <button class="proposal-btn btn-reject" :disabled="rejecting" @click="rejectOffer">拒绝</button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="showCounterInput" class="counter-input-wrap">
|
|
|
+ <view class="counter-input-row">
|
|
|
+ <input class="counter-input" v-model="counterPrice" type="number" placeholder="输入您的报价..." />
|
|
|
+ <button class="counter-submit" :disabled="countering || !counterPrice" @click="submitCounter">提交还价</button>
|
|
|
+ </view>
|
|
|
+ <text class="counter-hint">双方最多协商5轮</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+<!-- US-9.5: Pending payment — show pay button -->
|
|
|
+ <view v-if="planRequest && planRequest.status === 'pending_payment' && planOrder" class="proposal-section">
|
|
|
+ <view class="proposal-title">📋 方案已确认</view>
|
|
|
+ <view class="proposal-card pending-payment-card">
|
|
|
+ <view class="proposal-price">¥{{ planRequest.price }}</view>
|
|
|
+ <text class="payment-hint">请在24小时内完成支付</text>
|
|
|
+ <button class="payment-btn" @click="goPay">去支付 ¥{{ planRequest.price }}</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- US-9.5: Paid — show delivery -->
|
|
|
+ <view v-else-if="planRequest && planRequest.status === 'paid'" class="proposal-section">
|
|
|
+ <view class="proposal-card paid-card">
|
|
|
+ <view v-if="deliveryData && deliveryData.length > 0" class="delivery-content">
|
|
|
+ <view class="delivery-title">📋 方案交付</view>
|
|
|
+ <view v-for="d in deliveryData" :key="d.id" class="delivery-item">
|
|
|
+ <view class="delivery-header">
|
|
|
+ <text class="delivery-type">{{ d.deliveryType === 'text' ? '文字方案' : d.deliveryType === 'file' ? '文件方案' : '综合方案' }}</text>
|
|
|
+ <text class="delivery-time">{{ d.createdAt?.slice(0, 10) }}</text>
|
|
|
+ </view>
|
|
|
+ <text v-if="d.textContent" class="delivery-text">{{ d.textContent }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view v-else class="paid-waiting">
|
|
|
+ <text class="paid-waiting-text">方案已确认,等待能量师交付...</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- EPIC 9: Apply for practitioner plan -->
|
|
|
+ <view class="plan-apply-bar">
|
|
|
+ <button class="plan-apply-btn" @click="goPlanApply">
|
|
|
+ 💼 申请人工深度方案
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
|
|
|
<!-- Share canvas (hidden off-screen) -->
|
|
|
@@ -262,7 +356,7 @@ import { useChartStore } from '@/stores/chart'
|
|
|
import { useUserStore } from '@/stores/user'
|
|
|
import TriangleChart from '@/components/TriangleChart.vue'
|
|
|
import ChartShareCanvas from '@/components/ChartShareCanvas.vue'
|
|
|
-import { chartApi, chatApi, consultationApi, profileApi, annotationApi, exportApi } from '@/utils/api'
|
|
|
+import { chartApi, chatApi, consultationApi, profileApi, annotationApi, exportApi, interventionApi, planApi, proposalApi, deliveryApi, payApi } from '@/utils/api'
|
|
|
|
|
|
const store = useChartStore()
|
|
|
const userStore = useUserStore()
|
|
|
@@ -275,6 +369,23 @@ const maxChats = ref(0)
|
|
|
const usedAcademic = ref(0)
|
|
|
const maxAcademic = ref(0)
|
|
|
|
|
|
+// US-9.3: Intervention state
|
|
|
+const hasActiveIntervention = ref(false)
|
|
|
+const activeInterventionId = ref(null)
|
|
|
+
|
|
|
+// US-9.4: Plan proposal state
|
|
|
+const planRequest = ref(null)
|
|
|
+const logs = ref([])
|
|
|
+const counterPrice = ref(null)
|
|
|
+const showCounterInput = ref(false)
|
|
|
+const acceptingOffer = ref(false)
|
|
|
+const countering = ref(false)
|
|
|
+const rejecting = ref(false)
|
|
|
+
|
|
|
+// US-9.5: Payment and delivery state
|
|
|
+const planOrder = ref(null)
|
|
|
+const deliveryData = ref([])
|
|
|
+
|
|
|
// Share canvas ref
|
|
|
const shareCanvas = ref(null)
|
|
|
const shareGenerating = ref(false)
|
|
|
@@ -292,10 +403,7 @@ const interpretExpanded = ref({})
|
|
|
const academicData = ref(null)
|
|
|
const academicLoading = ref(false)
|
|
|
const academicExpanded = ref({})
|
|
|
-
|
|
|
-// US-2.1: Share state
|
|
|
-const shareGenerating = ref(false)
|
|
|
-const showShareActions = ref(false)
|
|
|
+// US-2.1: Share state (shareGenerating declared above with shareCanvas)
|
|
|
|
|
|
const quotaExceeded = computed(() => {
|
|
|
if (userStore.isVipActive) return false
|
|
|
@@ -314,6 +422,15 @@ const quotaAcademicExceeded = computed(() => {
|
|
|
return maxAcademic.value > 0 && usedAcademic.value >= maxAcademic.value
|
|
|
})
|
|
|
|
|
|
+// US-9.4: Computed proposal message from logs
|
|
|
+const proposalMessage = computed(() => {
|
|
|
+ const proposalLog = logs.value.find(l => l.action === 'proposal')
|
|
|
+ return proposalLog?.message || ''
|
|
|
+})
|
|
|
+
|
|
|
+// US-9.4: Current user ID for comparison in negotiation history
|
|
|
+const currentUserId = computed(() => userStore.user?.id || userStore.userId || '')
|
|
|
+
|
|
|
onMounted(async () => {
|
|
|
try {
|
|
|
await userStore.fetchQuota()
|
|
|
@@ -342,18 +459,25 @@ onMounted(async () => {
|
|
|
loadAnnotations()
|
|
|
}
|
|
|
|
|
|
+ // US-9.4: Load plan request for this chart
|
|
|
+ loadPlanRequest()
|
|
|
+
|
|
|
// Use consultationMessages from store (set during startConsultation) if available
|
|
|
if (store.consultationMessages && store.consultationMessages.length > 0) {
|
|
|
messages.value = store.consultationMessages.map(m => ({
|
|
|
role: m.role === 'ai' ? 'ai' : 'user',
|
|
|
- content: m.content
|
|
|
+ content: m.content,
|
|
|
+ senderType: m.senderType || null,
|
|
|
+ interventionId: m.interventionId || null
|
|
|
}))
|
|
|
} else if (store.currentRecordId) {
|
|
|
try {
|
|
|
const history = await chatApi.history(store.currentRecordId)
|
|
|
messages.value = history.map(m => ({
|
|
|
role: m.role === 'ai' ? 'ai' : 'user',
|
|
|
- content: m.content
|
|
|
+ content: m.content,
|
|
|
+ senderType: m.senderType || null,
|
|
|
+ interventionId: m.interventionId || null
|
|
|
}))
|
|
|
const userMsgCount = history.filter(m => m.role === 'user').length
|
|
|
usedChats.value = Math.max(usedChats.value, userMsgCount)
|
|
|
@@ -362,6 +486,9 @@ onMounted(async () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // US-9.3: Check intervention status from loaded messages
|
|
|
+ checkInterventionStatus(messages.value)
|
|
|
+
|
|
|
// Ensure at least a greeting message if still empty
|
|
|
if (messages.value.length === 0) {
|
|
|
messages.value = [{
|
|
|
@@ -377,6 +504,57 @@ onMounted(async () => {
|
|
|
}
|
|
|
})
|
|
|
|
|
|
+// US-9.3: Check if intervention is active from message history
|
|
|
+function checkInterventionStatus(messages) {
|
|
|
+ let active = false
|
|
|
+ let lastId = null
|
|
|
+ for (const msg of messages) {
|
|
|
+ if (msg.senderType === 'system') {
|
|
|
+ if (msg.content && msg.content.includes('能量师') && msg.content.includes('进入')) {
|
|
|
+ active = true
|
|
|
+ lastId = msg.interventionId
|
|
|
+ }
|
|
|
+ if (msg.content && (msg.content.includes('退出') || msg.content.includes('结束'))) {
|
|
|
+ active = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ hasActiveIntervention.value = active
|
|
|
+ activeInterventionId.value = lastId
|
|
|
+}
|
|
|
+
|
|
|
+// US-9.3: Long-press handler for practitioner / entry system messages
|
|
|
+function onLongPressMsg(msg) {
|
|
|
+ // Only show action if intervention is active
|
|
|
+ if (!hasActiveIntervention.value) return
|
|
|
+ // Only on practitioner messages or entry system messages
|
|
|
+ if (msg.senderType === 'practitioner' || (msg.senderType === 'system' && msg.content && msg.content.includes('能量师'))) {
|
|
|
+ uni.showActionSheet({
|
|
|
+ itemList: ['结束能量师介入'],
|
|
|
+ success: async (res) => {
|
|
|
+ if (res.tapIndex === 0) {
|
|
|
+ await endIntervention()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// US-9.3: End intervention by user's request
|
|
|
+async function endIntervention() {
|
|
|
+ uni.showLoading({ title: '处理中...' })
|
|
|
+ try {
|
|
|
+ await interventionApi.endByUser(activeInterventionId.value)
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: '已结束能量师介入', icon: 'success' })
|
|
|
+ hasActiveIntervention.value = false
|
|
|
+ activeInterventionId.value = null
|
|
|
+ } catch (e) {
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// US-3.1: Fetch AI structured interpretation
|
|
|
async function fetchInterpret() {
|
|
|
if (!store.currentRecordId) {
|
|
|
@@ -588,47 +766,6 @@ async function triggerShare() {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-// US-2.2: Export PDF report
|
|
|
-async function exportPdf() {
|
|
|
- if (!store.currentRecordId) {
|
|
|
- uni.showToast({ title: '暂无命盘数据', icon: 'none' })
|
|
|
- return
|
|
|
- }
|
|
|
- uni.showLoading({ title: '正在生成 PDF...' })
|
|
|
- try {
|
|
|
- // The API returns a PDF binary. We use uni.downloadFile to get it as a temp file.
|
|
|
- const token = uni.getStorageSync('token')
|
|
|
- const url = 'http://127.0.0.1:8186/api/export/pdf'
|
|
|
- const downloadRes = await new Promise((resolve, reject) => {
|
|
|
- uni.downloadFile({
|
|
|
- url,
|
|
|
- header: { Authorization: `Bearer ${token}` },
|
|
|
- method: 'POST',
|
|
|
- data: { recordId: store.currentRecordId },
|
|
|
- success: resolve,
|
|
|
- fail: reject
|
|
|
- })
|
|
|
- })
|
|
|
- if (downloadRes.statusCode !== 200) {
|
|
|
- throw new Error('下载失败')
|
|
|
- }
|
|
|
- uni.hideLoading()
|
|
|
- // Open the PDF file
|
|
|
- uni.openDocument({
|
|
|
- filePath: downloadRes.tempFilePath,
|
|
|
- success: () => {
|
|
|
- uni.showToast({ title: 'PDF 已生成', icon: 'success' })
|
|
|
- },
|
|
|
- fail: () => {
|
|
|
- uni.showToast({ title: '打开文件失败', icon: 'none' })
|
|
|
- }
|
|
|
- })
|
|
|
- } catch (e) {
|
|
|
- uni.hideLoading()
|
|
|
- uni.showToast({ title: e.message || '导出失败', icon: 'none' })
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
function goSubscribe() {
|
|
|
uni.navigateTo({ url: '/pages/subscribe/index' })
|
|
|
}
|
|
|
@@ -639,6 +776,19 @@ function goPoster() {
|
|
|
uni.navigateTo({ url: '/pages/subscribe/index' })
|
|
|
}
|
|
|
|
|
|
+// EPIC 9: Navigate to plan apply page
|
|
|
+function goPlanApply() {
|
|
|
+ if (!userStore.isLoggedIn) {
|
|
|
+ uni.navigateTo({ url: '/pages/login/index' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!store.currentRecordId) {
|
|
|
+ uni.showToast({ title: '暂无命盘数据', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ uni.navigateTo({ url: `/pages/plan/apply?chartRecordId=${store.currentRecordId}` })
|
|
|
+}
|
|
|
+
|
|
|
// US-2.1: Share chart image
|
|
|
async function onShareTap() {
|
|
|
if (shareGenerating.value) return
|
|
|
@@ -731,39 +881,9 @@ async function exportPdf() {
|
|
|
uni.hideLoading()
|
|
|
uni.showToast({ title: e.message || '导出失败', icon: 'none' })
|
|
|
}
|
|
|
-}
|
|
|
- }
|
|
|
-
|
|
|
- // Generate image via canvas
|
|
|
- if (!shareCanvas.value) throw new Error('Canvas未就绪')
|
|
|
- const filePath = await shareCanvas.value.generate()
|
|
|
-
|
|
|
- if (actionIndex === 0) {
|
|
|
- // Save to album
|
|
|
- await uni.saveImageToPhotosAlbum({ filePath })
|
|
|
- uni.showToast({ title: '已保存到相册', icon: 'success' })
|
|
|
- } else {
|
|
|
- // Share via WeChat
|
|
|
- const scene = actionIndex === 1 ? 'WXSceneSession' : 'WXSceneTimeline'
|
|
|
- const title = store.currentChart?.O
|
|
|
- ? `我的命盘主性格数字是${store.currentChart.O},快来测测你的!`
|
|
|
- : '数字能量命盘分析'
|
|
|
- await uni.share({
|
|
|
- provider: 'weixin',
|
|
|
- scene,
|
|
|
- type: 'image',
|
|
|
- imageUrl: filePath,
|
|
|
- title
|
|
|
- })
|
|
|
- }
|
|
|
- } catch (e) {
|
|
|
- uni.showToast({ title: e.message || '分享失败', icon: 'none' })
|
|
|
- } finally {
|
|
|
- shareGenerating.value = false
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-// === Annotation / Tag handlers (US-7.1 / US-7.2) ===
|
|
|
+ // === Annotation / Tag handlers (US-7.1 / US-7.2) ===
|
|
|
|
|
|
async function loadAnnotations() {
|
|
|
if (!store.currentRecordId) return
|
|
|
@@ -813,6 +933,104 @@ async function onTagCreate(tagData) {
|
|
|
uni.showToast({ title: '创建失败', icon: 'none' })
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+// US-9.4: Load plan request for current chart
|
|
|
+async function loadPlanRequest() {
|
|
|
+ if (!store.currentRecordId) return
|
|
|
+ try {
|
|
|
+ const requests = await planApi.myRequests()
|
|
|
+ const match = requests.find(r => r.chartRecordId === store.currentRecordId && (r.status === 'negotiating' || r.status === 'pending_payment' || r.status === 'paid'))
|
|
|
+ if (match) {
|
|
|
+ planRequest.value = match
|
|
|
+ const logList = await proposalApi.logs(match.id)
|
|
|
+ logs.value = logList || []
|
|
|
+ // US-9.5: Load delivery data when status is paid
|
|
|
+ if (match.status === 'paid') {
|
|
|
+ try {
|
|
|
+ const deliveries = await deliveryApi.list(match.id)
|
|
|
+ deliveryData.value = deliveries || []
|
|
|
+ } catch (e) { /* best-effort */ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // No plan request found — that's fine
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// US-9.4: Accept offer
|
|
|
+async function acceptOffer() {
|
|
|
+ uni.showModal({
|
|
|
+ title: '接受方案',
|
|
|
+ content: `确定以 ¥${planRequest.value.price} 接受此方案吗?`,
|
|
|
+ success: async (res) => {
|
|
|
+ if (!res.confirm) return
|
|
|
+ acceptingOffer.value = true
|
|
|
+ try {
|
|
|
+ const result = await proposalApi.accept({ requestId: planRequest.value.id })
|
|
|
+ planRequest.value = result.planRequest
|
|
|
+ planOrder.value = result.order
|
|
|
+ const logList = await proposalApi.logs(planRequest.value.id)
|
|
|
+ logs.value = logList || []
|
|
|
+ uni.showToast({ title: '已接受方案,请完成支付', icon: 'success' })
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ acceptingOffer.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// US-9.5: Navigate to payment page
|
|
|
+function goPay() {
|
|
|
+ if (!planOrder.value || !planRequest.value) return
|
|
|
+ uni.navigateTo({
|
|
|
+ url: `/pages/payment/index?product=practitioner_plan&totalFee=${planRequest.value.price}&outTradeNo=${planOrder.value.outTradeNo}`
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// US-9.4: Reject offer
|
|
|
+async function rejectOffer() {
|
|
|
+ uni.showModal({
|
|
|
+ title: '拒绝方案',
|
|
|
+ content: '确定拒绝此方案吗?',
|
|
|
+ success: async (res) => {
|
|
|
+ if (!res.confirm) return
|
|
|
+ rejecting.value = true
|
|
|
+ try {
|
|
|
+ await proposalApi.reject({ requestId: planRequest.value.id })
|
|
|
+ planRequest.value.status = 'rejected'
|
|
|
+ uni.showToast({ title: '已拒绝', icon: 'success' })
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ rejecting.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// US-9.4: Submit counter-offer
|
|
|
+async function submitCounter() {
|
|
|
+ const price = Number(counterPrice.value)
|
|
|
+ if (!price || price <= 0) {
|
|
|
+ uni.showToast({ title: '请输入有效价格', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ countering.value = true
|
|
|
+ try {
|
|
|
+ await proposalApi.counter({ requestId: planRequest.value.id, newPrice: price })
|
|
|
+ const logList = await proposalApi.logs(planRequest.value.id)
|
|
|
+ logs.value = logList || []
|
|
|
+ showCounterInput.value = false
|
|
|
+ counterPrice.value = null
|
|
|
+ uni.showToast({ title: '已提交还价', icon: 'success' })
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e.message || '还价失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ countering.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
@@ -1244,6 +1462,34 @@ async function onTagCreate(tagData) {
|
|
|
color: #fbbf24;
|
|
|
}
|
|
|
|
|
|
+/* US-9.3: Practitioner message bubble */
|
|
|
+.msg-row.practitioner {
|
|
|
+ justify-content: flex-start;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+}
|
|
|
+.msg-practitioner-label {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #10b981;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+.msg-practitioner-bubble {
|
|
|
+ background: rgba(16, 185, 129, 0.15);
|
|
|
+ border: 1px solid rgba(16, 185, 129, 0.3);
|
|
|
+ color: rgba(255, 255, 255, 0.85);
|
|
|
+}
|
|
|
+
|
|
|
+/* US-9.3: System message — centered italic text */
|
|
|
+.msg-system {
|
|
|
+ text-align: center;
|
|
|
+ padding: 8px 0;
|
|
|
+ color: rgba(255, 255, 255, 0.4);
|
|
|
+ font-style: italic;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+
|
|
|
.input-bar {
|
|
|
display: flex;
|
|
|
gap: 8px;
|
|
|
@@ -1415,4 +1661,280 @@ async function onTagCreate(tagData) {
|
|
|
font-size: 12px;
|
|
|
color: rgba(255,255,255,0.35);
|
|
|
}
|
|
|
-</style>
|
|
|
+
|
|
|
+/* US-9.4: Proposal section */
|
|
|
+.proposal-section {
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin: 0 14px 10px;
|
|
|
+}
|
|
|
+.proposal-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #a78bfa;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ padding-left: 2px;
|
|
|
+}
|
|
|
+.proposal-card {
|
|
|
+ background: rgba(124,58,237,0.06);
|
|
|
+ border: 1px solid rgba(124,58,237,0.12);
|
|
|
+ border-radius: 14px;
|
|
|
+ padding: 16px;
|
|
|
+}
|
|
|
+.proposal-price {
|
|
|
+ text-align: center;
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #fbbf24;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+.proposal-message {
|
|
|
+ background: rgba(255,255,255,0.03);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 10px 12px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+.proposal-msg-label {
|
|
|
+ display: block;
|
|
|
+ font-size: 12px;
|
|
|
+ color: rgba(255,255,255,0.4);
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+.proposal-msg-content {
|
|
|
+ display: block;
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgba(255,255,255,0.75);
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+.negotiation-history {
|
|
|
+ margin-bottom: 14px;
|
|
|
+}
|
|
|
+.history-title {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: rgba(255,255,255,0.5);
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+.history-timeline {
|
|
|
+ padding-left: 6px;
|
|
|
+}
|
|
|
+.history-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ position: relative;
|
|
|
+ padding-bottom: 6px;
|
|
|
+}
|
|
|
+.timeline-dot {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: rgba(255,255,255,0.2);
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-top: 4px;
|
|
|
+ margin-right: 10px;
|
|
|
+}
|
|
|
+.timeline-dot.accept {
|
|
|
+ background: #10b981;
|
|
|
+}
|
|
|
+.timeline-line {
|
|
|
+ position: absolute;
|
|
|
+ left: 3px;
|
|
|
+ top: 12px;
|
|
|
+ width: 2px;
|
|
|
+ height: calc(100% - 6px);
|
|
|
+ background: rgba(255,255,255,0.08);
|
|
|
+}
|
|
|
+.timeline-content {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgba(255,255,255,0.6);
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+.proposal-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+.proposal-btn {
|
|
|
+ flex: 1;
|
|
|
+ height: 40px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 10px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 40px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.proposal-btn.btn-accept {
|
|
|
+ background: linear-gradient(135deg, #10b981, #059669);
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+.proposal-btn.btn-accept[disabled] {
|
|
|
+ opacity: 0.5;
|
|
|
+}
|
|
|
+.proposal-btn.btn-counter {
|
|
|
+ background: transparent;
|
|
|
+ border: 1px solid #3b82f6;
|
|
|
+ color: #3b82f6;
|
|
|
+}
|
|
|
+.proposal-btn.btn-reject {
|
|
|
+ background: transparent;
|
|
|
+ border: 1px solid #ef4444;
|
|
|
+ color: #ef4444;
|
|
|
+}
|
|
|
+.proposal-btn.btn-reject[disabled] {
|
|
|
+ opacity: 0.5;
|
|
|
+}
|
|
|
+.counter-input-wrap {
|
|
|
+ margin-top: 12px;
|
|
|
+ padding-top: 12px;
|
|
|
+ border-top: 1px solid rgba(255,255,255,0.06);
|
|
|
+}
|
|
|
+.counter-input-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+.counter-input {
|
|
|
+ flex: 1;
|
|
|
+ height: 42px;
|
|
|
+ border: 1px solid rgba(255,255,255,0.1);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 0 14px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: rgba(255,255,255,0.85);
|
|
|
+ background: rgba(255,255,255,0.04);
|
|
|
+}
|
|
|
+.counter-submit {
|
|
|
+ height: 42px;
|
|
|
+ padding: 0 20px;
|
|
|
+ background: linear-gradient(135deg, #3b82f6, #2563eb);
|
|
|
+ color: #fff;
|
|
|
+ border: none;
|
|
|
+ border-radius: 10px;
|
|
|
+ line-height: 42px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+.counter-submit[disabled] {
|
|
|
+ opacity: 0.4;
|
|
|
+}
|
|
|
+.counter-hint {
|
|
|
+ display: block;
|
|
|
+ font-size: 11px;
|
|
|
+ color: rgba(255,255,255,0.3);
|
|
|
+ margin-top: 6px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.proposal-card.paid-card {
|
|
|
+ text-align: center;
|
|
|
+ padding: 16px;
|
|
|
+}
|
|
|
+.paid-text {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #10b981;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+/* EPIC 9: Plan apply button bar */
|
|
|
+.plan-apply-bar {
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin: 10px 14px calc(env(safe-area-inset-bottom) + 16px);
|
|
|
+}
|
|
|
+.plan-apply-btn {
|
|
|
+ width: 100%;
|
|
|
+ height: 48px;
|
|
|
+ background: linear-gradient(135deg, #a78bfa, #7c3aed);
|
|
|
+ color: #fff;
|
|
|
+ border: none;
|
|
|
+ border-radius: 14px;
|
|
|
+ line-height: 48px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 700;
|
|
|
+ letter-spacing: 1px;
|
|
|
+ box-shadow: 0 4px 16px rgba(124, 58, 237, 0.25);
|
|
|
+}
|
|
|
+.plan-apply-btn:active {
|
|
|
+ opacity: 0.85;
|
|
|
+}
|
|
|
+
|
|
|
+/* US-9.5: Pending payment card */
|
|
|
+.pending-payment-card {
|
|
|
+ background: linear-gradient(135deg, rgba(167, 139, 250, 0.08), rgba(124, 58, 237, 0.05));
|
|
|
+ border: 1px solid rgba(167, 139, 250, 0.2);
|
|
|
+ border-radius: 14px;
|
|
|
+ padding: 20px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.pending-payment-card .proposal-price {
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #fbbf24;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+.payment-hint {
|
|
|
+ font-size: 12px;
|
|
|
+ color: rgba(255, 255, 255, 0.4);
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 14px;
|
|
|
+}
|
|
|
+.payment-btn {
|
|
|
+ width: 100%;
|
|
|
+ height: 46px;
|
|
|
+ background: linear-gradient(135deg, #10b981, #059669);
|
|
|
+ color: #fff;
|
|
|
+ border: none;
|
|
|
+ border-radius: 12px;
|
|
|
+ line-height: 46px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.25);
|
|
|
+}
|
|
|
+
|
|
|
+/* US-9.5: Delivery content display */
|
|
|
+.delivery-content {
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+.delivery-title {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: rgba(255, 255, 255, 0.85);
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+.delivery-item {
|
|
|
+ background: rgba(255, 255, 255, 0.03);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 14px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+.delivery-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+.delivery-type {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #10b981;
|
|
|
+}
|
|
|
+.delivery-time {
|
|
|
+ font-size: 11px;
|
|
|
+ color: rgba(255, 255, 255, 0.3);
|
|
|
+}
|
|
|
+.delivery-text {
|
|
|
+ display: block;
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgba(255, 255, 255, 0.65);
|
|
|
+ line-height: 1.6;
|
|
|
+ white-space: pre-wrap;
|
|
|
+}
|
|
|
+
|
|
|
+/* US-9.5: Paid waiting state */
|
|
|
+.paid-waiting {
|
|
|
+ text-align: center;
|
|
|
+ padding: 10px 0;
|
|
|
+}
|
|
|
+.paid-waiting-text {
|
|
|
+ font-size: 14px;
|
|
|
+ color: rgba(255, 255, 255, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+</style>
|