| | |
| | | <div v-if="!hideTrigger" class="ai-chat-trigger" @click="toggleSidebar" v-show="!visible"> |
| | | <el-tooltip :content="currentAssistant.tooltip" placement="left"> |
| | | <div class="trigger-icon"> |
| | | <el-icon :size="30" color="#fff"><component :is="currentAssistant.icon" /></el-icon> |
| | | <el-icon :size="22" color="#fff"><component :is="currentAssistant.icon" /></el-icon> |
| | | </div> |
| | | </el-tooltip> |
| | | </div> |
| | |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="message.salesData" class="sales-structured-card"> |
| | | <div class="sales-structured-card__title">{{ getSalesTypeLabel(message.type) }}</div> |
| | | |
| | | <div v-if="message.salesData.summaryEntries?.length" class="sales-summary-grid"> |
| | | <div |
| | | v-for="(entry, entryIndex) in message.salesData.summaryEntries" |
| | | :key="`sales-summary-${entry.key}-${entryIndex}`" |
| | | class="sales-summary-item" |
| | | > |
| | | <span class="sales-summary-label">{{ entry.label }}</span> |
| | | <strong class="sales-summary-value">{{ entry.value }}</strong> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="message.type === 'sales_customer_churn_risk' && message.salesData.listItems?.length" class="sales-focus-list"> |
| | | <div |
| | | v-for="(item, itemIndex) in message.salesData.listItems" |
| | | :key="`risk-${item.customerName || itemIndex}`" |
| | | class="sales-focus-item" |
| | | > |
| | | <div class="sales-focus-item__head"> |
| | | <strong>{{ formatStructuredValue(item.customerName) }}</strong> |
| | | <div class="sales-focus-tags"> |
| | | <el-tag size="small" :type="getSalesLevelTagType(item.riskLevel)"> |
| | | {{ getSalesLevelLabel(item.riskLevel, 'risk') }} |
| | | </el-tag> |
| | | <el-tag size="small" type="warning">风险分 {{ formatStructuredValue(item.riskScore) }}</el-tag> |
| | | </div> |
| | | </div> |
| | | <div class="sales-focus-metrics"> |
| | | <span>待回款:{{ formatStructuredValue(item.pendingAmount) }}</span> |
| | | <span>待回款占比:{{ formatStructuredValue(item.pendingRate) }}</span> |
| | | <span>距上次下单:{{ formatStructuredValue(item.daysSinceLastOrder) }}</span> |
| | | </div> |
| | | <div v-if="toStructuredStringArray(item.riskReasons).length" class="sales-focus-reasons"> |
| | | <el-tag |
| | | v-for="(reason, reasonIndex) in toStructuredStringArray(item.riskReasons)" |
| | | :key="`${item.customerName || itemIndex}-reason-${reasonIndex}`" |
| | | size="small" |
| | | type="danger" |
| | | effect="plain" |
| | | > |
| | | {{ reason }} |
| | | </el-tag> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="message.type === 'sales_collection_quote_strategy' && message.salesData.listItems?.length" class="sales-focus-list"> |
| | | <div |
| | | v-for="(item, itemIndex) in message.salesData.listItems" |
| | | :key="`strategy-${item.customerName || itemIndex}`" |
| | | class="sales-focus-item sales-focus-item--strategy" |
| | | > |
| | | <div class="sales-focus-item__head"> |
| | | <strong>{{ formatStructuredValue(item.customerName) }}</strong> |
| | | <div class="sales-focus-tags"> |
| | | <el-tag size="small" :type="getSalesLevelTagType(item.priority)"> |
| | | {{ getSalesLevelLabel(item.priority, 'priority') }} |
| | | </el-tag> |
| | | <el-tag size="small" type="success">转化率 {{ formatStructuredValue(item.quoteConversionRate) }}</el-tag> |
| | | </div> |
| | | </div> |
| | | <div class="sales-focus-metrics"> |
| | | <span>待回款:{{ formatStructuredValue(item.pendingAmount) }}</span> |
| | | <span v-if="item.nextAction">下一步:{{ formatStructuredValue(item.nextAction) }}</span> |
| | | </div> |
| | | <p v-if="item.collectionStrategy" class="sales-strategy-line"> |
| | | <strong>回款策略:</strong>{{ formatStructuredValue(item.collectionStrategy) }} |
| | | </p> |
| | | <p v-if="item.quotationStrategy" class="sales-strategy-line"> |
| | | <strong>报价策略:</strong>{{ formatStructuredValue(item.quotationStrategy) }} |
| | | </p> |
| | | </div> |
| | | </div> |
| | | |
| | | <div |
| | | v-if="message.salesData.listItems?.length && message.salesData.columns?.length && !isSalesFocusType(message.type)" |
| | | class="table-wrapper manufacturing-table-wrapper" |
| | | > |
| | | <el-table :data="message.salesData.listItems" border stripe size="small" style="width: 100%"> |
| | | <el-table-column |
| | | v-for="col in message.salesData.columns" |
| | | :key="col" |
| | | :label="getStructuredFieldLabel(col)" |
| | | min-width="140" |
| | | show-overflow-tooltip |
| | | > |
| | | <template #default="{ row }"> |
| | | {{ formatStructuredValue(row[col]) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <div v-if="message.salesData.topCustomers?.length && message.salesData.topCustomerColumns?.length" class="table-wrapper manufacturing-table-wrapper"> |
| | | <div class="sales-section-title">重点客户</div> |
| | | <el-table :data="message.salesData.topCustomers" border stripe size="small" style="width: 100%"> |
| | | <el-table-column |
| | | v-for="col in message.salesData.topCustomerColumns" |
| | | :key="`top-customer-${col}`" |
| | | :label="getStructuredFieldLabel(col)" |
| | | min-width="120" |
| | | show-overflow-tooltip |
| | | > |
| | | <template #default="{ row }"> |
| | | {{ formatStructuredValue(row[col]) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <div v-if="message.salesData.contractTrend?.length && message.salesData.contractTrendColumns?.length" class="table-wrapper manufacturing-table-wrapper"> |
| | | <div class="sales-section-title">合同趋势</div> |
| | | <el-table :data="message.salesData.contractTrend" border stripe size="small" style="width: 100%"> |
| | | <el-table-column |
| | | v-for="col in message.salesData.contractTrendColumns" |
| | | :key="`contract-trend-${col}`" |
| | | :label="getStructuredFieldLabel(col)" |
| | | min-width="120" |
| | | show-overflow-tooltip |
| | | > |
| | | <template #default="{ row }"> |
| | | {{ formatStructuredValue(row[col]) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="message.purchaseData" class="sales-structured-card"> |
| | | <div class="sales-structured-card__title">{{ getPurchaseTypeLabel(message.type) }}</div> |
| | | |
| | | <div v-if="message.purchaseData.summaryEntries?.length" class="sales-summary-grid"> |
| | | <div |
| | | v-for="(entry, entryIndex) in message.purchaseData.summaryEntries" |
| | | :key="`purchase-summary-${entry.key}-${entryIndex}`" |
| | | class="sales-summary-item" |
| | | > |
| | | <span class="sales-summary-label">{{ entry.label }}</span> |
| | | <strong class="sales-summary-value">{{ entry.value }}</strong> |
| | | </div> |
| | | </div> |
| | | |
| | | <div |
| | | v-if="message.purchaseData.listItems?.length && message.purchaseData.columns?.length" |
| | | class="table-wrapper manufacturing-table-wrapper" |
| | | > |
| | | <el-table :data="message.purchaseData.listItems" border stripe size="small" style="width: 100%"> |
| | | <el-table-column |
| | | v-for="col in message.purchaseData.columns" |
| | | :key="col" |
| | | :label="getStructuredFieldLabel(col)" |
| | | min-width="140" |
| | | show-overflow-tooltip |
| | | > |
| | | <template #default="{ row }"> |
| | | {{ formatStructuredValue(row[col]) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="message.financeData" class="sales-structured-card"> |
| | | <div class="sales-structured-card__title">{{ getFinancialTypeLabel(message.type) }}</div> |
| | | |
| | | <div v-if="message.financeData.summaryEntries?.length" class="sales-summary-grid"> |
| | | <div |
| | | v-for="(entry, entryIndex) in message.financeData.summaryEntries" |
| | | :key="`finance-summary-${entry.key}-${entryIndex}`" |
| | | class="sales-summary-item" |
| | | > |
| | | <span class="sales-summary-label">{{ entry.label }}</span> |
| | | <strong class="sales-summary-value">{{ entry.value }}</strong> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="message.financeData.headline" class="finance-headline"> |
| | | {{ message.financeData.headline }} |
| | | </div> |
| | | |
| | | <div v-if="message.financeData.conclusions?.length" class="finance-text-section"> |
| | | <div class="sales-section-title">核心结论</div> |
| | | <ul> |
| | | <li v-for="(item, idx) in message.financeData.conclusions" :key="`finance-conclusion-${idx}`"> |
| | | {{ item }} |
| | | </li> |
| | | </ul> |
| | | </div> |
| | | |
| | | <div v-if="message.financeData.riskSuggestions?.length" class="finance-text-section"> |
| | | <div class="sales-section-title">风险建议</div> |
| | | <ul> |
| | | <li v-for="(item, idx) in message.financeData.riskSuggestions" :key="`finance-risk-${idx}`"> |
| | | {{ item }} |
| | | </li> |
| | | </ul> |
| | | </div> |
| | | |
| | | <div |
| | | v-for="section in message.financeData.sections" |
| | | :key="`finance-section-${section.key}`" |
| | | class="table-wrapper manufacturing-table-wrapper" |
| | | > |
| | | <div class="sales-section-title">{{ section.title }}</div> |
| | | <el-table :data="section.items" border stripe size="small" style="width: 100%"> |
| | | <el-table-column |
| | | v-for="col in section.columns" |
| | | :key="`finance-${section.key}-${col}`" |
| | | :label="getStructuredFieldLabel(col)" |
| | | min-width="120" |
| | | show-overflow-tooltip |
| | | > |
| | | <template #default="{ row }"> |
| | | {{ formatStructuredValue(row[col]) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="message.purchaseIntentData?.quickPrompts?.length" class="purchase-intent-quick-prompt-wrap"> |
| | | <div class="purchase-intent-quick-prompt-title">试试以下提问</div> |
| | | <div class="quick-prompt-list purchase-intent-quick-prompt-list"> |
| | | <button |
| | | v-for="prompt in message.purchaseIntentData.quickPrompts" |
| | | :key="`purchase-intent-${prompt}`" |
| | | type="button" |
| | | class="quick-prompt-btn" |
| | | :disabled="isSending" |
| | | @click="sendQuickPrompt(prompt)" |
| | | > |
| | | {{ prompt }} |
| | | </button> |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | purchase_return_order: '采购退货单', |
| | | unknown: '未知采购业务' |
| | | } |
| | | const salesStructuredTypeSet = new Set([ |
| | | 'sales_customer_profile_list', |
| | | 'sales_quotation_list', |
| | | 'sales_ledger_list', |
| | | 'sales_return_list', |
| | | 'sales_customer_interaction_list', |
| | | 'sales_shipping_list', |
| | | 'sales_dashboard', |
| | | 'sales_customer_churn_risk', |
| | | 'sales_collection_quote_strategy' |
| | | ]) |
| | | const salesFocusTypeSet = new Set([ |
| | | 'sales_customer_churn_risk', |
| | | 'sales_collection_quote_strategy' |
| | | ]) |
| | | const salesTypeLabelMap = { |
| | | sales_customer_profile_list: '客户档案', |
| | | sales_quotation_list: '销售报价', |
| | | sales_ledger_list: '销售台账', |
| | | sales_return_list: '销售退货', |
| | | sales_customer_interaction_list: '客户往来', |
| | | sales_shipping_list: '发货台账', |
| | | sales_dashboard: '销售指标统计', |
| | | sales_customer_churn_risk: '客户流失风险分析', |
| | | sales_collection_quote_strategy: '回款与报价策略建议' |
| | | } |
| | | const purchaseTypeLabelMap = { |
| | | purchase_material_rank: '采购物料金额排行', |
| | | purchase_pending_payment_list: '待付款采购单' |
| | | } |
| | | const financialStructuredTypeSet = new Set([ |
| | | 'financial_cost_accounting', |
| | | 'financial_order_profit_analysis', |
| | | 'financial_inventory_capital_analysis', |
| | | 'financial_cashflow_forecast', |
| | | 'financial_business_anomaly_warning', |
| | | 'financial_business_cockpit', |
| | | 'financial_operation_report', |
| | | 'financial_rag_knowledge' |
| | | ]) |
| | | const financialTypeLabelMap = { |
| | | financial_cost_accounting: '成本核算', |
| | | financial_order_profit_analysis: '订单利润分析', |
| | | financial_inventory_capital_analysis: '库存资金分析', |
| | | financial_cashflow_forecast: '现金流预测', |
| | | financial_business_anomaly_warning: '经营异常预警', |
| | | financial_business_cockpit: '经营驾驶舱', |
| | | financial_operation_report: '经营报告', |
| | | financial_rag_knowledge: '财务知识检索' |
| | | } |
| | | const manufacturingStructuredTypeSet = new Set([ |
| | | 'manufacturing_site_snapshot', |
| | | 'manufacturing_plan_list', |
| | |
| | | materialName: '物料名称', |
| | | stockQty: '库存量' |
| | | } |
| | | Object.assign(structuredFieldLabelMap, { |
| | | customerName: '客户名称', |
| | | riskLevel: '风险等级', |
| | | riskScore: '风险评分', |
| | | riskReasons: '风险原因', |
| | | pendingAmount: '待回款金额', |
| | | pendingRate: '待回款占比', |
| | | daysSinceLastOrder: '距上次下单天数', |
| | | priority: '优先级', |
| | | quoteConversionRate: '报价转化率', |
| | | collectionStrategy: '回款策略', |
| | | quotationStrategy: '报价策略', |
| | | nextAction: '下一步动作', |
| | | salesContractNo: '销售合同号', |
| | | revenue: '收入', |
| | | materialCost: '材料成本', |
| | | laborCost: '人工成本', |
| | | depreciationCost: '折旧成本', |
| | | scrapCost: '报废成本', |
| | | totalCost: '总成本', |
| | | profit: '利润', |
| | | profitRate: '利润率', |
| | | reasons: '原因', |
| | | suggestion: '建议', |
| | | productName: '产品名称', |
| | | model: '型号', |
| | | quantity: '数量', |
| | | inventoryValue: '库存资金', |
| | | stagnantDays: '呆滞天数', |
| | | overstock: '是否超储', |
| | | month: '月份', |
| | | income: '收入', |
| | | expense: '支出', |
| | | netFlow: '净现金流', |
| | | message: '预警信息', |
| | | headline: '报告标题', |
| | | conclusions: '核心结论', |
| | | riskSuggestions: '风险建议', |
| | | orderProfitTop: '利润Top', |
| | | contractAmountTotal: '合同总额', |
| | | receivedAmountTotal: '已回款金额', |
| | | pendingAmountTotal: '待回款总额', |
| | | shipRate: '发货率', |
| | | pendingOrderCount: '待付款订单数', |
| | | totalContractAmount: '待付款合同总额', |
| | | totalPaidAmount: '已付款总额', |
| | | totalPendingAmount: '待付款总额' |
| | | }) |
| | | const purchasePayloadFieldLabelMap = { |
| | | purchaseLedgers: '采购台账', |
| | | productData: '产品明细', |
| | |
| | | |
| | | const getManufacturingTypeLabel = (type = '') => manufacturingTypeLabelMap[String(type || '')] || '制造结果' |
| | | |
| | | const inferSalesColumns = (items = []) => { |
| | | if (!Array.isArray(items) || !items.length) return [] |
| | | const fieldSet = new Set() |
| | | items.forEach((item) => { |
| | | if (!isPlainObject(item)) return |
| | | Object.keys(item).forEach((key) => fieldSet.add(key)) |
| | | }) |
| | | return Array.from(fieldSet) |
| | | } |
| | | |
| | | const normalizeSalesListItems = (items) => { |
| | | if (!Array.isArray(items)) return [] |
| | | return items.filter(item => isPlainObject(item)) |
| | | } |
| | | |
| | | const buildSalesStructuredData = (parsedData) => { |
| | | const type = String(parsedData?.type || '') |
| | | if (!salesStructuredTypeSet.has(type)) return null |
| | | |
| | | const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {} |
| | | const listItems = normalizeSalesListItems(rawData.items) |
| | | const topCustomers = normalizeSalesListItems(rawData.topCustomers) |
| | | const contractTrend = normalizeSalesListItems(rawData.contractTrend) |
| | | |
| | | return { |
| | | type, |
| | | summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary), |
| | | listItems, |
| | | columns: inferSalesColumns(listItems), |
| | | topCustomers, |
| | | topCustomerColumns: inferSalesColumns(topCustomers), |
| | | contractTrend, |
| | | contractTrendColumns: inferSalesColumns(contractTrend) |
| | | } |
| | | } |
| | | |
| | | const getSalesTypeLabel = (type = '') => salesTypeLabelMap[String(type || '')] || '销售查询结果' |
| | | |
| | | const buildPurchaseStructuredData = (parsedData) => { |
| | | if (parsedData?.success !== true) return null |
| | | |
| | | const type = String(parsedData?.type || '') |
| | | if (!type.startsWith('purchase_')) return null |
| | | |
| | | const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {} |
| | | const listItems = normalizeSalesListItems(rawData.items) |
| | | |
| | | return { |
| | | type, |
| | | summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary), |
| | | listItems, |
| | | columns: inferSalesColumns(listItems) |
| | | } |
| | | } |
| | | |
| | | const getPurchaseTypeLabel = (type = '') => purchaseTypeLabelMap[String(type || '')] || '采购查询结果' |
| | | |
| | | const financialDataSectionConfig = [ |
| | | { key: 'orders', title: '订单明细' }, |
| | | { key: 'items', title: '明细列表' }, |
| | | { key: 'actualMonthly', title: '历史现金流' }, |
| | | { key: 'forecastMonthly', title: '预测现金流' }, |
| | | { key: 'receivableRiskTop', title: '应收风险Top' }, |
| | | { key: 'payablePressureTop', title: '应付压力Top' }, |
| | | { key: 'orderProfitTop', title: '利润Top' } |
| | | ] |
| | | |
| | | const buildFinancialStructuredSections = (rawData = {}) => { |
| | | const sections = [] |
| | | financialDataSectionConfig.forEach((section) => { |
| | | const items = normalizeSalesListItems(rawData?.[section.key]) |
| | | if (!items.length) return |
| | | sections.push({ |
| | | key: section.key, |
| | | title: section.title, |
| | | items, |
| | | columns: inferSalesColumns(items) |
| | | }) |
| | | }) |
| | | return sections |
| | | } |
| | | |
| | | const buildFinancialStructuredData = (parsedData) => { |
| | | const type = String(parsedData?.type || '') |
| | | if (!financialStructuredTypeSet.has(type) || parsedData?.success !== true) return null |
| | | |
| | | const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {} |
| | | const headline = String(rawData?.headline || '').trim() |
| | | |
| | | return { |
| | | type, |
| | | summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary), |
| | | headline, |
| | | conclusions: toStructuredStringArray(rawData?.conclusions), |
| | | riskSuggestions: toStructuredStringArray(rawData?.riskSuggestions), |
| | | sections: buildFinancialStructuredSections(rawData) |
| | | } |
| | | } |
| | | |
| | | const getFinancialTypeLabel = (type = '') => financialTypeLabelMap[String(type || '')] || '财务查询结果' |
| | | |
| | | const isSalesFocusType = (type = '') => salesFocusTypeSet.has(String(type || '')) |
| | | |
| | | const getSalesLevelTagType = (level = '') => { |
| | | const normalizedLevel = String(level || '').toLowerCase() |
| | | if (normalizedLevel === 'high') return 'danger' |
| | | if (normalizedLevel === 'medium') return 'warning' |
| | | if (normalizedLevel === 'low') return 'success' |
| | | return 'info' |
| | | } |
| | | |
| | | const getSalesLevelLabel = (level = '', mode = 'risk') => { |
| | | const normalizedLevel = String(level || '').toLowerCase() |
| | | const suffix = mode === 'priority' ? '优先级' : '风险' |
| | | if (normalizedLevel === 'high') return `高${suffix}` |
| | | if (normalizedLevel === 'medium') return `中${suffix}` |
| | | if (normalizedLevel === 'low') return `低${suffix}` |
| | | if (!normalizedLevel) return mode === 'priority' ? '未分级' : '未评估' |
| | | return normalizedLevel.toUpperCase() |
| | | } |
| | | |
| | | const toStructuredStringArray = (value) => { |
| | | if (Array.isArray(value)) { |
| | | return value.map(item => String(item || '').trim()).filter(Boolean) |
| | | } |
| | | if (typeof value === 'string') { |
| | | return value |
| | | .split(/[,\uFF0C\u3001;\uFF1B\n]/) |
| | | .map(item => item.trim()) |
| | | .filter(Boolean) |
| | | } |
| | | return [] |
| | | } |
| | | |
| | | const normalizePurchaseIntentNotRecognizedData = (parsedData) => { |
| | | if (String(parsedData?.type || '') !== 'purchase_intent_not_recognized') return null |
| | | const quickPrompts = toStructuredStringArray(parsedData?.data?.quickPrompts) |
| | | if (!quickPrompts.length) return null |
| | | return { quickPrompts } |
| | | } |
| | | |
| | | const getManufacturingWarningLevelType = (level = '') => { |
| | | const normalizedLevel = String(level || '').toLowerCase() |
| | | if (normalizedLevel === 'high') return 'danger' |
| | |
| | | payloadHiddenData: null, |
| | | purchaseAnalysisData: null, |
| | | manufacturingData: null, |
| | | salesData: null, |
| | | purchaseData: null, |
| | | purchaseIntentData: null, |
| | | financeData: null, |
| | | localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : [] |
| | | } |
| | | |
| | |
| | | const candidate = text.slice(i, j + 1) |
| | | try { |
| | | const parsed = JSON.parse(candidate) |
| | | if (parsed?.success === true) { |
| | | if (typeof parsed?.success === 'boolean') { |
| | | return { |
| | | data: parsed, |
| | | startIdx: i, |
| | |
| | | } |
| | | |
| | | const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => { |
| | | if (!messageObj || !parsedData?.success) return |
| | | const isPurchaseIntentNotRecognized = String(parsedData?.type || '') === 'purchase_intent_not_recognized' |
| | | if (!messageObj || (parsedData?.success !== true && !isPurchaseIntentNotRecognized)) return |
| | | |
| | | const previousManufacturingData = messageObj.manufacturingData |
| | | messageObj.type = parsedData.type || '' |
| | | messageObj.tableData = null |
| | | messageObj.purchaseAnalysisData = null |
| | | messageObj.manufacturingData = null |
| | | messageObj.salesData = null |
| | | messageObj.purchaseData = null |
| | | messageObj.purchaseIntentData = null |
| | | messageObj.financeData = null |
| | | |
| | | if (isPurchaseIntentNotRecognized) { |
| | | messageObj.purchaseIntentData = normalizePurchaseIntentNotRecognizedData(parsedData) |
| | | messageObj.chartOptions = null |
| | | messageObj.chartRenderReady = false |
| | | return |
| | | } |
| | | |
| | | if (messageObj.type === 'todo_list' && parsedData.data) { |
| | | messageObj.tableData = parsedData.data |
| | | } |
| | | |
| | | const salesData = buildSalesStructuredData(parsedData) |
| | | if (salesData) { |
| | | messageObj.salesData = salesData |
| | | } |
| | | |
| | | const manufacturingData = buildManufacturingStructuredData(parsedData, previousManufacturingData) |
| | |
| | | messageObj.manufacturingData = manufacturingData |
| | | } |
| | | |
| | | const purchaseData = buildPurchaseStructuredData(parsedData) |
| | | if (purchaseData) { |
| | | messageObj.purchaseData = purchaseData |
| | | } |
| | | |
| | | const financeData = buildFinancialStructuredData(parsedData) |
| | | if (financeData) { |
| | | messageObj.financeData = financeData |
| | | } |
| | | |
| | | if (parsedData.action === 'confirm_required' && parsedData.businessType) { |
| | | messageObj.type = 'purchase_analysis_confirm' |
| | | messageObj.purchaseAnalysisData = parsedData |
| | | messageObj.manufacturingData = null |
| | | messageObj.salesData = null |
| | | messageObj.purchaseData = null |
| | | messageObj.financeData = null |
| | | if (!Array.isArray(messageObj.payloadTreeData) || !messageObj.payloadTreeData.length) { |
| | | initializePurchasePayloadTree(messageObj, parsedData.payload || {}) |
| | | } |
| | |
| | | const getStructuredFallbackText = (parsedData) => { |
| | | if (!parsedData) return '正在为您展示分析结果...' |
| | | if (parsedData.type === 'todo_list') return '已为您整理好相关数据。' |
| | | if (parsedData.type === 'purchase_intent_not_recognized') return '未识别到可执行的采购查询条件,请补充查询条件后再试。' |
| | | if (salesStructuredTypeSet.has(parsedData.type)) { |
| | | if (parsedData.type === 'sales_customer_churn_risk') return '已为您生成客户流失风险分析。' |
| | | if (parsedData.type === 'sales_collection_quote_strategy') return '已为您生成回款与报价策略建议。' |
| | | if (parsedData.type === 'sales_dashboard') return '已为您生成销售指标统计。' |
| | | return '已返回销售查询结果。' |
| | | } |
| | | if (manufacturingStructuredTypeSet.has(parsedData.type)) { |
| | | if (parsedData.type === 'manufacturing_action_plan') return '已为您生成办理建议,请确认动作后执行。' |
| | | if (parsedData.type === 'manufacturing_warning') return '已为您生成制造预警看板。' |
| | | if (parsedData.type === 'manufacturing_analysis') return '已为您生成制造分析结果。' |
| | | return '已返回制造查询结果。' |
| | | } |
| | | if (financialStructuredTypeSet.has(parsedData.type)) return '已返回财务分析结果。' |
| | | if (String(parsedData.type || '').startsWith('purchase_')) return '已返回采购查询结果。' |
| | | if (parsedData.charts && Object.keys(parsedData.charts).length > 0) return '已为您生成分析图表。' |
| | | return '正在为您展示分析结果...' |
| | | } |
| | |
| | | payloadTreeData: null, |
| | | payloadHiddenData: null, |
| | | purchaseAnalysisData: null, |
| | | manufacturingData: null |
| | | manufacturingData: null, |
| | | salesData: null, |
| | | purchaseData: null, |
| | | purchaseIntentData: null, |
| | | financeData: null |
| | | }) |
| | | |
| | | outputState.value[botMsgIndex] = { |
| | |
| | | payloadTreeData: null, |
| | | payloadHiddenData: null, |
| | | purchaseAnalysisData: null, |
| | | manufacturingData: null |
| | | manufacturingData: null, |
| | | salesData: null, |
| | | purchaseData: null, |
| | | purchaseIntentData: null, |
| | | financeData: null |
| | | } |
| | | messages.value.push(botMsg) |
| | | |
| | |
| | | const extracted = extractEmbeddedSuccessJson(fullText) |
| | | if (extracted) { |
| | | applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex) |
| | | } else { |
| | | const extractJson = (text) => { |
| | | const startIdx = text.indexOf('{"success": true') |
| | | if (startIdx === -1) return null |
| | | |
| | | // 从后往前找最后一个 '}' |
| | | const lastBraceIdx = text.lastIndexOf('}') |
| | | if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null |
| | | |
| | | const potentialJson = text.substring(startIdx, lastBraceIdx + 1) |
| | | try { |
| | | return JSON.parse(potentialJson) |
| | | } catch (err) { |
| | | return null |
| | | } |
| | | } |
| | | |
| | | const parsedData = extractJson(fullText) |
| | | if (parsedData) { |
| | | applyStructuredMessageData(currentMsg, parsedData, botMsgIndex, true) |
| | | } |
| | | |
| | | } |
| | | |
| | | updateOutputState(fullText, botMsgIndex) |
| | |
| | | const extracted = extractEmbeddedSuccessJson(currentMsg.content) |
| | | if (extracted) { |
| | | applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex) |
| | | } else { |
| | | const extractJson = (text) => { |
| | | const startIdx = text.indexOf('{"success": true') |
| | | if (startIdx === -1) return null |
| | | const lastBraceIdx = text.lastIndexOf('}') |
| | | if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null |
| | | const potentialJson = text.substring(startIdx, lastBraceIdx + 1) |
| | | try { |
| | | return JSON.parse(potentialJson) |
| | | } catch (err) { |
| | | return null |
| | | } |
| | | } |
| | | |
| | | const finalParsed = extractJson(currentMsg.content) |
| | | if (finalParsed) { |
| | | applyStructuredMessageData(currentMsg, finalParsed, botMsgIndex) |
| | | } |
| | | } |
| | | }).catch(err => { |
| | | if (err.name === 'CanceledError' || err.name === 'AbortError') { |
| | |
| | | .ai-chat-trigger { |
| | | pointer-events: auto; |
| | | position: fixed; |
| | | right: 24px; |
| | | bottom: 100px; |
| | | width: 56px; |
| | | height: 56px; |
| | | right: 10px; |
| | | bottom: 12px; |
| | | width: 40px; |
| | | height: 40px; |
| | | background: $gradient-dark; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | cursor: pointer; |
| | | box-shadow: $shadow-deep, 0 0 0 2px rgba(0, 85, 212, 0.3) inset, 0 0 30px rgba(0, 119, 232, 0.2); |
| | | transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); |
| | | z-index: 2001; |
| | | animation: triggerPulse 3s ease-in-out infinite; |
| | | box-shadow: 0 8px 18px rgba(0, 68, 170, 0.32), 0 0 0 1px rgba(0, 136, 232, 0.32) inset; |
| | | transition: transform 0.22s ease, box-shadow 0.22s ease, opacity 0.22s ease; |
| | | z-index: 1020; |
| | | opacity: 0.9; |
| | | |
| | | &::before { |
| | | content: ''; |
| | | position: absolute; |
| | | inset: -6px; |
| | | inset: -4px; |
| | | background: linear-gradient(135deg, rgba(0, 85, 212, 0.4), rgba(0, 136, 232, 0.3), rgba(90, 159, 224, 0.2)); |
| | | border-radius: 50%; |
| | | z-index: -1; |
| | | filter: blur(16px); |
| | | animation: glowPulse 2s ease-in-out infinite alternate; |
| | | filter: blur(10px); |
| | | opacity: 0.35; |
| | | } |
| | | |
| | | &::after { |
| | |
| | | } |
| | | |
| | | &:hover { |
| | | transform: scale(1.12) translateY(-4px); |
| | | box-shadow: $shadow-deep, 0 0 0 3px rgba(0, 136, 232, 0.4) inset, 0 0 50px rgba(0, 136, 232, 0.3); |
| | | |
| | | &::before { |
| | | animation: glowPulse 1s ease-in-out infinite alternate; |
| | | } |
| | | transform: scale(1.05) translateY(-1px); |
| | | box-shadow: 0 10px 22px rgba(0, 68, 170, 0.38), 0 0 0 1px rgba(0, 136, 232, 0.42) inset; |
| | | |
| | | .trigger-icon { |
| | | transform: rotate(-8deg) scale(1.05); |
| | | filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.5)); |
| | | transform: scale(1.03); |
| | | filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.42)); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | .sales-structured-card { |
| | | margin-top: 12px; |
| | | width: 100%; |
| | | background: #fff; |
| | | border: 1px solid rgba(31, 122, 114, 0.2); |
| | | border-radius: 12px; |
| | | box-shadow: $shadow-card; |
| | | padding: 14px; |
| | | } |
| | | |
| | | .sales-structured-card__title { |
| | | font-size: 14px; |
| | | font-weight: 700; |
| | | color: #1f5ddf; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .sales-summary-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); |
| | | gap: 8px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .sales-summary-item { |
| | | border-radius: 10px; |
| | | padding: 10px 12px; |
| | | border: 1px solid rgba(30, 91, 255, 0.12); |
| | | background: linear-gradient(180deg, #f7fbff, #edf6ff); |
| | | min-height: 66px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: space-between; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .sales-summary-label { |
| | | font-size: 12px; |
| | | color: #4b5563; |
| | | } |
| | | |
| | | .sales-summary-value { |
| | | font-size: 15px; |
| | | color: #1f2937; |
| | | line-height: 1.4; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .sales-focus-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .sales-focus-item { |
| | | border-radius: 10px; |
| | | border: 1px solid rgba(30, 91, 255, 0.14); |
| | | background: #f8fbff; |
| | | padding: 10px 12px; |
| | | } |
| | | |
| | | .sales-focus-item--strategy { |
| | | border-color: rgba(31, 122, 114, 0.22); |
| | | background: linear-gradient(180deg, #f7fcfb, #edf9f6); |
| | | } |
| | | |
| | | .sales-focus-item__head { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | gap: 10px; |
| | | font-size: 13px; |
| | | color: #1f2937; |
| | | } |
| | | |
| | | .sales-focus-tags { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | flex-wrap: wrap; |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .sales-focus-metrics { |
| | | margin-top: 8px; |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 10px; |
| | | font-size: 12px; |
| | | color: #475467; |
| | | } |
| | | |
| | | .sales-focus-reasons { |
| | | margin-top: 8px; |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .sales-strategy-line { |
| | | margin: 8px 0 0; |
| | | font-size: 12px; |
| | | line-height: 1.6; |
| | | color: #334155; |
| | | } |
| | | |
| | | .sales-section-title { |
| | | margin: 4px 0 8px; |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | color: $deep-blue; |
| | | } |
| | | |
| | | .finance-headline { |
| | | margin-top: 4px; |
| | | font-size: 13px; |
| | | line-height: 1.7; |
| | | color: #344054; |
| | | } |
| | | |
| | | .finance-text-section { |
| | | margin-top: 10px; |
| | | |
| | | ul { |
| | | margin: 6px 0 0; |
| | | padding-left: 18px; |
| | | font-size: 13px; |
| | | line-height: 1.7; |
| | | color: #344054; |
| | | } |
| | | } |
| | | |
| | | .purchase-intent-quick-prompt-wrap { |
| | | margin-top: 12px; |
| | | } |
| | | |
| | | .purchase-intent-quick-prompt-title { |
| | | font-size: 12px; |
| | | color: #4b5563; |
| | | } |
| | | |
| | | .purchase-intent-quick-prompt-list { |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .purchase-confirm-card { |
| | | margin-top: 12px; |
| | | width: 100%; |