From 888d620d3f8d4a8bec785f96ab916d05e539121c Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 18 五月 2026 14:09:10 +0800
Subject: [PATCH] feat(multiple): 为构建过程添加环境变量管理功能
---
src/components/AIChatSidebar/assistants/index.js | 6
src/components/AIChatSidebar/index.vue | 378 +++++++++++++++++++++++++++++++++++++++++++++++++++++
src/components/AIChatSidebar/assistants/salesAssistant.js | 28 ++++
3 files changed, 408 insertions(+), 4 deletions(-)
diff --git a/src/components/AIChatSidebar/assistants/index.js b/src/components/AIChatSidebar/assistants/index.js
index 96c54fa..d7081b4 100644
--- a/src/components/AIChatSidebar/assistants/index.js
+++ b/src/components/AIChatSidebar/assistants/index.js
@@ -1,13 +1,15 @@
import { generalAssistant } from './generalAssistant'
import { purchaseAssistant } from './purchaseAssistant'
import { productionAssistant } from './productionAssistant'
+import { salesAssistant } from './salesAssistant'
-export { generalAssistant, purchaseAssistant, productionAssistant }
+export { generalAssistant, purchaseAssistant, productionAssistant, salesAssistant }
export const assistantRegistry = {
general: generalAssistant,
+ sales: salesAssistant,
purchase: purchaseAssistant,
production: productionAssistant
}
-export const builtInAssistants = [generalAssistant, purchaseAssistant, productionAssistant]
+export const builtInAssistants = [generalAssistant, salesAssistant, purchaseAssistant, productionAssistant]
diff --git a/src/components/AIChatSidebar/assistants/salesAssistant.js b/src/components/AIChatSidebar/assistants/salesAssistant.js
new file mode 100644
index 0000000..03cb102
--- /dev/null
+++ b/src/components/AIChatSidebar/assistants/salesAssistant.js
@@ -0,0 +1,28 @@
+import { TrendCharts } from '@element-plus/icons-vue'
+
+export const salesAssistant = {
+ key: 'sales',
+ label: '閿�鍞姪鎵�',
+ title: '閿�鍞櫤鑳藉姪鎵�',
+ tooltip: '閿�鍞櫤鑳藉姪鎵�',
+ icon: TrendCharts,
+ apiBase: '/sales-ai',
+ storageKey: 'sales_ai_chat_uuid',
+ placeholder: '璇疯緭鍏ラ攢鍞浉鍏抽棶棰�... (Enter 鍙戦�� / Shift+Enter 鎹㈣)',
+ welcomeMessage: '浣犲ソ',
+ description: '鎴戝彲浠ュ崗鍔╀綘鏌ヨ瀹㈡埛妗f銆侀攢鍞姤浠枫�侀攢鍞彴璐︺�侀攢鍞��璐с�佸鎴峰線鏉ャ�佸彂璐у彴璐︼紝骞堕噸鐐瑰垎鏋愬鎴锋祦澶遍闄╁強鍥炴/鎶ヤ环绛栫暐銆�',
+ allowFileUpload: false,
+ emptySessionText: '鏆傛棤閿�鍞細璇�',
+ quickPrompts: [
+ '鏌ヨ绉佹捣瀹㈡埛妗f鍓�10鏉�',
+ '鏌ヨ鍏捣瀹㈡埛妗f',
+ '鏌ヨ鏈湀閿�鍞姤浠�',
+ '鏌ヨ鏈湀閿�鍞彴璐�',
+ '鏌ヨ杩�30澶╅攢鍞��璐�',
+ '鏌ヨ杩�30澶╁鎴峰洖娆惧線鏉�',
+ '鏌ヨ鏈湀鍙戣揣鍙拌处',
+ '鏌ョ湅閿�鍞寚鏍囩粺璁�',
+ '甯垜鍋氬鎴锋祦澶遍闄╁垎鏋愶紝杩�30澶╋紝鍓�20鏉�',
+ '鐢熸垚鍥炴涓庢姤浠风瓥鐣ュ缓璁紝浼樺厛楂橀闄╁鎴�'
+ ]
+}
diff --git a/src/components/AIChatSidebar/index.vue b/src/components/AIChatSidebar/index.vue
index e3a9e3c..3d234d6 100644
--- a/src/components/AIChatSidebar/index.vue
+++ b/src/components/AIChatSidebar/index.vue
@@ -359,6 +359,136 @@
</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>
@@ -750,6 +880,32 @@
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: '瀹㈡埛妗f',
+ 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',
@@ -819,6 +975,24 @@
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: '浜у搧鏄庣粏',
@@ -1203,6 +1377,77 @@
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'
@@ -1581,6 +1826,7 @@
payloadHiddenData: null,
purchaseAnalysisData: null,
manufacturingData: null,
+ salesData: null,
localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
}
@@ -1825,9 +2071,15 @@
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)
@@ -1839,6 +2091,7 @@
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 || {})
}
@@ -1883,6 +2136,12 @@
const getStructuredFallbackText = (parsedData) => {
if (!parsedData) return '姝e湪涓烘偍灞曠ず鍒嗘瀽缁撴灉...'
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 '宸蹭负鎮ㄧ敓鎴愬埗閫犻璀︾湅鏉裤��'
@@ -3018,7 +3277,8 @@
payloadTreeData: null,
payloadHiddenData: null,
purchaseAnalysisData: null,
- manufacturingData: null
+ manufacturingData: null,
+ salesData: null
})
outputState.value[botMsgIndex] = {
@@ -3143,7 +3403,8 @@
payloadTreeData: null,
payloadHiddenData: null,
purchaseAnalysisData: null,
- manufacturingData: null
+ manufacturingData: null,
+ salesData: null
}
messages.value.push(botMsg)
@@ -4519,6 +4780,119 @@
}
}
+.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%;
--
Gitblit v1.9.3