CustomersView.vue 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. <template>
  2. <div class="data-page">
  3. <div class="data-header">
  4. <div class="data-title">📋 客户池 - 共{{ customerStats.total }}家</div>
  5. <div style="display: flex; gap: 8px;">
  6. <button class="btn btn-secondary"><i class="fas fa-download"></i> 导出</button>
  7. <!-- <button class="btn btn-primary" @click="showAddModal = true"><i class="fas fa-plus"></i> 添加客户</button> -->
  8. </div>
  9. </div>
  10. <!-- Stats -->
  11. <div class="stat-row">
  12. <div class="stat-card-lg"> <!-- @click="filterTab = 'all'" -->
  13. <div class="stat-icon-lg blue"><i class="fas fa-users"></i></div>
  14. <div class="stat-content-lg"><h3>{{ customerStats.total }}</h3><p>总客户数</p></div>
  15. </div>
  16. <div class="stat-card-lg"><!-- @click="filterTab = 'prospect'" -->
  17. <div class="stat-icon-lg purple"><i class="fas fa-user-clock"></i></div>
  18. <div class="stat-content-lg"><h3>{{ customerStats.prospect }}</h3><p>潜在客户</p></div>
  19. </div>
  20. <div class="stat-card-lg"><!-- @click="filterTab = 'intent'" -->
  21. <div class="stat-icon-lg orange"><i class="fas fa-star"></i></div>
  22. <div class="stat-content-lg"><h3>{{ customerStats.intent }}</h3><p>意向客户</p></div>
  23. </div>
  24. <div class="stat-card-lg"><!-- @click="filterTab = 'deal'" -->
  25. <div class="stat-icon-lg green"><i class="fas fa-handshake"></i></div>
  26. <div class="stat-content-lg"><h3>{{ customerStats.deal }}</h3><p>成交客户</p></div>
  27. </div>
  28. <div class="stat-card-lg"><!-- @click="filterTab = 'silent'" -->
  29. <div class="stat-icon-lg" style="background: rgba(148, 163, 184, 0.2); color: var(--text-secondary);"><i class="fas fa-moon"></i></div>
  30. <div class="stat-content-lg"><h3>{{ customerStats.silent }}</h3><p>沉默客户</p></div>
  31. </div>
  32. </div>
  33. <!-- Search & Filter -->
  34. <div style="display: flex; gap: 16px; margin-bottom: 20px;">
  35. <div class="search-box" style="flex: 1;">
  36. <i class="fas fa-search search-icon"></i>
  37. <input type="text" v-model="searchQuery" class="search-input" placeholder="搜索公司名称、联系人、邮箱..." @change="getCustomerData">
  38. </div>
  39. <div style="display: flex; gap: 8px;" class="form-select">
  40. <el-cascader placeholder="区域名称" :options="region"
  41. v-model="regionValue" clearable filterable @change="getRegion" />
  42. <!-- <select v-model="industryFilter" class="form-select" style="width: auto; padding: 8px 16px;">
  43. <option value="">全部行业</option>
  44. <option value="固态硬盘">固态硬盘</option>
  45. <option value="电子分销">电子分销</option>
  46. <option value="企业级存储">企业级存储</option>
  47. </select>
  48. <button class="btn btn-secondary"><i class="fas fa-sliders-h"></i> 高级筛选</button> -->
  49. </div>
  50. </div>
  51. <!-- Tabs -->
  52. <div style="display: flex; gap: 4px; margin-bottom: 20px; border-bottom: 1px solid var(--border); padding-bottom: 12px;">
  53. <div v-for="tab in tabs" :key="tab.key"
  54. :style="{
  55. padding: '8px 16px',
  56. borderRadius: '8px 8px 0 0',
  57. cursor: 'pointer',
  58. fontSize: '13px',
  59. background: filterTab === tab.key ? 'var(--bg-hover)' : 'transparent',
  60. color: filterTab === tab.key ? 'var(--text-primary)' : 'var(--text-secondary)',
  61. fontWeight: filterTab === tab.key ? '600' : '400'
  62. }"
  63. @click="filterTab = tab.key">
  64. {{ tab.label }} <span style="font-size: 11px; opacity: 0.7;">({{ tab.count }})</span>
  65. </div>
  66. </div>
  67. <!-- Customer List -->
  68. <!-- <div style="flex: 1; overflow-y: auto;"> -->
  69. <div v-loading="dataLoading" element-loading-background="rgba(0, 0, 0, 0.8)">
  70. <div v-for="customer in customers" :key="customer.id"
  71. class="customer-card"
  72. @click="showCustomerDetail(customer)">
  73. <div class="customer-flag">{{ customer.flag }}</div>
  74. <div class="customer-info">
  75. <div class="customer-header">
  76. <span class="customer-name">{{ customer.customerName }}</span>
  77. <!-- 客户类型标识 -->
  78. <!-- <span class="customer-status" :style="{ background: getStatusBg(customer.status), color: getStatusColor(customer.status) }">
  79. {{ getStatusLabel(customer.status) }}
  80. </span> -->
  81. </div>
  82. <div class="customer-contact">
  83. <i class="fas fa-user"></i> {{ customer.liaison?.name }}<span v-if="customer.liaison?.departmentPosition"> · </span>{{ customer.liaison?.departmentPosition }}
  84. </div>
  85. <div class="customer-meta">
  86. <span><i class="fas fa-map-marker-alt"></i> {{ customer.continent }} · {{ customer.country }}</span>
  87. <span><i class="fas fa-industry"></i> {{ customer.customerCompany?.industry }}</span>
  88. <!-- <span><i class="fas fa-dollar-sign"></i> {{ customer.annualPurchase }}</span> -->
  89. </div>
  90. </div>
  91. <!-- <div class="customer-score">
  92. <div class="score-circle" :class="getScoreClass(customer.score)">{{ customer.score }}</div>
  93. <div class="score-label">意向分</div>
  94. </div> -->
  95. <div class="customer-actions" @click.stop>
  96. <button class="btn btn-secondary btn-sm" @click="showCustomerDetail(customer)"><i class="fas fa-eye"></i>更多详情</button>
  97. </div>
  98. </div>
  99. </div>
  100. <div class="pagination-area">
  101. <el-pagination
  102. background
  103. :total="customerTotal"
  104. style="text-align: right;"
  105. :current-page="params.pageIndex"
  106. :page-size="params.pageSize"
  107. layout="total, prev, pager, next"
  108. @current-change="pageCurrentChange">
  109. </el-pagination>
  110. </div>
  111. <!-- </div> -->
  112. <!-- 引入添加客户弹窗组件 -->
  113. <!-- <AddCustomerModal
  114. :show-modal="showAddModal"
  115. @close="showAddModal = false"
  116. @add="handleAddCustomer"
  117. /> -->
  118. <!-- Customer Detail Modal -->
  119. <div v-if="showDetailModal && selectedCustomer" class="modal-overlay" @click.self="closeDetail()">
  120. <div class="modal customer-detail-modal">
  121. <div class="modal-header">
  122. <div class="modal-title">
  123. <span class="detail-flag">{{ selectedCustomer.flag }}</span>
  124. <div>
  125. <h3>{{ selectedCustomer.customerName }}</h3>
  126. <p>{{ selectedCustomer.industry }} · {{ selectedCustomer.continent }}·{{ selectedCustomer.country }}</p>
  127. </div>
  128. </div>
  129. <button class="modal-close" @click="showDetailModal = false"><i class="fas fa-times"></i></button>
  130. </div>
  131. <div class="modal-body">
  132. <!-- Quick Stats -->
  133. <div class="detail-stats">
  134. <div class="detail-stat-card">
  135. <div class="detail-stat-value" :class="getScoreClass(selectedCustomer?.customerAnalysisResult?.potentialScore ?? 0)">{{selectedCustomer?.customerAnalysisResult?.potentialScore ?? 0}}</div>
  136. <div class="detail-stat-label">意向分</div>
  137. </div>
  138. <div class="detail-stat-card">
  139. <div class="detail-stat-value">{{ selectedCustomer.totalOrderCount }}</div>
  140. <div class="detail-stat-label">累计订单</div>
  141. </div>
  142. <div class="detail-stat-card">
  143. <div class="detail-stat-value success">¥{{ formatNumber(selectedCustomer.totalOrderAmount) }}</div>
  144. <div class="detail-stat-label">累计金额</div>
  145. </div>
  146. <div class="detail-stat-card">
  147. <div class="detail-stat-value">{{selectedCustomer.lastContactDays}}天前</div>
  148. <div class="detail-stat-label">最后联系</div>
  149. </div>
  150. </div>
  151. <!-- Contact Info -->
  152. <div class="detail-section">
  153. <h4><i class="fas fa-address-card"></i> 联系信息</h4>
  154. <div class="contact-grid">
  155. <div class="contact-item">
  156. <i class="fas fa-user"></i>
  157. <div class="contact-content">
  158. <div class="contact-label">主要联系人</div>
  159. <div class="contact-value">{{ selectedCustomer.liaison?.name }}</div>
  160. <div class="contact-sub">{{ selectedCustomer.liaison?.departmentPosition }}</div>
  161. </div>
  162. </div>
  163. <div class="contact-item">
  164. <i class="fas fa-envelope"></i>
  165. <div class="contact-content">
  166. <div class="contact-label">邮箱</div>
  167. <div class="contact-value">{{ selectedCustomer.customerCompany?.email }}</div>
  168. </div>
  169. </div>
  170. <div class="contact-item">
  171. <i class="fas fa-phone"></i>
  172. <div class="contact-content">
  173. <div class="contact-label">电话</div>
  174. <div class="contact-value">{{ selectedCustomer.liaison?.telephone }}</div>
  175. </div>
  176. </div>
  177. <div class="contact-item">
  178. <i class="fas fa-globe"></i>
  179. <div class="contact-content">
  180. <div class="contact-label">官网</div>
  181. <div class="contact-value link">{{ selectedCustomer.customerCompany?.website }}</div>
  182. </div>
  183. </div>
  184. </div>
  185. </div>
  186. <!-- AI Insights -->
  187. <div class="detail-section" >
  188. <h4>
  189. <div><i class="fas fa-robot"></i> AI 洞察</div>
  190. <div><button class="btn btn-primary" @click="aiStreaming(selectedCustomer.id)" style="margin-left: 10px;padding: 4px 8px;border-radius: 4px;" :disabled="aiContentLoading"><i class="fas fa-sync" :class="{'icon-spin': aiContentLoading}" style="color: #fff;"></i> AI分析</button></div>
  191. </h4>
  192. <div class="ai-insight-cards" v-if="formattedText">
  193. <div class="ai-insight-card">
  194. <!-- <div class="insight-icon" :class="insight.level">
  195. <i :class="insight.icon"></i>
  196. </div> -->
  197. <div class="insight-content">
  198. <div class="insight-title">{{ formattedText }}</div>
  199. <!-- <div class="insight-desc">{{ insight.description }}</div> -->
  200. </div>
  201. </div>
  202. </div>
  203. </div>
  204. <!-- Interaction Timeline -->
  205. <div class="detail-section">
  206. <h4><i class="fas fa-history"></i> 互动记录</h4>
  207. <div class="timeline" v-if="selectedCustomer.interactionRecordList">
  208. <div class="timeline-item" v-for="record in selectedCustomer.interactionRecordList" :key="record.id">
  209. <div class="timeline-dot" :class="record.type"></div>
  210. <div class="timeline-content">
  211. <div class="timeline-header">
  212. <span class="timeline-action">{{ record.subject }}</span>
  213. <span class="timeline-time">{{ record.expectSendTime }}</span>
  214. </div>
  215. <div class="timeline-detail">{{ record.firstReplyTheme }}</div>
  216. </div>
  217. </div>
  218. </div>
  219. </div>
  220. <!-- Related Opportunities -->
  221. <!-- <div class="detail-section">
  222. <h4><i class="fas fa-briefcase"></i> 关联商机</h4>
  223. <div class="related-opps">
  224. <div class="related-opp" v-for="opp in selectedCustomer.opportunities" :key="opp.id">
  225. <div class="opp-info">
  226. <div class="opp-name">{{ opp.name }}</div>
  227. <div class="opp-meta">¥{{ formatNumber(opp.amount) }} · {{ opp.stage }}</div>
  228. </div>
  229. <div class="opp-status" :class="opp.statusClass">{{ opp.statusLabel }}</div>
  230. </div>
  231. </div>
  232. </div> -->
  233. </div>
  234. <div class="modal-footer">
  235. <button class="btn btn-secondary" @click="closeDetail()">关闭</button>
  236. <!-- <button class="btn btn-secondary"><i class="fas fa-edit"></i> 编辑</button> -->
  237. <button class="btn btn-primary" @click="goOtherProject(selectedCustomer)">
  238. <i class="fas fa-rocket"></i> 客户详情
  239. </button>
  240. </div>
  241. </div>
  242. </div>
  243. <!-- Toast -->
  244. <transition name="toast">
  245. <div v-if="showToast" class="toast" :class="toastType">
  246. <i :class="toastType === 'success' ? 'fas fa-check-circle' : 'fas fa-info-circle'"></i>
  247. {{ toastMessage }}
  248. </div>
  249. </transition>
  250. </div>
  251. </template>
  252. <script setup>
  253. import { ref, computed } from 'vue'
  254. import { getCustomerList,getCustomerDetail,streaming } from '../api/customer'
  255. import {region} from '@/utils/region';
  256. // 引入添加客户组件
  257. import AddCustomerModal from '../components/addCustomer.vue'
  258. const showAddModal = ref(false)
  259. const showDetailModal = ref(false)
  260. const selectedCustomer = ref(null)
  261. const filterTab = ref('all')
  262. const searchQuery = ref('')
  263. const regionFilter = ref('')
  264. const industryFilter = ref('')
  265. const showToast = ref(false)
  266. const toastMessage = ref('')
  267. const toastType = ref('success')
  268. const dataLoading = ref(false)
  269. const aiContentLoading = ref(false)
  270. const formattedText = ref('')
  271. const customerStats = ref({
  272. total: 0,
  273. prospect: 0,
  274. intent: 0,
  275. deal: 0,
  276. silent: 0
  277. })
  278. const tabs = ref([
  279. { key: 'all', label: '全部客户', count: 0 },
  280. // { key: 'prospect', label: '潜在客户', count: 812 },
  281. // { key: 'intent', label: '意向客户', count: 224 },
  282. // { key: 'deal', label: '成交客户', count: 156 },
  283. // { key: 'silent', label: '沉默客户', count: 55 }
  284. ])
  285. const customers = ref([])
  286. const customerTotal = ref()
  287. const params = ref({
  288. pageIndex: 1,
  289. pageSize: 10,
  290. blurry:null,
  291. country:null,
  292. })
  293. const getCustomer = (type) => {
  294. dataLoading.value= true
  295. params.value.blurry = searchQuery.value
  296. getCustomerList(params.value).then(res => {
  297. dataLoading.value= false
  298. if(type=='init'){ //页面初始记录客户总数量
  299. customerStats.value.total = res.result.total
  300. }
  301. customerTotal.value = tabs.value[0].count = res.result.total
  302. customers.value = res.result.records
  303. })
  304. }
  305. getCustomer('init')
  306. const pageCurrentChange = (page) => {
  307. params.value.pageIndex = page
  308. getCustomer()
  309. }
  310. const getCustomerData = ()=>{
  311. getCustomer()
  312. }
  313. const regionValue = ref([])
  314. const getRegion = (value) => {
  315. params.value.country = value?value[1]:''
  316. getCustomer()
  317. }
  318. const showCustomerDetail = (customer) => {
  319. getCustomerDetail({id:customer.id}).then(res => {
  320. if(res.success){
  321. selectedCustomer.value = res.result
  322. showDetailModal.value = true
  323. if(selectedCustomer.value.customerAnalysisResult){
  324. formattedText.value = selectedCustomer.value.customerAnalysisResult.reasonSummary.replace(/(?=【(沟通意见|注意事项)】)/g, '\n')
  325. }
  326. }else{
  327. ElMessage.warning(res.message)
  328. }
  329. })
  330. }
  331. const aiStreaming= (id) =>{
  332. let params = {
  333. appId: "app-rQrSHhudWbi5LVRkIX4tfVrg",
  334. inputs: {"analysis_batch_no":"1","customer_id":+id},
  335. query: "1",
  336. response_mode: "streaming",
  337. user: "1",
  338. files:[]
  339. }
  340. if(aiContentLoading.value) return
  341. aiContentLoading.value = true
  342. streaming(params).then(res => {
  343. aiContentLoading.value = false
  344. ElMessage.warning('AI分析时间大概为2-4分钟,请稍后查看')
  345. if(res.success){
  346. aiContentLoading.value = true
  347. }
  348. // else{
  349. // return ElMessage.error('请求超时,请稍后再试')
  350. // }
  351. })
  352. }
  353. const goOtherProject = (data) => {
  354. console.log(data)
  355. const baseUrl = 'https://sales.test.storlead.com/customer/followPanel' // 销售管理系统
  356. const params = {
  357. id: data.id,
  358. resourceType: 10,
  359. customerForm: 10,
  360. queryRangeType: 0,
  361. hasPublicCustomer: true,
  362. hasFollowMode: false,
  363. }
  364. // 拼接参数
  365. const queryStr = new URLSearchParams(params).toString()
  366. const jumpUrl = `${baseUrl}?${queryStr}`
  367. // 新窗口打开
  368. window.open(jumpUrl, '_blank')
  369. // 当前窗口打开:window.location.href = jumpUrl
  370. }
  371. const closeDetail = ()=>{
  372. showDetailModal.value = false
  373. aiContentLoading.value=false
  374. }
  375. // const customers = ref([
  376. // {
  377. // id: 1, flag: '🇸🇦', name: 'Riyadh Tech Distribution', status: 'prospect', contact: 'Ahmed Al-Rashid',
  378. // role: '采购总监', location: '沙特·利雅得', industry: '固态硬盘进口商', annualPurchase: '年采购$2M+', score: 92,
  379. // email: 'ahmed@riyahtech.sa', phone: '+966 50 123 4567', website: 'www.riyahtech.sa',
  380. // totalDeals: 3, totalAmount: 4560000, lastContact: '3天前',
  381. // aiInsights: [
  382. // { type: 'purchase', level: 'high', icon: 'fas fa-shopping-cart', title: '采购意愿强烈', description: '近30天内访问官网5次,下载产品手册,建议重点跟进' },
  383. // { type: 'timing', level: 'medium', icon: 'fas fa-clock', title: '最佳联系时间', description: '根据历史数据分析,周二上午10点联系成功率最高' },
  384. // { type: 'decision', level: 'high', icon: 'fas fa-user-tie', title: '决策人已识别', description: 'Ahmed Al-Rashid 为采购决策人,已验证邮箱有效性' }
  385. // ],
  386. // interactions: [
  387. // { id: 1, type: 'email', action: '发送报价单', detail: 'SSD采购报价已发送,等待回复', time: '3天前' },
  388. // { id: 2, type: 'visit', action: '官网访问', detail: '浏览了企业级SSD产品页面,停留8分钟', time: '5天前' },
  389. // { id: 3, type: 'call', action: '电话沟通', detail: '确认了采购需求,数量约5000片', time: '1周前' },
  390. // { id: 4, type: 'email', action: '首次联系', detail: '通过LinkedIn找到联系人,发送介绍邮件', time: '2周前' }
  391. // ],
  392. // opportunities: [
  393. // { id: 1, name: 'SSD批量采购', amount: 850000, stage: '谈判中', statusClass: 'negotiation', statusLabel: '跟进中' }
  394. // ]
  395. // },
  396. // {
  397. // id: 2, flag: '🇦🇪', name: 'Dubai Storage Solutions', status: 'prospect', contact: 'Mohammed Khan',
  398. // role: 'CEO', location: '阿联酋·迪拜', industry: '电子分销商', annualPurchase: '年采购$1.5M', score: 88,
  399. // email: 'mohammed@dubaistorage.ae', phone: '+971 50 987 6543', website: 'www.dubaistorage.ae',
  400. // totalDeals: 2, totalAmount: 2800000, lastContact: '1天前',
  401. // aiInsights: [
  402. // { type: 'purchase', level: 'medium', icon: 'fas fa-chart-line', title: '采购潜力', description: '公司规模扩张中,预计明年需求增长30%' }
  403. // ],
  404. // interactions: [
  405. // { id: 1, type: 'meeting', action: '视频会议', detail: '讨论Q2采购计划,对NVMe SSD感兴趣', time: '1天前' }
  406. // ],
  407. // opportunities: []
  408. // },
  409. // {
  410. // id: 3, flag: '🇰🇼', name: 'Kuwait Data Systems', status: 'intent', contact: 'Faisal Al-Mutairi',
  411. // role: 'IT总监', location: '科威特', industry: '数据中心集成商', annualPurchase: '年采购$2.5M', score: 85,
  412. // email: 'faisal@kwdata.kw', phone: '+965 99 888 777', website: 'www.kwdata.kw',
  413. // totalDeals: 5, totalAmount: 6800000, lastContact: '今天',
  414. // aiInsights: [
  415. // { type: 'timing', level: 'high', icon: 'fas fa-fire', title: '紧急需求', description: '数据中心扩容项目急需采购,建议立即跟进' }
  416. // ],
  417. // interactions: [
  418. // { id: 1, type: 'email', action: '回复询价', detail: '确认了技术规格,要求提供样品', time: '今天' }
  419. // ],
  420. // opportunities: [
  421. // { id: 1, name: '数据中心扩容项目', amount: 1200000, stage: '方案报价', statusClass: 'proposal', statusLabel: '待报价' }
  422. // ]
  423. // },
  424. // {
  425. // id: 4, flag: '🇩🇪', name: 'Berlin Tech GmbH', status: 'deal', contact: 'Hans Mueller',
  426. // role: '采购经理', location: '德国·柏林', industry: '企业级存储', annualPurchase: '年采购$3M+', score: 95,
  427. // email: 'hans@berlintech.de', phone: '+49 30 12345678', website: 'www.berlintech.de',
  428. // totalDeals: 8, totalAmount: 12500000, lastContact: '2天前',
  429. // aiInsights: [],
  430. // interactions: [
  431. // { id: 1, type: 'order', action: '订单确认', detail: 'PO-2026-0315 已确认,金额¥680,000', time: '2天前' }
  432. // ],
  433. // opportunities: [
  434. // { id: 1, name: 'NVMe SSD批量采购', amount: 680000, stage: '已成交', statusClass: 'won', statusLabel: '已成交' }
  435. // ]
  436. // }
  437. // ])
  438. // const filteredCustomers = computed(() => {
  439. // let result = customers.value
  440. // if (filterTab.value !== 'all') {
  441. // result = result.filter(c => c.status === filterTab.value)
  442. // }
  443. // if (searchQuery.value) {
  444. // const query = searchQuery.value.toLowerCase()
  445. // result = result.filter(c =>
  446. // c.name.toLowerCase().includes(query) ||
  447. // c.contact.toLowerCase().includes(query) ||
  448. // c.email?.toLowerCase().includes(query)
  449. // )
  450. // }
  451. // return result
  452. // })
  453. const formatNumber = (num) => num.toLocaleString()
  454. const getScoreClass = (score) => score >= 85 ? 'high' : score >= 70 ? 'medium' : 'low'
  455. const getStatusBg = (status) => ({
  456. prospect: 'rgba(139, 92, 246, 0.2)',
  457. intent: 'rgba(245, 158, 11, 0.2)',
  458. deal: 'rgba(16, 185, 129, 0.2)',
  459. silent: 'rgba(148, 163, 184, 0.2)'
  460. }[status] || 'rgba(148, 163, 184, 0.2)')
  461. const getStatusColor = (status) => ({
  462. prospect: 'var(--secondary)',
  463. intent: 'var(--warning)',
  464. deal: 'var(--success)',
  465. silent: 'var(--text-secondary)'
  466. }[status] || 'var(--text-secondary)')
  467. const getStatusLabel = (status) => ({
  468. prospect: '潜客',
  469. intent: '意向',
  470. deal: '成交',
  471. silent: '沉默'
  472. }[status] || status)
  473. const sendEmail = (customer) => {
  474. showToastMessage(`正在发送邮件给 ${customer.contact}...`, 'success')
  475. }
  476. const createOpportunity = (customer) => {
  477. showDetailModal.value = false
  478. showToastMessage(`正在为 ${customer.name} 创建商机...`, 'success')
  479. }
  480. // 辅助函数:根据国家代码获取国旗
  481. const getFlagByCountry = (countryCode) => {
  482. const flagMap = {
  483. 'SA': '🇸🇦',
  484. 'AE': '🇦🇪',
  485. 'DE': '🇩🇪',
  486. 'FR': '🇫🇷',
  487. '': '🌐'
  488. }
  489. return flagMap[countryCode] || '🌐'
  490. }
  491. // 辅助函数:根据国家代码获取地区
  492. const getLocationByCountry = (countryCode) => {
  493. const locationMap = {
  494. 'SA': '沙特阿拉伯',
  495. 'AE': '阿联酋',
  496. 'DE': '德国',
  497. 'FR': '法国',
  498. '': '未填写'
  499. }
  500. return locationMap[countryCode] || '未填写'
  501. }
  502. const showToastMessage = (message, type = 'success') => {
  503. toastMessage.value = message
  504. toastType.value = type
  505. showToast.value = true
  506. setTimeout(() => showToast.value = false, 3000)
  507. }
  508. // ---------------- 暂时无用 ---------------------
  509. // 处理添加客户逻辑
  510. const handleAddCustomer = (customerData) => {
  511. // 模拟生成客户ID(实际项目中由后端生成)
  512. const newId = customers.value.length + 1
  513. // 构建完整的客户数据(补充默认值)
  514. const newCustomer = {
  515. id: newId,
  516. flag: getFlagByCountry(customerData.country),
  517. name: customerData.name,
  518. status: customerData.type,
  519. contact: customerData.contact,
  520. role: '未填写',
  521. location: getLocationByCountry(customerData.country),
  522. industry: '未填写',
  523. annualPurchase: '未填写',
  524. score: 80,
  525. email: customerData.email || '未填写',
  526. phone: customerData.phone || '未填写',
  527. website: '未填写',
  528. totalDeals: 0,
  529. totalAmount: 0,
  530. lastContact: '刚刚',
  531. aiInsights: [],
  532. interactions: [],
  533. opportunities: []
  534. }
  535. // 添加到客户列表
  536. customers.value.push(newCustomer)
  537. // 更新统计数据
  538. customerStats.total += 1
  539. if (customerData.type === 'prospect') customerStats.prospect += 1
  540. else if (customerData.type === 'intent') customerStats.intent += 1
  541. else if (customerData.type === 'deal') customerStats.deal += 1
  542. // 更新tabs计数
  543. tabs.forEach(tab => {
  544. if (tab.key === 'all') tab.count += 1
  545. else if (tab.key === customerData.type) tab.count += 1
  546. })
  547. // 显示提示
  548. showToastMessage('客户添加成功', 'success')
  549. }
  550. </script>
  551. <style scoped>
  552. .data-page { flex: 1; display: flex; flex-direction: column; padding: 24px; overflow-y: auto; background: var(--bg-dark); }
  553. .data-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
  554. .data-title { font-size: 20px; font-weight: 600; }
  555. .stat-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin-bottom: 24px; }
  556. .stat-card-lg { display: flex; align-items: center; gap: 14px; padding: 18px; background: var(--bg-card); border-radius: 14px; border: 1px solid var(--border); cursor: auto; transition: all 0.2s; }
  557. /* .stat-card-lg:hover { border-color: var(--primary); } */
  558. .stat-icon-lg { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 20px; background: rgba(99, 102, 241, 0.2); color: var(--primary); }
  559. .stat-icon-lg.green { background: rgba(16, 185, 129, 0.2); color: var(--success); }
  560. .stat-icon-lg.purple { background: rgba(139, 92, 246, 0.2); color: var(--secondary); }
  561. .stat-icon-lg.orange { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
  562. .stat-content-lg h3 { font-size: 24px; font-weight: 700; }
  563. .stat-content-lg p { font-size: 12px; color: var(--text-secondary); }
  564. .search-box { position: relative; }
  565. .search-icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--text-muted); }
  566. .search-input { width: 100%; padding: 10px 14px 10px 40px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; color: var(--text-primary); font-size: 13px; }
  567. .search-input:focus { outline: none; border-color: var(--primary); }
  568. .form-select { background: var(--bg-card); border: none; color: var(--text-primary);width: auto;padding: 0; }
  569. .form-select :deep(.el-input__wrapper){ background: var(--bg-card);box-shadow:none;border: 1px solid var(--border);border-radius: 8px;padding: 3px 10px;}
  570. .customer-card { display: flex; align-items: center; gap: 16px; padding: 16px; background: var(--bg-card); border-radius: 12px; margin-bottom: 12px; border: 1px solid var(--border); cursor: pointer; transition: all 0.2s; }
  571. .customer-card:hover { border-color: var(--primary); }
  572. .customer-flag { font-size: 32px; }
  573. .customer-info { flex: 1; }
  574. .customer-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
  575. .customer-name { font-size: 15px; font-weight: 600; }
  576. .customer-status { font-size: 10px; padding: 2px 8px; border-radius: 10px; }
  577. .customer-contact { font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; }
  578. .customer-meta { display: flex; gap: 16px; font-size: 11px; color: var(--text-muted); }
  579. .customer-score { text-align: center; margin-right: 20px; }
  580. .score-circle { width: 50px; height: 50px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 700; }
  581. .score-circle.high { background: rgba(16, 185, 129, 0.2); color: var(--success); }
  582. .score-circle.medium { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
  583. .score-circle.low { background: rgba(148, 163, 184, 0.2); color: var(--text-secondary); }
  584. .score-label { font-size: 10px; color: var(--text-muted); margin-top: 4px; }
  585. .customer-actions { display: flex; gap: 8px; }
  586. .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 20px; }
  587. .modal { background: var(--bg-card); border-radius: 16px; width: 100%; max-width: 600px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; }
  588. .customer-detail-modal { max-width: 800px; }
  589. .modal-header { display: flex; justify-content: space-between; align-items: center; padding: 20px; border-bottom: 1px solid var(--border); }
  590. .modal-title { display: flex; align-items: center; gap: 12px; font-size: 16px; font-weight: 600; }
  591. .modal-title h3 { font-size: 16px; font-weight: 600; margin: 0;word-break: break-all;}
  592. .modal-title p { font-size: 12px; color: var(--text-secondary); margin: 4px 0 0 0; }
  593. .detail-flag { font-size: 32px; }
  594. .modal-close { width: 32px; height: 32px; background: var(--bg-hover); border: none; border-radius: 8px; color: var(--text-secondary); cursor: pointer; display: flex; align-items: center; justify-content: center; }
  595. .modal-close:hover { background: var(--border); color: var(--text-primary); }
  596. .modal-body { flex: 1; overflow-y: auto; padding: 20px; }
  597. .modal-footer { display: flex; justify-content: flex-end; gap: 12px; padding: 20px; border-top: 1px solid var(--border); }
  598. .detail-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 24px; }
  599. .detail-stat-card { background: var(--bg-hover); border-radius: 12px; padding: 16px; text-align: center; }
  600. .detail-stat-value { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
  601. .detail-stat-value.high { color: var(--success); }
  602. .detail-stat-value.medium { color: var(--warning); }
  603. .detail-stat-value.success { color: var(--success); }
  604. .detail-stat-label { font-size: 12px; color: var(--text-secondary); }
  605. .detail-section { margin-bottom: 24px; }
  606. .detail-section h4 { font-size: 14px; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
  607. .detail-section h4 i { color: var(--primary); }
  608. .contact-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
  609. .contact-item { display: flex; gap: 12px; padding: 12px; background: var(--bg-hover); border-radius: 10px; }
  610. .contact-item i { font-size: 16px; color: var(--primary); width: 20px; }
  611. .contact-label { font-size: 11px; color: var(--text-muted); margin-bottom: 2px; }
  612. .contact-value { font-size: 13px; font-weight: 500; }
  613. .contact-value.link { color: var(--primary); cursor: pointer; }
  614. .contact-sub { font-size: 11px; color: var(--text-secondary); }
  615. .ai-insight-cards { display: flex; flex-direction: column; gap: 10px; }
  616. .ai-insight-card { display: flex; gap: 12px; padding: 14px; background: var(--bg-hover); border-radius: 12px; border-left: 3px solid var(--primary); }
  617. .insight-icon { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 14px; }
  618. .insight-icon.high { background: rgba(16, 185, 129, 0.2); color: var(--success); }
  619. .insight-icon.medium { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
  620. .insight-title { font-size: 13px; margin-bottom: 4px;white-space: pre-line; }
  621. .insight-desc { font-size: 11px; color: var(--text-secondary); line-height: 1.5; }
  622. .timeline { display: flex; flex-direction: column; gap: 12px; position: relative; padding-left: 24px; }
  623. .timeline::before { content: ''; position: absolute; left: 6px; top: 0; bottom: 0; width: 2px; background: var(--border); }
  624. .timeline-item { position: relative; }
  625. .timeline-dot { position: absolute; left: -20px; top: 4px; width: 12px; height: 12px; border-radius: 50%; background: var(--primary); border: 2px solid var(--bg-card); }
  626. .timeline-dot.email { background: var(--primary); }
  627. .timeline-dot.call { background: var(--success); }
  628. .timeline-dot.meeting { background: var(--warning); }
  629. .timeline-dot.visit { background: var(--secondary); }
  630. .timeline-dot.order { background: var(--success); }
  631. .timeline-content { padding: 12px; background: var(--bg-hover); border-radius: 10px; }
  632. .timeline-header { display: flex; justify-content: space-between; margin-bottom: 4px; }
  633. .timeline-action { font-size: 13px; font-weight: 500; }
  634. .timeline-time { font-size: 11px; color: var(--text-muted); }
  635. .timeline-detail { font-size: 12px; color: var(--text-secondary); }
  636. .related-opps { display: flex; flex-direction: column; gap: 10px; }
  637. .related-opp { display: flex; justify-content: space-between; align-items: center; padding: 12px; background: var(--bg-hover); border-radius: 10px; }
  638. .opp-name { font-size: 13px; font-weight: 500; margin-bottom: 2px; }
  639. .opp-meta { font-size: 11px; color: var(--text-secondary); }
  640. .opp-status { font-size: 10px; padding: 3px 8px; border-radius: 10px; }
  641. .opp-status.negotiation { background: rgba(99, 102, 241, 0.2); color: var(--primary); }
  642. .opp-status.proposal { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
  643. .opp-status.won { background: rgba(16, 185, 129, 0.2); color: var(--success); }
  644. .toast { position: fixed; bottom: 24px; right: 24px; padding: 12px 20px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; display: flex; align-items: center; gap: 10px; font-size: 13px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); z-index: 2000; }
  645. .toast.success { border-color: var(--success); }
  646. .toast.success i { color: var(--success); }
  647. .toast-enter-active, .toast-leave-active { transition: all 0.3s ease; }
  648. .toast-enter-from, .toast-leave-to { opacity: 0; transform: translateY(20px); }
  649. .icon-spin {
  650. animation: spin 1s linear infinite !important;
  651. }
  652. @keyframes spin {
  653. 0% { transform: rotate(0deg); }
  654. 100% { transform: rotate(360deg); }
  655. }
  656. </style>