| | |
| | | <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-table> |
| | | </div> |
| | | |
| | | <!-- 打字中动画 --> |
| | | <div v-if="message.manufacturingData" class="manufacturing-card"> |
| | | <div class="manufacturing-card__title">{{ getManufacturingTypeLabel(message.type) }}</div> |
| | | |
| | | <div |
| | | v-if="message.manufacturingData.summaryEntries?.length || message.manufacturingData.coreMetrics?.length" |
| | | class="manufacturing-summary-grid" |
| | | > |
| | | <div |
| | | v-for="(entry, entryIndex) in message.manufacturingData.summaryEntries" |
| | | :key="`summary-${entry.key}-${entryIndex}`" |
| | | class="manufacturing-summary-item" |
| | | > |
| | | <span class="manufacturing-summary-label">{{ entry.label }}</span> |
| | | <strong class="manufacturing-summary-value">{{ entry.value }}</strong> |
| | | </div> |
| | | <div |
| | | v-for="(metric, metricIndex) in message.manufacturingData.coreMetrics" |
| | | :key="`core-${metric.key}-${metricIndex}`" |
| | | class="manufacturing-summary-item manufacturing-summary-item--core" |
| | | > |
| | | <span class="manufacturing-summary-label">{{ metric.label }}</span> |
| | | <strong class="manufacturing-summary-value">{{ metric.value }}</strong> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="message.manufacturingData.warningItems?.length" class="manufacturing-warning-list"> |
| | | <div |
| | | v-for="(warning, warningIndex) in message.manufacturingData.warningItems" |
| | | :key="`warning-${warning.title || warningIndex}`" |
| | | class="manufacturing-warning-item" |
| | | > |
| | | <div class="manufacturing-warning-item__head"> |
| | | <el-tag size="small" :type="getManufacturingWarningLevelType(warning.level)"> |
| | | {{ getManufacturingWarningLevelLabel(warning.level) }} |
| | | </el-tag> |
| | | <strong>{{ warning.title || `预警 ${warningIndex + 1}` }}</strong> |
| | | <span v-if="warning.count !== '' && warning.count !== null && warning.count !== undefined" class="manufacturing-warning-count"> |
| | | {{ warning.count }} |
| | | </span> |
| | | </div> |
| | | <p v-if="warning.detail" class="manufacturing-warning-detail">{{ warning.detail }}</p> |
| | | </div> |
| | | </div> |
| | | |
| | | <div |
| | | v-if="message.manufacturingData.listItems?.length && message.manufacturingData.columns?.length" |
| | | class="table-wrapper manufacturing-table-wrapper" |
| | | > |
| | | <el-table :data="message.manufacturingData.listItems" border stripe size="small" style="width: 100%"> |
| | | <el-table-column |
| | | v-for="col in message.manufacturingData.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.manufacturingData.actionCards?.length" class="manufacturing-action-list"> |
| | | <div |
| | | v-for="(card, cardIndex) in message.manufacturingData.actionCards" |
| | | :key="card.runtimeKey || `${card.code}-${card.targetApi}-${cardIndex}`" |
| | | class="manufacturing-action-card" |
| | | > |
| | | <div class="manufacturing-action-card__head"> |
| | | <strong>{{ card.name || `动作 ${cardIndex + 1}` }}</strong> |
| | | <el-tag size="small" type="info">{{ getNormalizedRequestMethod(card.method) }}</el-tag> |
| | | </div> |
| | | <div class="manufacturing-action-card__meta"> |
| | | <span>{{ card.code || '--' }}</span> |
| | | <span>{{ card.targetApi || '--' }}</span> |
| | | </div> |
| | | <p v-if="card.description" class="manufacturing-action-card__desc">{{ card.description }}</p> |
| | | <div v-if="card.requiredFields?.length" class="manufacturing-required-fields"> |
| | | <span>必填字段</span> |
| | | <el-tag |
| | | v-for="field in card.requiredFields" |
| | | :key="field" |
| | | size="small" |
| | | type="warning" |
| | | > |
| | | {{ getStructuredPathLabel(field) }} |
| | | </el-tag> |
| | | </div> |
| | | <el-input |
| | | v-model="card.payloadText" |
| | | type="textarea" |
| | | :rows="6" |
| | | resize="vertical" |
| | | :disabled="card.executing" |
| | | placeholder="请输入 JSON 请求参数" |
| | | /> |
| | | <div class="manufacturing-action-footer"> |
| | | <span |
| | | v-if="card.executeResult" |
| | | :class="['manufacturing-action-result', card.executeError ? 'error' : 'success']" |
| | | > |
| | | {{ card.executeResult }} |
| | | </span> |
| | | <el-button |
| | | type="primary" |
| | | size="small" |
| | | :loading="card.executing" |
| | | @click="executeManufacturingAction(message, card, cardIndex)" |
| | | > |
| | | 确认并执行 |
| | | </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.purchaseAnalysisData" class="purchase-confirm-card"> |
| | | <div class="purchase-confirm-header"> |
| | | <span>{{ businessTypeLabelMap[message.purchaseAnalysisData.businessType] || message.purchaseAnalysisData.businessType || '采购业务' }}</span> |
| | |
| | | import request from '@/utils/request' |
| | | import * as echarts from 'echarts' |
| | | import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, Promotion, RefreshRight } from '@element-plus/icons-vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { builtInAssistants, generalAssistant } from './assistants' |
| | | import todoAssistantAvatar from '@/assets/AI/待办助手.png' |
| | | import salesAssistantAvatar from '@/assets/AI/销售助手.png' |
| | |
| | | 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 manufacturingStructuredTypeSet = new Set([ |
| | | 'manufacturing_site_snapshot', |
| | | 'manufacturing_plan_list', |
| | | 'manufacturing_workorder_list', |
| | | 'manufacturing_device_list', |
| | | 'manufacturing_device_repair_list', |
| | | 'manufacturing_quality_list', |
| | | 'manufacturing_material_list', |
| | | 'manufacturing_exception_list', |
| | | 'manufacturing_warning', |
| | | 'manufacturing_analysis', |
| | | 'manufacturing_action_plan' |
| | | ]) |
| | | const manufacturingListTypeSet = new Set([ |
| | | 'manufacturing_plan_list', |
| | | 'manufacturing_workorder_list', |
| | | 'manufacturing_device_list', |
| | | 'manufacturing_device_repair_list', |
| | | 'manufacturing_quality_list', |
| | | 'manufacturing_material_list', |
| | | 'manufacturing_exception_list' |
| | | ]) |
| | | const manufacturingTypeLabelMap = { |
| | | manufacturing_site_snapshot: '生产现场概览', |
| | | manufacturing_plan_list: '计划查询', |
| | | manufacturing_workorder_list: '工单查询', |
| | | manufacturing_device_list: '设备查询', |
| | | manufacturing_device_repair_list: '设备维修记录查询', |
| | | manufacturing_quality_list: '质量查询', |
| | | manufacturing_material_list: '物料查询', |
| | | manufacturing_exception_list: '异常查询', |
| | | manufacturing_warning: '预警看板', |
| | | manufacturing_analysis: '经营分析', |
| | | manufacturing_action_plan: '办理建议' |
| | | } |
| | | const structuredFieldLabelMap = { |
| | | workOrderNo: '工单号', |
| | | planEndTime: '计划结束时间', |
| | | planStartTime: '计划开始时间', |
| | | timeRange: '时间范围', |
| | | startDate: '开始日期', |
| | | endDate: '结束日期', |
| | | warningCount: '预警数量', |
| | | overduePlanCount: '逾期计划数', |
| | | overdueWorkOrderCount: '逾期工单数', |
| | | actionCount: '建议动作数', |
| | | qualityOpenCount: '质量待处理数', |
| | | lowStockCount: '低库存数', |
| | | exceptionCount: '异常数', |
| | | userId: '用户ID', |
| | | tenantId: '租户ID', |
| | | status: '状态', |
| | | deviceName: '设备名称', |
| | | deviceModel: '设备型号', |
| | | pendingRepairCount: '待维修数', |
| | | repairTime: '维修时间', |
| | | repairName: '报修人', |
| | | maintenanceName: '维修人员', |
| | | level: '预警等级', |
| | | title: '标题', |
| | | count: '数量', |
| | | detail: '详情', |
| | | remark: '备注', |
| | | createTime: '创建时间', |
| | | updateTime: '更新时间', |
| | | exceptionType: '异常类型', |
| | | materialName: '物料名称', |
| | | stockQty: '库存量' |
| | | } |
| | | Object.assign(structuredFieldLabelMap, { |
| | | customerName: '客户名称', |
| | | riskLevel: '风险等级', |
| | | riskScore: '风险评分', |
| | | riskReasons: '风险原因', |
| | | pendingAmount: '待回款金额', |
| | | pendingRate: '待回款占比', |
| | | daysSinceLastOrder: '距上次下单天数', |
| | | priority: '优先级', |
| | | quoteConversionRate: '报价转化率', |
| | | collectionStrategy: '回款策略', |
| | | quotationStrategy: '报价策略', |
| | | nextAction: '下一步动作', |
| | | contractAmountTotal: '合同总额', |
| | | receivedAmountTotal: '已回款金额', |
| | | pendingAmountTotal: '待回款总额', |
| | | shipRate: '发货率' |
| | | }) |
| | | const purchasePayloadFieldLabelMap = { |
| | | purchaseLedgers: '采购台账', |
| | | productData: '产品明细', |
| | |
| | | inventoryWarningQuantity: 'inventoryWarningQuantity', |
| | | isInspected: 'isInspected', |
| | | isChecked: 'isInspected' |
| | | } |
| | | const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) |
| | | |
| | | const stringifyStructuredPayload = (value, spaces = 2) => { |
| | | if (typeof value === 'string') return value |
| | | try { |
| | | return JSON.stringify(value ?? {}, null, spaces) |
| | | } catch (err) { |
| | | return '{}' |
| | | } |
| | | } |
| | | |
| | | const structuredFieldTokenLabelMap = { |
| | | time: '时间', |
| | | range: '范围', |
| | | start: '开始', |
| | | end: '结束', |
| | | date: '日期', |
| | | warning: '预警', |
| | | overdue: '逾期', |
| | | plan: '计划', |
| | | work: '工', |
| | | order: '单', |
| | | workorder: '工单', |
| | | count: '数量', |
| | | quality: '质量', |
| | | low: '低', |
| | | stock: '库存', |
| | | exception: '异常', |
| | | action: '动作', |
| | | user: '用户', |
| | | tenant: '租户', |
| | | id: 'ID', |
| | | no: '编号', |
| | | number: '编号', |
| | | code: '编码', |
| | | name: '名称', |
| | | status: '状态', |
| | | level: '等级', |
| | | title: '标题', |
| | | detail: '详情', |
| | | total: '总数', |
| | | rate: '比率', |
| | | type: '类型', |
| | | pending: '待', |
| | | repair: '维修', |
| | | device: '设备', |
| | | material: '物料' |
| | | } |
| | | |
| | | const convertStructuredFieldKeyToChinese = (fieldKey = '') => { |
| | | const key = String(fieldKey || '').trim() |
| | | if (!key) return '-' |
| | | if (structuredFieldLabelMap[key]) return structuredFieldLabelMap[key] |
| | | if (/[\u4e00-\u9fa5]/.test(key)) return key |
| | | |
| | | const rawTokens = key |
| | | .replace(/([a-z0-9])([A-Z])/g, '$1 $2') |
| | | .replace(/[_-]+/g, ' ') |
| | | .trim() |
| | | .split(/\s+/) |
| | | .filter(Boolean) |
| | | .map(token => token.toLowerCase()) |
| | | |
| | | if (!rawTokens.length) return '字段' |
| | | |
| | | const mappedTokens = rawTokens |
| | | .map(token => structuredFieldTokenLabelMap[token] || '') |
| | | .filter(Boolean) |
| | | |
| | | if (mappedTokens.length) { |
| | | return mappedTokens.join('') |
| | | } |
| | | |
| | | return '字段' |
| | | } |
| | | |
| | | const getStructuredFieldLabel = (fieldKey = '') => { |
| | | return convertStructuredFieldKeyToChinese(fieldKey) |
| | | } |
| | | |
| | | const getStructuredPathLabel = (fieldPath = '') => { |
| | | const path = String(fieldPath || '').trim() |
| | | if (!path) return '-' |
| | | |
| | | const segments = path |
| | | .replace(/\[(\d+)]/g, '.$1') |
| | | .split('.') |
| | | .filter(Boolean) |
| | | |
| | | if (!segments.length) return getStructuredFieldLabel(path) |
| | | |
| | | return segments.map((segment) => { |
| | | if (/^\d+$/.test(segment)) { |
| | | return `第${Number(segment) + 1}项` |
| | | } |
| | | return getStructuredFieldLabel(segment) |
| | | }).join(' / ') |
| | | } |
| | | |
| | | const formatStructuredValue = (value) => { |
| | | if (value === null || value === undefined || value === '') return '-' |
| | | if (Array.isArray(value)) { |
| | | const preview = value.slice(0, 3).map(item => formatStructuredValue(item)).join('、') |
| | | return value.length > 3 ? `${preview} 等${value.length}项` : preview |
| | | } |
| | | if (isPlainObject(value)) return stringifyStructuredPayload(value, 0) |
| | | return String(value) |
| | | } |
| | | |
| | | const normalizeManufacturingSummaryEntries = (summary) => { |
| | | if (!isPlainObject(summary)) return [] |
| | | return Object.entries(summary) |
| | | .filter(([, value]) => value !== undefined && value !== null && `${value}`.trim() !== '') |
| | | .map(([key, value]) => ({ |
| | | key, |
| | | label: getStructuredFieldLabel(key), |
| | | value: formatStructuredValue(value) |
| | | })) |
| | | } |
| | | |
| | | const normalizeManufacturingCoreMetrics = (coreMetrics) => { |
| | | if (Array.isArray(coreMetrics)) { |
| | | return coreMetrics.map((item, index) => { |
| | | if (isPlainObject(item)) { |
| | | const label = item.label || item.name || item.key || `指标${index + 1}` |
| | | const metricValue = item.value ?? item.metricValue ?? item.data ?? '-' |
| | | const unit = item.unit ? ` ${item.unit}` : '' |
| | | return { |
| | | key: String(item.key || item.name || index), |
| | | label, |
| | | value: `${formatStructuredValue(metricValue)}${unit}`.trim() |
| | | } |
| | | } |
| | | return { |
| | | key: String(index), |
| | | label: `指标${index + 1}`, |
| | | value: formatStructuredValue(item) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | if (isPlainObject(coreMetrics)) { |
| | | return Object.entries(coreMetrics).map(([key, value]) => ({ |
| | | key, |
| | | label: getStructuredFieldLabel(key), |
| | | value: formatStructuredValue(value) |
| | | })) |
| | | } |
| | | |
| | | return [] |
| | | } |
| | | |
| | | const normalizeManufacturingWarningItems = (items = []) => { |
| | | if (!Array.isArray(items)) return [] |
| | | return items |
| | | .filter(item => isPlainObject(item)) |
| | | .map(item => ({ |
| | | level: String(item.level || '').toLowerCase(), |
| | | title: item.title || '', |
| | | count: item.count ?? '', |
| | | detail: item.detail ?? '' |
| | | })) |
| | | } |
| | | |
| | | const inferManufacturingColumns = (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 getManufacturingActionCardRuntimeKey = (card = {}, index = 0) => { |
| | | const code = String(card?.code || '').trim() |
| | | const api = String(card?.targetApi || '').trim() |
| | | const name = String(card?.name || '').trim() |
| | | return `${code}::${api}::${name}::${index}` |
| | | } |
| | | |
| | | const normalizeManufacturingActionCards = (actionCards = [], previousCards = []) => { |
| | | const previousMap = new Map() |
| | | if (Array.isArray(previousCards)) { |
| | | previousCards.forEach((card, index) => { |
| | | previousMap.set(getManufacturingActionCardRuntimeKey(card, index), card) |
| | | }) |
| | | } |
| | | |
| | | return (Array.isArray(actionCards) ? actionCards : []) |
| | | .filter(card => isPlainObject(card)) |
| | | .map((card, index) => { |
| | | const runtimeKey = getManufacturingActionCardRuntimeKey(card, index) |
| | | const previousCard = previousMap.get(runtimeKey) |
| | | const fallbackPayloadText = stringifyStructuredPayload(card.examplePayload, 2) |
| | | return { |
| | | ...card, |
| | | runtimeKey, |
| | | payloadText: previousCard?.payloadText ?? fallbackPayloadText, |
| | | executing: Boolean(previousCard?.executing), |
| | | executed: Boolean(previousCard?.executed), |
| | | executeResult: previousCard?.executeResult || '', |
| | | executeError: Boolean(previousCard?.executeError) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const buildManufacturingStructuredData = (parsedData, previousData = null) => { |
| | | const type = String(parsedData?.type || '') |
| | | if (!manufacturingStructuredTypeSet.has(type)) return null |
| | | |
| | | const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {} |
| | | const items = Array.isArray(rawData.items) ? rawData.items.filter(item => isPlainObject(item)) : [] |
| | | const warningItems = type === 'manufacturing_warning' ? normalizeManufacturingWarningItems(items) : [] |
| | | const listItems = manufacturingListTypeSet.has(type) ? items : [] |
| | | const actionCards = type === 'manufacturing_action_plan' |
| | | ? normalizeManufacturingActionCards(rawData.actionCards, previousData?.actionCards) |
| | | : [] |
| | | |
| | | return { |
| | | type, |
| | | summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary), |
| | | coreMetrics: normalizeManufacturingCoreMetrics(rawData.coreMetrics), |
| | | listItems, |
| | | columns: inferManufacturingColumns(listItems), |
| | | warningItems, |
| | | actionCards |
| | | } |
| | | } |
| | | |
| | | 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 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 getManufacturingWarningLevelType = (level = '') => { |
| | | const normalizedLevel = String(level || '').toLowerCase() |
| | | if (normalizedLevel === 'high') return 'danger' |
| | | if (normalizedLevel === 'medium') return 'warning' |
| | | return 'info' |
| | | } |
| | | |
| | | const getManufacturingWarningLevelLabel = (level = '') => { |
| | | const normalizedLevel = String(level || '').toLowerCase() |
| | | if (normalizedLevel === 'high') return '高' |
| | | if (normalizedLevel === 'medium') return '中' |
| | | return normalizedLevel ? normalizedLevel.toUpperCase() : '一般' |
| | | } |
| | | |
| | | const normalizeRequestMethod = (method = 'POST') => { |
| | | const normalized = String(method || 'POST').trim().toUpperCase() |
| | | if (['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(normalized)) return normalized |
| | | return 'POST' |
| | | } |
| | | |
| | | const getNormalizedRequestMethod = (method) => normalizeRequestMethod(method) |
| | | |
| | | const getPayloadValueByPath = (payload, fieldPath = '') => { |
| | | const normalizedPath = String(fieldPath || '').trim() |
| | | if (!normalizedPath || !isPlainObject(payload)) return undefined |
| | | |
| | | const pathSegments = normalizedPath |
| | | .replace(/\[(\d+)]/g, '.$1') |
| | | .split('.') |
| | | .filter(Boolean) |
| | | |
| | | return pathSegments.reduce((current, segment) => { |
| | | if (current === null || current === undefined) return undefined |
| | | if (!['object', 'function'].includes(typeof current)) return undefined |
| | | return current[segment] |
| | | }, payload) |
| | | } |
| | | |
| | | const getMissingRequiredFields = (requiredFields = [], payload = {}) => { |
| | | if (!Array.isArray(requiredFields) || !requiredFields.length) return [] |
| | | return requiredFields.filter((fieldPath) => { |
| | | const value = getPayloadValueByPath(payload, fieldPath) |
| | | return !hasMeaningfulPayloadValue(value) |
| | | }) |
| | | } |
| | | |
| | | const parseManufacturingActionPayload = (payloadText = '') => { |
| | | const text = String(payloadText ?? '').trim() |
| | | if (!text) return {} |
| | | return JSON.parse(text) |
| | | } |
| | | |
| | | const executeManufacturingAction = async (message, actionCard, cardIndex = 0) => { |
| | | if (!message?.manufacturingData || !actionCard || actionCard.executing) return |
| | | |
| | | const actionName = actionCard.name || `动作 ${cardIndex + 1}` |
| | | const targetApi = String(actionCard.targetApi || '').trim() |
| | | if (!targetApi) { |
| | | actionCard.executeError = true |
| | | actionCard.executeResult = '缺少 targetApi,无法执行动作' |
| | | return |
| | | } |
| | | |
| | | let payload = {} |
| | | try { |
| | | payload = parseManufacturingActionPayload(actionCard.payloadText) |
| | | } catch (err) { |
| | | actionCard.executeError = true |
| | | actionCard.executeResult = '请求参数不是合法 JSON,请检查后重试' |
| | | return |
| | | } |
| | | |
| | | const requiredFields = Array.isArray(actionCard.requiredFields) ? actionCard.requiredFields : [] |
| | | if (requiredFields.length && !isPlainObject(payload)) { |
| | | actionCard.executeError = true |
| | | actionCard.executeResult = '必填字段校验失败:请求参数必须是 JSON 对象' |
| | | return |
| | | } |
| | | |
| | | const missingFields = getMissingRequiredFields(requiredFields, payload) |
| | | if (missingFields.length) { |
| | | actionCard.executeError = true |
| | | const missingFieldLabels = missingFields.map(field => getStructuredPathLabel(field)) |
| | | actionCard.executeResult = `缺少必填字段:${missingFieldLabels.join('、')}` |
| | | return |
| | | } |
| | | |
| | | try { |
| | | await ElMessageBox.confirm(`确认执行「${actionName}」吗?`, '执行确认', { |
| | | confirmButtonText: '确认执行', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | }) |
| | | } catch (err) { |
| | | return |
| | | } |
| | | |
| | | actionCard.executing = true |
| | | actionCard.executeError = false |
| | | actionCard.executeResult = '' |
| | | |
| | | const method = normalizeRequestMethod(actionCard.method) |
| | | const requestConfig = { |
| | | url: targetApi, |
| | | method: method.toLowerCase() |
| | | } |
| | | |
| | | if (method === 'GET') { |
| | | requestConfig.params = payload |
| | | } else { |
| | | requestConfig.data = payload |
| | | } |
| | | |
| | | try { |
| | | const res = await request(requestConfig) |
| | | const successMsg = res?.msg || `${actionName}执行成功` |
| | | actionCard.executed = true |
| | | actionCard.executeError = false |
| | | actionCard.executeResult = successMsg |
| | | ElMessage.success(successMsg) |
| | | } catch (err) { |
| | | actionCard.executed = false |
| | | actionCard.executeError = true |
| | | actionCard.executeResult = err?.message || `${actionName}执行失败,请稍后重试` |
| | | } finally { |
| | | actionCard.executing = false |
| | | } |
| | | } |
| | | |
| | | // 历史会话相关 |
| | |
| | | tableData: null, |
| | | payloadTreeData: null, |
| | | payloadHiddenData: null, |
| | | purchaseAnalysisData: null, |
| | | manufacturingData: null, |
| | | salesData: null, |
| | | localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : [] |
| | | } |
| | | |
| | |
| | | const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => { |
| | | if (!messageObj || !parsedData?.success) return |
| | | |
| | | const previousManufacturingData = messageObj.manufacturingData |
| | | messageObj.type = parsedData.type || '' |
| | | messageObj.tableData = null |
| | | messageObj.purchaseAnalysisData = null |
| | | messageObj.manufacturingData = null |
| | | messageObj.salesData = null |
| | | |
| | | 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) |
| | | if (manufacturingData) { |
| | | messageObj.manufacturingData = manufacturingData |
| | | } |
| | | |
| | | if (parsedData.action === 'confirm_required' && parsedData.businessType) { |
| | | messageObj.type = 'purchase_analysis_confirm' |
| | | messageObj.purchaseAnalysisData = parsedData |
| | | messageObj.manufacturingData = null |
| | | messageObj.salesData = null |
| | | if (!Array.isArray(messageObj.payloadTreeData) || !messageObj.payloadTreeData.length) { |
| | | initializePurchasePayloadTree(messageObj, parsedData.payload || {}) |
| | | } |
| | |
| | | } |
| | | |
| | | return null |
| | | } |
| | | |
| | | const getStructuredFallbackText = (parsedData) => { |
| | | if (!parsedData) return '正在为您展示分析结果...' |
| | | if (parsedData.type === 'todo_list') 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 (parsedData.charts && Object.keys(parsedData.charts).length > 0) return '已为您生成分析图表。' |
| | | return '正在为您展示分析结果...' |
| | | } |
| | | |
| | | const buildPurchaseMaterialRankCharts = (parsedData) => { |
| | |
| | | if (visibleValue === undefined || visibleValue === null) return clonePurchasePayloadValue(hiddenValue) |
| | | |
| | | if (Array.isArray(visibleValue) && Array.isArray(hiddenValue)) { |
| | | const maxLength = Math.max(visibleValue.length, hiddenValue.length) |
| | | const merged = [] |
| | | for (let i = 0; i < maxLength; i++) { |
| | | merged[i] = mergePurchasePayloadWithHidden(visibleValue[i], hiddenValue[i]) |
| | | } |
| | | return merged |
| | | return visibleValue.map((item, index) => mergePurchasePayloadWithHidden(item, hiddenValue[index])) |
| | | } |
| | | |
| | | if ( |
| | |
| | | '' |
| | | } |
| | | |
| | | const prunePurchaseProductRecord = (record) => { |
| | | if (!record || typeof record !== 'object' || Array.isArray(record)) return null |
| | | const normalizedRecord = normalizePurchaseProductRecord(record) |
| | | const hasVisibleFieldValue = Object.entries(normalizedRecord).some(([key, value]) => { |
| | | if (shouldHidePurchaseField(key)) return false |
| | | return hasMeaningfulPayloadValue(value) |
| | | }) |
| | | return hasVisibleFieldValue ? normalizedRecord : null |
| | | } |
| | | |
| | | const normalizeAndFilterPurchaseProductData = (value) => { |
| | | if (!Array.isArray(value)) return value |
| | | return value |
| | | .map(item => prunePurchaseProductRecord(item)) |
| | | .filter(Boolean) |
| | | } |
| | | |
| | | const mergeLegacyProductDataIntoLedgers = (payload) => { |
| | | if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload |
| | | if (!Array.isArray(payload.purchaseLedgers) || !Array.isArray(payload.productData) || !payload.productData.length) { |
| | |
| | | |
| | | const ledgers = payload.purchaseLedgers.map(ledger => ({ |
| | | ...ledger, |
| | | productData: Array.isArray(ledger.productData) |
| | | ? ledger.productData.map(normalizePurchaseProductRecord) |
| | | : [] |
| | | productData: normalizeAndFilterPurchaseProductData(ledger.productData) || [] |
| | | })) |
| | | const unmatchedProducts = [] |
| | | |
| | | payload.productData.map(normalizePurchaseProductRecord).forEach(product => { |
| | | normalizeAndFilterPurchaseProductData(payload.productData).forEach(product => { |
| | | const productMatchKey = getPurchaseProductMatchKey(product) |
| | | const matchedLedger = ledgers.find(ledger => { |
| | | const ledgerKeys = [ |
| | |
| | | if (!record || typeof record !== 'object' || Array.isArray(record)) return record |
| | | const normalizedRecord = { |
| | | ...record, |
| | | productData: Array.isArray(record.productData) |
| | | ? record.productData.map(normalizePurchaseProductRecord) |
| | | : record.productData |
| | | productData: normalizeAndFilterPurchaseProductData(record.productData) |
| | | } |
| | | return Object.entries(normalizedRecord).reduce((result, [key, value]) => { |
| | | if (purchaseLedgerAllowedFieldKeys.has(key)) { |
| | |
| | | sanitized = normalizePurchasePayloadFieldTypes(sanitized) |
| | | if (Array.isArray(sanitized.purchaseLedgers)) { |
| | | sanitized.purchaseLedgers = sanitized.purchaseLedgers.map(filterPurchaseLedgerRecord) |
| | | } |
| | | if (Array.isArray(sanitized.productData)) { |
| | | sanitized.productData = normalizeAndFilterPurchaseProductData(sanitized.productData) |
| | | } |
| | | if (Array.isArray(sanitized.productData) && !sanitized.productData.length) { |
| | | delete sanitized.productData |
| | | } |
| | | |
| | | purchaseApprovalFieldKeys.forEach(key => { |
| | |
| | | type: '', |
| | | tableData: null, |
| | | payloadTreeData: null, |
| | | payloadHiddenData: null |
| | | payloadHiddenData: null, |
| | | purchaseAnalysisData: null, |
| | | manufacturingData: null, |
| | | salesData: null |
| | | }) |
| | | |
| | | outputState.value[botMsgIndex] = { |
| | |
| | | type: '', |
| | | tableData: null, |
| | | payloadTreeData: null, |
| | | payloadHiddenData: null |
| | | payloadHiddenData: null, |
| | | purchaseAnalysisData: null, |
| | | manufacturingData: null, |
| | | salesData: null |
| | | } |
| | | messages.value.push(botMsg) |
| | | |
| | |
| | | } |
| | | |
| | | if (!display) { |
| | | if (parsed.type === 'todo_list') { |
| | | display = '已为您整理好相关数据。' |
| | | } else if (parsed.charts && Object.keys(parsed.charts).length > 0) { |
| | | display = '已为您生成分析图表。' |
| | | } else { |
| | | display = '正在为您展示分析结果...' |
| | | } |
| | | display = getStructuredFallbackText(parsed) |
| | | } |
| | | } else if (startIdx !== -1) { |
| | | const lastBraceIdx = output.lastIndexOf('}') |
| | |
| | | } |
| | | |
| | | if (!display) { |
| | | if (parsed.type === 'todo_list') { |
| | | display = '已为您整理好相关数据:' |
| | | } else if (parsed.charts && Object.keys(parsed.charts).length > 0) { |
| | | display = '已为您生成分析图表:' |
| | | } else { |
| | | display = '正在为您展示分析结果...' |
| | | } |
| | | display = getStructuredFallbackText(parsed) |
| | | } |
| | | } catch (e) { |
| | | // 解析失败,说明 JSON 还在传输中或格式不正确 |
| | |
| | | .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)); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | .manufacturing-card { |
| | | margin-top: 12px; |
| | | width: 100%; |
| | | background: #fff; |
| | | border: 1px solid rgba(0, 85, 212, 0.12); |
| | | border-radius: 12px; |
| | | box-shadow: $shadow-card; |
| | | padding: 14px; |
| | | } |
| | | |
| | | .manufacturing-card__title { |
| | | font-size: 14px; |
| | | font-weight: 700; |
| | | color: $deep-blue; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .manufacturing-summary-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); |
| | | gap: 8px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .manufacturing-summary-item { |
| | | border-radius: 10px; |
| | | padding: 10px 12px; |
| | | border: 1px solid rgba(0, 85, 212, 0.08); |
| | | background: linear-gradient(180deg, #f8fbff, #f1f7ff); |
| | | min-height: 66px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: space-between; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .manufacturing-summary-item--core { |
| | | border-color: rgba(30, 91, 255, 0.24); |
| | | } |
| | | |
| | | .manufacturing-summary-label { |
| | | font-size: 12px; |
| | | color: #4b5563; |
| | | } |
| | | |
| | | .manufacturing-summary-value { |
| | | font-size: 15px; |
| | | color: #1f2937; |
| | | line-height: 1.4; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .manufacturing-warning-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .manufacturing-warning-item { |
| | | border-radius: 10px; |
| | | border: 1px solid rgba(245, 158, 11, 0.22); |
| | | background: linear-gradient(135deg, rgba(255, 247, 237, 0.9), rgba(255, 255, 255, 0.98)); |
| | | padding: 10px 12px; |
| | | } |
| | | |
| | | .manufacturing-warning-item__head { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | color: #92400e; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .manufacturing-warning-count { |
| | | margin-left: auto; |
| | | font-weight: 700; |
| | | color: #c2410c; |
| | | } |
| | | |
| | | .manufacturing-warning-detail { |
| | | margin: 8px 0 0; |
| | | font-size: 12px; |
| | | line-height: 1.6; |
| | | color: #7c2d12; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .manufacturing-table-wrapper { |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .manufacturing-action-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | margin-top: 12px; |
| | | } |
| | | |
| | | .manufacturing-action-card { |
| | | border: 1px solid rgba(0, 85, 212, 0.1); |
| | | border-radius: 10px; |
| | | padding: 10px 12px; |
| | | background: #f8fbff; |
| | | } |
| | | |
| | | .manufacturing-action-card__head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | font-size: 13px; |
| | | color: #1f2937; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .manufacturing-action-card__meta { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4px; |
| | | margin-bottom: 8px; |
| | | font-size: 12px; |
| | | color: #64748b; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .manufacturing-action-card__desc { |
| | | margin: 0 0 8px; |
| | | font-size: 12px; |
| | | line-height: 1.6; |
| | | color: #475467; |
| | | } |
| | | |
| | | .manufacturing-required-fields { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | gap: 6px; |
| | | margin-bottom: 8px; |
| | | font-size: 12px; |
| | | color: #7c2d12; |
| | | } |
| | | |
| | | .manufacturing-action-footer { |
| | | margin-top: 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .manufacturing-action-result { |
| | | flex: 1; |
| | | font-size: 12px; |
| | | line-height: 1.5; |
| | | |
| | | &.success { |
| | | color: #1f9d55; |
| | | } |
| | | |
| | | &.error { |
| | | color: #d93025; |
| | | } |
| | | } |
| | | |
| | | .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; |
| | | } |
| | | |
| | | .purchase-confirm-card { |
| | | margin-top: 12px; |
| | | width: 100%; |