Merge branch 'dev_NEW_pro' into dev_浪潮
# Conflicts:
# src/views/customerService/feedbackRegistration/components/formDia.vue
| | |
| | | "logo": "logo/SDJCLogo.png", |
| | | "favicon": "favicon/SDJCfavicon.ico" |
| | | }, |
| | | "KS": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "é»å康森åç ¼æéå
¬å¸", |
| | | "VITE_BASE_API": "http://36.138.236.176:9000", |
| | | "VITE_JAVA_API": "http://36.138.236.176:9001" |
| | | }, |
| | | "logo": "logo/KSLogo.png", |
| | | "favicon": "favicon/KSfavicon.ico" |
| | | }, |
| | | "DZZB": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "山西丹æ±è£
å¤å¶é è¡ä»½æéå
¬å¸", |
| | | "VITE_BASE_API": "http://36.138.236.176:9000", |
| | | "VITE_JAVA_API": "http://36.138.236.176:9001" |
| | | }, |
| | | "logo": "logo/DZZBLogo.png", |
| | | "favicon": "favicon/DZZBfavicon.ico" |
| | | }, |
| | | "logo": "/src/assets/logo/logo.png", |
| | | "favicon": "/public/favicon.ico" |
| | | } |
| | | } |
| | |
| | | }, |
| | | "overrides": { |
| | | "quill": "2.0.2" |
| | | } |
| | | }, |
| | | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // æ¥è¯¢æ»å¸ç§ç®å表 |
| | | export function listAccountSubject(query) { |
| | | return request({ |
| | | url: "/accountSubject/list", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢æ»å¸ç§ç® |
| | | export function addAccountSubject(data) { |
| | | return request({ |
| | | url: "/accountSubject/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // ä¿®æ¹æ»å¸ç§ç® |
| | | export function updateAccountSubject(data) { |
| | | return request({ |
| | | url: "/accountSubject/edit", |
| | | method: "put", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // å 餿»å¸ç§ç® |
| | | export function delAccountSubject(ids) { |
| | | return request({ |
| | | url: "/accountSubject/remove/" + ids, |
| | | method: "delete", |
| | | }); |
| | | } |
| | | |
| | | // å¯¼åºæ»å¸ç§ç® |
| | | export function exportAccountSubject(data) { |
| | | return request({ |
| | | url: "/accountSubject/export", |
| | | method: "post", |
| | | data: data, |
| | | responseType: "blob", |
| | | }); |
| | | } |
| | |
| | | method: "get", |
| | | }); |
| | | } |
| | | // ä¿®æ¹åè´§å°è´¦ |
| | | export function getDeliveryDetailByShippingNo(query) { |
| | | return request({ |
| | | url: "/shippingInfo/getDateilByShippingNo", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | export function addOrUpdateDeliveryLedger(query) { |
| | | return request({ |
| | |
| | | data, |
| | | }); |
| | | } |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { Cpu } from '@element-plus/icons-vue' |
| | | |
| | | export const generalAssistant = { |
| | | key: 'general', |
| | | label: 'å¾
åå©ç', |
| | | title: 'å¾
åæºè½å©ç', |
| | | tooltip: 'å¾
å婿', |
| | | icon: Cpu, |
| | | apiBase: '/xiaozhi', |
| | | storageKey: 'ai_chat_uuid', |
| | | placeholder: '请è¾å
¥æ¨çé®é¢... (Enter åé, Shift+Enter æ¢è¡)', |
| | | welcomeMessage: 'ä½ å¥½', |
| | | description: 'æå¯ä»¥åçä½ çé®é¢ï¼ä¸ºä½ æä¾ä¸å¡æ°æ®è§£è¯»ä¿¡æ¯ãå¤ç建议åè¾
å©å³çæ¯æã', |
| | | allowFileUpload: true, |
| | | emptySessionText: 'ææ åå²ä¼è¯', |
| | | quickPrompts: [ |
| | | 'æå½åæåªäºå®¡æ¹å¾
åéè¦å¤çï¼', |
| | | '帮æååºä»å¤©æ°å¢ç审æ¹å¾
åã', |
| | | 'å½åå¾
æå®¡æ¹çåæ®ï¼ææ¶é´ååºååºæ¥ã', |
| | | 'æåèµ·ç审æ¹éï¼åªäºè¿å¨å¤çä¸ï¼', |
| | | 'æ¥è¯¢æµç¨ç¼å· XXX ç审æ¹è¯¦æ
ã', |
| | | 'æµç¨ç¼å· XXX ç°å¨å¡å¨åªä¸ªå®¡æ¹èç¹ï¼å½å审æ¹äººæ¯è°ï¼', |
| | | 'å¸®ææ¥çæµç¨ç¼å· XXX çå®¡æ¹æµè½¬è®°å½ã', |
| | | 'è¿7天æç审æ¹å¾
åç»è®¡æ
嵿乿 ·ï¼', |
| | | 'æ¬ææç审æ¹ä¸ï¼éè¿ã驳åãå¤çä¸åæå¤å°ï¼', |
| | | 'è¿30天åç±»åå®¡æ¹æ°éå叿¯ä»ä¹ï¼', |
| | | '帮æå®¡æ¹éè¿æµç¨ç¼å· XXXï¼å¤æ³¨âåæâã', |
| | | '帮æé©³åæµç¨ç¼å· XXXï¼å¤æ³¨â请补å
说æâã', |
| | | 'æ¤éæåå对æµç¨ç¼å· XXX çå®¡æ¹æä½ã', |
| | | '帮æä¿®æ¹æµç¨ç¼å· XXX ç夿³¨ä¸ºâ已补å
éä»¶âã', |
| | | 'å 餿åèµ·çæµç¨ç¼å· XXXã' |
| | | ] |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { generalAssistant } from './generalAssistant' |
| | | import { purchaseAssistant } from './purchaseAssistant' |
| | | |
| | | export { generalAssistant, purchaseAssistant } |
| | | |
| | | export const builtInAssistants = [generalAssistant, purchaseAssistant] |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { ShoppingCart } from '@element-plus/icons-vue' |
| | | |
| | | export const purchaseAssistant = { |
| | | key: 'purchase', |
| | | label: 'éè´å©ç', |
| | | title: 'éè´æºè½å©ç', |
| | | tooltip: 'éè´æºè½å©ç', |
| | | icon: ShoppingCart, |
| | | apiBase: '/purchase-ai', |
| | | storageKey: 'purchase_ai_chat_uuid', |
| | | placeholder: '请è¾å
¥éè´é®é¢... (Enter åé, Shift+Enter æ¢è¡)', |
| | | welcomeMessage: 'ä½ å¥½', |
| | | description: 'æå¯ä»¥åå©ä½ åæéè´è®¢åãå°è´§è¿åº¦ãä¾åºå表ç°å仿¬¾æ
åµï¼å¸®å©ä½ å¿«éå®ä½éè´å¼å¸¸ã', |
| | | allowFileUpload: true, |
| | | allowMultipleFileUpload: true, |
| | | fileAnalyzeUrl: '/purchase-ai/analyze-files', |
| | | emptySessionText: 'ææ éè´ä¼è¯', |
| | | quickPrompts: [ |
| | | 'æ¬æéè´é颿åååçç©ææåªäºï¼', |
| | | 'åªäºéè´è®¢åè¿æªå
¥åºï¼', |
| | | 'æè¿7天ä¾åºåå°è´§å¼å¸¸æåªäºï¼', |
| | | '帮æç»è®¡å¾
仿¬¾éè´å', |
| | | 'ååºæ¬æéè´éè´§æ
åµ' |
| | | ] |
| | | } |
| | |
| | | <template> |
| | | <div class="ai-chat-sidebar-wrapper"> |
| | | <!-- æ¬æµ®å¾æ --> |
| | | <div class="ai-chat-trigger" @click="toggleSidebar" v-show="!visible"> |
| | | <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-drawer |
| | | v-model="visible" |
| | | :size="drawerSize" |
| | | direction="rtl" |
| | | :size="computedDrawerSize" |
| | | :direction="drawerDirection" |
| | | :with-header="true" |
| | | class="ai-chat-drawer" |
| | | :modal="false" |
| | | modal-class="ai-chat-overlay" |
| | | :show-close="false" |
| | | :append-to-body="false" |
| | | :close-on-press-escape="!hideTrigger" |
| | | :close-on-click-modal="!hideTrigger" |
| | | @close="handleClose" |
| | | > |
| | | <template #header> |
| | |
| | | <el-icon :size="18"><Plus /></el-icon> |
| | | </el-button> |
| | | </el-tooltip> |
| | | <div class="action-divider"></div> |
| | | <el-tooltip content="å
³é" placement="bottom"> |
| | | <el-button |
| | | v-if="headerExtraActionText" |
| | | link |
| | | class="header-action-btn header-action-btn--text" |
| | | @click="handleHeaderExtraAction" |
| | | > |
| | | {{ headerExtraActionText }} |
| | | </el-button> |
| | | <div v-if="!hideTrigger" class="action-divider"></div> |
| | | <el-tooltip v-if="!hideTrigger" content="å
³é" placement="bottom"> |
| | | <el-button link class="header-action-btn close-btn" @click="handleManualClose"> |
| | | <el-icon :size="18"><Close /></el-icon> |
| | | </el-button> |
| | |
| | | <div class="assistant-scan-ring"></div> |
| | | <div class="assistant-orbit assistant-orbit-a"></div> |
| | | <div class="assistant-orbit assistant-orbit-b"></div> |
| | | <div class="assistant-bot"> |
| | | <div class="assistant-bot-antenna assistant-bot-antenna-left"></div> |
| | | <div class="assistant-bot-antenna assistant-bot-antenna-right"></div> |
| | | <div class="assistant-bot-head"> |
| | | <div class="assistant-bot-head-glow"></div> |
| | | <div class="assistant-bot-eye assistant-bot-eye-left"></div> |
| | | <div class="assistant-bot-eye assistant-bot-eye-right"></div> |
| | | <div class="assistant-bot-mouth"></div> |
| | | </div> |
| | | <div class="assistant-bot-neck"></div> |
| | | <div class="assistant-bot-body"> |
| | | <div class="assistant-bot-core"> |
| | | <div class="assistant-bot-core-ring"></div> |
| | | <el-icon :size="22"><component :is="currentAssistant.icon" /></el-icon> |
| | | <div class="assistant-model-shell"> |
| | | <div class="assistant-model-cut"> |
| | | <img |
| | | v-if="currentAssistantAvatar" |
| | | class="assistant-model-img" |
| | | :src="currentAssistantAvatar" |
| | | :alt="currentAssistant.label" |
| | | /> |
| | | <div v-else class="assistant-model-fallback"> |
| | | <el-icon :size="30"><component :is="currentAssistant.icon" /></el-icon> |
| | | </div> |
| | | <div class="assistant-bot-arm assistant-bot-arm-left"></div> |
| | | <div class="assistant-bot-arm assistant-bot-arm-right"></div> |
| | | </div> |
| | | </div> |
| | | <div class="assistant-status"> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-show="!hasMessages" class="hero-dot-grid" aria-hidden="true"> |
| | | <span v-for="dot in 28" :key="dot"></span> |
| | | </div> |
| | | |
| | | <div class="message-list" ref="messageListRef"> |
| | | <div |
| | | v-for="(message, index) in messages" |
| | |
| | | <div class="message-content"> |
| | | <!-- ææ¬å
容 --> |
| | | <div class="text-box" v-html="message.htmlContent"></div> |
| | | |
| | | <div v-if="message.localUploadFiles?.length" class="message-local-file-list"> |
| | | <div |
| | | v-for="(file, fileIndex) in message.localUploadFiles" |
| | | :key="`${file.previewId || file.name}-${fileIndex}`" |
| | | :class="['message-local-file-item', { clickable: !!file.accessUrl && !file.isImage }]" |
| | | @click="handleMessageFileClick(file)" |
| | | > |
| | | <el-image |
| | | v-if="file.isImage && file.previewUrl" |
| | | :src="file.previewUrl" |
| | | :preview-src-list="getImagePreviewList(message.localUploadFiles)" |
| | | :initial-index="getImagePreviewInitialIndex(message.localUploadFiles, file.previewUrl)" |
| | | :z-index="4000" |
| | | preview-teleported |
| | | fit="cover" |
| | | class="message-local-file-thumb" |
| | | /> |
| | | <el-icon v-else class="message-local-file-icon"><Document /></el-icon> |
| | | <div class="message-local-file-meta"> |
| | | <span |
| | | :class="['message-local-file-name', { clickable: !!file.accessUrl }]" |
| | | :title="file.name" |
| | | @click.stop="openMessageAttachment(file)" |
| | | > |
| | | {{ file.name }} |
| | | </span> |
| | | <small v-if="Number(file.size) > 0" class="message-local-file-size">{{ formatFileSize(file.size) }}</small> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- å¾è¡¨å
容 --> |
| | | <div v-if="message.chartOptions && message.chartRenderReady" class="charts-wrapper"> |
| | |
| | | </div> |
| | | <div class="input-box"> |
| | | <div v-if="selectedFiles.length" class="selected-file-list"> |
| | | <div v-for="(file, fileIndex) in selectedFiles" :key="`${file.name}-${fileIndex}`" class="selected-file-tag"> |
| | | <el-icon><Document /></el-icon> |
| | | <span class="file-name">{{ file.name }}</span> |
| | | <div v-for="(file, fileIndex) in selectedFileSnapshots" :key="`${file.previewId || file.name}-${fileIndex}`" class="selected-file-tag"> |
| | | <el-image |
| | | v-if="file.isImage && file.previewUrl" |
| | | :src="file.previewUrl" |
| | | :preview-src-list="getImagePreviewList(selectedFileSnapshots)" |
| | | :initial-index="getImagePreviewInitialIndex(selectedFileSnapshots, file.previewUrl)" |
| | | :z-index="4000" |
| | | preview-teleported |
| | | fit="cover" |
| | | class="selected-file-thumb" |
| | | /> |
| | | <el-icon v-else><Document /></el-icon> |
| | | <div class="selected-file-meta"> |
| | | <span class="file-name">{{ file.name }}</span> |
| | | <small class="file-size">{{ formatFileSize(file.size) }}</small> |
| | | </div> |
| | | <el-icon class="remove-file" @click="removeSelectedFile(fileIndex)"><Close /></el-icon> |
| | | </div> |
| | | </div> |
| | |
| | | import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue' |
| | | import request from '@/utils/request' |
| | | import * as echarts from 'echarts' |
| | | import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, ShoppingCart, Promotion, RefreshRight } from '@element-plus/icons-vue' |
| | | import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, Promotion, RefreshRight } from '@element-plus/icons-vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { builtInAssistants, generalAssistant } from './assistants' |
| | | import todoAssistantAvatar from '@/assets/AI/å¾
å婿.png' |
| | | import salesAssistantAvatar from '@/assets/AI/éå®å©æ.png' |
| | | import purchaseAssistantAvatar from '@/assets/AI/éè´å©æ.png' |
| | | import productionAssistantAvatar from '@/assets/AI/çäº§å©æ.png' |
| | | import financeAssistantAvatar from '@/assets/AI/è´¢å¡å©æ.png' |
| | | import bossAssistantAvatar from '@/assets/AI/èæ¿å©æ.png' |
| | | |
| | | const emit = defineEmits(['header-extra-action']) |
| | | |
| | | const props = defineProps({ |
| | | assistants: { |
| | |
| | | defaultAssistant: { |
| | | type: String, |
| | | default: '' |
| | | }, |
| | | hideTrigger: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | autoOpen: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | drawerSize: { |
| | | type: [String, Number], |
| | | default: '' |
| | | }, |
| | | drawerDirection: { |
| | | type: String, |
| | | default: 'rtl' |
| | | }, |
| | | headerExtraActionText: { |
| | | type: String, |
| | | default: '' |
| | | } |
| | | }) |
| | | |
| | | const builtInAssistants = [ |
| | | { |
| | | key: 'general', |
| | | label: 'å¾
åå©ç', |
| | | title: 'å¾
åæºè½å©ç', |
| | | tooltip: 'å¾
å婿', |
| | | icon: Cpu, |
| | | apiBase: '/xiaozhi', |
| | | storageKey: 'ai_chat_uuid', |
| | | placeholder: '请è¾å
¥æ¨çé®é¢... (Enter åé, Shift+Enter æ¢è¡)', |
| | | welcomeMessage: 'ä½ å¥½', |
| | | description: 'æå¯ä»¥åçä½ çé®é¢ï¼ä¸ºä½ æä¾ä¸å¡æ°æ®è§£è¯»ä¿¡æ¯ãå¤ç建议åè¾
å©å³çæ¯æã', |
| | | allowFileUpload: true, |
| | | emptySessionText: 'ææ åå²ä¼è¯' |
| | | }, |
| | | { |
| | | key: 'purchase', |
| | | label: 'éè´å©ç', |
| | | title: 'éè´æºè½å©ç', |
| | | tooltip: 'éè´æºè½å©ç', |
| | | icon: ShoppingCart, |
| | | apiBase: '/purchase-ai', |
| | | storageKey: 'purchase_ai_chat_uuid', |
| | | placeholder: '请è¾å
¥éè´é®é¢... (Enter åé, Shift+Enter æ¢è¡)', |
| | | welcomeMessage: 'ä½ å¥½', |
| | | description: 'æå¯ä»¥åå©ä½ åæéè´è®¢åãå°è´§è¿åº¦ãä¾åºå表ç°å仿¬¾æ
åµï¼å¸®å©ä½ å¿«éå®ä½éè´å¼å¸¸ã', |
| | | allowFileUpload: true, |
| | | allowMultipleFileUpload: true, |
| | | fileAnalyzeUrl: '/purchase-ai/analyze-files', |
| | | emptySessionText: 'ææ éè´ä¼è¯' |
| | | } |
| | | ] |
| | | |
| | | const hideTrigger = computed(() => props.hideTrigger) |
| | | const headerExtraActionText = computed(() => String(props.headerExtraActionText || '').trim()) |
| | | const drawerDirection = computed(() => (props.drawerDirection === 'ttb' || props.drawerDirection === 'btt' || props.drawerDirection === 'ltr' || props.drawerDirection === 'rtl') |
| | | ? props.drawerDirection |
| | | : 'rtl') |
| | | const assistants = computed(() => props.assistants?.length ? props.assistants : builtInAssistants) |
| | | const selectedAssistantKey = ref(props.defaultAssistant || assistants.value[0]?.key || 'general') |
| | | const currentAssistant = computed(() => assistants.value.find(item => item.key === selectedAssistantKey.value) || assistants.value[0] || builtInAssistants[0]) |
| | | const showAssistantSwitch = computed(() => assistants.value.length > 1) |
| | | const assistantQuickPromptMap = { |
| | | general: [ |
| | | 'æå½åæåªäºå®¡æ¹å¾
åéè¦å¤çï¼', |
| | | '帮æååºä»å¤©æ°å¢ç审æ¹å¾
åã', |
| | | 'å½åå¾
æå®¡æ¹çåæ®ï¼ææ¶é´ååºååºæ¥ã', |
| | | 'æåèµ·ç审æ¹éï¼åªäºè¿å¨å¤çä¸ï¼', |
| | | 'æ¥è¯¢æµç¨ç¼å· XXX ç审æ¹è¯¦æ
ã', |
| | | 'æµç¨ç¼å· XXX ç°å¨å¡å¨åªä¸ªå®¡æ¹èç¹ï¼å½å审æ¹äººæ¯è°ï¼', |
| | | 'å¸®ææ¥çæµç¨ç¼å· XXX çå®¡æ¹æµè½¬è®°å½ã', |
| | | 'è¿7天æç审æ¹å¾
åç»è®¡æ
嵿乿 ·ï¼', |
| | | 'æ¬ææç审æ¹ä¸ï¼éè¿ã驳åãå¤çä¸åæå¤å°ï¼', |
| | | 'è¿30天åç±»åå®¡æ¹æ°éå叿¯ä»ä¹ï¼', |
| | | '帮æå®¡æ¹éè¿æµç¨ç¼å· XXXï¼å¤æ³¨âåæâã', |
| | | '帮æé©³åæµç¨ç¼å· XXXï¼å¤æ³¨â请补å
说æâã', |
| | | 'æ¤éæåå对æµç¨ç¼å· XXX çå®¡æ¹æä½ã', |
| | | '帮æä¿®æ¹æµç¨ç¼å· XXX ç夿³¨ä¸ºâ已补å
éä»¶âã', |
| | | 'å 餿åèµ·çæµç¨ç¼å· XXXã' |
| | | ], |
| | | purchase: [ |
| | | 'æ¬æéè´é颿åååçç©ææåªäºï¼', |
| | | 'åªäºéè´è®¢åè¿æªå
¥åºï¼', |
| | | 'æè¿7天ä¾åºåå°è´§å¼å¸¸æåªäºï¼', |
| | | '帮æç»è®¡å¾
仿¬¾éè´å', |
| | | 'ååºæ¬æéè´éè´§æ
åµ' |
| | | ] |
| | | const assistantAvatarByKey = { |
| | | general: todoAssistantAvatar, |
| | | todo: todoAssistantAvatar, |
| | | purchase: purchaseAssistantAvatar, |
| | | sales: salesAssistantAvatar, |
| | | production: productionAssistantAvatar, |
| | | finance: financeAssistantAvatar, |
| | | boss: bossAssistantAvatar |
| | | } |
| | | const currentAssistantAvatar = computed(() => { |
| | | const assistant = currentAssistant.value || {} |
| | | return assistant.avatar || assistantAvatarByKey[assistant.key] || '' |
| | | }) |
| | | const showAssistantSwitch = computed(() => assistants.value.length > 1) |
| | | const quickPromptLimit = 3 |
| | | const quickPromptStart = ref(0) |
| | | const quickPrompts = computed(() => { |
| | |
| | | if (Array.isArray(assistant.quickPrompts) && assistant.quickPrompts.length) { |
| | | return assistant.quickPrompts |
| | | } |
| | | return assistantQuickPromptMap[assistant.key] || assistantQuickPromptMap.general |
| | | return generalAssistant.quickPrompts || [] |
| | | }) |
| | | const displayedQuickPrompts = computed(() => { |
| | | const prompts = quickPrompts.value || [] |
| | |
| | | |
| | | const visible = ref(false) |
| | | const windowWidth = ref(window.innerWidth) |
| | | const drawerSize = computed(() => { |
| | | const responsiveDrawerSize = computed(() => { |
| | | if (windowWidth.value < 768) return '100%' |
| | | if (windowWidth.value < 1200) return '50%' |
| | | return '50%' |
| | | }) |
| | | const computedDrawerSize = computed(() => props.drawerSize || responsiveDrawerSize.value) |
| | | const messageListRef = ref(null) |
| | | const isSending = ref(false) |
| | | const currentAbortController = ref(null) |
| | | const inputMessage = ref('') |
| | | const selectedFiles = ref([]) |
| | | const uploadFileList = ref([]) |
| | | const selectedFileSnapshots = ref([]) |
| | | const messages = ref([]) |
| | | const uuid = ref('') |
| | | const chartInstances = ref({}) |
| | |
| | | const sessions = ref([]) |
| | | const loadingSessions = ref(false) |
| | | |
| | | const isImageFileType = (fileType = '') => String(fileType || '').toLowerCase().startsWith('image/') |
| | | const imageFilePathPattern = /\.(png|jpe?g|gif|webp|bmp|svg)$/i |
| | | |
| | | const getPathnameFromFilePath = (filePath = '') => { |
| | | const rawPath = String(filePath || '').trim() |
| | | if (!rawPath) return '' |
| | | try { |
| | | const baseOrigin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost' |
| | | return new URL(rawPath, baseOrigin).pathname || '' |
| | | } catch (err) { |
| | | return rawPath.split('?')[0] |
| | | } |
| | | } |
| | | |
| | | const isImageFilePath = (filePath = '') => { |
| | | const pathname = getPathnameFromFilePath(filePath).toLowerCase() |
| | | return imageFilePathPattern.test(pathname) |
| | | } |
| | | |
| | | const getHistoryFileName = (filePath = '', index = 0) => { |
| | | const pathname = getPathnameFromFilePath(filePath) |
| | | const fileName = pathname.split('/').filter(Boolean).pop() |
| | | if (!fileName) return `file-${index + 1}` |
| | | try { |
| | | return decodeURIComponent(fileName) |
| | | } catch (err) { |
| | | return fileName |
| | | } |
| | | } |
| | | |
| | | const getImagePreviewList = (files = []) => { |
| | | if (!Array.isArray(files)) return [] |
| | | return files |
| | | .filter(item => item?.isImage && item?.previewUrl) |
| | | .map(item => item.previewUrl) |
| | | } |
| | | |
| | | const getImagePreviewInitialIndex = (files = [], previewUrl = '') => { |
| | | const list = getImagePreviewList(files) |
| | | const index = list.indexOf(previewUrl) |
| | | return index >= 0 ? index : 0 |
| | | } |
| | | |
| | | const formatFileSize = (size) => { |
| | | const bytes = Number(size) |
| | | if (!Number.isFinite(bytes) || bytes <= 0) return '0 B' |
| | | if (bytes < 1024) return `${bytes} B` |
| | | if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1).replace(/\.0$/, '')} KB` |
| | | return `${(bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, '')} MB` |
| | | } |
| | | |
| | | const createLocalFileSnapshot = (file, index = 0) => { |
| | | const rawFile = typeof File !== 'undefined' && file instanceof File ? file : null |
| | | const fileType = rawFile?.type || '' |
| | | const isImage = isImageFileType(fileType) |
| | | const canCreateObjectURL = typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function' |
| | | const previewUrl = isImage && rawFile && canCreateObjectURL ? URL.createObjectURL(rawFile) : '' |
| | | return { |
| | | previewId: `${rawFile?.name || 'file'}-${rawFile?.size || 0}-${rawFile?.lastModified || Date.now()}-${index}`, |
| | | name: rawFile?.name || `file-${index + 1}`, |
| | | size: rawFile?.size || 0, |
| | | type: fileType, |
| | | isImage, |
| | | previewUrl, |
| | | accessUrl: '', |
| | | rawFile, |
| | | isObjectUrl: !!previewUrl |
| | | } |
| | | } |
| | | |
| | | const createHistoryFileSnapshot = (filePath, memoryId = '', messageIndex = 0, fileIndex = 0) => { |
| | | const normalizedPath = String(filePath || '').trim() |
| | | if (!normalizedPath) return null |
| | | const isImage = isImageFilePath(normalizedPath) |
| | | return { |
| | | previewId: `${memoryId || 'history'}-${messageIndex}-${fileIndex}`, |
| | | name: getHistoryFileName(normalizedPath, fileIndex), |
| | | size: 0, |
| | | type: '', |
| | | isImage, |
| | | previewUrl: isImage ? normalizedPath : '', |
| | | accessUrl: normalizedPath, |
| | | rawFile: null, |
| | | isObjectUrl: false |
| | | } |
| | | } |
| | | |
| | | const revokeLocalFileSnapshots = (snapshots = []) => { |
| | | const canRevokeObjectURL = typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function' |
| | | if (!canRevokeObjectURL) return |
| | | if (!Array.isArray(snapshots)) return |
| | | snapshots.forEach((snapshot) => { |
| | | if (snapshot?.isObjectUrl && snapshot?.previewUrl) { |
| | | URL.revokeObjectURL(snapshot.previewUrl) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const mapHistoryFilePathsToSnapshots = (filePaths = [], memoryId = '', messageIndex = 0) => { |
| | | if (!Array.isArray(filePaths)) return [] |
| | | return filePaths |
| | | .map((filePath, fileIndex) => createHistoryFileSnapshot(filePath, memoryId, messageIndex, fileIndex)) |
| | | .filter(Boolean) |
| | | } |
| | | |
| | | const openMessageAttachment = (file) => { |
| | | const accessUrl = String(file?.accessUrl || '').trim() |
| | | if (!accessUrl) return |
| | | if (typeof window === 'undefined' || typeof window.open !== 'function') return |
| | | window.open(accessUrl, '_blank', 'noopener,noreferrer') |
| | | } |
| | | |
| | | const handleMessageFileClick = (file) => { |
| | | if (!file?.accessUrl || file?.isImage) return |
| | | openMessageAttachment(file) |
| | | } |
| | | |
| | | const revokeMessageLocalFileSnapshots = (messageList = []) => { |
| | | if (!Array.isArray(messageList)) return |
| | | messageList.forEach((msg) => { |
| | | if (Array.isArray(msg?.localUploadFiles)) { |
| | | revokeLocalFileSnapshots(msg.localUploadFiles) |
| | | msg.localUploadFiles = [] |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const clearSelectedFiles = ({ releaseSnapshots = true } = {}) => { |
| | | if (releaseSnapshots) { |
| | | revokeLocalFileSnapshots(selectedFileSnapshots.value) |
| | | } |
| | | selectedFiles.value = [] |
| | | uploadFileList.value = [] |
| | | selectedFileSnapshots.value = [] |
| | | } |
| | | |
| | | const abortCurrentRequest = () => { |
| | | if (!currentAbortController.value) return |
| | | |
| | |
| | | |
| | | const selectSession = async (session) => { |
| | | showHistory.value = false |
| | | clearSelectedFiles() |
| | | uuid.value = session.memoryId |
| | | localStorage.setItem(currentAssistant.value.storageKey, uuid.value) |
| | | |
| | |
| | | try { |
| | | const res = await request.get(`${currentAssistant.value.apiBase}/history/messages/${uuid.value}`) |
| | | if (res.code === 200) { |
| | | revokeMessageLocalFileSnapshots(messages.value) |
| | | disposeCharts() |
| | | messages.value = [] |
| | | const historyMsgs = res.data || [] |
| | |
| | | |
| | | const messageObj = { |
| | | isUser, |
| | | content: msg.content, |
| | | content: msg.content || '', |
| | | htmlContent: '', |
| | | isTyping: false, |
| | | chartOptions: null, |
| | |
| | | type: '', |
| | | tableData: null, |
| | | payloadTreeData: null, |
| | | payloadHiddenData: null |
| | | payloadHiddenData: null, |
| | | localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : [] |
| | | } |
| | | |
| | | messages.value.push(messageObj) |
| | |
| | | } |
| | | |
| | | // è§£æå岿¶æ¯ä¸ç JSON |
| | | const extracted = extractEmbeddedSuccessJson(msg.content) |
| | | const extracted = extractEmbeddedSuccessJson(msg.content || '') |
| | | if (extracted) { |
| | | applyStructuredMessageData(messageObj, extracted.data, botMsgIndex) |
| | | } |
| | | |
| | | updateOutputState(msg.content, botMsgIndex) |
| | | messageObj.htmlContent = convertStreamOutput(msg.content, botMsgIndex) |
| | | updateOutputState(msg.content || '', botMsgIndex) |
| | | messageObj.htmlContent = convertStreamOutput(msg.content || '', botMsgIndex) |
| | | } else { |
| | | messageObj.htmlContent = convertTextToHtml(msg.content) |
| | | messageObj.htmlContent = convertTextToHtml(msg.content || '') |
| | | } |
| | | }) |
| | | scrollToBottom() |
| | |
| | | } |
| | | |
| | | onMounted(() => { |
| | | initUUID() |
| | | // åå§æ¬¢è¿ |
| | | if (messages.value.length === 0) { |
| | | hello() |
| | | if (props.autoOpen) { |
| | | visible.value = true |
| | | } |
| | | initUUID() |
| | | window.addEventListener('resize', handleWindowResize) |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | revokeMessageLocalFileSnapshots(messages.value) |
| | | clearSelectedFiles() |
| | | disposeCharts() |
| | | window.removeEventListener('resize', handleWindowResize) |
| | | }) |
| | |
| | | if (!prevKey || nextKey === prevKey) return |
| | | |
| | | abortCurrentRequest() |
| | | revokeMessageLocalFileSnapshots(messages.value) |
| | | disposeCharts() |
| | | messages.value = [] |
| | | outputState.value = {} |
| | | sessions.value = [] |
| | | showHistory.value = false |
| | | selectedFiles.value = [] |
| | | uploadFileList.value = [] |
| | | clearSelectedFiles() |
| | | inputMessage.value = '' |
| | | quickPromptStart.value = 0 |
| | | initUUID() |
| | | hello() |
| | | }) |
| | | |
| | | watch(() => props.defaultAssistant, (nextKey) => { |
| | | if (!nextKey || nextKey === selectedAssistantKey.value) return |
| | | if (!assistants.value.some(item => item.key === nextKey)) return |
| | | selectedAssistantKey.value = nextKey |
| | | }) |
| | | |
| | | watch(() => props.autoOpen, (nextValue) => { |
| | | if (nextValue) { |
| | | visible.value = true |
| | | } |
| | | }) |
| | | |
| | | const handleWindowResize = () => { |
| | | windowWidth.value = window.innerWidth |
| | | } |
| | | |
| | | const handleHeaderExtraAction = () => { |
| | | emit('header-extra-action') |
| | | } |
| | | |
| | | const toggleSidebar = () => { |
| | |
| | | } |
| | | |
| | | const handleClose = () => { |
| | | if (hideTrigger.value) return |
| | | visible.value = false |
| | | } |
| | | |
| | | const handleManualClose = () => { |
| | | if (hideTrigger.value) return |
| | | if (isSending.value) { |
| | | abortCurrentRequest() |
| | | } |
| | |
| | | uuid.value = storedUUID |
| | | } |
| | | |
| | | const hello = () => { |
| | | sendRequest(currentAssistant.value.welcomeMessage || 'ä½ å¥½') |
| | | } |
| | | |
| | | const newChat = () => { |
| | | revokeMessageLocalFileSnapshots(messages.value) |
| | | disposeCharts() |
| | | messages.value = [] |
| | | outputState.value = {} |
| | | sessions.value = [] |
| | | showHistory.value = false |
| | | selectedFiles.value = [] |
| | | uploadFileList.value = [] |
| | | clearSelectedFiles() |
| | | quickPromptStart.value = 0 |
| | | localStorage.removeItem(currentAssistant.value.storageKey) |
| | | initUUID() |
| | | hello() |
| | | } |
| | | |
| | | const handleNewChat = () => { |
| | |
| | | '审æ¹ç¨æ·IDå表' |
| | | ]) |
| | | |
| | | const purchaseIntegerFieldKeys = new Set([ |
| | | 'id', |
| | | 'supplierId', |
| | | 'recorderId', |
| | | 'salesContractNoId', |
| | | 'salesLedgerId', |
| | | 'Type', |
| | | 'businessPersonId', |
| | | 'productId', |
| | | 'productModelId', |
| | | 'ticketRegistrationId', |
| | | 'type', |
| | | 'approvalStatus', |
| | | 'inventoryWarningQuantity' |
| | | ]) |
| | | |
| | | const purchaseDecimalFieldKeys = new Set([ |
| | | 'invoiceAmount', |
| | | 'contractAmount', |
| | | 'receiptPaymentAmount', |
| | | 'unReceiptPaymentAmount', |
| | | 'quantity', |
| | | 'taxRate', |
| | | 'taxInclusiveUnitPrice', |
| | | 'taxInclusiveTotalPrice', |
| | | 'taxExclusiveTotalPrice', |
| | | 'priceWithTax', |
| | | 'totalPriceWithTax' |
| | | ]) |
| | | |
| | | const purchaseBooleanFieldKeys = new Set([ |
| | | 'hasChildren', |
| | | 'isWhite', |
| | | 'isInspected', |
| | | 'isChecked' |
| | | ]) |
| | | |
| | | const purchaseStringFieldKeys = new Set([ |
| | | 'entryDateStart', |
| | | 'entryDateEnd', |
| | | 'purchaseContractNumber', |
| | | 'supplierName', |
| | | 'recorderName', |
| | | 'salesContractNo', |
| | | 'projectName', |
| | | 'entryDate', |
| | | 'executionDate', |
| | | 'remarks', |
| | | 'attachmentMaterials', |
| | | 'createdAt', |
| | | 'updatedAt', |
| | | 'phoneNumber', |
| | | 'invoiceNumber', |
| | | 'paymentMethod', |
| | | 'templateName', |
| | | 'productCategory', |
| | | 'specificationModel', |
| | | 'unit', |
| | | 'invoiceType' |
| | | ]) |
| | | |
| | | const purchaseGenericArrayFieldKeys = new Set([ |
| | | 'purchaseLedgers', |
| | | 'productData' |
| | | ]) |
| | | |
| | | const purchaseStringArrayFieldKeys = new Set(['tempFileIds']) |
| | | |
| | | const purchaseObjectArrayFieldKeys = new Set(['SalesLedgerFiles']) |
| | | |
| | | const normalizePurchaseProductRecord = (record) => { |
| | | if (!record || typeof record !== 'object' || Array.isArray(record)) return record |
| | | return mapPayloadKeys(record, purchasePayloadFieldKeyMap) |
| | |
| | | }, {}) |
| | | } |
| | | |
| | | const normalizeAttachmentMaterialsValue = (value) => { |
| | | if (value === null || value === undefined) return value |
| | | if (typeof value === 'string') return value |
| | | if (typeof value === 'number' || typeof value === 'boolean') return String(value) |
| | | try { |
| | | return JSON.stringify(value) |
| | | } catch (err) { |
| | | return String(value) |
| | | } |
| | | } |
| | | |
| | | const normalizePurchaseAttachmentMaterialsField = (value) => { |
| | | if (Array.isArray(value)) { |
| | | return value.map(item => normalizePurchaseAttachmentMaterialsField(item)) |
| | | } |
| | | if (value && typeof value === 'object') { |
| | | return Object.entries(value).reduce((result, [key, item]) => { |
| | | if (key === 'attachmentMaterials') { |
| | | result[key] = normalizeAttachmentMaterialsValue(item) |
| | | } else { |
| | | result[key] = normalizePurchaseAttachmentMaterialsField(item) |
| | | } |
| | | return result |
| | | }, {}) |
| | | } |
| | | return value |
| | | } |
| | | |
| | | const normalizePurchaseNumericText = (text = '') => { |
| | | return String(text) |
| | | .replace(/[,\sï¼]/g, '') |
| | | .replace(/[¥¥å
%]/g, '') |
| | | } |
| | | |
| | | const parsePurchaseNumberValue = (value) => { |
| | | if (typeof value === 'number') { |
| | | return Number.isFinite(value) ? value : null |
| | | } |
| | | if (typeof value === 'boolean') { |
| | | return value ? 1 : 0 |
| | | } |
| | | if (typeof value !== 'string') { |
| | | return null |
| | | } |
| | | const text = normalizePurchaseNumericText(value.trim()) |
| | | if (!text) return null |
| | | if (!/^[-+]?\d+(\.\d+)?$/.test(text)) return null |
| | | const numberValue = Number(text) |
| | | return Number.isFinite(numberValue) ? numberValue : null |
| | | } |
| | | |
| | | const normalizePurchaseIntegerFieldValue = (value) => { |
| | | if (value === null || value === undefined) return value |
| | | if (value === '') return null |
| | | const numberValue = parsePurchaseNumberValue(value) |
| | | if (numberValue === null) return null |
| | | return Math.trunc(numberValue) |
| | | } |
| | | |
| | | const normalizePurchaseDecimalFieldValue = (value) => { |
| | | if (value === null || value === undefined) return value |
| | | if (value === '') return null |
| | | const numberValue = parsePurchaseNumberValue(value) |
| | | return numberValue === null ? null : numberValue |
| | | } |
| | | |
| | | const normalizePurchaseBooleanFieldValue = (value) => { |
| | | if (value === null || value === undefined) return value |
| | | if (value === '') return null |
| | | if (typeof value === 'boolean') return value |
| | | if (typeof value === 'number') return value !== 0 |
| | | if (typeof value !== 'string') return null |
| | | |
| | | const text = value.trim().toLowerCase() |
| | | if (!text) return null |
| | | if (['true', '1', 'yes', 'y', 'æ¯', 'å·²', 'checked'].includes(text)) return true |
| | | if (['false', '0', 'no', 'n', 'å¦', 'æª', 'unchecked'].includes(text)) return false |
| | | return null |
| | | } |
| | | |
| | | const normalizePurchaseStringFieldValue = (value) => { |
| | | if (value === null || value === undefined) return value |
| | | if (typeof value === 'string') return value |
| | | if (typeof value === 'number' || typeof value === 'boolean') return String(value) |
| | | try { |
| | | return JSON.stringify(value) |
| | | } catch (err) { |
| | | return String(value) |
| | | } |
| | | } |
| | | |
| | | const normalizePurchaseStringArrayFieldValue = (value) => { |
| | | if (Array.isArray(value)) { |
| | | return value |
| | | .map(item => normalizePurchaseStringFieldValue(item)) |
| | | .filter(item => item !== null && item !== undefined && item !== '') |
| | | } |
| | | if (value === null || value === undefined || value === '') return [] |
| | | if (typeof value === 'string') { |
| | | const text = value.trim() |
| | | if (!text) return [] |
| | | if (/^\[.*\]$/.test(text)) { |
| | | try { |
| | | const parsedValue = JSON.parse(text) |
| | | if (Array.isArray(parsedValue)) { |
| | | return parsedValue |
| | | .map(item => normalizePurchaseStringFieldValue(item)) |
| | | .filter(item => item !== null && item !== undefined && item !== '') |
| | | } |
| | | } catch (err) { |
| | | // Keep as plain text when not valid JSON array. |
| | | } |
| | | } |
| | | const splitValues = text |
| | | .split(/[,\nï¼]/) |
| | | .map(item => item.trim()) |
| | | .filter(Boolean) |
| | | return splitValues.length > 1 ? splitValues : [text] |
| | | } |
| | | const normalizedValue = normalizePurchaseStringFieldValue(value) |
| | | return normalizedValue === null || normalizedValue === undefined || normalizedValue === '' |
| | | ? [] |
| | | : [normalizedValue] |
| | | } |
| | | |
| | | const normalizePurchaseObjectArrayFieldValue = (value) => { |
| | | if (Array.isArray(value)) { |
| | | return value.filter(item => item && typeof item === 'object') |
| | | } |
| | | if (value && typeof value === 'object') { |
| | | return [value] |
| | | } |
| | | return [] |
| | | } |
| | | |
| | | const normalizePurchaseValueByFieldKey = (fieldKey, value) => { |
| | | if (fieldKey === 'attachmentMaterials') return normalizeAttachmentMaterialsValue(value) |
| | | if (purchaseIntegerFieldKeys.has(fieldKey)) return normalizePurchaseIntegerFieldValue(value) |
| | | if (purchaseDecimalFieldKeys.has(fieldKey)) return normalizePurchaseDecimalFieldValue(value) |
| | | if (purchaseBooleanFieldKeys.has(fieldKey)) return normalizePurchaseBooleanFieldValue(value) |
| | | if (purchaseStringArrayFieldKeys.has(fieldKey)) return normalizePurchaseStringArrayFieldValue(value) |
| | | if (purchaseObjectArrayFieldKeys.has(fieldKey)) return normalizePurchaseObjectArrayFieldValue(value) |
| | | if (purchaseStringFieldKeys.has(fieldKey)) return normalizePurchaseStringFieldValue(value) |
| | | return value |
| | | } |
| | | |
| | | const normalizePurchasePayloadFieldTypes = (value, fieldKey = '') => { |
| | | if (purchaseGenericArrayFieldKeys.has(fieldKey)) { |
| | | if (Array.isArray(value)) { |
| | | return value.map(item => normalizePurchasePayloadFieldTypes(item)) |
| | | } |
| | | if (value && typeof value === 'object') { |
| | | return [normalizePurchasePayloadFieldTypes(value)] |
| | | } |
| | | return [] |
| | | } |
| | | |
| | | if (purchaseStringArrayFieldKeys.has(fieldKey)) { |
| | | return normalizePurchaseStringArrayFieldValue(value) |
| | | } |
| | | |
| | | if (purchaseObjectArrayFieldKeys.has(fieldKey)) { |
| | | return normalizePurchaseObjectArrayFieldValue(value) |
| | | } |
| | | |
| | | if (Array.isArray(value)) { |
| | | return value.map(item => normalizePurchasePayloadFieldTypes(item)) |
| | | } |
| | | |
| | | if (value && typeof value === 'object') { |
| | | return Object.entries(value).reduce((result, [key, item]) => { |
| | | result[key] = normalizePurchasePayloadFieldTypes(item, key) |
| | | return result |
| | | }, {}) |
| | | } |
| | | |
| | | return normalizePurchaseValueByFieldKey(fieldKey, value) |
| | | } |
| | | |
| | | const sanitizePurchasePayloadForSubmit = (payload, businessType) => { |
| | | if (businessType !== 'purchase_ledger' || !payload || typeof payload !== 'object') return payload |
| | | |
| | | const sanitized = mergeLegacyProductDataIntoLedgers(Array.isArray(payload) ? [...payload] : { ...payload }) |
| | | let sanitized = mergeLegacyProductDataIntoLedgers(Array.isArray(payload) ? [...payload] : { ...payload }) |
| | | sanitized = normalizePurchaseAttachmentMaterialsField(sanitized) |
| | | sanitized = normalizePurchasePayloadFieldTypes(sanitized) |
| | | if (Array.isArray(sanitized.purchaseLedgers)) { |
| | | sanitized.purchaseLedgers = sanitized.purchaseLedgers.map(filterPurchaseLedgerRecord) |
| | | } |
| | |
| | | return isLt10M |
| | | }) |
| | | |
| | | clearSelectedFiles() |
| | | selectedFiles.value = validFiles |
| | | uploadFileList.value = fileList.filter(item => item.raw && validFiles.includes(item.raw)) |
| | | selectedFileSnapshots.value = validFiles.map((rawFile, index) => createLocalFileSnapshot(rawFile, index)) |
| | | } |
| | | |
| | | const removeSelectedFile = (index) => { |
| | | const [removedSnapshot] = selectedFileSnapshots.value.splice(index, 1) |
| | | revokeLocalFileSnapshots(removedSnapshot ? [removedSnapshot] : []) |
| | | selectedFiles.value.splice(index, 1) |
| | | uploadFileList.value.splice(index, 1) |
| | | } |
| | | |
| | | const analyzeFiles = async (files, message = '') => { |
| | | const analyzeFiles = async (files, message = '', localFileSnapshots = []) => { |
| | | const uploadFiles = Array.isArray(files) ? files : [files].filter(Boolean) |
| | | if (!uploadFiles.length) return |
| | | if (isSending.value) return |
| | |
| | | isUser: true, |
| | | content: userMsg, |
| | | htmlContent: convertTextToHtml(userMsg), |
| | | isTyping: false |
| | | isTyping: false, |
| | | localUploadFiles: Array.isArray(localFileSnapshots) ? localFileSnapshots : [] |
| | | }) |
| | | |
| | | const botMsgIndex = messages.value.length |
| | |
| | | const msg = inputMessage.value?.trim() || '' |
| | | if ((msg || selectedFiles.value.length) && !isSending.value) { |
| | | if (selectedFiles.value.length) { |
| | | analyzeFiles([...selectedFiles.value], msg) |
| | | selectedFiles.value = [] |
| | | uploadFileList.value = [] |
| | | const localFileSnapshots = selectedFileSnapshots.value |
| | | analyzeFiles([...selectedFiles.value], msg, localFileSnapshots) |
| | | clearSelectedFiles({ releaseSnapshots: false }) |
| | | } else { |
| | | sendRequest(msg) |
| | | } |
| | |
| | | height: 100%; |
| | | } |
| | | :deep(.el-drawer__header) { |
| | | margin-bottom: 0; |
| | | padding: 0; |
| | | margin-bottom: 0 !important; |
| | | padding: 0 !important; |
| | | border-bottom: 1px solid rgba(255, 255, 255, 0.12); |
| | | background: $gradient-dark; |
| | | color: #fff; |
| | | } |
| | |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | width: 100%; |
| | | padding: 18px 20px; |
| | | padding: 12px 18px; |
| | | background: $gradient-dark; |
| | | position: relative; |
| | | overflow: hidden; |
| | |
| | | opacity: 1; |
| | | } |
| | | } |
| | | |
| | | :deep(.header-action-btn--text) { |
| | | width: auto !important; |
| | | min-width: 104px; |
| | | padding: 8px 14px !important; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | white-space: nowrap; |
| | | } |
| | | } |
| | | |
| | | .assistant-switcher { |
| | |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | height: 240px; |
| | | height: 128px; |
| | | background: linear-gradient(180deg, rgba(0, 85, 212, 0.06) 0%, transparent 100%); |
| | | pointer-events: none; |
| | | } |
| | |
| | | background: rgba(0, 85, 212, 0.25); |
| | | border-radius: 2px; |
| | | } |
| | | } |
| | | |
| | | .message-local-file-list { |
| | | margin-top: 8px; |
| | | display: grid; |
| | | gap: 8px; |
| | | max-width: 100%; |
| | | } |
| | | |
| | | .message-local-file-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | padding: 8px 10px; |
| | | border-radius: 10px; |
| | | border: 1px solid rgba(88, 117, 255, 0.2); |
| | | background: rgba(255, 255, 255, 0.9); |
| | | max-width: 100%; |
| | | |
| | | &.clickable { |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | |
| | | &:hover { |
| | | border-color: rgba(44, 109, 255, 0.38); |
| | | background: rgba(243, 247, 255, 0.96); |
| | | } |
| | | } |
| | | } |
| | | |
| | | .message-local-file-thumb { |
| | | width: 40px; |
| | | height: 40px; |
| | | border-radius: 6px; |
| | | overflow: hidden; |
| | | flex-shrink: 0; |
| | | border: 1px solid rgba(124, 148, 255, 0.26); |
| | | background: #f4f7ff; |
| | | cursor: zoom-in; |
| | | |
| | | :deep(.el-image__inner) { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | } |
| | | |
| | | .message-local-file-icon { |
| | | font-size: 20px; |
| | | color: $primary-blue; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .message-local-file-meta { |
| | | min-width: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 2px; |
| | | } |
| | | |
| | | .message-local-file-name { |
| | | font-size: 12px; |
| | | color: #1f2a44; |
| | | font-weight: 600; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | |
| | | &.clickable { |
| | | color: $primary-blue; |
| | | cursor: pointer; |
| | | |
| | | &:hover { |
| | | text-decoration: underline; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .message-local-file-size { |
| | | font-size: 11px; |
| | | color: #7f8ba1; |
| | | line-height: 1.2; |
| | | } |
| | | } |
| | | |
| | |
| | | font-size: 18px; |
| | | } |
| | | |
| | | .selected-file-thumb { |
| | | width: 30px; |
| | | height: 30px; |
| | | border-radius: 6px; |
| | | overflow: hidden; |
| | | border: 1px solid rgba(0, 85, 212, 0.2); |
| | | flex-shrink: 0; |
| | | cursor: zoom-in; |
| | | |
| | | :deep(.el-image__inner) { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | } |
| | | |
| | | .selected-file-meta { |
| | | min-width: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 2px; |
| | | } |
| | | |
| | | .file-name { |
| | | font-size: 13px; |
| | | color: $deep-blue; |
| | |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .file-size { |
| | | font-size: 11px; |
| | | color: #5f86b4; |
| | | line-height: 1.1; |
| | | } |
| | | |
| | | .remove-file { |
| | |
| | | |
| | | .chat-hero { |
| | | display: grid; |
| | | grid-template-columns: 164px minmax(0, 1fr); |
| | | gap: 18px; |
| | | align-items: start; |
| | | padding: 14px 18px 6px; |
| | | grid-template-columns: 176px minmax(0, 1fr); |
| | | gap: 14px; |
| | | align-items: stretch; |
| | | padding: 8px 18px 4px; |
| | | |
| | | &.compact { |
| | | grid-template-columns: 122px minmax(0, 1fr); |
| | | gap: 12px; |
| | | padding: 8px 18px 2px; |
| | | grid-template-columns: 132px minmax(0, 1fr); |
| | | gap: 10px; |
| | | padding: 4px 18px 2px; |
| | | } |
| | | } |
| | | |
| | | .assistant-stand { |
| | | position: relative; |
| | | min-height: 252px; |
| | | min-height: 206px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-direction: column; |
| | | padding-top: 18px; |
| | | padding-top: 8px; |
| | | overflow: hidden; |
| | | |
| | | &.compact { |
| | | min-height: 176px; |
| | | padding-top: 8px; |
| | | min-height: 160px; |
| | | padding-top: 4px; |
| | | } |
| | | |
| | | &.thinking { |
| | | .assistant-halo { |
| | | opacity: 1; |
| | | transform: scale(1.08); |
| | | filter: blur(8px); |
| | | transform: scale(1.12); |
| | | filter: blur(9px); |
| | | } |
| | | |
| | | .assistant-scan-ring { |
| | | opacity: 1; |
| | | animation-duration: 1.6s; |
| | | opacity: 0.95; |
| | | animation-duration: 1.5s; |
| | | } |
| | | |
| | | .assistant-orbit { |
| | | opacity: 1; |
| | | opacity: 0.76; |
| | | } |
| | | |
| | | .assistant-bot { |
| | | transform: translateY(-4px) scale(1.02); |
| | | .assistant-model-shell { |
| | | transform: translateY(-5px) scale(1.02); |
| | | } |
| | | |
| | | .assistant-bot-head { |
| | | box-shadow: 0 0 30px rgba(80, 157, 255, 0.36); |
| | | .assistant-model-cut { |
| | | animation-duration: 2.2s; |
| | | } |
| | | |
| | | .assistant-bot-eye { |
| | | animation: robotBlinkFast 1.1s infinite; |
| | | box-shadow: 0 0 16px rgba(72, 186, 255, 0.95); |
| | | } |
| | | |
| | | .assistant-bot-mouth { |
| | | width: 28px; |
| | | opacity: 1; |
| | | animation: robotTalk 1.2s ease-in-out infinite; |
| | | } |
| | | |
| | | .assistant-bot-core { |
| | | animation: corePulse 1.4s ease-in-out infinite; |
| | | box-shadow: 0 0 24px rgba(78, 120, 255, 0.26); |
| | | } |
| | | |
| | | .assistant-bot-core-ring { |
| | | animation: coreRotate 3s linear infinite; |
| | | .assistant-model-img { |
| | | filter: saturate(1.06) drop-shadow(0 18px 20px rgba(22, 48, 80, 0.22)); |
| | | } |
| | | |
| | | .assistant-status { |
| | |
| | | animation: thinkingDot 1s ease-in-out infinite; |
| | | } |
| | | |
| | | .assistant-base-lg { |
| | | animation-duration: 1.8s; |
| | | } |
| | | |
| | | .assistant-base-md { |
| | | animation-duration: 1.5s; |
| | | } |
| | | |
| | | .assistant-base-sm { |
| | | box-shadow: 0 0 24px rgba(255, 93, 122, 0.48); |
| | | box-shadow: 0 0 24px rgba(30, 91, 255, 0.36); |
| | | animation-duration: 1.25s; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .assistant-halo { |
| | | position: absolute; |
| | | top: 22px; |
| | | width: 130px; |
| | | height: 130px; |
| | | top: 24px; |
| | | width: 146px; |
| | | height: 146px; |
| | | border-radius: 50%; |
| | | background: radial-gradient(circle, rgba(46, 140, 224, 0.3) 0%, rgba(0, 85, 212, 0.18) 42%, rgba(113, 54, 244, 0.12) 60%, transparent 78%); |
| | | background: radial-gradient(circle, rgba(31, 122, 114, 0.26) 0%, rgba(30, 91, 255, 0.2) 42%, rgba(109, 65, 237, 0.12) 66%, transparent 80%); |
| | | filter: blur(6px); |
| | | opacity: 0.82; |
| | | opacity: 0.78; |
| | | transition: all 0.35s ease; |
| | | } |
| | | |
| | | .assistant-scan-ring { |
| | | position: absolute; |
| | | top: 40px; |
| | | width: 132px; |
| | | height: 132px; |
| | | top: 44px; |
| | | width: 136px; |
| | | height: 136px; |
| | | border-radius: 50%; |
| | | border: 1px solid rgba(90, 159, 224, 0.22); |
| | | border: 1px solid rgba(67, 145, 223, 0.24); |
| | | box-shadow: inset 0 0 16px rgba(255, 255, 255, 0.25); |
| | | opacity: 0.55; |
| | | opacity: 0.52; |
| | | animation: scanRing 4s linear infinite; |
| | | } |
| | | |
| | | .assistant-orbit { |
| | | position: absolute; |
| | | top: 52px; |
| | | width: 150px; |
| | | height: 150px; |
| | | width: 156px; |
| | | height: 156px; |
| | | border-radius: 50%; |
| | | border: 1px dashed rgba(92, 135, 255, 0.22); |
| | | opacity: 0.45; |
| | | border: 1px dashed rgba(92, 135, 255, 0.24); |
| | | opacity: 0.42; |
| | | } |
| | | |
| | | .assistant-orbit-a { |
| | | animation: orbitRotate 8s linear infinite; |
| | | animation: orbitRotate 8.6s linear infinite; |
| | | } |
| | | |
| | | .assistant-orbit-b { |
| | | width: 118px; |
| | | height: 118px; |
| | | width: 124px; |
| | | height: 124px; |
| | | top: 68px; |
| | | border-color: rgba(255, 108, 150, 0.22); |
| | | animation: orbitRotateReverse 5.6s linear infinite; |
| | | border-color: rgba(31, 122, 114, 0.24); |
| | | animation: orbitRotateReverse 6.2s linear infinite; |
| | | } |
| | | |
| | | .assistant-bot { |
| | | .assistant-model-shell { |
| | | position: relative; |
| | | z-index: 1; |
| | | width: 148px; |
| | | height: 178px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | align-items: flex-end; |
| | | justify-content: center; |
| | | margin-top: 12px; |
| | | margin-top: 4px; |
| | | transition: transform 0.35s ease; |
| | | } |
| | | |
| | | .assistant-bot-antenna { |
| | | position: absolute; |
| | | top: -4px; |
| | | width: 4px; |
| | | height: 20px; |
| | | border-radius: 999px; |
| | | background: linear-gradient(180deg, #fefefe, #aac9ff); |
| | | |
| | | &::before { |
| | | content: ''; |
| | | position: absolute; |
| | | top: -6px; |
| | | left: 50%; |
| | | width: 10px; |
| | | height: 10px; |
| | | border-radius: 50%; |
| | | bottom: 2px; |
| | | width: 164px; |
| | | height: 42px; |
| | | transform: translateX(-50%); |
| | | background: linear-gradient(135deg, #54bfff, #7a41ff); |
| | | box-shadow: 0 0 14px rgba(84, 191, 255, 0.65); |
| | | border-radius: 50%; |
| | | background: radial-gradient( |
| | | ellipse at center, |
| | | rgba(43, 126, 211, 0.32) 0%, |
| | | rgba(43, 126, 211, 0.14) 46%, |
| | | rgba(43, 126, 211, 0) 74% |
| | | ); |
| | | filter: blur(2.6px); |
| | | animation: baseGlow 4.6s ease-in-out infinite; |
| | | z-index: 1; |
| | | } |
| | | |
| | | &::after { |
| | | content: ''; |
| | | position: absolute; |
| | | left: 50%; |
| | | bottom: 10px; |
| | | width: 138px; |
| | | height: 28px; |
| | | transform: translateX(-50%); |
| | | border-radius: 50%; |
| | | border: 1px solid rgba(36, 116, 198, 0.6); |
| | | box-shadow: |
| | | inset 0 0 0 1px rgba(255, 255, 255, 0.58), |
| | | 0 0 22px rgba(42, 116, 196, 0.24); |
| | | animation: basePulse 3.2s ease-in-out infinite; |
| | | z-index: 4; |
| | | } |
| | | } |
| | | |
| | | .assistant-bot-antenna-left { |
| | | left: 36px; |
| | | transform: rotate(-14deg); |
| | | } |
| | | |
| | | .assistant-bot-antenna-right { |
| | | right: 36px; |
| | | transform: rotate(14deg); |
| | | } |
| | | |
| | | .assistant-bot-head { |
| | | .assistant-model-cut { |
| | | position: relative; |
| | | width: 132px; |
| | | height: 178px; |
| | | z-index: 6; |
| | | display: flex; |
| | | align-items: flex-end; |
| | | justify-content: center; |
| | | transform-origin: center 84%; |
| | | animation: avatarFloat 3.2s ease-in-out infinite; |
| | | } |
| | | |
| | | .assistant-model-img { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: contain; |
| | | object-position: center bottom; |
| | | display: block; |
| | | filter: saturate(1.03) drop-shadow(0 14px 18px rgba(22, 49, 79, 0.2)); |
| | | transition: filter 0.35s ease; |
| | | } |
| | | |
| | | .assistant-model-fallback { |
| | | width: 92px; |
| | | height: 78px; |
| | | border-radius: 28px; |
| | | background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #e8f1ff 100%); |
| | | border: 1px solid rgba(0, 85, 212, 0.14); |
| | | box-shadow: 0 16px 32px rgba(0, 85, 212, 0.14); |
| | | height: 92px; |
| | | border-radius: 24px; |
| | | color: #fff; |
| | | background: linear-gradient(145deg, rgba(31, 122, 114, 0.9), rgba(30, 91, 255, 0.9)); |
| | | border: 1px solid rgba(255, 255, 255, 0.3); |
| | | box-shadow: 0 12px 24px rgba(31, 85, 173, 0.22); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .assistant-bot-head-glow { |
| | | position: absolute; |
| | | inset: 10px 16px auto; |
| | | height: 20px; |
| | | border-radius: 999px; |
| | | background: linear-gradient(180deg, rgba(0, 85, 212, 0.16), transparent); |
| | | } |
| | | |
| | | .assistant-bot-eye { |
| | | position: absolute; |
| | | top: 30px; |
| | | width: 16px; |
| | | height: 16px; |
| | | border-radius: 50%; |
| | | background: radial-gradient(circle, #8ef0ff 0%, #56c0ff 42%, #2869ff 100%); |
| | | box-shadow: 0 0 12px rgba(72, 186, 255, 0.72); |
| | | animation: robotBlink 3.2s infinite; |
| | | } |
| | | |
| | | .assistant-bot-eye-left { |
| | | left: 22px; |
| | | } |
| | | |
| | | .assistant-bot-eye-right { |
| | | right: 22px; |
| | | } |
| | | |
| | | .assistant-bot-mouth { |
| | | .assistant-base { |
| | | position: absolute; |
| | | left: 50%; |
| | | bottom: 16px; |
| | | width: 22px; |
| | | height: 4px; |
| | | bottom: 8px; |
| | | transform: translateX(-50%); |
| | | border-radius: 999px; |
| | | background: linear-gradient(90deg, rgba(72, 186, 255, 0.2), rgba(72, 186, 255, 0.9), rgba(72, 186, 255, 0.2)); |
| | | } |
| | | |
| | | .assistant-bot-neck { |
| | | width: 16px; |
| | | height: 10px; |
| | | border-radius: 0 0 10px 10px; |
| | | background: linear-gradient(180deg, #dceaff, #bdd5ff); |
| | | margin-top: -2px; |
| | | } |
| | | |
| | | .assistant-bot-body { |
| | | position: relative; |
| | | width: 104px; |
| | | height: 92px; |
| | | margin-top: 2px; |
| | | border-radius: 28px 28px 34px 34px; |
| | | background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #e3eeff 100%); |
| | | border: 1px solid rgba(0, 85, 212, 0.14); |
| | | box-shadow: 0 18px 36px rgba(0, 85, 212, 0.16); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .assistant-bot-arm { |
| | | position: absolute; |
| | | top: 18px; |
| | | width: 16px; |
| | | height: 44px; |
| | | border-radius: 999px; |
| | | background: linear-gradient(180deg, #eff5ff, #c7dbff); |
| | | border: 1px solid rgba(0, 85, 212, 0.12); |
| | | } |
| | | |
| | | .assistant-bot-arm-left { |
| | | left: -10px; |
| | | transform: rotate(16deg); |
| | | } |
| | | |
| | | .assistant-bot-arm-right { |
| | | right: -10px; |
| | | transform: rotate(-16deg); |
| | | } |
| | | |
| | | .assistant-bot-core { |
| | | position: relative; |
| | | width: 46px; |
| | | height: 46px; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: $primary-blue; |
| | | background: radial-gradient(circle, rgba(255, 255, 255, 1) 0%, #dae8ff 55%, #adc7ff 100%); |
| | | } |
| | | |
| | | .assistant-bot-core-ring { |
| | | position: absolute; |
| | | inset: -6px; |
| | | border-radius: 50%; |
| | | border: 1px solid rgba(88, 135, 255, 0.3); |
| | | border-top-color: rgba(255, 96, 139, 0.85); |
| | | border-right-color: rgba(79, 145, 255, 0.9); |
| | | border: 1px solid rgba(36, 116, 198, 0.28); |
| | | background: radial-gradient( |
| | | ellipse at center, |
| | | rgba(255, 255, 255, 0.94) 0%, |
| | | rgba(81, 164, 233, 0.16) 58%, |
| | | rgba(30, 91, 255, 0.06) 100% |
| | | ); |
| | | box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2); |
| | | } |
| | | |
| | | .assistant-status { |
| | | position: relative; |
| | | z-index: 1; |
| | | margin-top: 14px; |
| | | padding: 6px 12px; |
| | | margin-top: 7px; |
| | | padding: 5px 10px; |
| | | border-radius: 999px; |
| | | font-size: 12px; |
| | | font-size: 11px; |
| | | font-weight: 600; |
| | | color: $deep-blue; |
| | | background: rgba(255, 255, 255, 0.95); |
| | |
| | | } |
| | | |
| | | .assistant-base { |
| | | position: absolute; |
| | | bottom: 0; |
| | | left: 50%; |
| | | transform: translateX(-50%); |
| | | border-radius: 50%; |
| | | border: 2px solid rgba(255, 93, 122, 0.22); |
| | | background: radial-gradient(circle, rgba(255, 255, 255, 0.9) 0%, rgba(255, 111, 145, 0.1) 70%, transparent 100%); |
| | | } |
| | | |
| | | .assistant-base-lg { |
| | | width: 118px; |
| | | height: 30px; |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .assistant-base-md { |
| | | bottom: 6px; |
| | | width: 88px; |
| | | height: 20px; |
| | | border-color: rgba(255, 93, 122, 0.34); |
| | | bottom: 15px; |
| | | width: 104px; |
| | | height: 22px; |
| | | border-color: rgba(36, 116, 198, 0.48); |
| | | animation: basePulse 2.8s ease-in-out infinite; |
| | | } |
| | | |
| | | .assistant-base-sm { |
| | | bottom: 11px; |
| | | width: 54px; |
| | | height: 10px; |
| | | background: linear-gradient(90deg, rgba(255, 93, 122, 0.95), rgba(255, 173, 188, 0.9)); |
| | | bottom: 20px; |
| | | width: 68px; |
| | | height: 14px; |
| | | background: linear-gradient(90deg, rgba(31, 122, 114, 0.82), rgba(45, 124, 255, 0.9)); |
| | | border: none; |
| | | box-shadow: 0 0 18px rgba(255, 93, 122, 0.38); |
| | | } |
| | | |
| | | @keyframes robotBlink { |
| | | 0%, 44%, 48%, 100% { |
| | | transform: scaleY(1); |
| | | } |
| | | 46% { |
| | | transform: scaleY(0.14); |
| | | } |
| | | } |
| | | |
| | | @keyframes robotBlinkFast { |
| | | 0%, 100% { |
| | | transform: scaleY(1); |
| | | } |
| | | 50% { |
| | | transform: scaleY(0.3); |
| | | } |
| | | } |
| | | |
| | | @keyframes robotTalk { |
| | | 0%, 100% { |
| | | transform: translateX(-50%) scaleX(1); |
| | | } |
| | | 50% { |
| | | transform: translateX(-50%) scaleX(1.35); |
| | | } |
| | | } |
| | | |
| | | @keyframes corePulse { |
| | | 0%, 100% { |
| | | transform: scale(1); |
| | | filter: brightness(1); |
| | | } |
| | | 50% { |
| | | transform: scale(1.08); |
| | | filter: brightness(1.08); |
| | | } |
| | | } |
| | | |
| | | @keyframes coreRotate { |
| | | from { |
| | | transform: rotate(0deg); |
| | | } |
| | | to { |
| | | transform: rotate(360deg); |
| | | } |
| | | box-shadow: 0 0 18px rgba(45, 124, 255, 0.34); |
| | | animation: basePulse 2.2s ease-in-out infinite; |
| | | } |
| | | |
| | | @keyframes orbitRotate { |
| | |
| | | } |
| | | } |
| | | |
| | | .assistant-base-lg { |
| | | width: 142px; |
| | | height: 32px; |
| | | animation: basePulse 3.4s ease-in-out infinite; |
| | | |
| | | &::before { |
| | | content: ''; |
| | | position: absolute; |
| | | left: 50%; |
| | | top: 50%; |
| | | width: 130px; |
| | | height: 130px; |
| | | transform: translate(-50%, -50%); |
| | | border-radius: 50%; |
| | | background: conic-gradient( |
| | | from 180deg, |
| | | transparent 0deg, |
| | | rgba(36, 116, 198, 0.65) 48deg, |
| | | transparent 114deg, |
| | | rgba(36, 116, 198, 0.55) 212deg, |
| | | transparent 286deg, |
| | | rgba(31, 122, 114, 0.45) 334deg, |
| | | transparent 360deg |
| | | ); |
| | | -webkit-mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%); |
| | | mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%); |
| | | opacity: 0.62; |
| | | animation: baseSpin 9s linear infinite; |
| | | } |
| | | } |
| | | |
| | | @keyframes avatarFloat { |
| | | 0%, |
| | | 100% { |
| | | transform: translateY(0); |
| | | } |
| | | 50% { |
| | | transform: translateY(-7px); |
| | | } |
| | | } |
| | | |
| | | @keyframes basePulse { |
| | | 0%, |
| | | 100% { |
| | | transform: translateX(-50%) scale(1); |
| | | opacity: 0.88; |
| | | } |
| | | 50% { |
| | | transform: translateX(-50%) scale(1.05); |
| | | opacity: 0.98; |
| | | } |
| | | } |
| | | |
| | | @keyframes baseSpin { |
| | | from { |
| | | transform: translate(-50%, -50%) rotate(0deg); |
| | | } |
| | | to { |
| | | transform: translate(-50%, -50%) rotate(360deg); |
| | | } |
| | | } |
| | | |
| | | @keyframes baseGlow { |
| | | 0%, |
| | | 100% { |
| | | transform: translateX(-50%) scaleX(1); |
| | | opacity: 0.82; |
| | | } |
| | | 50% { |
| | | transform: translateX(-50%) scaleX(1.06); |
| | | opacity: 0.96; |
| | | } |
| | | } |
| | | |
| | | .welcome-card { |
| | | position: relative; |
| | | padding: 14px 14px 12px; |
| | | align-self: stretch; |
| | | min-height: 206px; |
| | | padding: 9px 10px 8px; |
| | | border-radius: 16px; |
| | | background: |
| | | linear-gradient(#fff, #fff) padding-box, |
| | |
| | | box-shadow: 0 16px 36px rgba(0, 85, 212, 0.12); |
| | | |
| | | &.compact { |
| | | padding: 10px 12px; |
| | | min-height: 160px; |
| | | padding: 8px 9px 7px; |
| | | border-radius: 12px; |
| | | box-shadow: 0 8px 16px rgba(0, 85, 212, 0.07); |
| | | |
| | |
| | | } |
| | | |
| | | .welcome-title { |
| | | font-size: 17px; |
| | | line-height: 1.3; |
| | | font-size: 16px; |
| | | line-height: 1.25; |
| | | |
| | | br { |
| | | display: none; |
| | |
| | | } |
| | | |
| | | .welcome-desc { |
| | | margin-top: 6px; |
| | | font-size: 12px; |
| | | line-height: 1.55; |
| | | margin-top: 4px; |
| | | font-size: 11px; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .quick-prompt-list { |
| | | margin-top: 10px; |
| | | gap: 6px; |
| | | margin-top: 8px; |
| | | gap: 5px; |
| | | } |
| | | |
| | | .quick-prompt-btn { |
| | | padding: 8px 10px; |
| | | font-size: 12px; |
| | | padding: 7px 9px; |
| | | font-size: 11px; |
| | | border-radius: 7px; |
| | | } |
| | | |
| | | .more-prompts-btn { |
| | | margin-top: 8px; |
| | | font-size: 12px; |
| | | margin-top: 6px; |
| | | font-size: 11px; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .welcome-eyebrow { |
| | | font-size: 11px; |
| | | font-size: 10px; |
| | | font-weight: 700; |
| | | letter-spacing: 2px; |
| | | color: rgba(0, 85, 212, 0.58); |
| | | margin-bottom: 8px; |
| | | margin-bottom: 5px; |
| | | } |
| | | |
| | | .welcome-title { |
| | | margin: 0; |
| | | font-size: 26px; |
| | | line-height: 1.2; |
| | | font-size: 20px; |
| | | line-height: 1.15; |
| | | font-weight: 800; |
| | | color: #172033; |
| | | |
| | | br { |
| | | display: none; |
| | | } |
| | | } |
| | | |
| | | .welcome-desc { |
| | | margin: 10px 0 0; |
| | | font-size: 13px; |
| | | line-height: 1.7; |
| | | margin: 5px 0 0; |
| | | font-size: 12px; |
| | | line-height: 1.5; |
| | | color: #5f6980; |
| | | } |
| | | |
| | | .quick-prompt-list { |
| | | display: grid; |
| | | gap: 8px; |
| | | margin-top: 14px; |
| | | gap: 6px; |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .quick-prompt-btn { |
| | | width: 100%; |
| | | border: none; |
| | | border-radius: 10px; |
| | | padding: 11px 14px; |
| | | border-radius: 9px; |
| | | padding: 7px 10px; |
| | | text-align: left; |
| | | font-size: 13px; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | color: #fff; |
| | | cursor: pointer; |
| | |
| | | } |
| | | |
| | | .more-prompts-btn { |
| | | margin-top: 10px; |
| | | padding: 0 12px; |
| | | height: 32px; |
| | | margin-top: 6px; |
| | | padding: 0 10px; |
| | | height: 26px; |
| | | border: 1px solid rgba(208, 65, 81, 0.12); |
| | | border-radius: 999px; |
| | | background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(255, 241, 245, 0.96)); |
| | | color: #d04151; |
| | | font-size: 13px; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | cursor: pointer; |
| | | display: inline-flex; |
| | |
| | | border-color: transparent; |
| | | color: #fff; |
| | | box-shadow: 0 14px 24px rgba(138, 61, 246, 0.18); |
| | | } |
| | | } |
| | | |
| | | .hero-dot-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(14, 1fr); |
| | | gap: 7px; |
| | | padding: 0 18px 14px; |
| | | |
| | | span { |
| | | display: block; |
| | | width: 100%; |
| | | aspect-ratio: 1; |
| | | border-radius: 2px; |
| | | background: linear-gradient(135deg, rgba(255, 110, 138, 0.95), rgba(255, 190, 201, 0.55)); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | .welcome-title { |
| | | font-size: 21px; |
| | | } |
| | | |
| | | .hero-dot-grid { |
| | | grid-template-columns: repeat(12, 1fr); |
| | | gap: 6px; |
| | | padding: 0 14px 12px; |
| | | } |
| | | |
| | | .message-list { |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ShoppingCart } from '@element-plus/icons-vue' |
| | | import AIChatSidebar from '@/components/AIChatSidebar/index.vue' |
| | | import { purchaseAssistant } from '@/components/AIChatSidebar/assistants' |
| | | |
| | | const assistants = [ |
| | | { |
| | | key: 'purchase', |
| | | label: 'éè´å©ç', |
| | | title: 'éè´æºè½å©ç', |
| | | tooltip: 'éè´æºè½å©ç', |
| | | icon: ShoppingCart, |
| | | apiBase: '/purchase-ai', |
| | | storageKey: 'purchase_ai_chat_uuid', |
| | | placeholder: '请è¾å
¥éè´é®é¢... (Enter åé, Shift+Enter æ¢è¡)', |
| | | welcomeMessage: 'ä½ å¥½', |
| | | allowFileUpload: true, |
| | | allowMultipleFileUpload: true, |
| | | fileAnalyzeUrl: '/purchase-ai/analyze-files', |
| | | emptySessionText: 'ææ éè´ä¼è¯' |
| | | } |
| | | ] |
| | | const assistants = [purchaseAssistant] |
| | | </script> |
| | |
| | | <app-main /> |
| | | <settings ref="settingRef" /> |
| | | </div> |
| | | <AIChatSidebar v-if="aiEnabled" /> |
| | | <AIChatSidebar v-if="showGlobalAiChat" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { useWindowSize } from "@vueuse/core"; |
| | | import { useRoute } from "vue-router"; |
| | | import Sidebar from "./components/Sidebar/index.vue"; |
| | | import { AppMain, Navbar, Settings, TagsView } from "./components"; |
| | | import AIChatSidebar from "@/components/AIChatSidebar/index.vue"; |
| | |
| | | |
| | | const settingsStore = useSettingsStore(); |
| | | const userStore = useUserStore(); |
| | | const route = useRoute(); |
| | | const theme = computed(() => settingsStore.theme); |
| | | const sideTheme = computed(() => settingsStore.sideTheme); |
| | | const sidebar = computed(() => useAppStore().sidebar); |
| | |
| | | const needTagsView = computed(() => settingsStore.tagsView); |
| | | const fixedHeader = computed(() => settingsStore.fixedHeader); |
| | | const aiEnabled = computed(() => Number(userStore.aiEnabled) === 1); |
| | | const showGlobalAiChat = computed(() => { |
| | | const isIndustrialBrainRoute = String(route.path || "").startsWith("/ai-industrial-brain"); |
| | | return !isIndustrialBrainRoute && aiEnabled.value; |
| | | }); |
| | | |
| | | const classObj = computed(() => ({ |
| | | hideSidebar: !sidebar.value.opened, |
| | |
| | | ], |
| | | }, |
| | | { |
| | | path: "/ai-industrial-brain", |
| | | component: Layout, |
| | | children: [ |
| | | { |
| | | path: "index", |
| | | component: () => import("@/views/aiIndustrialBrain/index.vue"), |
| | | name: "AiIndustrialBrain", |
| | | meta: { title: "AIå·¥ä¸å¤§è", icon: "skill" }, |
| | | }, |
| | | ], |
| | | }, |
| | | { |
| | | path: "/user", |
| | | component: Layout, |
| | | hidden: true, |
| | |
| | | ], |
| | | }, |
| | | // è´¢å¡ç®¡ç模åè·¯ç± |
| | | // { |
| | | // path: "/financial", |
| | | // component: Layout, |
| | | // hidden: false, |
| | | // redirect: "/financial/general-ledger", |
| | | // alwaysShow: true, |
| | | // meta: { title: "è´¢å¡ç®¡ç", icon: "money" }, |
| | | // children: [ |
| | | // { |
| | | // path: "general-ledger", |
| | | // component: () => import("@/views/financialManagement/generalLedger/index.vue"), |
| | | // name: "GeneralLedger", |
| | | // meta: { title: "æ»å¸ç§ç®" }, |
| | | // }, |
| | | // { |
| | | // path: "sales-out", |
| | | // component: () => import("@/views/financialManagement/receivable/salesOut.vue"), |
| | | // name: "SalesOut", |
| | | // meta: { title: "éå®åºåº" }, |
| | | // }, |
| | | // { |
| | | // path: "sales-return", |
| | | // component: () => import("@/views/financialManagement/receivable/salesReturn.vue"), |
| | | // name: "SalesReturn", |
| | | // meta: { title: "éå®éè´§" }, |
| | | // }, |
| | | // { |
| | | // path: "receivable-reconciliation", |
| | | // component: () => import("@/views/financialManagement/receivable/reconciliation.vue"), |
| | | // name: "ReceivableReconciliation", |
| | | // meta: { title: "åºæ¶å¯¹è´¦" }, |
| | | // }, |
| | | // { |
| | | // path: "invoice-apply", |
| | | // component: () => import("@/views/financialManagement/receivable/invoiceApply.vue"), |
| | | // name: "InvoiceApply", |
| | | // meta: { title: "å¼ç¥¨ç³è¯·" }, |
| | | // }, |
| | | // { |
| | | // path: "output-invoice", |
| | | // component: () => import("@/views/financialManagement/receivable/outputInvoice.vue"), |
| | | // name: "OutputInvoice", |
| | | // meta: { title: "é项å票" }, |
| | | // }, |
| | | // { |
| | | // path: "receipt", |
| | | // component: () => import("@/views/financialManagement/receivable/receipt.vue"), |
| | | // name: "Receipt", |
| | | // meta: { title: "æ¶æ¬¾å" }, |
| | | // }, |
| | | // { |
| | | // path: "purchase-in", |
| | | // component: () => import("@/views/financialManagement/payable/purchaseIn.vue"), |
| | | // name: "PurchaseIn", |
| | | // meta: { title: "éè´å
¥åº" }, |
| | | // }, |
| | | // { |
| | | // path: "payable-reconciliation", |
| | | // component: () => import("@/views/financialManagement/payable/reconciliation.vue"), |
| | | // name: "PayableReconciliation", |
| | | // meta: { title: "åºä»å¯¹è´¦" }, |
| | | // }, |
| | | // { |
| | | // path: "input-invoice", |
| | | // component: () => import("@/views/financialManagement/payable/input-invoice.vue"), |
| | | // name: "InputInvoice", |
| | | // meta: { title: "è¿é¡¹å票" }, |
| | | // }, |
| | | // { |
| | | // path: "payment-apply", |
| | | // component: () => import("@/views/financialManagement/payable/paymentApply.vue"), |
| | | // name: "PaymentApply", |
| | | // meta: { title: "仿¬¾ç³è¯·" }, |
| | | // }, |
| | | // { |
| | | // path: "payment", |
| | | // component: () => import("@/views/financialManagement/payable/payment.vue"), |
| | | // name: "Payment", |
| | | // meta: { title: "仿¬¾å" }, |
| | | // }, |
| | | // { |
| | | // path: "fixed-assets", |
| | | // component: () => import("@/views/financialManagement/assets/fixedAssets.vue"), |
| | | // name: "FixedAssets", |
| | | // meta: { title: "åºå®èµäº§" }, |
| | | // }, |
| | | // { |
| | | // path: "intangible-assets", |
| | | // component: () => import("@/views/financialManagement/assets/intangibleAssets.vue"), |
| | | // name: "IntangibleAssets", |
| | | // meta: { title: "æ å½¢èµäº§" }, |
| | | // }, |
| | | // { |
| | | // path: "voucher", |
| | | // component: () => import("@/views/financialManagement/voucher/index.vue"), |
| | | // name: "Voucher", |
| | | // meta: { title: "åè¯" }, |
| | | // }, |
| | | // { |
| | | // path: "voucher-general-ledger", |
| | | // component: () => import("@/views/financialManagement/voucher/generalLedger.vue"), |
| | | // name: "VoucherGeneralLedger", |
| | | // meta: { title: "ç§ç®æ»å¸" }, |
| | | // }, |
| | | // { |
| | | // path: "voucher-detail-ledger", |
| | | // component: () => import("@/views/financialManagement/voucher/detailLedger.vue"), |
| | | // name: "VoucherDetailLedger", |
| | | // meta: { title: "ç§ç®æç»å¸" }, |
| | | // }, |
| | | // ], |
| | | // }, |
| | | { |
| | | path: "/financial", |
| | | component: Layout, |
| | | hidden: false, |
| | | redirect: "/financial/general-ledger", |
| | | alwaysShow: true, |
| | | meta: { title: "è´¢å¡ç®¡ç", icon: "money" }, |
| | | children: [ |
| | | { |
| | | path: "sales-out", |
| | | component: () => import("@/views/financialManagement/receivable/salesOut.vue"), |
| | | name: "SalesOut", |
| | | meta: { title: "éå®åºåº" }, |
| | | }, |
| | | { |
| | | path: "sales-return", |
| | | component: () => import("@/views/financialManagement/receivable/salesReturn.vue"), |
| | | name: "SalesReturn", |
| | | meta: { title: "éå®éè´§" }, |
| | | }, |
| | | |
| | | { |
| | | path: "invoice-apply", |
| | | component: () => import("@/views/financialManagement/receivable/invoiceApply.vue"), |
| | | name: "InvoiceApply", |
| | | meta: { title: "å¼ç¥¨ç³è¯·" }, |
| | | }, |
| | | { |
| | | path: "output-invoice", |
| | | component: () => import("@/views/financialManagement/receivable/outputInvoice.vue"), |
| | | name: "OutputInvoice", |
| | | meta: { title: "é项å票" }, |
| | | }, |
| | | { |
| | | path: "receipt", |
| | | component: () => import("@/views/financialManagement/receivable/receipt.vue"), |
| | | name: "Receipt", |
| | | meta: { title: "æ¶æ¬¾å" }, |
| | | }, |
| | | { |
| | | path: "receivable-reconciliation", |
| | | component: () => import("@/views/financialManagement/receivable/reconciliation.vue"), |
| | | name: "ReceivableReconciliation", |
| | | meta: { title: "åºæ¶å¯¹è´¦" }, |
| | | }, |
| | | { |
| | | path: "purchase-in", |
| | | component: () => import("@/views/financialManagement/payable/purchaseIn.vue"), |
| | | name: "PurchaseIn", |
| | | meta: { title: "éè´å
¥åº" }, |
| | | }, |
| | | |
| | | { |
| | | path: "input-invoice", |
| | | component: () => import("@/views/financialManagement/payable/input-invoice.vue"), |
| | | name: "InputInvoice", |
| | | meta: { title: "è¿é¡¹å票" }, |
| | | }, |
| | | { |
| | | path: "payment-apply", |
| | | component: () => import("@/views/financialManagement/payable/paymentApply.vue"), |
| | | name: "PaymentApply", |
| | | meta: { title: "仿¬¾ç³è¯·" }, |
| | | }, |
| | | |
| | | { |
| | | path: "payment", |
| | | component: () => import("@/views/financialManagement/payable/payment.vue"), |
| | | name: "Payment", |
| | | meta: { title: "仿¬¾å" }, |
| | | }, |
| | | { |
| | | path: "payable-reconciliation", |
| | | component: () => import("@/views/financialManagement/payable/reconciliation.vue"), |
| | | name: "PayableReconciliation", |
| | | meta: { title: "åºä»å¯¹è´¦" }, |
| | | }, |
| | | { |
| | | path: "fixed-assets", |
| | | component: () => import("@/views/financialManagement/assets/fixedAssets.vue"), |
| | | name: "FixedAssets", |
| | | meta: { title: "åºå®èµäº§" }, |
| | | }, |
| | | { |
| | | path: "intangible-assets", |
| | | component: () => import("@/views/financialManagement/assets/intangibleAssets.vue"), |
| | | name: "IntangibleAssets", |
| | | meta: { title: "æ å½¢èµäº§" }, |
| | | }, |
| | | { |
| | | path: "general-ledger", |
| | | component: () => import("@/views/financialManagement/generalLedger/index.vue"), |
| | | name: "GeneralLedger", |
| | | meta: { title: "æ»å¸ç§ç®" }, |
| | | }, |
| | | { |
| | | path: "voucher", |
| | | component: () => import("@/views/financialManagement/voucher/index.vue"), |
| | | name: "Voucher", |
| | | meta: { title: "åè¯" }, |
| | | }, |
| | | { |
| | | path: "voucher-general-ledger", |
| | | component: () => import("@/views/financialManagement/voucher/generalLedger.vue"), |
| | | name: "VoucherGeneralLedger", |
| | | meta: { title: "ç§ç®æ»å¸" }, |
| | | }, |
| | | { |
| | | path: "voucher-detail-ledger", |
| | | component: () => import("@/views/financialManagement/voucher/detailLedger.vue"), |
| | | name: "VoucherDetailLedger", |
| | | meta: { title: "ç§ç®æç»å¸" }, |
| | | }, |
| | | ], |
| | | }, |
| | | ]; |
| | | |
| | | // å¨æè·¯ç±ï¼åºäºç¨æ·æé卿å»å è½½ |
| | |
| | | const defaultData = JSON.parse(JSON.stringify(rawRoutes)) |
| | | const sidebarRoutes = filterAsyncRouter(sdata)
|
| | | const rewriteRoutes = filterAsyncRouter(rdata, false, true)
|
| | | const defaultRoutes = filterAsyncRouter(defaultData)
|
| | | const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
|
| | | asyncRoutes.forEach(route => { router.addRoute(route) })
|
| | | this.setRoutes(rewriteRoutes)
|
| | | // å°è´¢å¡ç®¡çè·¯ç±åå¹¶å°ä¾§è¾¹æ
|
| | | this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
|
| | | this.setDefaultRoutes(sidebarRoutes)
|
| | | this.setTopbarRoutes(defaultRoutes)
|
| | | resolve(rewriteRoutes)
|
| | | })
|
| | | })
|
| | | const defaultRoutes = filterAsyncRouter(defaultData) |
| | | const asyncRoutes = filterDynamicRoutes(dynamicRoutes) |
| | | asyncRoutes.forEach(route => { router.addRoute(route) }) |
| | | this.setRoutes(rewriteRoutes) |
| | | const constantSidebarRoutes = filterAiFeatureRoutes(constantRoutes, aiEnabled) |
| | | // å°è´¢å¡ç®¡çè·¯ç±åå¹¶å°ä¾§è¾¹æ |
| | | this.setSidebarRouters(constantSidebarRoutes.concat(sidebarRoutes)) |
| | | this.setDefaultRoutes(sidebarRoutes) |
| | | this.setTopbarRoutes(defaultRoutes) |
| | | resolve(rewriteRoutes) |
| | | }) |
| | | }) |
| | | }
|
| | | }
|
| | | })
|
| | |
| | | })
|
| | | }
|
| | |
|
| | | function filterChildren(childrenMap, lastRouter = false) {
|
| | | function filterChildren(childrenMap, lastRouter = false) { |
| | | var children = []
|
| | | childrenMap.forEach(el => {
|
| | | el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path
|
| | |
| | | children.push(el)
|
| | | }
|
| | | })
|
| | | return children
|
| | | }
|
| | |
|
| | | // å¨æè·¯ç±éåï¼éªè¯æ¯å¦å
·å¤æé
|
| | | export function filterDynamicRoutes(routes) {
|
| | | return children |
| | | } |
| | | |
| | | // å¨æè·¯ç±éåï¼éªè¯æ¯å¦å
·å¤æé |
| | | export function filterDynamicRoutes(routes) { |
| | | const res = []
|
| | | routes.forEach(route => {
|
| | | if (route.permissions) {
|
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <transition name="fade"> |
| | | <section v-if="visible" class="assistant-workspace"> |
| | | <div class="assistant-workspace__panel"> |
| | | <button |
| | | v-if="assistantMode === 'pending'" |
| | | type="button" |
| | | class="workspace-back-btn" |
| | | @click="$emit('close')" |
| | | > |
| | | <el-icon><ArrowLeftBold /></el-icon> |
| | | <span>è¿åå·¥ä¸å¤§å±</span> |
| | | </button> |
| | | |
| | | <div class="assistant-workspace__body"> |
| | | <AIChatSidebar |
| | | v-if="assistantMode !== 'pending'" |
| | | :key="assistantMode" |
| | | class="workspace-chat" |
| | | :assistants="assistantMode === 'purchase' ? [purchaseAssistant] : [generalAssistant]" |
| | | :default-assistant="assistantMode" |
| | | :hide-trigger="true" |
| | | :auto-open="true" |
| | | drawer-size="100%" |
| | | drawer-direction="ttb" |
| | | header-extra-action-text="è¿åå·¥ä¸å¤§å±" |
| | | @header-extra-action="$emit('close')" |
| | | /> |
| | | |
| | | <div v-else class="workspace-pending"> |
| | | <div class="workspace-pending__content"> |
| | | <h3>{{ agentTitle }}</h3> |
| | | <p>æ£å¨å¼åï¼æ¬è¯·æå¾
......</p> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | </transition> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | import { ArrowLeftBold } from "@element-plus/icons-vue"; |
| | | import AIChatSidebar from "@/components/AIChatSidebar/index.vue"; |
| | | import { generalAssistant, purchaseAssistant } from "@/components/AIChatSidebar/assistants"; |
| | | |
| | | const props = defineProps({ |
| | | visible: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | agent: { |
| | | type: Object, |
| | | default: () => ({}), |
| | | }, |
| | | }); |
| | | |
| | | defineEmits(["close"]); |
| | | |
| | | const agentKey = computed(() => String(props.agent?.key || "")); |
| | | const agentTitle = computed(() => String(props.agent?.name || "AI婿")); |
| | | const assistantMode = computed(() => { |
| | | if (agentKey.value === "purchase") return "purchase"; |
| | | if (agentKey.value === "general") return "general"; |
| | | return "pending"; |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .assistant-workspace { |
| | | position: fixed; |
| | | inset: 0; |
| | | z-index: 2100; |
| | | padding: 12px; |
| | | background: rgba(33, 49, 63, 0.24); |
| | | backdrop-filter: blur(2px); |
| | | } |
| | | |
| | | .assistant-workspace__panel { |
| | | position: relative; |
| | | height: 100%; |
| | | border-radius: 22px; |
| | | border: 1px solid var(--surface-border); |
| | | background: linear-gradient(180deg, #f9fcfb 0%, #f0f5f2 100%); |
| | | box-shadow: var(--shadow-md); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .assistant-workspace__body { |
| | | height: 100%; |
| | | min-height: 100%; |
| | | } |
| | | |
| | | .workspace-back-btn { |
| | | position: absolute; |
| | | top: 16px; |
| | | right: 20px; |
| | | z-index: 5; |
| | | height: 36px; |
| | | padding: 0 14px; |
| | | border: 1px solid rgba(38, 112, 183, 0.3); |
| | | border-radius: 10px; |
| | | background: rgba(255, 255, 255, 0.92); |
| | | color: #25528f; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | } |
| | | |
| | | .workspace-back-btn:hover { |
| | | border-color: rgba(31, 122, 114, 0.45); |
| | | color: #1f5ddf; |
| | | box-shadow: 0 8px 16px rgba(31, 122, 114, 0.14); |
| | | transform: translateY(-1px); |
| | | } |
| | | |
| | | .workspace-chat { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .workspace-chat :deep(.ai-chat-sidebar-wrapper) { |
| | | height: 100%; |
| | | } |
| | | |
| | | .workspace-chat :deep(.ai-chat-drawer) { |
| | | height: 100%; |
| | | } |
| | | |
| | | .workspace-chat :deep(.el-drawer) { |
| | | height: 100% !important; |
| | | width: 100% !important; |
| | | } |
| | | |
| | | .workspace-pending { |
| | | height: 100%; |
| | | display: grid; |
| | | place-items: center; |
| | | padding: 20px; |
| | | color: var(--text-secondary); |
| | | } |
| | | |
| | | .workspace-pending__content { |
| | | display: grid; |
| | | gap: 12px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .workspace-pending__content h3 { |
| | | margin: 0; |
| | | font-size: 36px; |
| | | color: var(--text-primary); |
| | | } |
| | | |
| | | .workspace-pending__content p { |
| | | margin: 0; |
| | | font-size: 24px; |
| | | } |
| | | |
| | | .fade-enter-active, |
| | | .fade-leave-active { |
| | | transition: opacity 0.2s ease; |
| | | } |
| | | |
| | | .fade-enter-from, |
| | | .fade-leave-to { |
| | | opacity: 0; |
| | | } |
| | | |
| | | @media (max-width: 1600px) { |
| | | .workspace-back-btn { |
| | | top: 12px; |
| | | right: 14px; |
| | | height: 32px; |
| | | padding: 0 12px; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .workspace-pending__content h3 { |
| | | font-size: 30px; |
| | | } |
| | | |
| | | .workspace-pending__content p { |
| | | font-size: 20px; |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div ref="screenRef" class="ai-brain-screen"> |
| | | <section class="brain-stage"> |
| | | <header class="brain-head"> |
| | | <div class="head-date"> |
| | | <p>{{ weekLabel }}</p> |
| | | <p>{{ dateLabel }}</p> |
| | | </div> |
| | | |
| | | <div class="head-title"> |
| | | <span>AIå·¥ä¸å¤§è</span> |
| | | </div> |
| | | |
| | | <div class="head-actions"> |
| | | <button type="button" class="head-back-btn" @click="goBack"> |
| | | <el-icon><ArrowLeftBold /></el-icon> |
| | | <span>è¿å</span> |
| | | </button> |
| | | </div> |
| | | </header> |
| | | |
| | | <section class="brain-intro"> |
| | | <h2>å·¥ä¸AIæ°ååå·¥ï¼èµè½æºé æ°çºªå
</h2> |
| | | <p>å
大AI婿ååä¼ä¸ç®¡çãéå®ãéè´ãç产ãè´¢å¡åæ°æ®å
¨é¾è·¯</p> |
| | | <div class="intro-sign">é¿éäº Ã åé®å¤§æ¨¡å à æºè½ä½AI</div> |
| | | </section> |
| | | |
| | | <section class="carousel-area"> |
| | | <button type="button" class="nav-btn nav-btn--left" @click="prevCard"> |
| | | <el-icon><ArrowLeftBold /></el-icon> |
| | | </button> |
| | | |
| | | <div class="carousel-track"> |
| | | <article |
| | | v-for="card in visibleCards" |
| | | :key="card.agent.key" |
| | | class="agent-card" |
| | | :class="{ 'agent-card--active': card.offset === 0 }" |
| | | :style="getCardStyle(card.offset)" |
| | | @click="openAssistant(card.realIndex)" |
| | | > |
| | | <div class="agent-card__head" :class="{ 'agent-card__head--active': card.offset === 0 }"> |
| | | {{ card.agent.name }} |
| | | </div> |
| | | |
| | | <div class="agent-card__body" :class="{ 'agent-card__body--active': card.offset === 0 }"> |
| | | <div class="avatar-shell" :class="{ 'avatar-shell--active': card.offset === 0 }"> |
| | | <div class="avatar-base"></div> |
| | | <div class="avatar-cut"> |
| | | <img v-if="card.agent.avatar" class="avatar-cut__img" :src="card.agent.avatar" :alt="card.agent.name" /> |
| | | </div> |
| | | </div> |
| | | <div v-if="card.offset === 0" class="highlight-list"> |
| | | <div |
| | | v-for="highlight in card.agent.highlights" |
| | | :key="highlight" |
| | | class="highlight-item" |
| | | > |
| | | {{ highlight }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </article> |
| | | </div> |
| | | |
| | | <button type="button" class="nav-btn nav-btn--right" @click="nextCard"> |
| | | <el-icon><ArrowRightBold /></el-icon> |
| | | </button> |
| | | </section> |
| | | |
| | | <section class="brain-footer"> |
| | | <div class="footer-grid-overlay"></div> |
| | | |
| | | <div class="footer-metrics"> |
| | | <article class="footer-metric"> |
| | | <span class="footer-metric__label">å¨çº¿æºè½ä½</span> |
| | | <strong class="footer-metric__value">{{ agents.length }}个</strong> |
| | | <small class="footer-metric__hint">å
¨é¾è·¯ååè¿è¡</small> |
| | | </article> |
| | | |
| | | <article class="footer-metric footer-metric--focus"> |
| | | <span class="footer-metric__label">å½åç¦ç¹</span> |
| | | <strong class="footer-metric__value">{{ getFooterAgentName(focusAgent.name) }}</strong> |
| | | <small class="footer-metric__hint">{{ focusAgent.highlights?.[0] || "æºè½åæèå¨" }}</small> |
| | | </article> |
| | | |
| | | <article class="footer-metric footer-metric--period"> |
| | | <span class="footer-metric__label">è½®æå¨æ</span> |
| | | <strong class="footer-metric__value">{{ carouselSecondsText }}</strong> |
| | | <div class="footer-period-control"> |
| | | <button type="button" class="period-btn" @click="adjustCarouselSeconds(-0.5)">-</button> |
| | | <input |
| | | v-model.number="carouselSeconds" |
| | | class="period-input" |
| | | type="number" |
| | | min="2" |
| | | max="12" |
| | | step="0.5" |
| | | /> |
| | | <span class="period-unit">s</span> |
| | | <button type="button" class="period-btn" @click="adjustCarouselSeconds(0.5)">+</button> |
| | | </div> |
| | | <input |
| | | v-model.number="carouselSeconds" |
| | | class="footer-period-slider" |
| | | type="range" |
| | | min="2" |
| | | max="12" |
| | | step="0.5" |
| | | /> |
| | | <small class="footer-metric__hint">坿å¨è®¾ç½® 2.0s - 12.0s</small> |
| | | </article> |
| | | </div> |
| | | |
| | | <div class="footer-rail"> |
| | | <div class="footer-rail__line"> |
| | | <span class="footer-rail__flow"></span> |
| | | </div> |
| | | <div class="footer-rail__nodes"> |
| | | <button |
| | | v-for="node in footerNodes" |
| | | :key="node.key" |
| | | type="button" |
| | | class="footer-node" |
| | | :class="{ 'footer-node--active': node.index === carouselIndex }" |
| | | @click="openAssistant(node.index)" |
| | | > |
| | | <span class="footer-node__dot"></span> |
| | | <span class="footer-node__name">{{ getFooterAgentName(node.name) }}</span> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | </section> |
| | | |
| | | <AiAssistantWorkspace |
| | | :visible="fullscreenVisible" |
| | | :agent="currentAgent" |
| | | @close="closeFullscreen" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"; |
| | | import { useRouter } from "vue-router"; |
| | | import { ArrowLeftBold, ArrowRightBold } from "@element-plus/icons-vue"; |
| | | import AiAssistantWorkspace from "./components/AiAssistantWorkspace.vue"; |
| | | import todoAvatar from "@/assets/AI/å¾
å婿.png"; |
| | | import salesAvatar from "@/assets/AI/éå®å©æ.png"; |
| | | import purchaseAvatar from "@/assets/AI/éè´å©æ.png"; |
| | | import productionAvatar from "@/assets/AI/çäº§å©æ.png"; |
| | | import financeAvatar from "@/assets/AI/è´¢å¡å©æ.png"; |
| | | |
| | | const router = useRouter(); |
| | | |
| | | const agents = [ |
| | | { |
| | | key: "general", |
| | | name: "AIå¾
å婿", |
| | | highlights: ["è·¨æ¨¡åæµç¨è¯æ", "ç»è¥é£é©æºè½æé"], |
| | | }, |
| | | { |
| | | key: "sales", |
| | | name: "AIéå®å©æ", |
| | | highlights: ["å®¢æ·æµå¤±é£é©åæ", "忬¾ä¸æ¥ä»·çç¥å»ºè®®"], |
| | | }, |
| | | { |
| | | key: "purchase", |
| | | name: "AIéè´å©æ", |
| | | highlights: ["ä¾åºé¾ææ åæ", "éè´è®¢åæºè½çæ"], |
| | | }, |
| | | { |
| | | key: "production", |
| | | name: "AIçäº§å©æ", |
| | | highlights: ["å·¥åºç¶é¢å®ä½", "产è½ä¸æ¥åºæºè½é¢è¦"], |
| | | }, |
| | | { |
| | | key: "finance", |
| | | name: "AIè´¢å¡å©æ", |
| | | highlights: ["ç°éæµååé¢å¤", "è´¹ç¨ç»ææºè½åæ"], |
| | | }, |
| | | ]; |
| | | |
| | | const avatarByAgentKey = { |
| | | general: todoAvatar, |
| | | sales: salesAvatar, |
| | | purchase: purchaseAvatar, |
| | | production: productionAvatar, |
| | | finance: financeAvatar, |
| | | }; |
| | | |
| | | for (let i = agents.length - 1; i >= 0; i -= 1) { |
| | | const agent = agents[i]; |
| | | const avatar = avatarByAgentKey[agent.key]; |
| | | if (!avatar) { |
| | | agents.splice(i, 1); |
| | | continue; |
| | | } |
| | | agent.avatar = avatar; |
| | | } |
| | | |
| | | const carouselIndex = ref(Math.min(2, Math.max(agents.length - 1, 0))); |
| | | const fullscreenVisible = ref(false); |
| | | const screenRef = ref(null); |
| | | const carouselIntervalMs = ref(4500); |
| | | |
| | | let carouselTimer = null; |
| | | |
| | | const fallbackAgent = { |
| | | key: "fallback", |
| | | name: "AI婿", |
| | | avatar: "", |
| | | highlights: [], |
| | | }; |
| | | |
| | | const currentAgent = computed(() => agents[carouselIndex.value] || agents[0] || fallbackAgent); |
| | | const focusAgent = computed(() => currentAgent.value || fallbackAgent); |
| | | const footerNodes = computed(() => |
| | | agents.map((agent, index) => ({ |
| | | key: agent.key, |
| | | name: agent.name, |
| | | index, |
| | | })) |
| | | ); |
| | | const carouselSeconds = computed({ |
| | | get: () => Number((carouselIntervalMs.value / 1000).toFixed(1)), |
| | | set: (value) => { |
| | | const next = Number(value); |
| | | if (!Number.isFinite(next)) return; |
| | | const clamped = Math.max(2, Math.min(12, Math.round(next * 2) / 2)); |
| | | carouselIntervalMs.value = Math.round(clamped * 1000); |
| | | }, |
| | | }); |
| | | const carouselSecondsText = computed(() => `${carouselSeconds.value.toFixed(1)}s`); |
| | | |
| | | const weekLabel = computed(() => { |
| | | const weekMap = ["æææ¥", "ææä¸", "ææäº", "ææä¸", "ææå", "ææäº", "ææå
"]; |
| | | return weekMap[new Date().getDay()]; |
| | | }); |
| | | |
| | | const dateLabel = computed(() => { |
| | | const now = new Date(); |
| | | const year = now.getFullYear(); |
| | | const month = String(now.getMonth() + 1).padStart(2, "0"); |
| | | const day = String(now.getDate()).padStart(2, "0"); |
| | | return `${year}å¹´${month}æ${day}æ¥`; |
| | | }); |
| | | |
| | | const visibleCards = computed(() => { |
| | | const total = agents.length; |
| | | return agents |
| | | .map((agent, index) => { |
| | | let offset = index - carouselIndex.value; |
| | | if (offset > total / 2) offset -= total; |
| | | if (offset < -total / 2) offset += total; |
| | | return { agent, offset, realIndex: index }; |
| | | }) |
| | | .filter((item) => Math.abs(item.offset) <= 2) |
| | | .sort((a, b) => a.offset - b.offset); |
| | | }); |
| | | |
| | | function getCardStyle(offset) { |
| | | const distance = Math.abs(offset); |
| | | const scale = distance === 0 ? 1 : distance === 1 ? 0.88 : 0.78; |
| | | const opacity = distance === 0 ? 1 : distance === 1 ? 0.92 : 0.76; |
| | | return { |
| | | transform: `translateX(${offset * 340}px) scale(${scale})`, |
| | | zIndex: String(50 - distance), |
| | | opacity, |
| | | }; |
| | | } |
| | | |
| | | function getFooterAgentName(name) { |
| | | return String(name || "AI婿").replace(/^AI/, ""); |
| | | } |
| | | |
| | | function adjustCarouselSeconds(delta) { |
| | | carouselSeconds.value = carouselSeconds.value + delta; |
| | | } |
| | | |
| | | function prevCard() { |
| | | const total = agents.length; |
| | | if (!total) return; |
| | | carouselIndex.value = (carouselIndex.value - 1 + total) % total; |
| | | } |
| | | |
| | | function nextCard() { |
| | | const total = agents.length; |
| | | if (!total) return; |
| | | carouselIndex.value = (carouselIndex.value + 1) % total; |
| | | } |
| | | |
| | | async function enterBrowserFullscreen() { |
| | | if (document.fullscreenElement) return; |
| | | const target = screenRef.value || document.documentElement; |
| | | if (!target || typeof target.requestFullscreen !== "function") return; |
| | | try { |
| | | await target.requestFullscreen(); |
| | | } catch (error) { |
| | | // Ignore: browser may block fullscreen when there is no direct user activation. |
| | | } |
| | | } |
| | | |
| | | async function exitBrowserFullscreen() { |
| | | if (!document.fullscreenElement || typeof document.exitFullscreen !== "function") return; |
| | | try { |
| | | await document.exitFullscreen(); |
| | | } catch (error) { |
| | | // Ignore fullscreen exit failures. |
| | | } |
| | | } |
| | | |
| | | function goBack() { |
| | | closeFullscreen(); |
| | | exitBrowserFullscreen(); |
| | | if (window.history.length > 1) { |
| | | router.back(); |
| | | return; |
| | | } |
| | | router.push("/index"); |
| | | } |
| | | |
| | | function openAssistant(index) { |
| | | if (!agents.length) return; |
| | | carouselIndex.value = index; |
| | | fullscreenVisible.value = true; |
| | | } |
| | | |
| | | function closeFullscreen() { |
| | | fullscreenVisible.value = false; |
| | | } |
| | | |
| | | function startCarousel() { |
| | | stopCarousel(); |
| | | if (fullscreenVisible.value) return; |
| | | carouselTimer = window.setInterval(() => { |
| | | nextCard(); |
| | | }, carouselIntervalMs.value); |
| | | } |
| | | |
| | | function stopCarousel() { |
| | | if (carouselTimer) { |
| | | window.clearInterval(carouselTimer); |
| | | carouselTimer = null; |
| | | } |
| | | } |
| | | |
| | | function handleEscClose(event) { |
| | | if (event.key === "Escape" && fullscreenVisible.value) { |
| | | closeFullscreen(); |
| | | } |
| | | } |
| | | |
| | | watch( |
| | | () => fullscreenVisible.value, |
| | | (opened) => { |
| | | if (opened) { |
| | | stopCarousel(); |
| | | } else { |
| | | startCarousel(); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | watch( |
| | | () => carouselIntervalMs.value, |
| | | () => { |
| | | if (!fullscreenVisible.value) { |
| | | startCarousel(); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | onMounted(() => { |
| | | startCarousel(); |
| | | window.addEventListener("keydown", handleEscClose); |
| | | window.requestAnimationFrame(() => { |
| | | enterBrowserFullscreen(); |
| | | }); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | stopCarousel(); |
| | | window.removeEventListener("keydown", handleEscClose); |
| | | exitBrowserFullscreen(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .ai-brain-screen { |
| | | position: fixed; |
| | | inset: 0; |
| | | z-index: 1900; |
| | | padding: 10px; |
| | | overflow: hidden; |
| | | background: var(--app-bg); |
| | | } |
| | | |
| | | .brain-stage { |
| | | position: relative; |
| | | height: 100%; |
| | | min-height: 100%; |
| | | border-radius: 22px; |
| | | border: 1px solid var(--surface-border); |
| | | background: |
| | | radial-gradient(circle at 14% 8%, rgba(31, 122, 114, 0.14), transparent 40%), |
| | | radial-gradient(circle at 86% 12%, rgba(30, 91, 255, 0.1), transparent 42%), |
| | | linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(245, 249, 247, 0.94)), |
| | | repeating-linear-gradient( |
| | | 135deg, |
| | | rgba(255, 255, 255, 0.05) 0, |
| | | rgba(255, 255, 255, 0.05) 14px, |
| | | rgba(31, 122, 114, 0.03) 14px, |
| | | rgba(31, 122, 114, 0.03) 28px |
| | | ); |
| | | box-shadow: var(--shadow-sm); |
| | | } |
| | | |
| | | .brain-head { |
| | | display: grid; |
| | | grid-template-columns: 220px minmax(0, 1fr) 180px; |
| | | align-items: center; |
| | | padding: 12px 18px 0; |
| | | } |
| | | |
| | | .head-date { |
| | | color: var(--text-secondary); |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .head-date p { |
| | | margin: 0; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .head-title { |
| | | justify-self: center; |
| | | width: min(760px, 95%); |
| | | height: 68px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 0 0 46px 46px; |
| | | color: #fff; |
| | | font-size: 42px; |
| | | font-style: italic; |
| | | font-weight: 700; |
| | | letter-spacing: 1px; |
| | | background: linear-gradient(135deg, #1f7a72 0%, #1e5bff 100%); |
| | | box-shadow: 0 16px 30px rgba(31, 122, 114, 0.24); |
| | | } |
| | | |
| | | .head-actions { |
| | | justify-self: end; |
| | | } |
| | | |
| | | .head-back-btn { |
| | | height: 40px; |
| | | padding: 0 14px; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | border: none; |
| | | border-radius: 999px; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: var(--colorPrimary); |
| | | background: var(--surface-base); |
| | | box-shadow: 0 8px 18px rgba(31, 49, 38, 0.12); |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .brain-intro { |
| | | text-align: center; |
| | | margin-top: 34px; |
| | | } |
| | | |
| | | .brain-intro h2 { |
| | | margin: 0; |
| | | font-size: 44px; |
| | | font-style: italic; |
| | | font-weight: 700; |
| | | color: var(--text-primary); |
| | | } |
| | | |
| | | .brain-intro p { |
| | | margin: 12px 0 10px; |
| | | font-size: 28px; |
| | | color: var(--text-secondary); |
| | | } |
| | | |
| | | .intro-sign { |
| | | display: inline-block; |
| | | padding: 6px 18px; |
| | | border-radius: 999px; |
| | | font-size: 24px; |
| | | font-weight: 700; |
| | | color: #1e5bff; |
| | | background: rgba(255, 255, 255, 0.82); |
| | | border: 1px solid rgba(30, 91, 255, 0.18); |
| | | } |
| | | |
| | | .carousel-area { |
| | | position: relative; |
| | | margin-top: 34px; |
| | | padding: 0 72px 12px; |
| | | } |
| | | |
| | | .carousel-track { |
| | | position: relative; |
| | | height: 500px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .brain-footer { |
| | | position: relative; |
| | | margin: 0 72px; |
| | | height: clamp(226px, 25vh,0); |
| | | border-radius: 18px; |
| | | border: 1px solid rgba(31, 122, 114, 0.28); |
| | | background: |
| | | linear-gradient(120deg, rgba(31, 122, 114, 0.14), rgba(30, 91, 255, 0.14)), |
| | | linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(236, 244, 249, 0.9)); |
| | | box-shadow: |
| | | 0 16px 34px rgba(31, 81, 131, 0.12), |
| | | inset 0 1px 0 rgba(255, 255, 255, 0.72); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .brain-footer::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: -22%; |
| | | bottom: -120%; |
| | | width: 52%; |
| | | height: 260%; |
| | | background: radial-gradient(ellipse at center, rgba(30, 91, 255, 0.2) 0%, rgba(30, 91, 255, 0) 72%); |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .brain-footer::after { |
| | | content: ""; |
| | | position: absolute; |
| | | inset: 0; |
| | | background: linear-gradient(110deg, transparent 12%, rgba(255, 255, 255, 0.24) 38%, transparent 64%); |
| | | transform: translateX(-120%); |
| | | animation: footerSweep 5.8s linear infinite; |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .footer-grid-overlay { |
| | | position: absolute; |
| | | inset: 0; |
| | | background: |
| | | repeating-linear-gradient( |
| | | 90deg, |
| | | rgba(31, 122, 114, 0.07) 0, |
| | | rgba(31, 122, 114, 0.07) 1px, |
| | | transparent 1px, |
| | | transparent 36px |
| | | ), |
| | | repeating-linear-gradient( |
| | | 0deg, |
| | | rgba(30, 91, 255, 0.06) 0, |
| | | rgba(30, 91, 255, 0.06) 1px, |
| | | transparent 1px, |
| | | transparent 28px |
| | | ); |
| | | opacity: 0.72; |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .footer-metrics { |
| | | position: relative; |
| | | z-index: 2; |
| | | padding: 14px 20px 72px; |
| | | display: grid; |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | gap: 12px; |
| | | } |
| | | |
| | | .footer-metric { |
| | | min-height: 76px; |
| | | border-radius: 12px; |
| | | padding: 10px 14px; |
| | | border: 1px solid rgba(37, 124, 188, 0.2); |
| | | background: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(245, 250, 255, 0.82)); |
| | | box-shadow: 0 10px 18px rgba(29, 83, 134, 0.08); |
| | | display: grid; |
| | | grid-template-rows: auto auto 1fr; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .footer-metric--focus { |
| | | border-color: rgba(38, 122, 194, 0.34); |
| | | box-shadow: |
| | | 0 12px 22px rgba(30, 91, 255, 0.12), |
| | | inset 0 0 0 1px rgba(85, 148, 232, 0.2); |
| | | } |
| | | |
| | | .footer-metric__label { |
| | | font-size: 14px; |
| | | color: rgba(38, 72, 108, 0.88); |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .footer-metric__value { |
| | | font-size: 30px; |
| | | line-height: 1; |
| | | font-style: italic; |
| | | font-weight: 700; |
| | | color: #1f5ddf; |
| | | text-shadow: 0 3px 10px rgba(30, 91, 255, 0.18); |
| | | } |
| | | |
| | | .footer-metric__hint { |
| | | margin-top: auto; |
| | | font-size: 13px; |
| | | color: rgba(52, 89, 128, 0.82); |
| | | } |
| | | |
| | | .footer-metric--period .footer-metric__hint { |
| | | margin-top: 0; |
| | | line-height: 1.25; |
| | | } |
| | | |
| | | .footer-metric--period { |
| | | min-height: 122px; |
| | | grid-template-rows: auto auto auto auto auto; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .footer-period-control { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .period-btn { |
| | | width: 22px; |
| | | height: 22px; |
| | | border-radius: 50%; |
| | | border: 1px solid rgba(38, 112, 183, 0.28); |
| | | background: rgba(255, 255, 255, 0.9); |
| | | color: #2054c9; |
| | | font-size: 14px; |
| | | font-weight: 700; |
| | | line-height: 1; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .period-input { |
| | | width: 56px; |
| | | height: 24px; |
| | | border-radius: 8px; |
| | | border: 1px solid rgba(38, 112, 183, 0.24); |
| | | background: rgba(255, 255, 255, 0.94); |
| | | color: #1f5ddf; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | text-align: center; |
| | | padding: 0 4px; |
| | | } |
| | | |
| | | .period-unit { |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | color: rgba(40, 80, 117, 0.86); |
| | | } |
| | | |
| | | .footer-period-slider { |
| | | width: min(250px, 100%); |
| | | height: 3px; |
| | | accent-color: #2a6ded; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .footer-rail { |
| | | position: absolute; |
| | | left: 20px; |
| | | right: 20px; |
| | | bottom: 18px; |
| | | z-index: 2; |
| | | } |
| | | |
| | | .footer-rail__line { |
| | | position: relative; |
| | | height: 2px; |
| | | border-radius: 999px; |
| | | background: linear-gradient(90deg, rgba(31, 122, 114, 0.12), rgba(30, 91, 255, 0.6), rgba(31, 122, 114, 0.12)); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .footer-rail__flow { |
| | | position: absolute; |
| | | top: -1px; |
| | | left: -18%; |
| | | width: 22%; |
| | | height: 4px; |
| | | border-radius: 999px; |
| | | background: linear-gradient(90deg, rgba(31, 122, 114, 0), rgba(59, 146, 244, 0.92), rgba(30, 91, 255, 0)); |
| | | filter: blur(0.2px); |
| | | animation: railFlow 3.1s ease-in-out infinite; |
| | | } |
| | | |
| | | .footer-rail__nodes { |
| | | margin-top: 12px; |
| | | display: grid; |
| | | grid-template-columns: repeat(5, minmax(0, 1fr)); |
| | | gap: 8px; |
| | | } |
| | | |
| | | .footer-node { |
| | | height: 34px; |
| | | border: 1px solid rgba(38, 112, 183, 0.18); |
| | | border-radius: 999px; |
| | | background: rgba(255, 255, 255, 0.76); |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | color: rgba(40, 80, 117, 0.92); |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | cursor: pointer; |
| | | transition: all 0.26s ease; |
| | | } |
| | | |
| | | .footer-node:hover { |
| | | transform: translateY(-1px); |
| | | border-color: rgba(31, 122, 114, 0.34); |
| | | box-shadow: 0 8px 14px rgba(31, 122, 114, 0.14); |
| | | } |
| | | |
| | | .footer-node--active { |
| | | color: #fff; |
| | | border-color: transparent; |
| | | background: linear-gradient(135deg, rgba(31, 122, 114, 0.94), rgba(30, 91, 255, 0.94)); |
| | | box-shadow: 0 10px 18px rgba(30, 91, 255, 0.28); |
| | | } |
| | | |
| | | .footer-node__dot { |
| | | width: 8px; |
| | | height: 8px; |
| | | border-radius: 50%; |
| | | background: rgba(30, 91, 255, 0.72); |
| | | box-shadow: 0 0 10px rgba(30, 91, 255, 0.52); |
| | | } |
| | | |
| | | .footer-node--active .footer-node__dot { |
| | | background: #fff; |
| | | box-shadow: 0 0 12px rgba(255, 255, 255, 0.72); |
| | | animation: nodePulse 1.4s ease-in-out infinite; |
| | | } |
| | | |
| | | .agent-card { |
| | | position: absolute; |
| | | left: 50%; |
| | | top: 0; |
| | | width: 460px; |
| | | margin-left: -230px; |
| | | cursor: pointer; |
| | | transform-origin: center bottom; |
| | | transition: transform 0.35s ease, opacity 0.35s ease; |
| | | } |
| | | |
| | | .agent-card__head { |
| | | height: 56px; |
| | | border-radius: 12px 12px 0 0; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 28px; |
| | | color: #fff; |
| | | font-weight: 700; |
| | | background: linear-gradient(135deg, #1f7a72 0%, #1e5bff 100%); |
| | | } |
| | | |
| | | .agent-card__head--active { |
| | | box-shadow: |
| | | 0 12px 22px rgba(30, 91, 255, 0.26), |
| | | inset 0 0 0 1px rgba(255, 255, 255, 0.28); |
| | | position: relative; |
| | | } |
| | | |
| | | .agent-card__head--active::after { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 12px; |
| | | right: 12px; |
| | | bottom: 6px; |
| | | height: 3px; |
| | | border-radius: 999px; |
| | | background: linear-gradient(90deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.96), rgba(255, 255, 255, 0.22)); |
| | | } |
| | | |
| | | .agent-card__body { |
| | | position: relative; |
| | | height: 430px; |
| | | border: 1px solid var(--surface-border-strong); |
| | | border-top: none; |
| | | border-radius: 0 0 20px 20px; |
| | | background: rgba(255, 255, 255, 0.96); |
| | | overflow: hidden; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: flex-end; |
| | | isolation: isolate; |
| | | box-shadow: 0 12px 24px rgba(31, 49, 38, 0.1); |
| | | } |
| | | |
| | | .agent-card__body--active { |
| | | background: linear-gradient(180deg, rgba(248, 252, 251, 0.96), rgba(225, 241, 250, 0.9)); |
| | | border-color: rgba(31, 122, 114, 0.35); |
| | | } |
| | | |
| | | .agent-card__body--active::after { |
| | | content: ""; |
| | | position: absolute; |
| | | inset: 0; |
| | | background: linear-gradient(108deg, transparent 28%, rgba(255, 255, 255, 0.34) 50%, transparent 72%); |
| | | transform: translateX(-125%); |
| | | animation: bodySweep 3.6s linear infinite; |
| | | pointer-events: none; |
| | | z-index: 1; |
| | | } |
| | | |
| | | .avatar-shell { |
| | | position: relative; |
| | | width: 248px; |
| | | height: 430px; |
| | | display: flex; |
| | | align-items: flex-end; |
| | | justify-content: center; |
| | | --base-core: rgba(53, 143, 222, 0.4); |
| | | --base-ring: rgba(39, 122, 201, 0.62); |
| | | --base-glow: rgba(46, 133, 214, 0.28); |
| | | } |
| | | |
| | | .avatar-shell::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 50%; |
| | | bottom: -10px; |
| | | width: 268px; |
| | | height: 58px; |
| | | transform: translateX(-50%); |
| | | border-radius: 50%; |
| | | background: radial-gradient( |
| | | ellipse at center, |
| | | rgba(55, 140, 219, 0.22) 0%, |
| | | rgba(55, 140, 219, 0.11) 46%, |
| | | rgba(55, 140, 219, 0) 74% |
| | | ); |
| | | filter: blur(2.4px); |
| | | animation: baseGlow 4.6s ease-in-out infinite; |
| | | z-index: 1; |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .avatar-shell::after { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 50%; |
| | | bottom: 0; |
| | | width: 248px; |
| | | height: 46px; |
| | | transform: translateX(-50%); |
| | | border-radius: 50%; |
| | | border: 2px solid var(--base-ring); |
| | | box-shadow: |
| | | inset 0 0 0 1px rgba(255, 255, 255, 0.64), |
| | | 0 0 24px var(--base-glow); |
| | | animation: basePulse 3.1s ease-in-out infinite; |
| | | z-index: 4; |
| | | } |
| | | |
| | | .avatar-base { |
| | | position: absolute; |
| | | left: 50%; |
| | | bottom: 2px; |
| | | width: 224px; |
| | | height: 38px; |
| | | transform: translateX(-50%); |
| | | z-index: 2; |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .avatar-base::before { |
| | | content: ""; |
| | | position: absolute; |
| | | inset: 0; |
| | | border-radius: 50%; |
| | | background: |
| | | radial-gradient( |
| | | ellipse at center, |
| | | rgba(255, 255, 255, 0.96) 0%, |
| | | rgba(255, 255, 255, 0.92) 36%, |
| | | var(--base-core) 68%, |
| | | rgba(38, 118, 195, 0.08) 100% |
| | | ); |
| | | box-shadow: |
| | | 0 0 30px var(--base-core), |
| | | 0 0 10px rgba(255, 255, 255, 0.34) inset; |
| | | z-index: 3; |
| | | } |
| | | |
| | | .avatar-base::after { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 50%; |
| | | top: 50%; |
| | | width: 194px; |
| | | height: 194px; |
| | | transform: translate(-50%, -50%); |
| | | border-radius: 50%; |
| | | background: |
| | | conic-gradient( |
| | | from 180deg, |
| | | transparent 0deg, |
| | | var(--base-ring) 48deg, |
| | | transparent 112deg, |
| | | var(--base-ring) 208deg, |
| | | transparent 284deg, |
| | | rgba(33, 114, 191, 0.48) 332deg, |
| | | transparent 360deg |
| | | ); |
| | | -webkit-mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%); |
| | | mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%); |
| | | opacity: 0.62; |
| | | animation: baseRotate 10.5s linear infinite; |
| | | z-index: 2; |
| | | } |
| | | |
| | | .avatar-shell--active { |
| | | --base-core: rgba(50, 141, 217, 0.52); |
| | | --base-ring: rgba(42, 127, 205, 0.76); |
| | | --base-glow: rgba(38, 130, 211, 0.38); |
| | | } |
| | | |
| | | .avatar-cut { |
| | | position: relative; |
| | | width: 220px; |
| | | height: 430px; |
| | | z-index: 6; |
| | | display: flex; |
| | | align-items: flex-end; |
| | | justify-content: center; |
| | | filter: saturate(1.04) drop-shadow(0 14px 18px rgba(24, 44, 66, 0.14)); |
| | | transform-origin: center 82%; |
| | | animation: avatarFloat 3.2s ease-in-out infinite; |
| | | } |
| | | |
| | | .avatar-cut__img { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: contain; |
| | | object-position: center bottom; |
| | | display: block; |
| | | } |
| | | |
| | | .agent-card--active .avatar-cut { |
| | | animation-duration: 2.6s; |
| | | } |
| | | |
| | | .highlight-list { |
| | | position: absolute; |
| | | right: 10px; |
| | | top: 14px; |
| | | display: grid; |
| | | gap: 8px; |
| | | width: 220px; |
| | | z-index: 16; |
| | | } |
| | | |
| | | .highlight-item { |
| | | border-radius: 10px; |
| | | padding: 8px 10px; |
| | | font-size: 18px; |
| | | line-height: 1.4; |
| | | color: #fff; |
| | | background: rgba(33, 49, 63, 0.92); |
| | | box-shadow: 0 8px 16px rgba(21, 30, 40, 0.22); |
| | | } |
| | | |
| | | .agent-card--active .highlight-item { |
| | | background: rgba(31, 122, 114, 0.9); |
| | | } |
| | | |
| | | .nav-btn { |
| | | position: absolute; |
| | | top: 212px; |
| | | z-index: 80; |
| | | width: 50px; |
| | | height: 50px; |
| | | border-radius: 50%; |
| | | border: none; |
| | | font-size: 30px; |
| | | color: var(--colorPrimary); |
| | | background: var(--surface-base); |
| | | box-shadow: 0 10px 20px rgba(31, 49, 38, 0.16); |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .nav-btn--left { |
| | | left: 14px; |
| | | } |
| | | |
| | | .nav-btn--right { |
| | | right: 14px; |
| | | } |
| | | |
| | | .ai-fullscreen { |
| | | position: fixed; |
| | | inset: 0; |
| | | z-index: 2100; |
| | | padding: 12px; |
| | | background: rgba(33, 49, 63, 0.24); |
| | | backdrop-filter: blur(2px); |
| | | } |
| | | |
| | | .ai-panel { |
| | | height: 100%; |
| | | border-radius: 22px; |
| | | border: 1px solid var(--surface-border); |
| | | background: linear-gradient(180deg, #f9fcfb 0%, #f0f5f2 100%); |
| | | display: grid; |
| | | grid-template-rows: 62px minmax(0, 1fr) 110px; |
| | | box-shadow: var(--shadow-md); |
| | | } |
| | | |
| | | .ai-panel__top { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 0 24px; |
| | | } |
| | | |
| | | .ai-brand { |
| | | font-size: 34px; |
| | | color: var(--text-primary); |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .ai-close { |
| | | width: 40px; |
| | | height: 40px; |
| | | border: none; |
| | | border-radius: 50%; |
| | | background: transparent; |
| | | font-size: 30px; |
| | | color: var(--text-secondary); |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .ai-panel__center { |
| | | padding: 8px 20px 10px; |
| | | display: grid; |
| | | grid-template-rows: 120px 290px minmax(0, 1fr); |
| | | gap: 10px; |
| | | min-height: 0; |
| | | } |
| | | |
| | | .welcome-card { |
| | | border-radius: 14px; |
| | | background: linear-gradient(135deg, rgba(232, 244, 242, 0.95), rgba(230, 237, 250, 0.9)); |
| | | padding: 16px 18px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | border: 1px solid var(--surface-border); |
| | | } |
| | | |
| | | .welcome-card__text h3 { |
| | | margin: 0; |
| | | font-size: 28px; |
| | | color: var(--text-primary); |
| | | } |
| | | |
| | | .welcome-card__text p { |
| | | margin: 8px 0 0; |
| | | font-size: 20px; |
| | | color: var(--text-secondary); |
| | | } |
| | | |
| | | .mini-avatar { |
| | | width: 120px; |
| | | height: 120px; |
| | | border-radius: 14px; |
| | | border: 1px solid var(--surface-border); |
| | | background-color: #fff; |
| | | background-clip: border-box; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .mini-avatar__img { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: contain; |
| | | object-position: center bottom; |
| | | display: block; |
| | | } |
| | | |
| | | .recommend-card { |
| | | border-radius: 14px; |
| | | border: 1px solid var(--surface-border); |
| | | background: rgba(255, 255, 255, 0.86); |
| | | padding: 12px 14px; |
| | | } |
| | | |
| | | .recommend-card__head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 10px; |
| | | color: var(--text-primary); |
| | | font-size: 24px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .refresh-btn { |
| | | border: none; |
| | | background: transparent; |
| | | color: var(--text-secondary); |
| | | font-size: 18px; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .recommend-grid { |
| | | display: grid; |
| | | grid-template-columns: 1fr 1fr; |
| | | gap: 8px 18px; |
| | | } |
| | | |
| | | .recommend-item { |
| | | border: 1px solid var(--surface-border); |
| | | border-radius: 8px; |
| | | text-align: left; |
| | | padding: 8px 10px; |
| | | font-size: 18px; |
| | | color: var(--text-secondary); |
| | | background: #fff; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .recommend-item:hover { |
| | | background: rgba(31, 122, 114, 0.08); |
| | | color: var(--colorPrimary); |
| | | } |
| | | |
| | | .chat-card { |
| | | border-radius: 14px; |
| | | border: 1px solid var(--surface-border); |
| | | background: #fff; |
| | | min-height: 0; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .chat-empty { |
| | | height: 100%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: var(--text-tertiary); |
| | | font-size: 18px; |
| | | } |
| | | |
| | | .chat-messages { |
| | | height: 100%; |
| | | overflow-y: auto; |
| | | padding: 14px; |
| | | display: grid; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .chat-row { |
| | | display: flex; |
| | | } |
| | | |
| | | .chat-row--assistant { |
| | | justify-content: flex-start; |
| | | } |
| | | |
| | | .chat-row--user { |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .chat-bubble { |
| | | max-width: 72%; |
| | | border-radius: 12px; |
| | | padding: 10px 12px; |
| | | font-size: 18px; |
| | | line-height: 1.5; |
| | | white-space: pre-wrap; |
| | | color: var(--text-primary); |
| | | background: var(--surface-soft); |
| | | border: 1px solid var(--surface-border); |
| | | } |
| | | |
| | | .chat-row--user .chat-bubble { |
| | | color: #fff; |
| | | background: linear-gradient(135deg, #1f7a72 0%, #1e5bff 100%); |
| | | border: none; |
| | | } |
| | | |
| | | .ai-panel__input { |
| | | display: grid; |
| | | grid-template-columns: minmax(0, 1fr) 130px; |
| | | gap: 12px; |
| | | padding: 14px 20px 18px; |
| | | } |
| | | |
| | | .ask-input :deep(.el-input__wrapper) { |
| | | height: 74px; |
| | | border-radius: 18px; |
| | | box-shadow: 0 0 0 1px var(--surface-border) inset; |
| | | background: #fff; |
| | | } |
| | | |
| | | .ask-input :deep(.el-input__inner) { |
| | | font-size: 20px; |
| | | } |
| | | |
| | | .send-btn { |
| | | align-self: center; |
| | | height: 56px; |
| | | font-size: 20px; |
| | | min-width: 98px; |
| | | } |
| | | |
| | | .fade-enter-active, |
| | | .fade-leave-active { |
| | | transition: opacity 0.2s ease; |
| | | } |
| | | |
| | | .fade-enter-from, |
| | | .fade-leave-to { |
| | | opacity: 0; |
| | | } |
| | | |
| | | @keyframes avatarFloat { |
| | | 0%, |
| | | 100% { |
| | | transform: translateY(0); |
| | | } |
| | | 50% { |
| | | transform: translateY(-8px); |
| | | } |
| | | } |
| | | |
| | | @keyframes basePulse { |
| | | 0%, |
| | | 100% { |
| | | transform: translateX(-50%) scale(1); |
| | | opacity: 0.88; |
| | | } |
| | | 50% { |
| | | transform: translateX(-50%) scale(1.045); |
| | | opacity: 0.95; |
| | | } |
| | | } |
| | | |
| | | @keyframes baseRotate { |
| | | from { |
| | | transform: translate(-50%, -50%) rotate(0deg); |
| | | } |
| | | to { |
| | | transform: translate(-50%, -50%) rotate(360deg); |
| | | } |
| | | } |
| | | |
| | | @keyframes baseGlow { |
| | | 0%, |
| | | 100% { |
| | | transform: translateX(-50%) scaleX(1); |
| | | opacity: 0.84; |
| | | } |
| | | 50% { |
| | | transform: translateX(-50%) scaleX(1.06); |
| | | opacity: 0.96; |
| | | } |
| | | } |
| | | |
| | | @keyframes bodySweep { |
| | | 0% { |
| | | transform: translateX(-125%); |
| | | } |
| | | 100% { |
| | | transform: translateX(135%); |
| | | } |
| | | } |
| | | |
| | | @keyframes footerSweep { |
| | | 0% { |
| | | transform: translateX(-120%); |
| | | } |
| | | 100% { |
| | | transform: translateX(140%); |
| | | } |
| | | } |
| | | |
| | | @keyframes railFlow { |
| | | 0% { |
| | | transform: translateX(0); |
| | | opacity: 0; |
| | | } |
| | | 20% { |
| | | opacity: 1; |
| | | } |
| | | 80% { |
| | | opacity: 1; |
| | | } |
| | | 100% { |
| | | transform: translateX(520%); |
| | | opacity: 0; |
| | | } |
| | | } |
| | | |
| | | @keyframes nodePulse { |
| | | 0%, |
| | | 100% { |
| | | transform: scale(1); |
| | | } |
| | | 50% { |
| | | transform: scale(1.25); |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 1600px) { |
| | | .head-title { |
| | | font-size: 34px; |
| | | height: 60px; |
| | | } |
| | | |
| | | .brain-intro h2 { |
| | | font-size: 36px; |
| | | } |
| | | |
| | | .brain-intro p { |
| | | font-size: 22px; |
| | | } |
| | | |
| | | .intro-sign { |
| | | font-size: 20px; |
| | | } |
| | | |
| | | .agent-card { |
| | | width: 380px; |
| | | margin-left: -190px; |
| | | } |
| | | |
| | | .agent-card__head { |
| | | font-size: 24px; |
| | | height: 54px; |
| | | } |
| | | |
| | | .agent-card__body { |
| | | height: 390px; |
| | | } |
| | | |
| | | .highlight-list { |
| | | width: 184px; |
| | | } |
| | | |
| | | .highlight-item { |
| | | font-size: 15px; |
| | | } |
| | | |
| | | .avatar-shell { |
| | | width: 220px; |
| | | height: 390px; |
| | | } |
| | | |
| | | .avatar-cut { |
| | | width: 202px; |
| | | height: 390px; |
| | | } |
| | | |
| | | .avatar-base { |
| | | width: 194px; |
| | | height: 34px; |
| | | } |
| | | |
| | | .avatar-base::after { |
| | | width: 164px; |
| | | height: 164px; |
| | | } |
| | | |
| | | .avatar-shell::before { |
| | | width: 236px; |
| | | height: 48px; |
| | | bottom: -9px; |
| | | } |
| | | |
| | | .avatar-shell::after { |
| | | width: 220px; |
| | | height: 40px; |
| | | bottom: 0; |
| | | } |
| | | |
| | | .brain-footer { |
| | | margin: 0 52px; |
| | | height: clamp(210px, 23vh, 264px); |
| | | } |
| | | |
| | | .footer-metrics { |
| | | padding: 12px 14px 66px; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .footer-metric { |
| | | min-height: 66px; |
| | | padding: 8px 10px; |
| | | } |
| | | |
| | | .footer-metric--period { |
| | | min-height: 108px; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .footer-metric__label { |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .footer-metric__value { |
| | | font-size: 24px; |
| | | } |
| | | |
| | | .footer-metric__hint { |
| | | font-size: 11px; |
| | | } |
| | | |
| | | .footer-period-control { |
| | | gap: 6px; |
| | | } |
| | | |
| | | .period-btn { |
| | | width: 20px; |
| | | height: 20px; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .period-input { |
| | | width: 50px; |
| | | height: 22px; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .footer-period-slider { |
| | | width: 100%; |
| | | } |
| | | |
| | | .footer-rail { |
| | | left: 14px; |
| | | right: 14px; |
| | | bottom: 14px; |
| | | } |
| | | |
| | | .footer-rail__nodes { |
| | | margin-top: 10px; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .footer-node { |
| | | height: 30px; |
| | | font-size: 12px; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .footer-node__dot { |
| | | width: 7px; |
| | | height: 7px; |
| | | } |
| | | |
| | | .ai-brand { |
| | | font-size: 28px; |
| | | } |
| | | |
| | | .welcome-card__text h3, |
| | | .recommend-card__head { |
| | | font-size: 22px; |
| | | } |
| | | |
| | | .welcome-card__text p, |
| | | .recommend-item, |
| | | .chat-bubble, |
| | | .refresh-btn, |
| | | .chat-empty, |
| | | .ask-input :deep(.el-input__inner), |
| | | .send-btn { |
| | | font-size: 16px; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | :title="operationType === 'approval' ? '审æ¹' : '详æ
'" |
| | | width="700px" |
| | | @close="closeDia" |
| | | > |
| | | <el-form :model="form" label-width="140px" label-position="top" ref="formRef"> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item label="æµç¨ç¼å·ï¼" prop="approveId"> |
| | | <el-input v-model="form.approveId" placeholder="èªå¨ç¼å·" clearable disabled/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item label="ç³è¯·é¨é¨ï¼"> |
| | | <el-select |
| | | disabled |
| | | v-model="form.approveDeptId" |
| | | placeholder="éæ©é¨é¨" |
| | | > |
| | | <el-option |
| | | v-for="user in productOptions" |
| | | :key="user.deptId" |
| | | :label="user.deptName" |
| | | :value="user.deptId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row v-if="!isQuotationApproval && !isPurchaseApproval"> |
| | | <el-col :span="24"> |
| | | <el-form-item :label="props.approveType == 5 ? 'éè´ååå·ï¼' : '审æ¹äºç±ï¼'" prop="approveReason"> |
| | | <el-input v-model="form.approveReason" placeholder="请è¾å
¥" clearable type="textarea" disabled/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | |
| | | <el-dialog v-model="dialogFormVisible" |
| | | :title="operationType === 'approval' ? '审æ¹' : '详æ
'" |
| | | width="700px" |
| | | @close="closeDia"> |
| | | <el-form :model="form" |
| | | label-width="140px" |
| | | label-position="top" |
| | | ref="formRef"> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item label="æµç¨ç¼å·ï¼" |
| | | prop="approveId"> |
| | | <el-input v-model="form.approveId" |
| | | placeholder="èªå¨ç¼å·" |
| | | clearable |
| | | disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item label="ç³è¯·é¨é¨ï¼"> |
| | | <el-select disabled |
| | | v-model="form.approveDeptId" |
| | | placeholder="éæ©é¨é¨"> |
| | | <el-option v-for="user in productOptions" |
| | | :key="user.deptId" |
| | | :label="user.deptName" |
| | | :value="user.deptId" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row v-if="!isQuotationApproval && !isPurchaseApproval"> |
| | | <el-col :span="24"> |
| | | <el-form-item :label="props.approveType == 5 ? 'éè´ååå·ï¼' : '审æ¹äºç±ï¼'" |
| | | prop="approveReason"> |
| | | <el-input v-model="form.approveReason" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | type="textarea" |
| | | disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <!-- æ¥ä»·å®¡æ¹ï¼å±ç¤ºæ¥ä»·è¯¦æ
ï¼å¤ç¨é宿¥ä»·"æ¥ç详æ
å¯¹è¯æ¡"å
å®¹ç»æï¼ --> |
| | | <div v-if="isQuotationApproval" style="margin: 10px 0 18px;"> |
| | | <div v-if="isQuotationApproval" |
| | | style="margin: 10px 0 18px;"> |
| | | <el-divider content-position="left">æ¥ä»·è¯¦æ
</el-divider> |
| | | <el-skeleton :loading="quotationLoading" animated> |
| | | <el-skeleton :loading="quotationLoading" |
| | | animated> |
| | | <template #template> |
| | | <el-skeleton-item variant="h3" style="width: 30%" /> |
| | | <el-skeleton-item variant="text" style="width: 100%" /> |
| | | <el-skeleton-item variant="text" style="width: 100%" /> |
| | | <el-skeleton-item variant="h3" |
| | | style="width: 30%" /> |
| | | <el-skeleton-item variant="text" |
| | | style="width: 100%" /> |
| | | <el-skeleton-item variant="text" |
| | | style="width: 100%" /> |
| | | </template> |
| | | <template #default> |
| | | <el-empty v-if="!currentQuotation || !currentQuotation.quotationNo" description="æªæ¥è¯¢å°å¯¹åºæ¥ä»·è¯¦æ
" /> |
| | | <el-empty v-if="!currentQuotation || !currentQuotation.quotationNo" |
| | | description="æªæ¥è¯¢å°å¯¹åºæ¥ä»·è¯¦æ
" /> |
| | | <template v-else> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions :column="2" |
| | | border> |
| | | <el-descriptions-item label="æ¥ä»·åå·">{{ currentQuotation.quotationNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="客æ·åç§°">{{ currentQuotation.customer }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¸å¡å">{{ currentQuotation.salesperson }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¥ä»·æ¥æ">{{ currentQuotation.quotationDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="æææè³">{{ currentQuotation.validDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="仿¬¾æ¹å¼">{{ currentQuotation.paymentMethod }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¥ä»·æ»é¢" :span="2"> |
| | | <el-descriptions-item label="æ¥ä»·æ»é¢" |
| | | :span="2"> |
| | | <span style="font-size: 18px; color: #e6a23c; font-weight: bold;"> |
| | | ¥{{ Number(currentQuotation.totalAmount ?? 0).toFixed(2) }} |
| | | </span> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <div style="margin-top: 20px;"> |
| | | <h4>产åæç»</h4> |
| | | <el-table :data="currentQuotation.products || []" border style="width: 100%"> |
| | | <el-table-column prop="product" label="产ååç§°" /> |
| | | <el-table-column prop="specification" label="è§æ ¼åå·" /> |
| | | <el-table-column prop="unit" label="åä½" /> |
| | | <el-table-column prop="unitPrice" label="åä»·"> |
| | | <el-table :data="currentQuotation.products || []" |
| | | border |
| | | style="width: 100%"> |
| | | <el-table-column prop="product" |
| | | label="产ååç§°" /> |
| | | <el-table-column prop="specification" |
| | | label="è§æ ¼åå·" /> |
| | | <el-table-column prop="unit" |
| | | label="åä½" /> |
| | | <el-table-column prop="unitPrice" |
| | | label="åä»·"> |
| | | <template #default="scope">Â¥{{ Number(scope.row.unitPrice ?? 0).toFixed(2) }}</template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <div v-if="currentQuotation.remark" style="margin-top: 20px;"> |
| | | <div v-if="currentQuotation.remark" |
| | | style="margin-top: 20px;"> |
| | | <h4>夿³¨</h4> |
| | | <p>{{ currentQuotation.remark }}</p> |
| | | </div> |
| | |
| | | </template> |
| | | </el-skeleton> |
| | | </div> |
| | | |
| | | <!-- éè´å®¡æ¹ï¼å±ç¤ºéè´è¯¦æ
--> |
| | | <div v-if="isPurchaseApproval" style="margin: 10px 0 18px;"> |
| | | <div v-if="isPurchaseApproval" |
| | | style="margin: 10px 0 18px;"> |
| | | <el-divider content-position="left">éè´è¯¦æ
</el-divider> |
| | | <el-skeleton :loading="purchaseLoading" animated> |
| | | <el-skeleton :loading="purchaseLoading" |
| | | animated> |
| | | <template #template> |
| | | <el-skeleton-item variant="h3" style="width: 30%" /> |
| | | <el-skeleton-item variant="text" style="width: 100%" /> |
| | | <el-skeleton-item variant="text" style="width: 100%" /> |
| | | <el-skeleton-item variant="h3" |
| | | style="width: 30%" /> |
| | | <el-skeleton-item variant="text" |
| | | style="width: 100%" /> |
| | | <el-skeleton-item variant="text" |
| | | style="width: 100%" /> |
| | | </template> |
| | | <template #default> |
| | | <el-empty v-if="!currentPurchase || !currentPurchase.purchaseContractNumber" description="æªæ¥è¯¢å°å¯¹åºéè´è¯¦æ
" /> |
| | | <el-empty v-if="!currentPurchase || !currentPurchase.purchaseContractNumber" |
| | | description="æªæ¥è¯¢å°å¯¹åºéè´è¯¦æ
" /> |
| | | <template v-else> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions :column="2" |
| | | border> |
| | | <el-descriptions-item label="éè´ååå·">{{ currentPurchase.purchaseContractNumber }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¾åºååç§°">{{ currentPurchase.supplierName }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®åç§°">{{ currentPurchase.projectName }}</el-descriptions-item> |
| | |
| | | <el-descriptions-item label="ç¾è®¢æ¥æ">{{ currentPurchase.executionDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="å½å
¥æ¥æ">{{ currentPurchase.entryDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="仿¬¾æ¹å¼">{{ currentPurchase.paymentMethod }}</el-descriptions-item> |
| | | <el-descriptions-item label="ååéé¢" :span="2"> |
| | | <el-descriptions-item label="ååéé¢" |
| | | :span="2"> |
| | | <span style="font-size: 18px; color: #e6a23c; font-weight: bold;"> |
| | | ¥{{ Number(currentPurchase.contractAmount ?? 0).toFixed(2) }} |
| | | </span> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <div style="margin-top: 20px;"> |
| | | <h4>产åæç»</h4> |
| | | <el-table :data="currentPurchase.productData || []" border style="width: 100%"> |
| | | <el-table-column prop="productCategory" label="产ååç§°" /> |
| | | <el-table-column prop="specificationModel" label="è§æ ¼åå·" /> |
| | | <el-table-column prop="unit" label="åä½" /> |
| | | <el-table-column prop="quantity" label="æ°é" /> |
| | | <el-table-column prop="taxInclusiveUnitPrice" label="å«ç¨åä»·"> |
| | | <el-table :data="currentPurchase.productData || []" |
| | | border |
| | | style="width: 100%"> |
| | | <el-table-column prop="productCategory" |
| | | label="产ååç§°" /> |
| | | <el-table-column prop="specificationModel" |
| | | label="è§æ ¼åå·" /> |
| | | <el-table-column prop="unit" |
| | | label="åä½" /> |
| | | <el-table-column prop="quantity" |
| | | label="æ°é" /> |
| | | <el-table-column prop="taxInclusiveUnitPrice" |
| | | label="å«ç¨åä»·"> |
| | | <template #default="scope">Â¥{{ Number(scope.row.taxInclusiveUnitPrice ?? 0).toFixed(2) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="taxInclusiveTotalPrice" label="å«ç¨æ»ä»·"> |
| | | <el-table-column prop="taxInclusiveTotalPrice" |
| | | label="å«ç¨æ»ä»·"> |
| | | <template #default="scope">Â¥{{ Number(scope.row.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | </template> |
| | | </el-skeleton> |
| | | </div> |
| | | |
| | | <el-form :model="{ activities }" ref="formRef" label-position="top"> |
| | | <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical"> |
| | | <el-step |
| | | v-for="(activity, index) in activities" |
| | | :key="index" |
| | | finish-status="success" |
| | | :title="getNodeTitle(index, activities.length)" |
| | | :description="activity.approveNodeUser" |
| | | :icon="getNodeIcon(activity, index)" |
| | | > |
| | | <template #icon> |
| | | <el-icon v-if="activity.approveNodeStatus === 2" color="red" :size="22"><WarningFilled /></el-icon> |
| | | <el-icon v-else-if="activity.isShen" color="#1890ff" :size="22"><Edit /></el-icon> |
| | | <el-icon v-else-if="activity.approveNodeStatus === 1" color="#67C23A" :size="26"><Check /></el-icon> |
| | | <el-icon v-else color="#C0C4CC" :size="22"><MoreFilled /></el-icon> |
| | | </template> |
| | | <!-- å货审æ¹ï¼å±ç¤ºå货详æ
--> |
| | | <div v-if="isDeliveryApproval" |
| | | style="margin: 10px 0 18px;"> |
| | | <el-divider content-position="left">å货详æ
</el-divider> |
| | | <el-skeleton :loading="deliveryLoading" |
| | | animated> |
| | | <template #template> |
| | | <el-skeleton-item variant="h3" |
| | | style="width: 30%" /> |
| | | <el-skeleton-item variant="text" |
| | | style="width: 100%" /> |
| | | <el-skeleton-item variant="text" |
| | | style="width: 100%" /> |
| | | </template> |
| | | <template #default> |
| | | <el-empty v-if="!currentDelivery || !currentDelivery.shippingInfo" |
| | | description="æªæ¥è¯¢å°å¯¹åºå货详æ
" /> |
| | | <template v-else> |
| | | <el-descriptions :column="2" |
| | | border> |
| | | <el-descriptions-item label="éå®è®¢å">{{ currentDelivery.shippingInfo.salesContractNo || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å货订åå·">{{ currentDelivery.shippingInfo.shippingNo || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="客æ·åç§°">{{ currentDelivery.shippingInfo.customerName || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="åè´§ç±»å">{{ currentDelivery.shippingInfo.type || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="åè´§æ¥æ">{{ currentDelivery.shippingInfo.shippingDate || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®¡æ ¸ç¶æ">{{ currentDelivery.shippingInfo.status || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å货车çå·">{{ currentDelivery.shippingInfo.shippingCarNumber || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¿«éå
¬å¸">{{ currentDelivery.shippingInfo.expressCompany || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¿«éåå·" |
| | | :span="2">{{ currentDelivery.shippingInfo.expressNumber || '--' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | <div style="margin-top: 20px;"> |
| | | <h4>产åæç»</h4> |
| | | <el-table :data="deliveryProductList" |
| | | border |
| | | size="small" |
| | | style="width: 100%"> |
| | | <el-table-column prop="batchNo" |
| | | label="æ¹å·" |
| | | show-overflow-tooltip /> |
| | | <el-table-column prop="productName" |
| | | label="产ååç§°" |
| | | show-overflow-tooltip /> |
| | | <el-table-column prop="specificationModel" |
| | | label="è§æ ¼åå·" |
| | | show-overflow-tooltip /> |
| | | <el-table-column prop="deliveryQuantity" |
| | | label="åè´§æ°é" |
| | | align="center" /> |
| | | </el-table> |
| | | </div> |
| | | <div v-if="currentDelivery.shippingInfo.storageBlobVOs && currentDelivery.shippingInfo.storageBlobVOs.length" |
| | | style="margin-top: 20px;"> |
| | | <h4>åè´§å¾ç</h4> |
| | | <ImagePreview :file-list="currentDelivery.shippingInfo.storageBlobVOs" /> |
| | | </div> |
| | | </template> |
| | | </template> |
| | | </el-skeleton> |
| | | </div> |
| | | <el-form :model="{ activities }" |
| | | ref="formRef" |
| | | label-position="top"> |
| | | <el-steps :active="getActiveStep()" |
| | | finish-status="success" |
| | | process-status="process" |
| | | align-center |
| | | direction="vertical"> |
| | | <el-step v-for="(activity, index) in activities" |
| | | :key="index" |
| | | finish-status="success" |
| | | :title="getNodeTitle(index, activities.length)" |
| | | :description="activity.approveNodeUser" |
| | | :icon="getNodeIcon(activity, index)"> |
| | | <template #icon> |
| | | <el-icon v-if="activity.approveNodeStatus === 2" |
| | | color="red" |
| | | :size="22"> |
| | | <WarningFilled /> |
| | | </el-icon> |
| | | <el-icon v-else-if="activity.isShen" |
| | | color="#1890ff" |
| | | :size="22"> |
| | | <Edit /> |
| | | </el-icon> |
| | | <el-icon v-else-if="activity.approveNodeStatus === 1" |
| | | color="#67C23A" |
| | | :size="26"> |
| | | <Check /> |
| | | </el-icon> |
| | | <el-icon v-else |
| | | color="#C0C4CC" |
| | | :size="22"> |
| | | <MoreFilled /> |
| | | </el-icon> |
| | | </template> |
| | | <template #title> |
| | | <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span> |
| | | </template> |
| | | <template #description> |
| | | <div class="node-user"> |
| | | <div class="avatar-wrapper"> |
| | | <img :src="userStore.avatar" class="user-avatar" alt=""/> |
| | | <img :src="userStore.avatar" |
| | | class="user-avatar" |
| | | alt="" /> |
| | | </div> |
| | | <span style="color: #000000">{{ activity.approveNodeUser }}-{{activity.isApproval}}</span> |
| | | </div> |
| | | <div v-if="!activity.isShen" class="node-reason"> |
| | | <div v-if="!activity.isShen" |
| | | class="node-reason"> |
| | | <span>å®¡æ¹æè§ï¼</span>{{ activity.approveNodeReason }} |
| | | </div> |
| | | <div v-else-if="activity.isShen"> |
| | | <el-form-item |
| | | :prop="'activities.' + index + '.approveNodeReason'" |
| | | :rules="[{ required: true, message: 'å®¡æ¹æè§ä¸è½ä¸ºç©º', trigger: 'blur' }]" |
| | | > |
| | | <el-input v-model="activity.approveNodeReason" clearable type="textarea" :disabled="operationType === 'view'"></el-input> |
| | | <el-form-item :prop="'activities.' + index + '.approveNodeReason'" |
| | | :rules="[{ required: true, message: 'å®¡æ¹æè§ä¸è½ä¸ºç©º', trigger: 'blur' }]"> |
| | | <el-input v-model="activity.approveNodeReason" |
| | | clearable |
| | | type="textarea" |
| | | :disabled="operationType === 'view'"></el-input> |
| | | </el-form-item> |
| | | </div> |
| | | </template> |
| | | </el-step> |
| | | </el-steps> |
| | | </el-form> |
| | | <template #footer v-if="operationType === 'approval'"> |
| | | <template #footer |
| | | v-if="operationType === 'approval'"> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm(2)">ä¸éè¿</el-button> |
| | | <el-button type="primary" @click="submitForm(1)">éè¿</el-button> |
| | | <el-button type="primary" |
| | | @click="submitForm(2)">ä¸éè¿</el-button> |
| | | <el-button type="primary" |
| | | @click="submitForm(1)">éè¿</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, getCurrentInstance, nextTick, reactive, ref, toRefs } from "vue"; |
| | | import { |
| | | approveProcessDetails, |
| | | getDept, |
| | | updateApproveNode |
| | | } from "@/api/collaborativeApproval/approvalProcess.js"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue' |
| | | import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; |
| | | import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js"; |
| | | const emit = defineEmits(['close']) |
| | | const { proxy } = getCurrentInstance() |
| | | import { |
| | | computed, |
| | | getCurrentInstance, |
| | | nextTick, |
| | | reactive, |
| | | ref, |
| | | toRefs, |
| | | } from "vue"; |
| | | import { |
| | | approveProcessDetails, |
| | | getDept, |
| | | updateApproveNode, |
| | | } from "@/api/collaborativeApproval/approvalProcess.js"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import { |
| | | WarningFilled, |
| | | Edit, |
| | | Check, |
| | | MoreFilled, |
| | | } from "@element-plus/icons-vue"; |
| | | import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; |
| | | import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js"; |
| | | import { getDeliveryDetailByShippingNo } from "@/api/salesManagement/deliveryLedger.js"; |
| | | import ImagePreview from "@/components/AttachmentPreview/image/index.vue"; |
| | | const emit = defineEmits(["close"]); |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const props = defineProps({ |
| | | approveType: { |
| | | type: [Number, String], |
| | | default: 0 |
| | | } |
| | | }) |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const activities = ref([]) |
| | | const formRef = ref(null); |
| | | const userStore = useUserStore() |
| | | const productOptions = ref([]); |
| | | const quotationLoading = ref(false) |
| | | const currentQuotation = ref({}) |
| | | const purchaseLoading = ref(false) |
| | | const currentPurchase = ref({}) |
| | | const isQuotationApproval = computed(() => Number(props.approveType) === 6) |
| | | const isPurchaseApproval = computed(() => Number(props.approveType) === 5) |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | approveId: "", |
| | | approveDeptId: "", |
| | | approveReason: "", |
| | | checkResult: "", |
| | | }, |
| | | }); |
| | | const { form } = toRefs(data); |
| | | |
| | | // èç¹æ é¢ |
| | | const getNodeTitle = (index, len) => { |
| | | if (index === len - 1) return 'ç»æ'; |
| | | return '审æ¹'; |
| | | }; |
| | | |
| | | // è·åå½åæ¿æ´»æ¥éª¤ |
| | | const getActiveStep = () => { |
| | | // å¦æææ isShen é½ä¸º falseï¼è¿åæåä¸ä¸ªæ¥éª¤ï¼å
¨é¨å®æï¼ |
| | | const hasActive = activities.value.some(a => a.isShen === true); |
| | | if (!hasActive) return activities.value.length; |
| | | // å½åèç¹ç´¢å¼ |
| | | return activities.value.findIndex(a => a.isShen == true); |
| | | }; |
| | | // æ¥éª¤icon |
| | | const getNodeIcon = (activity, index) => { |
| | | if (activity.approveNodeStatus === 2) return 'el-icon-warning'; // ä¸éè¿ |
| | | if (activity.isShen) return 'Edit'; |
| | | return ''; |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | currentQuotation.value = {} |
| | | currentPurchase.value = {} |
| | | form.value = {...row} |
| | | // ç«å³æ¸
é¤è¡¨åéªè¯ç¶æï¼å ä¸ºåæ®µæ¯disabledçï¼ä¸éè¦éªè¯ï¼ |
| | | nextTick(() => { |
| | | if (formRef.value) { |
| | | formRef.value.clearValidate(); |
| | | } |
| | | }); |
| | | // ç¡®ä¿é项å è½½å®æååå¹é
å¼ç±»å |
| | | getProductOptions().then(() => { |
| | | // ç¡®ä¿å¼ç±»åå¹é
ï¼å¦æé项已å è½½ï¼ |
| | | if (productOptions.value.length > 0 && form.value.approveDeptId) { |
| | | const matchedOption = productOptions.value.find(opt => |
| | | opt.deptId == form.value.approveDeptId || |
| | | String(opt.deptId) === String(form.value.approveDeptId) |
| | | ); |
| | | if (matchedOption) { |
| | | form.value.approveDeptId = matchedOption.deptId; |
| | | } |
| | | } |
| | | // 忬¡æ¸
é¤éªè¯ï¼ç¡®ä¿é项å è½½åå¼å¹é
æ£ç¡® |
| | | nextTick(() => { |
| | | if (formRef.value) { |
| | | formRef.value.clearValidate(); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | // æ¥ä»·å®¡æ¹ï¼ç¨å®¡æ¹äºç±å段æ¿è½½ç"æ¥ä»·åå·"廿¥æ¥ä»·å表 |
| | | if (isQuotationApproval.value) { |
| | | const quotationNo = row?.approveReason; |
| | | if (quotationNo) { |
| | | quotationLoading.value = true |
| | | getQuotationList({ quotationNo }).then((res) => { |
| | | const records = res?.data?.records || [] |
| | | currentQuotation.value = records[0] || {} |
| | | }).finally(() => { |
| | | quotationLoading.value = false |
| | | }) |
| | | } |
| | | } |
| | | |
| | | // éè´å®¡æ¹ï¼ç¨å®¡æ¹äºç±å段æ¿è½½ç"éè´ååå·"廿¥éè´è¯¦æ
|
| | | if (isPurchaseApproval.value) { |
| | | const purchaseContractNumber = row?.approveReason; |
| | | if (purchaseContractNumber) { |
| | | purchaseLoading.value = true |
| | | getPurchaseByCode({ purchaseContractNumber }).then((res) => { |
| | | currentPurchase.value = res |
| | | }).catch((err) => { |
| | | console.error('æ¥è¯¢éè´è¯¦æ
失败:', err) |
| | | proxy.$modal.msgError('æ¥è¯¢éè´è¯¦æ
失败') |
| | | }).finally(() => { |
| | | purchaseLoading.value = false |
| | | }) |
| | | } |
| | | } |
| | | |
| | | approveProcessDetails(row.approveId).then((res) => { |
| | | activities.value = res.data |
| | | // å¢å isApprovalåæ®µ |
| | | activities.value.forEach(item => { |
| | | if (item.url && item.url.includes('word')) { |
| | | item.urlTem = item.url.replaceAll('word', 'img') |
| | | } else { |
| | | item.urlTem = item.url |
| | | } |
| | | if (item.approveNodeStatus === 2) { |
| | | item.isApproval = '已驳å'; |
| | | } else if (item.approveNodeStatus === 1) { |
| | | item.isApproval = 'å·²åæ'; |
| | | } else { |
| | | item.isApproval = 'æªå®¡æ¹'; |
| | | } |
| | | }) |
| | | }) |
| | | } |
| | | const getProductOptions = () => { |
| | | return getDept().then((res) => { |
| | | productOptions.value = res.data; |
| | | }); |
| | | }; |
| | | // æäº¤å®¡æ¹ |
| | | const submitForm = (status) => { |
| | | const filteredActivities = activities.value.filter(activity => activity.isShen); |
| | | if (!filteredActivities || filteredActivities.length === 0) { |
| | | proxy.$modal.msgError("æªæ¾å°å¾
审æ¹çèç¹"); |
| | | return; |
| | | } |
| | | const currentActivity = filteredActivities[0]; |
| | | if (!currentActivity) { |
| | | proxy.$modal.msgError("æªæ¾å°å¾
审æ¹çèç¹"); |
| | | return; |
| | | } |
| | | currentActivity.approveNodeStatus = status; |
| | | // 夿æ¯å¦ä¸ºæå䏿¥ |
| | | const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length-1; |
| | | updateApproveNode({ ...currentActivity, isLast }).then(() => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | const props = defineProps({ |
| | | approveType: { |
| | | type: [Number, String], |
| | | default: 0, |
| | | }, |
| | | }); |
| | | }; |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | dialogFormVisible.value = false; |
| | | quotationLoading.value = false |
| | | currentQuotation.value = {} |
| | | purchaseLoading.value = false |
| | | currentPurchase.value = {} |
| | | emit('close') |
| | | }; |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref(""); |
| | | const activities = ref([]); |
| | | const formRef = ref(null); |
| | | const userStore = useUserStore(); |
| | | const productOptions = ref([]); |
| | | const quotationLoading = ref(false); |
| | | const currentQuotation = ref({}); |
| | | const purchaseLoading = ref(false); |
| | | const currentPurchase = ref({}); |
| | | const deliveryLoading = ref(false); |
| | | const currentDelivery = ref({}); |
| | | const deliveryProductList = ref([]); |
| | | const isQuotationApproval = computed(() => Number(props.approveType) === 6); |
| | | const isPurchaseApproval = computed(() => Number(props.approveType) === 5); |
| | | const isDeliveryApproval = computed(() => Number(props.approveType) === 7); |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | approveId: "", |
| | | approveDeptId: "", |
| | | approveReason: "", |
| | | checkResult: "", |
| | | }, |
| | | }); |
| | | const { form } = toRefs(data); |
| | | |
| | | // èç¹æ é¢ |
| | | const getNodeTitle = (index, len) => { |
| | | if (index === len - 1) return "ç»æ"; |
| | | return "审æ¹"; |
| | | }; |
| | | |
| | | // è·åå½åæ¿æ´»æ¥éª¤ |
| | | const getActiveStep = () => { |
| | | // å¦æææ isShen é½ä¸º falseï¼è¿åæåä¸ä¸ªæ¥éª¤ï¼å
¨é¨å®æï¼ |
| | | const hasActive = activities.value.some(a => a.isShen === true); |
| | | if (!hasActive) return activities.value.length; |
| | | // å½åèç¹ç´¢å¼ |
| | | return activities.value.findIndex(a => a.isShen == true); |
| | | }; |
| | | // æ¥éª¤icon |
| | | const getNodeIcon = (activity, index) => { |
| | | if (activity.approveNodeStatus === 2) return "el-icon-warning"; // ä¸éè¿ |
| | | if (activity.isShen) return "Edit"; |
| | | return ""; |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | currentQuotation.value = {}; |
| | | currentPurchase.value = {}; |
| | | form.value = { ...row }; |
| | | // ç«å³æ¸
é¤è¡¨åéªè¯ç¶æï¼å ä¸ºåæ®µæ¯disabledçï¼ä¸éè¦éªè¯ï¼ |
| | | nextTick(() => { |
| | | if (formRef.value) { |
| | | formRef.value.clearValidate(); |
| | | } |
| | | }); |
| | | // ç¡®ä¿é项å è½½å®æååå¹é
å¼ç±»å |
| | | getProductOptions().then(() => { |
| | | // ç¡®ä¿å¼ç±»åå¹é
ï¼å¦æé项已å è½½ï¼ |
| | | if (productOptions.value.length > 0 && form.value.approveDeptId) { |
| | | const matchedOption = productOptions.value.find( |
| | | opt => |
| | | opt.deptId == form.value.approveDeptId || |
| | | String(opt.deptId) === String(form.value.approveDeptId) |
| | | ); |
| | | if (matchedOption) { |
| | | form.value.approveDeptId = matchedOption.deptId; |
| | | } |
| | | } |
| | | // 忬¡æ¸
é¤éªè¯ï¼ç¡®ä¿é项å è½½åå¼å¹é
æ£ç¡® |
| | | nextTick(() => { |
| | | if (formRef.value) { |
| | | formRef.value.clearValidate(); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | // æ¥ä»·å®¡æ¹ï¼ç¨å®¡æ¹äºç±å段æ¿è½½ç"æ¥ä»·åå·"廿¥æ¥ä»·å表 |
| | | if (isQuotationApproval.value) { |
| | | const quotationNo = row?.approveReason; |
| | | if (quotationNo) { |
| | | quotationLoading.value = true; |
| | | getQuotationList({ quotationNo }) |
| | | .then(res => { |
| | | const records = res?.data?.records || []; |
| | | currentQuotation.value = records[0] || {}; |
| | | }) |
| | | .finally(() => { |
| | | quotationLoading.value = false; |
| | | }); |
| | | } |
| | | } |
| | | |
| | | // éè´å®¡æ¹ï¼ç¨å®¡æ¹äºç±å段æ¿è½½ç"éè´ååå·"廿¥éè´è¯¦æ
|
| | | if (isPurchaseApproval.value) { |
| | | const purchaseContractNumber = row?.approveReason; |
| | | if (purchaseContractNumber) { |
| | | purchaseLoading.value = true; |
| | | getPurchaseByCode({ purchaseContractNumber }) |
| | | .then(res => { |
| | | currentPurchase.value = res; |
| | | }) |
| | | .catch(err => { |
| | | console.error("æ¥è¯¢éè´è¯¦æ
失败:", err); |
| | | proxy.$modal.msgError("æ¥è¯¢éè´è¯¦æ
失败"); |
| | | }) |
| | | .finally(() => { |
| | | purchaseLoading.value = false; |
| | | }); |
| | | } |
| | | } |
| | | // å货审æ¹ï¼ç¨å®¡æ¹äºç±å段æ¿è½½ç"åè´§åå·"廿¥å货详æ
|
| | | if (isDeliveryApproval.value) { |
| | | const deliveryNo = row?.approveReason; |
| | | if (deliveryNo) { |
| | | deliveryLoading.value = true; |
| | | currentDelivery.value = {}; |
| | | deliveryProductList.value = []; |
| | | getDeliveryDetailByShippingNo({ shippingNo: deliveryNo }) |
| | | .then(res => { |
| | | const detailData = res?.data || res || {}; |
| | | currentDelivery.value = detailData; |
| | | deliveryProductList.value = |
| | | detailData.shippingProductDetailDtoList || []; |
| | | }) |
| | | .catch(err => { |
| | | console.error("æ¥è¯¢å货详æ
失败:", err); |
| | | proxy.$modal.msgError("æ¥è¯¢å货详æ
失败"); |
| | | }) |
| | | .finally(() => { |
| | | deliveryLoading.value = false; |
| | | }); |
| | | } |
| | | } |
| | | |
| | | approveProcessDetails(row.approveId).then(res => { |
| | | activities.value = res.data; |
| | | // å¢å isApprovalåæ®µ |
| | | activities.value.forEach(item => { |
| | | if (item.url && item.url.includes("word")) { |
| | | item.urlTem = item.url.replaceAll("word", "img"); |
| | | } else { |
| | | item.urlTem = item.url; |
| | | } |
| | | if (item.approveNodeStatus === 2) { |
| | | item.isApproval = "已驳å"; |
| | | } else if (item.approveNodeStatus === 1) { |
| | | item.isApproval = "å·²åæ"; |
| | | } else { |
| | | item.isApproval = "æªå®¡æ¹"; |
| | | } |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const getDeliveryProductInfoList = () => { |
| | | const row = currentDelivery.value; |
| | | if (!row) return []; |
| | | const normalizeBatchNoList = value => { |
| | | if (Array.isArray(value)) return value; |
| | | if (typeof value === "string" && value.includes(",")) { |
| | | return value |
| | | .split(",") |
| | | .map(item => item.trim()) |
| | | .filter(Boolean); |
| | | } |
| | | return value ? [value] : []; |
| | | }; |
| | | const detailList = deliveryProductList.value.length |
| | | ? deliveryProductList.value |
| | | : [ |
| | | row.batchNoDetailList, |
| | | row.batchNoList, |
| | | row.shippingBatchList, |
| | | row.shippingInfoDetailList, |
| | | row.detailList, |
| | | row.batchDetailList, |
| | | ].find(value => Array.isArray(value) && value.length); |
| | | const batchNoList = normalizeBatchNoList(row.batchNo); |
| | | const toTableRow = (item = {}) => ({ |
| | | batchNo: |
| | | typeof item === "string" || typeof item === "number" |
| | | ? item |
| | | : item.batchNo ?? item.batchNumber ?? row.batchNo ?? "--", |
| | | productName: item.productName ?? row.productName ?? "--", |
| | | specificationModel: |
| | | item.specificationModel ?? item.model ?? row.specificationModel ?? "--", |
| | | deliveryQuantity: |
| | | item.deliveryQuantity ?? |
| | | item.quantity ?? |
| | | item.shippingQuantity ?? |
| | | row.deliveryQuantity ?? |
| | | row.quantity ?? |
| | | "--", |
| | | }); |
| | | if (detailList?.length) { |
| | | return detailList.map(toTableRow); |
| | | } |
| | | if (batchNoList.length) { |
| | | return batchNoList.map(batchNo => toTableRow({ batchNo })); |
| | | } |
| | | return [toTableRow()]; |
| | | }; |
| | | const getApprovalStatusText = status => { |
| | | const statusMap = { |
| | | 0: "å¾
å®¡æ ¸", |
| | | 1: "å®¡æ ¸éè¿", |
| | | 2: "å®¡æ ¸æç»", |
| | | 3: "å®¡æ ¸ä¸", |
| | | }; |
| | | return statusMap[status] || "å¾
å®¡æ ¸"; |
| | | }; |
| | | |
| | | const getProductOptions = () => { |
| | | return getDept().then(res => { |
| | | productOptions.value = res.data; |
| | | }); |
| | | }; |
| | | // æäº¤å®¡æ¹ |
| | | const submitForm = status => { |
| | | const filteredActivities = activities.value.filter( |
| | | activity => activity.isShen |
| | | ); |
| | | if (!filteredActivities || filteredActivities.length === 0) { |
| | | proxy.$modal.msgError("æªæ¾å°å¾
审æ¹çèç¹"); |
| | | return; |
| | | } |
| | | const currentActivity = filteredActivities[0]; |
| | | if (!currentActivity) { |
| | | proxy.$modal.msgError("æªæ¾å°å¾
审æ¹çèç¹"); |
| | | return; |
| | | } |
| | | currentActivity.approveNodeStatus = status; |
| | | // 夿æ¯å¦ä¸ºæå䏿¥ |
| | | const isLast = |
| | | activities.value.findIndex(a => a.isShen) === activities.value.length - 1; |
| | | updateApproveNode({ ...currentActivity, isLast }).then(() => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }); |
| | | }; |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | dialogFormVisible.value = false; |
| | | quotationLoading.value = false; |
| | | currentQuotation.value = {}; |
| | | purchaseLoading.value = false; |
| | | currentPurchase.value = {}; |
| | | emit("close"); |
| | | }; |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | |
| | | .node-user { |
| | | margin: 10px 0; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .node-status { |
| | | color: #1890ff; |
| | | margin-left: 8px; |
| | | font-size: 14px; |
| | | } |
| | | .node-reason { |
| | | font-size: 15px; |
| | | color: #333; |
| | | margin: 10px 0; |
| | | } |
| | | .user-avatar { |
| | | cursor: pointer; |
| | | width: 30px; |
| | | height: 30px; |
| | | border-radius: 50px; |
| | | } |
| | | .signImg { |
| | | cursor: pointer; |
| | | width: 200px; |
| | | height: 60px; |
| | | } |
| | | .node-user { |
| | | margin: 10px 0; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .node-status { |
| | | color: #1890ff; |
| | | margin-left: 8px; |
| | | font-size: 14px; |
| | | } |
| | | .node-reason { |
| | | font-size: 15px; |
| | | color: #333; |
| | | margin: 10px 0; |
| | | } |
| | | .user-avatar { |
| | | cursor: pointer; |
| | | width: 30px; |
| | | height: 30px; |
| | | border-radius: 50px; |
| | | } |
| | | .signImg { |
| | | cursor: pointer; |
| | | width: 200px; |
| | | height: 60px; |
| | | } |
| | | </style> |
| | |
| | | v-model="form.productName" |
| | | placeholder="请è¾å
¥äº§ååç§°" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('productName')" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | v-model="form.batchNumber" |
| | | placeholder="请è¾å
¥äº§åæ¹å·" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('batchNumber')" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | type="date" |
| | | placeholder="è¯·éæ©ä¸´ææ¥æ" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('expiryDate')" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | :min="0" |
| | | placeholder="请è¾å
¥åºåæ°é" |
| | | style="width: 100%" |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('stockQuantity')" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | v-model="form.customerName" |
| | | placeholder="请è¾å
¥å®¢æ·åç§°" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('customerName')" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | v-model="form.contactPhone" |
| | | placeholder="请è¾å
¥èç³»çµè¯" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('contactPhone')" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | v-model="form.problemDesc" |
| | | placeholder="请è¾å
¥é®é¢æè¿°" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('problemDesc')" |
| | | type="textarea" |
| | | :rows="3" |
| | | /> |
| | |
| | | v-model="form.handlerId" |
| | | placeholder="è¯·éæ©å¤ç人" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('handlerId')" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | |
| | | type="date" |
| | | placeholder="è¯·éæ©å¤çæ¥æ" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('handleDate')" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | v-model="form.handleResult" |
| | | placeholder="请è¾å
¥å¤çç»æ" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('handleResult')" |
| | | type="textarea" |
| | | :rows="3" |
| | | /> |
| | |
| | | return 'æ°å¢ä¸´æå®å'; |
| | | case 'edit': |
| | | return 'ç¼è¾ä¸´æå®å'; |
| | | case 'handle': |
| | | return 'å¤ç临æå®å'; |
| | | case 'view': |
| | | return 'æ¥ç临æå®å'; |
| | | default: |
| | |
| | | }) |
| | | const { form, rules } = toRefs(data); |
| | | const userList = ref([]) |
| | | const handleEditableFields = ["handlerId", "handleDate", "handleResult"]; |
| | | |
| | | const isFieldDisabled = (field) => { |
| | | if (operationType.value === "view") return true; |
| | | if (operationType.value === "handle") return !handleEditableFields.includes(field); |
| | | return false; |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog = (type, row) => { |
| | |
| | | } else { |
| | | // ç¼è¾ææ¥çæ¶å¡«å
æ°æ® |
| | | form.value = { ...row }; |
| | | if (type === 'edit' && !form.value.handlerId) { |
| | | if (type === 'handle' && !form.value.handlerId) { |
| | | form.value.handlerId = userStore.id; |
| | | form.value.handleDate = getCurrentDate(); |
| | | } |
| | |
| | | } |
| | | |
| | | const submitForm = () => { |
| | | if (operationType.value === "handle") { |
| | | if (!form.value.handlerId || !form.value.handleDate || !form.value.handleResult) { |
| | | proxy.$modal.msgWarning("请填åå¤ç人ãå¤çæ¥æåå¤çç»æ"); |
| | | return; |
| | | } |
| | | handleSubmit(); |
| | | return; |
| | | } |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | const submitData = { |
| | | id: form.value.id, |
| | | productName: form.value.productName, |
| | | batchNumber: form.value.batchNumber, |
| | | expireDate: form.value.expiryDate, |
| | | stockQuantity: form.value.stockQuantity, |
| | | customerName: form.value.customerName, |
| | | contactPhone: form.value.contactPhone, |
| | | disRes: form.value.problemDesc, |
| | | status: form.value.status, |
| | | disposeUserId: form.value.handlerId, |
| | | disposeNickName: userList.value.find(item => item.userId === form.value.handlerId)?.nickName, |
| | | disposeResult: form.value.handleResult, |
| | | disDate: form.value.handleDate |
| | | }; |
| | | |
| | | const apiCall = operationType.value === 'add' ? expiryAfterSalesAdd : expiryAfterSalesUpdate; |
| | | apiCall(submitData).then(() => { |
| | | proxy.$modal.msgSuccess(operationType.value === 'add' ? "æ°å¢æå" : "æ´æ°æå"); |
| | | closeDia(); |
| | | }).catch(error => { |
| | | console.error('æäº¤æ°æ®å¤±è´¥:', error); |
| | | proxy.$modal.msgError('æäº¤æ°æ®å¤±è´¥ï¼è¯·ç¨åéè¯'); |
| | | }); |
| | | handleSubmit(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | const handleSubmit = () => { |
| | | const submitData = { |
| | | id: form.value.id, |
| | | productName: form.value.productName, |
| | | batchNumber: form.value.batchNumber, |
| | | expireDate: form.value.expiryDate, |
| | | stockQuantity: form.value.stockQuantity, |
| | | customerName: form.value.customerName, |
| | | contactPhone: form.value.contactPhone, |
| | | disRes: form.value.problemDesc, |
| | | status: operationType.value === "handle" ? 2 : form.value.status, |
| | | disposeUserId: form.value.handlerId, |
| | | disposeNickName: userList.value.find(item => item.userId === form.value.handlerId)?.nickName, |
| | | disposeResult: form.value.handleResult, |
| | | disDate: form.value.handleDate |
| | | }; |
| | | |
| | | const apiCall = operationType.value === 'add' ? expiryAfterSalesAdd : expiryAfterSalesUpdate; |
| | | apiCall(submitData).then(() => { |
| | | const successText = operationType.value === "add" ? "æ°å¢æå" : operationType.value === "handle" ? "å¤çæå" : "æ´æ°æå"; |
| | | proxy.$modal.msgSuccess(successText); |
| | | closeDia(); |
| | | }).catch(error => { |
| | | console.error('æäº¤æ°æ®å¤±è´¥:', error); |
| | | proxy.$modal.msgError('æäº¤æ°æ®å¤±è´¥ï¼è¯·ç¨åéè¯'); |
| | | }); |
| | | } |
| | | |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | |
| | | <el-button type="danger" @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | |
| | | |
| | | <template #operation="{ row }"> |
| | | <el-button type="primary" link @click="openForm('view', row)">æ¥ç</el-button> |
| | | <el-button type="primary" link @click="openForm('edit', row)" v-if="row.status === 1">ç¼è¾</el-button> |
| | | <el-button type="primary" link @click="openForm('handle', row)" v-if="row.status === 1">å¤ç</el-button> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | |
| | | current: page.value.current, |
| | | size: page.value.size |
| | | }; |
| | | |
| | | |
| | | expiryAfterSalesListPage(queryParams).then(res => { |
| | | // æ å°å端è¿åæ°æ®å°åç«¯è¡¨æ ¼ |
| | | tableData.value = res.data.records.map(item => ({ |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | title="æ°å¢å®åå" |
| | | width="90%" |
| | | @close="closeDia" |
| | | > |
| | | <el-dialog v-model="dialogFormVisible" |
| | | title="æ°å¢å®åå" |
| | | width="90%" |
| | | @close="closeDia"> |
| | | <div> |
| | | <span class="descriptions">åºç¡èµæ</span> |
| | | <el-form |
| | | :model="form" |
| | | label-width="140px" |
| | | label-position="top" |
| | | :rules="rules" |
| | | ref="formRef" |
| | | > |
| | | <el-form :model="form" |
| | | label-width="140px" |
| | | label-position="top" |
| | | :rules="rules" |
| | | ref="formRef"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="4"> |
| | | <el-form-item label="客æ·åç§°ï¼" prop="customerName"> |
| | | <el-select |
| | | v-model="form.customerName" |
| | | filterable |
| | | @change="customerNameChange" |
| | | > |
| | | <el-option |
| | | v-for="item in customerNameOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | <el-form-item label="客æ·åç§°ï¼" |
| | | prop="customerName"> |
| | | <el-select v-model="form.customerName" |
| | | filterable |
| | | @change="customerNameChange"> |
| | | <el-option v-for="item in customerNameOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item label="å®åç±»åï¼" prop="serviceType"> |
| | | <el-select |
| | | v-model="form.serviceType" |
| | | filterable |
| | | > |
| | | <el-option |
| | | v-for="dict in serviceTypeOptions" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | | <el-form-item label="å®åç±»åï¼" |
| | | prop="serviceType"> |
| | | <el-select v-model="form.serviceType" |
| | | filterable> |
| | | <el-option v-for="dict in serviceTypeOptions" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item label="å
³èéå®åå·ï¼" prop="salesContractNo"> |
| | | <el-select |
| | | v-model="form.salesContractNo" |
| | | @change="associatedSalesOrderNumberChange" |
| | | filterable |
| | | > |
| | | <el-option |
| | | v-for="item in associatedSalesOrderNumberOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | <el-form-item label="å
³èéå®åå·ï¼" |
| | | prop="salesContractNo"> |
| | | <el-select v-model="form.salesContractNo" |
| | | @change="associatedSalesOrderNumberChange" |
| | | filterable> |
| | | <el-option v-for="item in associatedSalesOrderNumberOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item label="ç´§æ¥ç¨åº¦ï¼" prop="urgency"> |
| | | <el-select |
| | | v-model="form.urgency" |
| | | filterable |
| | | > |
| | | <el-option |
| | | v-for="dict in urgencyOptions" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | | <el-form-item label="ç´§æ¥ç¨åº¦ï¼" |
| | | prop="urgency"> |
| | | <el-select v-model="form.urgency" |
| | | filterable> |
| | | <el-option v-for="dict in urgencyOptions" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item label="é®é¢æè¿°ï¼" prop="proDesc"> |
| | | <el-input |
| | | v-model="form.proDesc" |
| | | placeholder="请è¾å
¥é®é¢æè¿°" |
| | | /> |
| | | <el-form-item label="é®é¢æè¿°ï¼" |
| | | prop="proDesc"> |
| | | <el-input v-model="form.proDesc" |
| | | placeholder="请è¾å
¥é®é¢æè¿°" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <hr> |
| | | <div style="padding-top: 20px"> |
| | | <div style="display: flex; justify-content: space-between"> |
| | | <span class="descriptions">å
³è产å</span> |
| | | <el-button |
| | | type="primary" |
| | | style="margin-right: 12px; margin-bottom: 10px" |
| | | @click="isShowProductSelectDialog = true" |
| | | > |
| | | <div style="padding-top: 20px"> |
| | | <div style="display: flex; justify-content: space-between"> |
| | | <span class="descriptions">å
³è产å</span> |
| | | <el-button type="primary" |
| | | style="margin-right: 12px; margin-bottom: 10px" |
| | | @click="isShowProductSelectDialog = true"> |
| | | éæ©äº§å |
| | | </el-button> |
| | | </div> |
| | | <PIMTable |
| | | :isShowPagination="false" |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | > |
| | | <template #approveStatus="{ row }"> |
| | | <el-tag :type="getApproveStatusType(row)" size="small"> |
| | | {{ getApproveStatusText(row) }} |
| | | </el-tag> |
| | | </template> |
| | | <template #shippingStatus="{ row }"> |
| | | <el-tag :type="getShippingStatusType(row)" size="small"> |
| | | {{ getShippingStatusText(row) }} |
| | | </el-tag> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | <PIMTable :isShowPagination="false" |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData"> |
| | | <template #approveStatus="{ row }"> |
| | | <el-tag :type="getApproveStatusType(row)" |
| | | size="small"> |
| | | {{ getApproveStatusText(row) }} |
| | | </el-tag> |
| | | </template> |
| | | <template #shippingStatus="{ row }"> |
| | | <el-tag :type="getShippingStatusType(row)" |
| | | size="small"> |
| | | {{ getShippingStatusText(row) }} |
| | | </el-tag> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" |
| | | @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- éæ©äº§åå¼¹çª --> |
| | | <ProductSelectDialog |
| | | v-model="isShowProductSelectDialog" |
| | | :products="currentSalesOrderProducts" |
| | | :selected-ids="currentSelectedProductIds" |
| | | @confirm="handleSelectProducts" |
| | | /> |
| | | <ProductSelectDialog v-model="isShowProductSelectDialog" |
| | | :products="currentSalesOrderProducts" |
| | | :selected-ids="currentSelectedProductIds" |
| | | @confirm="handleSelectProducts" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, toRefs, getCurrentInstance, computed } from "vue"; |
| | | import ProductSelectDialog from "./ProductSelectDialog.vue"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import {userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | import {afterSalesServiceAdd, afterSalesServiceUpdate, getAllCustomerList, getSalesLedger } from "@/api/customerService/index.js"; |
| | | import { getCurrentDate } from "@/utils/index.js"; |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const formRef = ref(null) |
| | | const customerNameOptions = ref([]) |
| | | const userStore = useUserStore(); |
| | | import { ref, reactive, toRefs, getCurrentInstance, computed } from "vue"; |
| | | import ProductSelectDialog from "./ProductSelectDialog.vue"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import { |
| | | afterSalesServiceAdd, |
| | | afterSalesServiceUpdate, |
| | | getAllCustomerList, |
| | | getSalesLedger, |
| | | } from "@/api/customerService/index.js"; |
| | | import { getCurrentDate } from "@/utils/index.js"; |
| | | const { proxy } = getCurrentInstance(); |
| | | const emit = defineEmits(["close"]); |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref(""); |
| | | const formRef = ref(null); |
| | | const customerNameOptions = ref([]); |
| | | const userStore = useUserStore(); |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | topic: "", |
| | | serviceType: "", |
| | | urgency: "", |
| | | salesLedgerId: null, |
| | | productModelIds: "", |
| | | customerId: null, |
| | | salesContractNo: "", |
| | | proDesc: "", |
| | | customerName: "" |
| | | }, |
| | | rules: { |
| | | customerName: [{required: true, message: "è¯·éæ©å®¢æ·åç§°", trigger: "change"}], |
| | | serviceType: [{required: true, message: "è¯·éæ©å®åç±»å", trigger: "change"}], |
| | | urgency: [{required: true, message: "è¯·éæ©ç´§æ¥ç¨åº¦", trigger: "change"}], |
| | | feedbackDate: [{required: true, message: "è¯·éæ©", trigger: "change"}], |
| | | } |
| | | }) |
| | | |
| | | // èªå®ä¹æ ¡éªå½æ°ï¼å¤ææ¯å¦éè¦æ ¡éªå®åç¼å· |
| | | |
| | | const { form, rules } = toRefs(data); |
| | | const userList = ref([]) |
| | | |
| | | const formatCurrency = (val) => { |
| | | if (val === null || val === undefined || val === '') return '-' |
| | | const num = Number(val) |
| | | return Number.isFinite(num) ? num.toFixed(2) : '-' |
| | | } |
| | | |
| | | const { post_sale_waiting_list, degree_of_urgency } = proxy.useDict( |
| | | "post_sale_waiting_list", |
| | | "degree_of_urgency" |
| | | ); |
| | | |
| | | const serviceTypeOptions = computed(() => post_sale_waiting_list?.value || []); |
| | | const urgencyOptions = computed(() => degree_of_urgency?.value || []); |
| | | |
| | | const getProductRowId = (row) => { |
| | | return row?.id ?? row?.productModelId ?? row?.modelId ?? `${row?.productCategory || row?.productName || ""}-${row?.specificationModel || row?.model || ""}-${row?.unit || ""}` |
| | | } |
| | | |
| | | const normalizeProductRow = (row) => { |
| | | return { |
| | | ...row, |
| | | id: getProductRowId(row), |
| | | productCategory: row?.productCategory ?? row?.productName ?? '', |
| | | specificationModel: row?.specificationModel ?? row?.model ?? '', |
| | | unit: row?.unit ?? '', |
| | | approveStatus: row?.approveStatus ?? null, |
| | | shippingStatus: row?.shippingStatus ?? '', |
| | | expressCompany: row?.expressCompany ?? '', |
| | | expressNumber: row?.expressNumber ?? '', |
| | | shippingCarNumber: row?.shippingCarNumber ?? '', |
| | | shippingDate: row?.shippingDate ?? '', |
| | | quantity: row?.quantity ?? 0, |
| | | taxRate: row?.taxRate ?? 0, |
| | | taxInclusiveUnitPrice: row?.taxInclusiveUnitPrice ?? 0, |
| | | taxInclusiveTotalPrice: row?.taxInclusiveTotalPrice ?? 0, |
| | | taxExclusiveTotalPrice: row?.taxExclusiveTotalPrice ?? 0, |
| | | noQuantity: row?.noQuantity ?? 0, |
| | | } |
| | | } |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "产å大类", prop: "productCategory" }, |
| | | { label: "è§æ ¼åå·", prop: "specificationModel" }, |
| | | { label: "åä½", prop: "unit" }, |
| | | { |
| | | label: "产åç¶æ", |
| | | prop: "approveStatus", |
| | | width: 100, |
| | | align: "center", |
| | | dataType: "slot", |
| | | slot: "approveStatus", |
| | | }, |
| | | { |
| | | label: "åè´§ç¶æ", |
| | | align: "center", |
| | | width: 140, |
| | | dataType: "slot", |
| | | slot: "shippingStatus", |
| | | }, |
| | | { label: "å¿«éå
¬å¸", prop: "expressCompany", width: 140 }, |
| | | { label: "å¿«éåå·", prop: "expressNumber", width: 160 }, |
| | | { label: "å货车ç", prop: "shippingCarNumber", minWidth: 100, align: "center" }, |
| | | { label: "åè´§æ¥æ", prop: "shippingDate", minWidth: 100, align: "center" }, |
| | | { label: "æ°é", prop: "quantity", width: 100 }, |
| | | { label: "ç¨ç(%)", prop: "taxRate", width: 100 }, |
| | | { |
| | | label: "å«ç¨åä»·(å
)", |
| | | prop: "taxInclusiveUnitPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | label: "å«ç¨æ»ä»·(å
)", |
| | | prop: "taxInclusiveTotalPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | label: "ä¸å«ç¨æ»ä»·(å
)", |
| | | prop: "taxExclusiveTotalPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | operation: [ |
| | | { |
| | | name: "å é¤", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | tableData.value = tableData.value.filter(i => getProductRowId(i) !== getProductRowId(row)) |
| | | }, |
| | | |
| | | }, |
| | | ], |
| | | }, |
| | | ]) |
| | | const tableData = ref([]) |
| | | // éæ©äº§åå¼¹çª |
| | | const isShowProductSelectDialog = ref(false) |
| | | const handleSelectProducts = (rows) => { |
| | | if (!Array.isArray(rows)) return |
| | | const existingIds = new Set(tableData.value.map(i => String(getProductRowId(i)))) |
| | | const mapped = rows |
| | | .map(normalizeProductRow) |
| | | .filter(r => !existingIds.has(String(getProductRowId(r)))) |
| | | tableData.value = tableData.value.concat(mapped) |
| | | } |
| | | const currentSelectedProductIds = computed(() => { |
| | | return tableData.value.map(item => getProductRowId(item)).filter(item => item !== undefined && item !== null && item !== '') |
| | | }) |
| | | |
| | | const associatedSalesOrderNumberChange = () => { |
| | | const opt = associatedSalesOrderNumberOptions.value.find( |
| | | (item) => item.value === form.value.salesContractNo |
| | | ) |
| | | tableData.value = (opt?.productData || []).map(normalizeProductRow) |
| | | form.value.salesLedgerId = opt?.id || null |
| | | } |
| | | |
| | | const associatedSalesOrderNumberOptions = ref([]) |
| | | |
| | | const currentSalesOrderProducts = computed(() => { |
| | | const opt = associatedSalesOrderNumberOptions.value.find( |
| | | (item) => item.value === form.value.salesContractNo |
| | | ) |
| | | return (opt?.productData || []).map(normalizeProductRow) |
| | | }) |
| | | |
| | | const customerNameChange = (val) => { |
| | | form.value.salesContractNo = ""; |
| | | form.value.salesLedgerId = null; |
| | | tableData.value = []; |
| | | associatedSalesOrderNumberOptions.value = []; |
| | | const opt = customerNameOptions.value.find(item => item.value === val); |
| | | if (opt) { |
| | | form.value.customerId = opt.id; |
| | | } else { |
| | | form.value.customerId = null; |
| | | } |
| | | getSalesLedger({ |
| | | customerName: form.value.customerName |
| | | }).then(res => { |
| | | if(res.code === 200){ |
| | | associatedSalesOrderNumberOptions.value = res.data.records.map(item => ({ |
| | | label: item.salesContractNo, |
| | | value: item.salesContractNo, |
| | | productData:item.productData, |
| | | id: item.id |
| | | })) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const getApproveStatusText = (row) => { |
| | | if (!row) return 'ä¸è¶³' |
| | | if (row.approveStatus === 1 && (!row.shippingDate || !row.shippingCarNumber)) { |
| | | return 'å
è¶³' |
| | | } |
| | | if (row.approveStatus === 0 && (row.shippingDate || row.shippingCarNumber)) { |
| | | return 'å·²åºåº' |
| | | } |
| | | return 'ä¸è¶³' |
| | | } |
| | | |
| | | const getApproveStatusType = (row) => { |
| | | const statusText = getApproveStatusText(row) |
| | | return statusText === 'ä¸è¶³' ? 'danger' : 'success' |
| | | } |
| | | |
| | | const getShippingStatusText = (row) => { |
| | | if (!row) return 'å¾
åè´§' |
| | | if (row.shippingDate || row.shippingCarNumber) { |
| | | return 'å·²åè´§' |
| | | } |
| | | const status = row.shippingStatus |
| | | if (status === null || status === undefined || status === '') { |
| | | return 'å¾
åè´§' |
| | | } |
| | | const map = { |
| | | 'å¾
åè´§': 'å¾
åè´§', |
| | | 'å¾
å®¡æ ¸': 'å¾
å®¡æ ¸', |
| | | 'å®¡æ ¸ä¸': 'å®¡æ ¸ä¸', |
| | | 'å®¡æ ¸æç»': 'å®¡æ ¸æç»', |
| | | 'å®¡æ ¸éè¿': 'å®¡æ ¸éè¿', |
| | | 'å·²åè´§': 'å·²åè´§' |
| | | } |
| | | return map[String(status).trim()] || 'å¾
åè´§' |
| | | } |
| | | |
| | | const getShippingStatusType = (row) => { |
| | | if (!row) return 'info' |
| | | if (row.shippingDate || row.shippingCarNumber) { |
| | | return 'success' |
| | | } |
| | | const status = row.shippingStatus |
| | | if (status === null || status === undefined || status === '') { |
| | | return 'info' |
| | | } |
| | | const map = { |
| | | 'å¾
åè´§': 'info', |
| | | 'å¾
å®¡æ ¸': 'warning', |
| | | 'å®¡æ ¸ä¸': 'warning', |
| | | 'å®¡æ ¸æç»': 'danger', |
| | | 'å®¡æ ¸éè¿': 'success', |
| | | 'å·²åè´§': 'success' |
| | | } |
| | | return map[String(status).trim()] || 'info' |
| | | } |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog =async (type, row) => { |
| | | // 请æ±å¤ä¸ªæ¥å£ï¼è·åæ°æ® |
| | | let res = await getAllCustomerList({ |
| | | current: 1, |
| | | size: 1000, |
| | | total: 0, |
| | | const data = reactive({ |
| | | form: { |
| | | topic: "", |
| | | serviceType: "", |
| | | urgency: "", |
| | | salesLedgerId: null, |
| | | productModelIds: "", |
| | | customerId: null, |
| | | salesContractNo: "", |
| | | proDesc: "", |
| | | customerName: "", |
| | | }, |
| | | rules: { |
| | | customerName: [ |
| | | { required: true, message: "è¯·éæ©å®¢æ·åç§°", trigger: "change" }, |
| | | ], |
| | | serviceType: [ |
| | | { required: true, message: "è¯·éæ©å®åç±»å", trigger: "change" }, |
| | | ], |
| | | urgency: [{ required: true, message: "è¯·éæ©ç´§æ¥ç¨åº¦", trigger: "change" }], |
| | | feedbackDate: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | }, |
| | | }); |
| | | if(res.data.records){ |
| | | customerNameOptions.value = res.data.records.map(item => ({ |
| | | label: item.customerName, |
| | | value: item.customerName, |
| | | id: item.id |
| | | })); |
| | | } |
| | | |
| | | // èªå®ä¹æ ¡éªå½æ°ï¼å¤ææ¯å¦éè¦æ ¡éªå®åç¼å· |
| | | |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | form.value = {} |
| | | proxy.resetForm("formRef"); |
| | | form.value.checkUserId = userStore.id; |
| | | form.value.feedbackDate = getCurrentDate(); |
| | | // æ°å¢æ¶æ¸
空已éå
³è产å |
| | | if (type === "add") { |
| | | tableData.value = [] |
| | | } |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data; |
| | | }); |
| | | if (type === "edit") { |
| | | form.value = {...row} |
| | | if (form.value.customerName) { |
| | | const res = await getSalesLedger({ customerName: form.value.customerName }) |
| | | if (res?.code === 200) { |
| | | console.log(res) |
| | | associatedSalesOrderNumberOptions.value = (res.data?.records || []).map(item => ({ |
| | | const { form, rules } = toRefs(data); |
| | | const userList = ref([]); |
| | | |
| | | const formatCurrency = val => { |
| | | if (val === null || val === undefined || val === "") return "-"; |
| | | const num = Number(val); |
| | | return Number.isFinite(num) ? num.toFixed(2) : "-"; |
| | | }; |
| | | |
| | | const { post_sale_waiting_list, degree_of_urgency } = proxy.useDict( |
| | | "post_sale_waiting_list", |
| | | "degree_of_urgency" |
| | | ); |
| | | |
| | | const serviceTypeOptions = computed(() => post_sale_waiting_list?.value || []); |
| | | const urgencyOptions = computed(() => degree_of_urgency?.value || []); |
| | | |
| | | const getProductRowId = row => { |
| | | return ( |
| | | row?.id ?? |
| | | row?.productModelId ?? |
| | | row?.modelId ?? |
| | | `${row?.productCategory || row?.productName || ""}-${ |
| | | row?.specificationModel || row?.model || "" |
| | | }-${row?.unit || ""}` |
| | | ); |
| | | }; |
| | | |
| | | const normalizeProductRow = row => { |
| | | return { |
| | | ...row, |
| | | id: getProductRowId(row), |
| | | productCategory: row?.productCategory ?? row?.productName ?? "", |
| | | specificationModel: row?.specificationModel ?? row?.model ?? "", |
| | | unit: row?.unit ?? "", |
| | | approveStatus: row?.approveStatus ?? null, |
| | | shippingStatus: row?.shippingStatus ?? "", |
| | | expressCompany: row?.expressCompany ?? "", |
| | | expressNumber: row?.expressNumber ?? "", |
| | | shippingCarNumber: row?.shippingCarNumber ?? "", |
| | | shippingDate: row?.shippingDate ?? "", |
| | | quantity: row?.quantity ?? 0, |
| | | taxRate: row?.taxRate ?? 0, |
| | | taxInclusiveUnitPrice: row?.taxInclusiveUnitPrice ?? 0, |
| | | taxInclusiveTotalPrice: row?.taxInclusiveTotalPrice ?? 0, |
| | | taxExclusiveTotalPrice: row?.taxExclusiveTotalPrice ?? 0, |
| | | noQuantity: row?.noQuantity ?? 0, |
| | | }; |
| | | }; |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "产å大类", prop: "productCategory" }, |
| | | { label: "è§æ ¼åå·", prop: "specificationModel" }, |
| | | { label: "åä½", prop: "unit" }, |
| | | { |
| | | label: "产åç¶æ", |
| | | prop: "approveStatus", |
| | | width: 100, |
| | | align: "center", |
| | | dataType: "slot", |
| | | slot: "approveStatus", |
| | | }, |
| | | { |
| | | label: "åè´§ç¶æ", |
| | | align: "center", |
| | | width: 140, |
| | | dataType: "slot", |
| | | slot: "shippingStatus", |
| | | }, |
| | | { label: "å¿«éå
¬å¸", prop: "expressCompany", width: 140 }, |
| | | { label: "å¿«éåå·", prop: "expressNumber", width: 160 }, |
| | | { |
| | | label: "å货车ç", |
| | | prop: "shippingCarNumber", |
| | | minWidth: 100, |
| | | align: "center", |
| | | }, |
| | | { label: "åè´§æ¥æ", prop: "shippingDate", minWidth: 100, align: "center" }, |
| | | { label: "æ°é", prop: "quantity", width: 100 }, |
| | | { label: "ç¨ç(%)", prop: "taxRate", width: 100 }, |
| | | { |
| | | label: "å«ç¨åä»·(å
)", |
| | | prop: "taxInclusiveUnitPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | label: "å«ç¨æ»ä»·(å
)", |
| | | prop: "taxInclusiveTotalPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | label: "ä¸å«ç¨æ»ä»·(å
)", |
| | | prop: "taxExclusiveTotalPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | operation: [ |
| | | { |
| | | name: "å é¤", |
| | | type: "text", |
| | | clickFun: row => { |
| | | tableData.value = tableData.value.filter( |
| | | i => getProductRowId(i) !== getProductRowId(row) |
| | | ); |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | | // éæ©äº§åå¼¹çª |
| | | const isShowProductSelectDialog = ref(false); |
| | | const handleSelectProducts = rows => { |
| | | if (!Array.isArray(rows)) return; |
| | | const existingIds = new Set( |
| | | tableData.value.map(i => String(getProductRowId(i))) |
| | | ); |
| | | const mapped = rows |
| | | .map(normalizeProductRow) |
| | | .filter(r => !existingIds.has(String(getProductRowId(r)))); |
| | | tableData.value = tableData.value.concat(mapped); |
| | | }; |
| | | const currentSelectedProductIds = computed(() => { |
| | | return tableData.value |
| | | .map(item => getProductRowId(item)) |
| | | .filter(item => item !== undefined && item !== null && item !== ""); |
| | | }); |
| | | |
| | | const associatedSalesOrderNumberChange = () => { |
| | | const opt = associatedSalesOrderNumberOptions.value.find( |
| | | item => item.value === form.value.salesContractNo |
| | | ); |
| | | tableData.value = (opt?.productData || []).map(normalizeProductRow); |
| | | form.value.salesLedgerId = opt?.id || null; |
| | | }; |
| | | |
| | | const associatedSalesOrderNumberOptions = ref([]); |
| | | |
| | | const currentSalesOrderProducts = computed(() => { |
| | | const opt = associatedSalesOrderNumberOptions.value.find( |
| | | item => item.value === form.value.salesContractNo |
| | | ); |
| | | return (opt?.productData || []).map(normalizeProductRow); |
| | | }); |
| | | |
| | | const customerNameChange = val => { |
| | | form.value.salesContractNo = ""; |
| | | form.value.salesLedgerId = null; |
| | | tableData.value = []; |
| | | associatedSalesOrderNumberOptions.value = []; |
| | | const opt = customerNameOptions.value.find(item => item.value === val); |
| | | if (opt) { |
| | | form.value.customerId = opt.id; |
| | | } else { |
| | | form.value.customerId = null; |
| | | } |
| | | getSalesLedger({ |
| | | customerName: form.value.customerName, |
| | | }).then(res => { |
| | | if (res.code === 200) { |
| | | associatedSalesOrderNumberOptions.value = res.data.records.map(item => ({ |
| | | label: item.salesContractNo, |
| | | value: item.salesContractNo, |
| | | productData: item.productData, |
| | | id: item.id |
| | | })) |
| | | id: item.id, |
| | | })); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const getApproveStatusText = row => { |
| | | if (!row) return "ä¸è¶³"; |
| | | if ( |
| | | row.approveStatus === 1 && |
| | | (!row.shippingDate || !row.shippingCarNumber) |
| | | ) { |
| | | return "å
è¶³"; |
| | | } |
| | | console.log(form.value) |
| | | } |
| | | } |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | // å¹é
产ååå·IDs |
| | | form.value.productModelIds = tableData.value.map(item => item.id).join(",") |
| | | if (operationType.value === "add") { |
| | | afterSalesServiceAdd(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("æ°å¢æå") |
| | | closeDia() |
| | | }) |
| | | } else { |
| | | afterSalesServiceUpdate(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå") |
| | | closeDia() |
| | | }) |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | dialogFormVisible.value = false; |
| | | emit('close') |
| | | }; |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | if (row.approveStatus === 0 && (row.shippingDate || row.shippingCarNumber)) { |
| | | return "å·²åºåº"; |
| | | } |
| | | return "ä¸è¶³"; |
| | | }; |
| | | |
| | | const getApproveStatusType = row => { |
| | | const statusText = getApproveStatusText(row); |
| | | return statusText === "ä¸è¶³" ? "danger" : "success"; |
| | | }; |
| | | |
| | | const getShippingStatusText = row => { |
| | | if (!row) return "å¾
åè´§"; |
| | | if (row.shippingDate || row.shippingCarNumber) { |
| | | return "å·²åè´§"; |
| | | } |
| | | const status = row.shippingStatus; |
| | | if (status === null || status === undefined || status === "") { |
| | | return "å¾
åè´§"; |
| | | } |
| | | const map = { |
| | | å¾
åè´§: "å¾
åè´§", |
| | | å¾
å®¡æ ¸: "å¾
å®¡æ ¸", |
| | | å®¡æ ¸ä¸: "å®¡æ ¸ä¸", |
| | | å®¡æ ¸æç»: "å®¡æ ¸æç»", |
| | | å®¡æ ¸éè¿: "å®¡æ ¸éè¿", |
| | | å·²åè´§: "å·²åè´§", |
| | | }; |
| | | return map[String(status).trim()] || "å¾
åè´§"; |
| | | }; |
| | | |
| | | const getShippingStatusType = row => { |
| | | if (!row) return "info"; |
| | | if (row.shippingDate || row.shippingCarNumber) { |
| | | return "success"; |
| | | } |
| | | const status = row.shippingStatus; |
| | | if (status === null || status === undefined || status === "") { |
| | | return "info"; |
| | | } |
| | | const map = { |
| | | å¾
åè´§: "info", |
| | | å¾
å®¡æ ¸: "warning", |
| | | å®¡æ ¸ä¸: "warning", |
| | | å®¡æ ¸æç»: "danger", |
| | | å®¡æ ¸éè¿: "success", |
| | | å·²åè´§: "success", |
| | | }; |
| | | return map[String(status).trim()] || "info"; |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog = async (type, row) => { |
| | | // 请æ±å¤ä¸ªæ¥å£ï¼è·åæ°æ® |
| | | let res = await getAllCustomerList({ |
| | | current: 1, |
| | | size: 1000, |
| | | total: 0, |
| | | }); |
| | | console.log(res, "res"); |
| | | |
| | | if (res.data.records) { |
| | | customerNameOptions.value = res.data.records.map(item => ({ |
| | | label: item.customerName, |
| | | value: item.customerName, |
| | | id: item.id, |
| | | })); |
| | | } else { |
| | | } |
| | | |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | form.value = {}; |
| | | proxy.resetForm("formRef"); |
| | | form.value.checkUserId = userStore.id; |
| | | form.value.feedbackDate = getCurrentDate(); |
| | | // æ°å¢æ¶æ¸
空已éå
³è产å |
| | | if (type === "add") { |
| | | tableData.value = []; |
| | | } |
| | | userListNoPageByTenantId().then(res => { |
| | | userList.value = res.data; |
| | | }); |
| | | if (type === "edit") { |
| | | form.value = { ...row }; |
| | | if (form.value.customerName) { |
| | | const res = await getSalesLedger({ |
| | | customerName: form.value.customerName, |
| | | }); |
| | | if (res?.code === 200) { |
| | | console.log(res); |
| | | associatedSalesOrderNumberOptions.value = (res.data?.records || []).map( |
| | | item => ({ |
| | | label: item.salesContractNo, |
| | | value: item.salesContractNo, |
| | | productData: item.productData, |
| | | id: item.id, |
| | | }) |
| | | ); |
| | | } |
| | | } |
| | | console.log(form.value); |
| | | } |
| | | }; |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | // å¹é
产ååå·IDs |
| | | form.value.productModelIds = tableData.value |
| | | .map(item => item.id) |
| | | .join(","); |
| | | if (operationType.value === "add") { |
| | | afterSalesServiceAdd(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("æ°å¢æå"); |
| | | closeDia(); |
| | | }); |
| | | } else { |
| | | afterSalesServiceUpdate(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå"); |
| | | closeDia(); |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | dialogFormVisible.value = false; |
| | | emit("close"); |
| | | }; |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .descriptions { |
| | | margin-bottom: 20px; |
| | | display: inline-block; |
| | | font-size: 1rem; |
| | | font-weight: 600; |
| | | padding-left: 12px; |
| | | position: relative; |
| | | } |
| | | .descriptions { |
| | | margin-bottom: 20px; |
| | | display: inline-block; |
| | | font-size: 1rem; |
| | | font-weight: 600; |
| | | padding-left: 12px; |
| | | position: relative; |
| | | } |
| | | |
| | | .descriptions::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 4px; |
| | | height: 1rem; |
| | | background-color: #002FA7; /* Element é»è®¤çº¢è² */ |
| | | border-radius: 2px; |
| | | } |
| | | .descriptions::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 4px; |
| | | height: 1rem; |
| | | background-color: #002fa7; /* Element é»è®¤çº¢è² */ |
| | | border-radius: 2px; |
| | | } |
| | | </style> |
| | |
| | | align="center" |
| | | > |
| | | <template #default="scope"> |
| | | {{ scope.row.runtimeDuration || '-' }} |
| | | {{ getRuntimeDurationDisplay(scope.row) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, computed } from 'vue' |
| | | import { ref, onMounted, onUnmounted, computed } from 'vue' |
| | | import dayjs from 'dayjs' |
| | | import { ElMessage } from 'element-plus' |
| | | import { |
| | | VideoPlay, |
| | |
| | | |
| | | return filtered |
| | | }) |
| | | |
| | | // è¿è¡ä¸æ ç»ææ¶é´æ¶ï¼è¿è¡æ¶é¿ééå½åæ¶é´ååï¼ç¨ tick è§¦åæ¨¡æ¿éç® |
| | | const runtimeDisplayTick = ref(0) |
| | | |
| | | /** åå端å¯è½ä½¿ç¨çå¼å§/ç»ææ¶é´å段 */ |
| | | const pickStartTime = (row) => row?.startRuntimeTime ?? row?.startTime ?? row?.start_time |
| | | const pickEndTime = (row) => row?.endRuntimeTime ?? row?.endTime ?? row?.end_time |
| | | |
| | | /** |
| | | * è§£ææ¥å£/å端åå
¥çåç±»æ¶é´ï¼æ¶é´æ³ãISO å符串ãyyyy-MM-dd HH:mm:ssãJackson æ°ç» [y,M,d,h,m,s]ãå«ä¸æç toLocaleString ç |
| | | */ |
| | | const parseDeviceTime = (input) => { |
| | | if (input === null || input === undefined || input === '') return null |
| | | if (typeof input === 'number' && !Number.isNaN(input)) { |
| | | const d = dayjs(input) |
| | | return d.isValid() ? d.toDate() : null |
| | | } |
| | | if (Array.isArray(input)) { |
| | | const [y, mo, day, h = 0, mi = 0, se = 0] = input |
| | | if (y == null || y === '') return null |
| | | const d = dayjs() |
| | | .year(Number(y)) |
| | | .month(Number(mo || 1) - 1) |
| | | .date(Number(day || 1)) |
| | | .hour(Number(h) || 0) |
| | | .minute(Number(mi) || 0) |
| | | .second(Number(se) || 0) |
| | | return d.isValid() ? d.toDate() : null |
| | | } |
| | | const s = String(input).trim() |
| | | if (!s || s === '-') return null |
| | | let d = dayjs(s) |
| | | if (d.isValid()) return d.toDate() |
| | | d = dayjs(s.replace(/-/g, '/')) |
| | | if (d.isValid()) return d.toDate() |
| | | d = dayjs(s.replace(/\//g, '-')) |
| | | if (d.isValid()) return d.toDate() |
| | | return null |
| | | } |
| | | |
| | | const formatDurationMs = (durationMs) => { |
| | | if (durationMs == null || Number.isNaN(durationMs) || durationMs < 0) return '-' |
| | | const hours = Math.floor(durationMs / (1000 * 60 * 60)) |
| | | const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)) |
| | | if (hours === 0 && minutes === 0) return 'ä¸è¶³1åé' |
| | | return `${hours}å°æ¶${minutes}åé` |
| | | } |
| | | |
| | | const hasMeaningfulEnd = (endRaw) => |
| | | endRaw !== null && |
| | | endRaw !== undefined && |
| | | String(endRaw).trim() !== '' && |
| | | String(endRaw).trim() !== '-' |
| | | |
| | | const formatStoredDuration = (row) => { |
| | | const rd = row?.runtimeDuration |
| | | if (rd === null || rd === undefined) return '' |
| | | const t = String(rd).trim() |
| | | return t === '' || t === '-' ? '' : String(rd) |
| | | } |
| | | |
| | | /** è¿è¡ä¸ï¼å§ç»ç¨ãå½åæ¶é´ - å¼å§æ¶é´ãï¼å·²åæ¢ï¼ä¼å
æ¥å£ runtimeDurationï¼å¦åç¨ç»æ-å¼å§ï¼æ ç»æå¯çå·²åæ¶é¿æå¨ææ¨ç® */ |
| | | const getRuntimeDurationDisplay = (row) => { |
| | | void runtimeDisplayTick.value |
| | | const start = parseDeviceTime(pickStartTime(row)) |
| | | if (!start) { |
| | | return formatStoredDuration(row) || '-' |
| | | } |
| | | |
| | | const statusStr = String(row?.status ?? '').trim() |
| | | const isRunning = statusStr === 'è¿è¡ä¸' || statusStr === '1' |
| | | const endRaw = pickEndTime(row) |
| | | const hasEnd = hasMeaningfulEnd(endRaw) |
| | | |
| | | // æ ç»ææ¶é´ï¼è¿è¡ä¸ä¸å®å¨æç®ï¼å·²åæ¢åä¼å
å±ç¤ºåç«¯å·²åæ¶é¿ï¼æ²¡æåæå½åæ¶é´æ¨ç® |
| | | if (!hasEnd) { |
| | | if (isRunning) return formatDurationMs(Date.now() - start.getTime()) |
| | | const stored = formatStoredDuration(row) |
| | | if (stored) return stored |
| | | return formatDurationMs(Date.now() - start.getTime()) |
| | | } |
| | | |
| | | if (isRunning) { |
| | | return formatDurationMs(Date.now() - start.getTime()) |
| | | } |
| | | |
| | | const end = parseDeviceTime(endRaw) |
| | | const stored = formatStoredDuration(row) |
| | | if (stored) return stored |
| | | if (end) return formatDurationMs(end.getTime() - start.getTime()) |
| | | return '-' |
| | | } |
| | | |
| | | // æ£æ¥è®¾å¤æ¯å¦è¶
æ¶æªå¯å¨ |
| | | const isOverdue = (device) => { |
| | |
| | | device.endRuntimeTime = currentTime |
| | | // 计ç®è¿è¡æ¶é¿ |
| | | if (device.startRuntimeTime) { |
| | | const startTime = new Date(device.startRuntimeTime) |
| | | const endTime = new Date(currentTime) |
| | | const duration = endTime - startTime |
| | | const hours = Math.floor(duration / (1000 * 60 * 60)) |
| | | const minutes = Math.floor((duration % (1000 * 60 * 60)) / (1000 * 60)) |
| | | device.runtimeDuration = `${hours}å°æ¶${minutes}åé` |
| | | const startTime = parseDeviceTime(device.startRuntimeTime) |
| | | const endTime = parseDeviceTime(currentTime) |
| | | if (startTime && endTime) { |
| | | device.runtimeDuration = formatDurationMs(endTime.getTime() - startTime.getTime()) |
| | | } |
| | | } |
| | | } |
| | | const params = { |
| | |
| | | |
| | | |
| | | |
| | | // ç»ä»¶æè½½æ¶åå§åæ°æ® |
| | | const POLL_MS = 60 * 1000 |
| | | const RUNTIME_TICK_MS = 30 * 1000 |
| | | let listPollTimer = null |
| | | let runtimeTickTimer = null |
| | | |
| | | // ç»ä»¶æè½½æ¶æåæ°æ®ï¼å¹¶æ¯åéå·æ°ä¸æ¬¡å表ï¼è¿è¡ä¸æ¶é¿æ¯ 30 ç§å·æ°æ¾ç¤º |
| | | onMounted(() => { |
| | | getList() |
| | | listPollTimer = setInterval(() => { |
| | | getList() |
| | | }, POLL_MS) |
| | | runtimeTickTimer = setInterval(() => { |
| | | runtimeDisplayTick.value++ |
| | | }, RUNTIME_TICK_MS) |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | if (listPollTimer != null) { |
| | | clearInterval(listPollTimer) |
| | | listPollTimer = null |
| | | } |
| | | if (runtimeTickTimer != null) { |
| | | clearInterval(runtimeTickTimer) |
| | | runtimeTickTimer = null |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-form :model="filters" :inline="true"> |
| | | <el-form :model="filters" |
| | | :inline="true"> |
| | | <el-form-item label="ç§ç®ç¼ç :"> |
| | | <el-input v-model="filters.subjectCode" placeholder="请è¾å
¥ç§ç®ç¼ç " clearable style="width: 200px;" /> |
| | | <el-input v-model="filters.subjectCode" |
| | | placeholder="请è¾å
¥ç§ç®ç¼ç " |
| | | clearable |
| | | style="width: 200px;" /> |
| | | </el-form-item> |
| | | <el-form-item label="ç§ç®åç§°:"> |
| | | <el-input v-model="filters.subjectName" placeholder="请è¾å
¥ç§ç®åç§°" clearable style="width: 200px;" /> |
| | | <el-input v-model="filters.subjectName" |
| | | placeholder="请è¾å
¥ç§ç®åç§°" |
| | | clearable |
| | | style="width: 200px;" /> |
| | | </el-form-item> |
| | | <el-form-item label="ç§ç®ç±»å:"> |
| | | <el-select v-model="filters.subjectType" placeholder="è¯·éæ©" clearable style="width: 200px;"> |
| | | <el-option label="èµäº§ç±»" value="asset" /> |
| | | <el-option label="è´åºç±»" value="liability" /> |
| | | <el-option label="æçç±»" value="equity" /> |
| | | <el-option label="ææ¬ç±»" value="cost" /> |
| | | <el-option label="æçç±»" value="profit_loss" /> |
| | | <el-select v-model="filters.subjectType" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 200px;"> |
| | | <el-option label="èµäº§ç±»" |
| | | value="èµäº§ç±»" /> |
| | | <el-option label="è´åºç±»" |
| | | value="è´åºç±»" /> |
| | | <el-option label="æçç±»" |
| | | value="æçç±»" /> |
| | | <el-option label="ææ¬ç±»" |
| | | value="ææ¬ç±»" /> |
| | | <el-option label="æçç±»" |
| | | value="æçç±»" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="getTableData">æç´¢</el-button> |
| | | <el-button type="primary" |
| | | @click="getTableData">æç´¢</el-button> |
| | | <el-button @click="resetFilters">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | |
| | | <div class="actions"> |
| | | <div></div> |
| | | <div> |
| | | <el-button type="primary" @click="add" icon="Plus">æ°å¢</el-button> |
| | | <el-button @click="handleOut" icon="Download">导åº</el-button> |
| | | <el-button type="primary" |
| | | @click="add" |
| | | icon="Plus">æ°å¢</el-button> |
| | | <el-button @click="handleOut" |
| | | icon="Download">导åº</el-button> |
| | | </div> |
| | | </div> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :page="{ |
| | | <PIMTable rowKey="id" |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | | total: pagination.total, |
| | | }" |
| | | @pagination="changePage" |
| | | > |
| | | <template #subjectType="{ row }"> |
| | | <el-tag :type="getSubjectTypeType(row.subjectType)">{{ getSubjectTypeLabel(row.subjectType) }}</el-tag> |
| | | </template> |
| | | <template #balanceDirection="{ row }"> |
| | | <el-tag :type="row.balanceDirection === 'debit' ? 'success' : 'danger'"> |
| | | {{ row.balanceDirection === 'debit' ? 'åæ¹' : 'è´·æ¹' }} |
| | | </el-tag> |
| | | </template> |
| | | <template #status="{ row }"> |
| | | <el-tag :type="row.status === 'active' ? 'success' : 'info'"> |
| | | {{ row.status === 'active' ? 'å¯ç¨' : 'ç¦ç¨' }} |
| | | </el-tag> |
| | | </template> |
| | | <template #operation="{ row }"> |
| | | <el-button type="primary" link @click="edit(row)">ç¼è¾</el-button> |
| | | <el-button type="danger" link @click="handleDelete(row)">å é¤</el-button> |
| | | </template> |
| | | @pagination="changePage"> |
| | | </PIMTable> |
| | | </div> |
| | | |
| | | <FormDialog :title="dialogTitle" v-model="dialogVisible" width="600px" @confirm="submitForm" @cancel="dialogVisible = false"> |
| | | <el-form :model="form" :rules="rules" ref="formRef" label-width="100px"> |
| | | <el-form-item label="ç§ç®ç¼ç " prop="subjectCode"> |
| | | <el-input v-model="form.subjectCode" placeholder="请è¾å
¥ç§ç®ç¼ç " /> |
| | | <FormDialog :title="dialogTitle" |
| | | v-model="dialogVisible" |
| | | width="600px" |
| | | @confirm="submitForm" |
| | | @cancel="dialogVisible = false"> |
| | | <el-form :model="form" |
| | | :rules="rules" |
| | | ref="formRef" |
| | | label-width="100px"> |
| | | <el-form-item label="ç§ç®ç¼ç " |
| | | prop="subjectCode"> |
| | | <el-input v-model="form.subjectCode" |
| | | placeholder="请è¾å
¥ç§ç®ç¼ç " /> |
| | | </el-form-item> |
| | | <el-form-item label="ç§ç®åç§°" prop="subjectName"> |
| | | <el-input v-model="form.subjectName" placeholder="请è¾å
¥ç§ç®åç§°" /> |
| | | <el-form-item label="ç§ç®åç§°" |
| | | prop="subjectName"> |
| | | <el-input v-model="form.subjectName" |
| | | placeholder="请è¾å
¥ç§ç®åç§°" /> |
| | | </el-form-item> |
| | | <el-form-item label="ç§ç®ç±»å" prop="subjectType"> |
| | | <el-select v-model="form.subjectType" placeholder="è¯·éæ©ç§ç®ç±»å" style="width: 100%;"> |
| | | <el-option label="èµäº§ç±»" value="asset" /> |
| | | <el-option label="è´åºç±»" value="liability" /> |
| | | <el-option label="æçç±»" value="equity" /> |
| | | <el-option label="ææ¬ç±»" value="cost" /> |
| | | <el-option label="æçç±»" value="profit_loss" /> |
| | | <el-form-item label="ç§ç®ç±»å" |
| | | prop="subjectType"> |
| | | <el-select v-model="form.subjectType" |
| | | placeholder="è¯·éæ©ç§ç®ç±»å" |
| | | style="width: 100%;"> |
| | | <el-option label="èµäº§ç±»" |
| | | value="èµäº§ç±»" /> |
| | | <el-option label="è´åºç±»" |
| | | value="è´åºç±»" /> |
| | | <el-option label="æçç±»" |
| | | value="æçç±»" /> |
| | | <el-option label="ææ¬ç±»" |
| | | value="ææ¬ç±»" /> |
| | | <el-option label="æçç±»" |
| | | value="æçç±»" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="ä½é¢æ¹å" prop="balanceDirection"> |
| | | <el-form-item label="ä½é¢æ¹å" |
| | | prop="balanceDirection"> |
| | | <el-radio-group v-model="form.balanceDirection"> |
| | | <el-radio label="debit">åæ¹</el-radio> |
| | | <el-radio label="credit">è´·æ¹</el-radio> |
| | | <el-radio label="åæ¹">åæ¹</el-radio> |
| | | <el-radio label="è´·æ¹">è´·æ¹</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="ç¶æ" prop="status"> |
| | | <el-form-item label="ç¶æ" |
| | | prop="status"> |
| | | <el-radio-group v-model="form.status"> |
| | | <el-radio label="active">å¯ç¨</el-radio> |
| | | <el-radio label="inactive">ç¦ç¨</el-radio> |
| | | <el-radio :label="0">å¯ç¨</el-radio> |
| | | <el-radio :label="1">ç¦ç¨</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨" prop="remark"> |
| | | <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请è¾å
¥å¤æ³¨" /> |
| | | <el-form-item label="夿³¨" |
| | | prop="remark"> |
| | | <el-input v-model="form.remark" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请è¾å
¥å¤æ³¨" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button type="primary" @click="submitForm">ç¡®å®</el-button> |
| | | <el-button type="primary" |
| | | @click="submitForm">ç¡®å®</el-button> |
| | | <el-button @click="dialogVisible = false">åæ¶</el-button> |
| | | </template> |
| | | </FormDialog> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { ref, reactive, onMounted, getCurrentInstance } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { |
| | | listAccountSubject, |
| | | addAccountSubject, |
| | | updateAccountSubject, |
| | | delAccountSubject, |
| | | exportAccountSubject, |
| | | } from "@/api/financialManagement/accountSubject"; |
| | | |
| | | defineOptions({ |
| | | name: "æ»å¸ç§ç®", |
| | | }); |
| | | defineOptions({ |
| | | name: "æ»å¸ç§ç®", |
| | | }); |
| | | |
| | | const filters = reactive({ |
| | | subjectCode: "", |
| | | subjectName: "", |
| | | subjectType: "", |
| | | }); |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const pagination = reactive({ |
| | | currentPage: 1, |
| | | pageSize: 10, |
| | | total: 0, |
| | | }); |
| | | |
| | | const columns = [ |
| | | { label: "ç§ç®ç¼ç ", prop: "subjectCode", width: "120" }, |
| | | { label: "ç§ç®åç§°", prop: "subjectName", width: "150" }, |
| | | { label: "ç§ç®ç±»å", prop: "subjectType", slot: "subjectType" }, |
| | | { label: "ä½é¢æ¹å", prop: "balanceDirection", slot: "balanceDirection" }, |
| | | { label: "ç¶æ", prop: "status", slot: "status" }, |
| | | { label: "夿³¨", prop: "remark", showOverflowTooltip: true }, |
| | | { label: "æä½", prop: "operation", slot: "operation", width: "150", fixed: "right" }, |
| | | ]; |
| | | |
| | | const dataList = ref([]); |
| | | const dialogVisible = ref(false); |
| | | const dialogTitle = ref(""); |
| | | const formRef = ref(null); |
| | | const isEdit = ref(false); |
| | | const currentId = ref(null); |
| | | |
| | | const form = reactive({ |
| | | subjectCode: "", |
| | | subjectName: "", |
| | | subjectType: "", |
| | | balanceDirection: "debit", |
| | | status: "active", |
| | | remark: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | subjectCode: [{ required: true, message: "请è¾å
¥ç§ç®ç¼ç ", trigger: "blur" }], |
| | | subjectName: [{ required: true, message: "请è¾å
¥ç§ç®åç§°", trigger: "blur" }], |
| | | subjectType: [{ required: true, message: "è¯·éæ©ç§ç®ç±»å", trigger: "change" }], |
| | | }; |
| | | |
| | | const mockData = [ |
| | | { id: 1, subjectCode: "1001", subjectName: "åºåç°é", subjectType: "asset", balanceDirection: "debit", status: "active", remark: "" }, |
| | | { id: 2, subjectCode: "1002", subjectName: "é¶è¡å款", subjectType: "asset", balanceDirection: "debit", status: "active", remark: "" }, |
| | | { id: 3, subjectCode: "1122", subjectName: "åºæ¶è´¦æ¬¾", subjectType: "asset", balanceDirection: "debit", status: "active", remark: "" }, |
| | | { id: 4, subjectCode: "2202", subjectName: "åºä»è´¦æ¬¾", subjectType: "liability", balanceDirection: "credit", status: "active", remark: "" }, |
| | | { id: 5, subjectCode: "4001", subjectName: "宿¶èµæ¬", subjectType: "equity", balanceDirection: "credit", status: "active", remark: "" }, |
| | | { id: 6, subjectCode: "5001", subjectName: "çäº§ææ¬", subjectType: "cost", balanceDirection: "debit", status: "active", remark: "" }, |
| | | { id: 7, subjectCode: "6001", subjectName: "主è¥ä¸å¡æ¶å
¥", subjectType: "profit_loss", balanceDirection: "credit", status: "active", remark: "" }, |
| | | { id: 8, subjectCode: "6401", subjectName: "主è¥ä¸å¡ææ¬", subjectType: "profit_loss", balanceDirection: "debit", status: "active", remark: "" }, |
| | | ]; |
| | | |
| | | const getSubjectTypeLabel = (type) => { |
| | | const map = { |
| | | asset: "èµäº§ç±»", |
| | | liability: "è´åºç±»", |
| | | equity: "æçç±»", |
| | | cost: "ææ¬ç±»", |
| | | profit_loss: "æçç±»", |
| | | }; |
| | | return map[type] || type; |
| | | }; |
| | | |
| | | const getSubjectTypeType = (type) => { |
| | | const map = { |
| | | asset: "success", |
| | | liability: "danger", |
| | | equity: "warning", |
| | | cost: "info", |
| | | profit_loss: "primary", |
| | | }; |
| | | return map[type] || ""; |
| | | }; |
| | | |
| | | const getTableData = () => { |
| | | let result = [...mockData]; |
| | | if (filters.subjectCode) { |
| | | result = result.filter(item => item.subjectCode.includes(filters.subjectCode)); |
| | | } |
| | | if (filters.subjectName) { |
| | | result = result.filter(item => item.subjectName.includes(filters.subjectName)); |
| | | } |
| | | if (filters.subjectType) { |
| | | result = result.filter(item => item.subjectType === filters.subjectType); |
| | | } |
| | | pagination.total = result.length; |
| | | dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize); |
| | | }; |
| | | |
| | | const resetFilters = () => { |
| | | filters.subjectCode = ""; |
| | | filters.subjectName = ""; |
| | | filters.subjectType = ""; |
| | | pagination.currentPage = 1; |
| | | getTableData(); |
| | | }; |
| | | |
| | | const changePage = ({ current, size }) => { |
| | | pagination.currentPage = current; |
| | | pagination.pageSize = size; |
| | | getTableData(); |
| | | }; |
| | | |
| | | const add = () => { |
| | | isEdit.value = false; |
| | | dialogTitle.value = "æ°å¢ç§ç®"; |
| | | Object.assign(form, { |
| | | const filters = reactive({ |
| | | subjectCode: "", |
| | | subjectName: "", |
| | | subjectType: "", |
| | | balanceDirection: "debit", |
| | | status: "active", |
| | | }); |
| | | |
| | | const pagination = reactive({ |
| | | currentPage: 1, |
| | | pageSize: 10, |
| | | total: 0, |
| | | }); |
| | | |
| | | const columns = [ |
| | | { label: "ç§ç®ç¼ç ", prop: "subjectCode", width: "120" }, |
| | | { label: "ç§ç®åç§°", prop: "subjectName", width: "150" }, |
| | | { label: "ç§ç®ç±»å", prop: "subjectType" }, |
| | | { |
| | | label: "ä½é¢æ¹å", |
| | | prop: "balanceDirection", |
| | | dataType: "tag", |
| | | formatData: value => { |
| | | if (value === "åæ¹") { |
| | | return "åæ¹"; |
| | | } |
| | | return "è´·æ¹"; |
| | | }, |
| | | formatType: value => { |
| | | if (value === "åæ¹") { |
| | | return "primary"; |
| | | } |
| | | return "danger"; |
| | | }, |
| | | }, |
| | | { |
| | | label: "ç¶æ", |
| | | prop: "status", |
| | | dataType: "tag", |
| | | formatData: value => { |
| | | if (value === 0 || value === "0") { |
| | | return "å¯ç¨"; |
| | | } |
| | | return "ç¦ç¨"; |
| | | }, |
| | | formatType: value => { |
| | | if (value === 0 || value === "0") { |
| | | return "success"; |
| | | } |
| | | return "info"; |
| | | }, |
| | | }, |
| | | |
| | | { label: "夿³¨", prop: "remark", showOverflowTooltip: true }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: "150", |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "primary", |
| | | clickFun: row => { |
| | | edit(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "å é¤", |
| | | type: "danger", |
| | | clickFun: row => { |
| | | handleDelete(row); |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | ]; |
| | | |
| | | const dataList = ref([]); |
| | | const dialogVisible = ref(false); |
| | | const dialogTitle = ref(""); |
| | | const formRef = ref(null); |
| | | const isEdit = ref(false); |
| | | |
| | | const form = reactive({ |
| | | id: undefined, |
| | | subjectCode: "", |
| | | subjectName: "", |
| | | subjectType: "", |
| | | balanceDirection: "åæ¹", |
| | | status: 0, |
| | | remark: "", |
| | | }); |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | const edit = (row) => { |
| | | isEdit.value = true; |
| | | currentId.value = row.id; |
| | | dialogTitle.value = "ç¼è¾ç§ç®"; |
| | | Object.assign(form, row); |
| | | dialogVisible.value = true; |
| | | }; |
| | | const rules = { |
| | | subjectCode: [{ required: true, message: "请è¾å
¥ç§ç®ç¼ç ", trigger: "blur" }], |
| | | subjectName: [{ required: true, message: "请è¾å
¥ç§ç®åç§°", trigger: "blur" }], |
| | | subjectType: [ |
| | | { required: true, message: "è¯·éæ©ç§ç®ç±»å", trigger: "change" }, |
| | | ], |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | formRef.value.validate((valid) => { |
| | | if (valid) { |
| | | if (isEdit.value) { |
| | | const index = mockData.findIndex(item => item.id === currentId.value); |
| | | if (index !== -1) { |
| | | mockData[index] = { ...mockData[index], ...form }; |
| | | const getSubjectTypeType = type => { |
| | | const map = { |
| | | èµäº§ç±»: "success", |
| | | è´åºç±»: "danger", |
| | | æçç±»: "warning", |
| | | ææ¬ç±»: "info", |
| | | æçç±»: "primary", |
| | | }; |
| | | return map[type] || ""; |
| | | }; |
| | | |
| | | const getTableData = () => { |
| | | const query = { |
| | | pageNum: pagination.currentPage, |
| | | pageSize: pagination.pageSize, |
| | | ...filters, |
| | | }; |
| | | listAccountSubject(query).then(response => { |
| | | dataList.value = response.data.records; |
| | | pagination.total = response.data.total; |
| | | }); |
| | | }; |
| | | |
| | | const resetFilters = () => { |
| | | filters.subjectCode = ""; |
| | | filters.subjectName = ""; |
| | | filters.subjectType = ""; |
| | | pagination.currentPage = 1; |
| | | getTableData(); |
| | | }; |
| | | |
| | | const changePage = obj => { |
| | | pagination.currentPage = obj.page; |
| | | pagination.pageSize = obj.limit; |
| | | getTableData(); |
| | | }; |
| | | |
| | | const add = () => { |
| | | isEdit.value = false; |
| | | dialogTitle.value = "æ°å¢ç§ç®"; |
| | | Object.assign(form, { |
| | | id: undefined, |
| | | subjectCode: "", |
| | | subjectName: "", |
| | | subjectType: "", |
| | | balanceDirection: "åæ¹", |
| | | status: 0, |
| | | remark: "", |
| | | }); |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | const edit = row => { |
| | | isEdit.value = true; |
| | | dialogTitle.value = "ç¼è¾ç§ç®"; |
| | | Object.assign(form, row); |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | formRef.value.validate(valid => { |
| | | if (valid) { |
| | | if (isEdit.value) { |
| | | updateAccountSubject(form).then(() => { |
| | | ElMessage.success("ç¼è¾æå"); |
| | | dialogVisible.value = false; |
| | | getTableData(); |
| | | }); |
| | | } else { |
| | | addAccountSubject(form).then(() => { |
| | | ElMessage.success("æ°å¢æå"); |
| | | dialogVisible.value = false; |
| | | getTableData(); |
| | | }); |
| | | } |
| | | ElMessage.success("ç¼è¾æå"); |
| | | } else { |
| | | const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1; |
| | | mockData.push({ id: newId, ...form }); |
| | | ElMessage.success("æ°å¢æå"); |
| | | } |
| | | dialogVisible.value = false; |
| | | getTableData(); |
| | | } |
| | | }); |
| | | }; |
| | | }); |
| | | }; |
| | | |
| | | const handleDelete = (row) => { |
| | | ElMessageBox.confirm("确认å é¤è¯¥ç§ç®åï¼", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }).then(() => { |
| | | const index = mockData.findIndex(item => item.id === row.id); |
| | | if (index !== -1) { |
| | | mockData.splice(index, 1); |
| | | } |
| | | ElMessage.success("å 餿å"); |
| | | const handleDelete = row => { |
| | | const ids = row.id; |
| | | ElMessageBox.confirm("确认å é¤è¯¥ç§ç®åï¼", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | return delAccountSubject(ids); |
| | | }) |
| | | .then(() => { |
| | | ElMessage.success("å 餿å"); |
| | | getTableData(); |
| | | }); |
| | | }; |
| | | |
| | | const handleOut = () => { |
| | | proxy.download( |
| | | "accountSubject/export", |
| | | { |
| | | ...filters, |
| | | }, |
| | | `account_subject_${new Date().getTime()}.xlsx` |
| | | ); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getTableData(); |
| | | }); |
| | | }; |
| | | |
| | | const handleOut = () => { |
| | | ElMessage.success("å¯¼åºæå"); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getTableData(); |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .actions { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | margin-bottom: 15px; |
| | | } |
| | | .actions { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | margin-bottom: 15px; |
| | | } |
| | | </style> |
| | |
| | | </el-table-column> |
| | | <el-table-column label="审æ¹ç¶æ" prop="approvalStatus" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getApprovalStatusLabel(scope.row.approvalStatus) }} |
| | | <el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)" size="small"> |
| | | {{ getApprovalStatusLabel(scope.row.approvalStatus) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | }; |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | getStockOutPage({ ...searchForm.value, ...page, type: props.type, topParentProductId: props.topParentProductId }) |
| | | getStockOutPage({ ...searchForm.value, ...page, topParentProductId: props.topParentProductId }) |
| | | .then((res) => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records; |
| | |
| | | return approvalStatusLabelMap[status] || "å¾
审æ¹"; |
| | | }; |
| | | |
| | | // éè¿/驳ååºå®è²ï¼å
¶ä½ï¼å«å¾
审æ¹ã空å¼ãæªæ å°ä½ææ¡ä¸ºå¾
审æ¹ï¼ç»ä¸ç¨ warning é¢è¦è² |
| | | const getApprovalStatusTagType = (status) => { |
| | | if (status === 1 || status === "1" || status === "approved" || status === "APPROVED") return "success"; |
| | | if (status === 2 || status === "2" || status === "rejected" || status === "REJECTED") return "danger"; |
| | | return "warning"; |
| | | }; |
| | | |
| | | // è·åæ¥æºç±»åé项 |
| | | const fetchStockRecordTypeOptions = () => { |
| | | if (props.type === '0') { |
| | |
| | | prop="approvalStatus" |
| | | show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getApprovalStatusLabel(scope.row.approvalStatus) }} |
| | | <el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)" size="small"> |
| | | {{ getApprovalStatusLabel(scope.row.approvalStatus) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | return approvalStatusLabelMap[status] || "å¾
审æ¹"; |
| | | }; |
| | | |
| | | // éè¿/驳ååºå®è²ï¼å
¶ä½ï¼å«å¾
审æ¹ã空å¼ãæªæ å°ä½ææ¡ä¸ºå¾
审æ¹ï¼ç»ä¸ç¨ warning é¢è¦è² |
| | | const getApprovalStatusTagType = (status) => { |
| | | if (status === 1 || status === "1" || status === "approved" || status === "APPROVED") return "success"; |
| | | if (status === 2 || status === "2" || status === "rejected" || status === "REJECTED") return "danger"; |
| | | return "warning"; |
| | | }; |
| | | |
| | | const pageProductChange = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | |
| | | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | const params = {...page, type: props.type, topParentProductId: props.topParentProductId}; |
| | | const params = {...page, topParentProductId: props.topParentProductId}; |
| | | params.timeStr = searchForm.value.timeStr; |
| | | params.productName = searchForm.value.productName; |
| | | params.recordType = searchForm.value.recordType; |
| | |
| | | <div class="search_form"> |
| | | <div class="search_left"> |
| | | <span class="search_title">æ¥è¡¨ç±»åï¼</span> |
| | | <el-select |
| | | v-model="searchForm.reportType" |
| | | style="width: 150px;" |
| | | placeholder="è¯·éæ©" |
| | | @change="handleReportTypeChange" |
| | | > |
| | | <el-option label="æ¥æ¥" value="daily" /> |
| | | <el-option label="ææ¥" value="monthly" /> |
| | | <el-option label="è¿åºåæ¥è¡¨" value="inout" /> |
| | | <el-select v-model="searchForm.reportType" |
| | | style="width: 150px;" |
| | | placeholder="è¯·éæ©" |
| | | @change="handleReportTypeChange"> |
| | | <el-option label="æ¥æ¥" |
| | | value="daily" /> |
| | | <el-option label="ææ¥" |
| | | value="monthly" /> |
| | | <el-option label="è¿åºåæ¥è¡¨" |
| | | value="inout" /> |
| | | </el-select> |
| | | |
| | | <span class="search_title ml10">æ¶é´èå´ï¼</span> |
| | | <el-date-picker |
| | | v-if="searchForm.reportType === 'daily'" |
| | | v-model="searchForm.singleDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 200px;" |
| | | /> |
| | | <el-date-picker |
| | | v-else-if="searchForm.reportType === 'monthly'" |
| | | v-model="searchForm.monthRange" |
| | | type="monthrange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æä»½" |
| | | end-placeholder="ç»ææä»½" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 240px;" |
| | | /> |
| | | <el-date-picker |
| | | v-else |
| | | v-model="searchForm.dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 240px;" |
| | | /> |
| | | |
| | | <el-button type="primary" @click="onSearch" style="margin-left: 10px"> |
| | | <el-date-picker v-if="searchForm.reportType === 'daily'" |
| | | v-model="searchForm.singleDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 200px;" /> |
| | | <el-date-picker v-else-if="searchForm.reportType === 'monthly'" |
| | | v-model="searchForm.monthRange" |
| | | type="monthrange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æä»½" |
| | | end-placeholder="ç»ææä»½" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 240px;" /> |
| | | <el-date-picker v-else |
| | | v-model="searchForm.dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 240px;" /> |
| | | <el-button type="primary" |
| | | @click="onSearch" |
| | | style="margin-left: 10px"> |
| | | æ¥è¯¢ |
| | | </el-button> |
| | | <el-button @click="handleReset">éç½®</el-button> |
| | | </div> |
| | | |
| | | <div class="search_right"> |
| | | <!-- <el-button type="success" @click="handleExport" icon="Download">--> |
| | | <!-- å¯¼åºæ¥è¡¨--> |
| | | <!-- </el-button>--> |
| | | <!-- <el-button type="success" @click="handleExport" icon="Download">--> |
| | | <!-- å¯¼åºæ¥è¡¨--> |
| | | <!-- </el-button>--> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- <!– ç»è®¡å¡ç –>--> |
| | | <!-- <div class="stats_cards" v-if="reportData.summary">--> |
| | | <!-- <el-row :gutter="20">--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon in">--> |
| | | <!-- <el-icon><TrendCharts /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.totalIn || 0 }}</div>--> |
| | | <!-- <div class="stats_label">æ»å
¥åºé</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon out">--> |
| | | <!-- <el-icon><TrendCharts /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.totalOut || 0 }}</div>--> |
| | | <!-- <div class="stats_label">æ»åºåºé</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon stock">--> |
| | | <!-- <el-icon><Box /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.currentStock || 0 }}</div>--> |
| | | <!-- <div class="stats_label">å½ååºå</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon turnover">--> |
| | | <!-- <el-icon><Refresh /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.turnoverRate || 0 }}%</div>--> |
| | | <!-- <div class="stats_label">å¨è½¬ç</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- </el-row>--> |
| | | <!-- </div>--> |
| | | |
| | | <!-- <!– å¾è¡¨åºå –>--> |
| | | <!-- <div class="chart_section" v-if="reportData.chartData">--> |
| | | <!-- <el-row :gutter="20">--> |
| | | <!-- <el-col :span="12">--> |
| | | <!-- <el-card>--> |
| | | <!-- <template #header>--> |
| | | <!-- <span>åºåè¶å¿å¾</span>--> |
| | | <!-- </template>--> |
| | | <!-- <div ref="trendChart" style="height: 300px;"></div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="12">--> |
| | | <!-- <el-card>--> |
| | | <!-- <template #header>--> |
| | | <!-- <span>è¿åºåºå¯¹æ¯</span>--> |
| | | <!-- </template>--> |
| | | <!-- <div ref="comparisonChart" style="height: 300px;"></div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- </el-row>--> |
| | | <!-- </div>--> |
| | | |
| | | <!-- <!– ç»è®¡å¡ç –>--> |
| | | <!-- <div class="stats_cards" v-if="reportData.summary">--> |
| | | <!-- <el-row :gutter="20">--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon in">--> |
| | | <!-- <el-icon><TrendCharts /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.totalIn || 0 }}</div>--> |
| | | <!-- <div class="stats_label">æ»å
¥åºé</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon out">--> |
| | | <!-- <el-icon><TrendCharts /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.totalOut || 0 }}</div>--> |
| | | <!-- <div class="stats_label">æ»åºåºé</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon stock">--> |
| | | <!-- <el-icon><Box /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.currentStock || 0 }}</div>--> |
| | | <!-- <div class="stats_label">å½ååºå</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon turnover">--> |
| | | <!-- <el-icon><Refresh /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.turnoverRate || 0 }}%</div>--> |
| | | <!-- <div class="stats_label">å¨è½¬ç</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- </el-row>--> |
| | | <!-- </div>--> |
| | | <!-- <!– å¾è¡¨åºå –>--> |
| | | <!-- <div class="chart_section" v-if="reportData.chartData">--> |
| | | <!-- <el-row :gutter="20">--> |
| | | <!-- <el-col :span="12">--> |
| | | <!-- <el-card>--> |
| | | <!-- <template #header>--> |
| | | <!-- <span>åºåè¶å¿å¾</span>--> |
| | | <!-- </template>--> |
| | | <!-- <div ref="trendChart" style="height: 300px;"></div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="12">--> |
| | | <!-- <el-card>--> |
| | | <!-- <template #header>--> |
| | | <!-- <span>è¿åºåºå¯¹æ¯</span>--> |
| | | <!-- </template>--> |
| | | <!-- <div ref="comparisonChart" style="height: 300px;"></div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- </el-row>--> |
| | | <!-- </div>--> |
| | | <!-- è¯¦ç»æ°æ®è¡¨æ ¼ --> |
| | | <div class="table_section"> |
| | | <el-card> |
| | | <template #header> |
| | | <span>{{ getTableTitle() }}</span> |
| | | </template> |
| | | <el-table |
| | | v-loading="tableLoading" |
| | | :data="reportData.tableData" |
| | | border |
| | | height="400" |
| | | style="width: 100%" |
| | | :header-cell-style="{ background: '#F0F1F5', color: '#333333' }" |
| | | > |
| | | <el-table-column |
| | | align="center" |
| | | label="åºå·" |
| | | type="index" |
| | | width="60" |
| | | /> |
| | | <el-table-column |
| | | label="å
¥åºæ¶é´" |
| | | prop="createTime" |
| | | width="200" |
| | | show-overflow-tooltip |
| | | v-if="searchForm.reportType !== 'inout'" |
| | | /> |
| | | <el-table-column |
| | | label="å
¥åºæ¹æ¬¡" |
| | | prop="inboundBatches" |
| | | width="240" |
| | | show-overflow-tooltip |
| | | v-if="searchForm.reportType !== 'inout'" |
| | | /> |
| | | <el-table-column |
| | | label="产å大类" |
| | | prop="productName" |
| | | show-overflow-tooltip |
| | | /> |
| | | <el-table-column |
| | | label="è§æ ¼åå·" |
| | | prop="model" |
| | | show-overflow-tooltip |
| | | /> |
| | | <el-table-column |
| | | label="åä½" |
| | | prop="unit" |
| | | show-overflow-tooltip |
| | | /> |
| | | <el-table-column |
| | | label="å
¥åºæ°é" |
| | | prop="totalStockIn" |
| | | align="center" |
| | | v-if="searchForm.reportType === 'inout'" |
| | | /> |
| | | <el-table-column |
| | | label="å
¥åºæ°é" |
| | | prop="stockInNum" |
| | | align="center" |
| | | v-else |
| | | /> |
| | | <el-table-column |
| | | label="åºåºæ°é" |
| | | prop="totalStockOut" |
| | | width="100" |
| | | align="center" |
| | | v-if="searchForm.reportType === 'inout'" |
| | | /> |
| | | <el-table-column |
| | | label="ç°å¨åºå" |
| | | prop="currentStock" |
| | | align="center" |
| | | /> |
| | | <el-table-column label="æ¥æº" |
| | | prop="recordType" |
| | | v-if="searchForm.reportType !== 'inout'" |
| | | show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getRecordType(scope.row.recordType) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | label="å
¥åºäºº" |
| | | prop="createBy" |
| | | width="80" |
| | | v-if="searchForm.reportType !== 'inout'" |
| | | show-overflow-tooltip |
| | | /> |
| | | <el-table v-loading="tableLoading" |
| | | :data="reportData.tableData" |
| | | border |
| | | height="400" |
| | | style="width: 100%" |
| | | :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"> |
| | | <el-table-column align="center" |
| | | label="åºå·" |
| | | type="index" |
| | | width="60" /> |
| | | <el-table-column label="å
¥åºæ¶é´" |
| | | prop="createTime" |
| | | width="200" |
| | | show-overflow-tooltip |
| | | v-if="searchForm.reportType !== 'inout'" /> |
| | | <el-table-column label="å
¥åºæ¹æ¬¡" |
| | | prop="inboundBatches" |
| | | width="180" |
| | | show-overflow-tooltip |
| | | v-if="searchForm.reportType !== 'inout'" /> |
| | | <el-table-column label="æ¹å·" |
| | | prop="batchNo" |
| | | width="180" |
| | | show-overflow-tooltip |
| | | v-if="searchForm.reportType !== 'inout'" /> |
| | | <el-table-column label="产å大类" |
| | | prop="productName" |
| | | show-overflow-tooltip /> |
| | | <el-table-column label="è§æ ¼åå·" |
| | | prop="model" |
| | | show-overflow-tooltip /> |
| | | <el-table-column label="åä½" |
| | | prop="unit" |
| | | show-overflow-tooltip /> |
| | | <el-table-column label="å
¥åºæ°é" |
| | | prop="totalStockIn" |
| | | align="center" |
| | | v-if="searchForm.reportType === 'inout'" /> |
| | | <el-table-column label="å
¥åºæ°é" |
| | | prop="stockInNum" |
| | | align="center" |
| | | v-else /> |
| | | <el-table-column label="åºåºæ°é" |
| | | prop="totalStockOut" |
| | | width="100" |
| | | align="center" |
| | | v-if="searchForm.reportType === 'inout'" /> |
| | | <el-table-column label="ç°å¨åºå" |
| | | prop="currentStock" |
| | | align="center" /> |
| | | <el-table-column label="æ¥æº" |
| | | prop="recordType" |
| | | v-if="searchForm.reportType !== 'inout'" |
| | | show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getRecordType(scope.row.recordType) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å
¥åºäºº" |
| | | prop="createBy" |
| | | width="80" |
| | | v-if="searchForm.reportType !== 'inout'" |
| | | show-overflow-tooltip /> |
| | | </el-table> |
| | | <pagination |
| | | :total="total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page="page.current" |
| | | :limit="page.size" |
| | | @pagination="paginationChange" |
| | | /> |
| | | <pagination :total="total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page="page.current" |
| | | :limit="page.size" |
| | | @pagination="paginationChange" /> |
| | | </el-card> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, nextTick, getCurrentInstance } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import * as echarts from 'echarts' |
| | | import pagination from '@/components/PIMTable/Pagination.vue' |
| | | import { |
| | | getStockInventoryInAndOutReportList, |
| | | getStockInventoryReportList |
| | | } from "@/api/inventoryManagement/stockInventory.js"; |
| | | import { |
| | | findAllQualifiedStockInRecordTypeOptions,findAllUnQualifiedStockInRecordTypeOptions, |
| | | } from "@/api/basicData/enum.js"; |
| | | import { ref, reactive, onMounted, nextTick, getCurrentInstance } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import * as echarts from "echarts"; |
| | | import pagination from "@/components/PIMTable/Pagination.vue"; |
| | | import { |
| | | getStockInventoryInAndOutReportList, |
| | | getStockInventoryReportList, |
| | | } from "@/api/inventoryManagement/stockInventory.js"; |
| | | import { |
| | | findAllQualifiedStockInRecordTypeOptions, |
| | | findAllUnQualifiedStockInRecordTypeOptions, |
| | | } from "@/api/basicData/enum.js"; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | // ååºå¼æ°æ® |
| | | const tableLoading = ref(false); |
| | | const trendChart = ref(null); |
| | | const comparisonChart = ref(null); |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | // ååºå¼æ°æ® |
| | | const tableLoading = ref(false) |
| | | const trendChart = ref(null) |
| | | const comparisonChart = ref(null) |
| | | const searchForm = reactive({ |
| | | reportType: "daily", |
| | | singleDate: "", |
| | | dateRange: [], |
| | | monthRange: [], |
| | | }); |
| | | |
| | | const searchForm = reactive({ |
| | | reportType: 'daily', |
| | | singleDate: '', |
| | | dateRange: [], |
| | | monthRange: [] |
| | | }) |
| | | |
| | | const reportData = ref({ |
| | | summary: null, |
| | | chartData: null, |
| | | tableData: [] |
| | | }) |
| | | |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | }) |
| | | |
| | | const total = ref(0) |
| | | |
| | | const stockRecordTypeOptions = ref([]) |
| | | |
| | | const getRecordType = (recordType) => { |
| | | return stockRecordTypeOptions.value.find(item => item.value === recordType)?.label || '' |
| | | } |
| | | |
| | | // è·åæ¥æºç±»åé项 |
| | | const fetchStockRecordTypeOptions = () => { |
| | | findAllQualifiedStockInRecordTypeOptions() |
| | | .then(res => { |
| | | stockRecordTypeOptions.value = res.data; |
| | | findAllUnQualifiedStockInRecordTypeOptions() |
| | | .then(res => { |
| | | stockRecordTypeOptions.value = [...stockRecordTypeOptions.value,...res.data]; |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | // è·åè¡¨æ ¼æ é¢ |
| | | const getTableTitle = () => { |
| | | const typeMap = { |
| | | daily: 'æ¥æ¥è¯¦ç»æ°æ®', |
| | | monthly: 'ææ¥è¯¦ç»æ°æ®', |
| | | inout: 'è¿åºåæ¥è¡¨è¯¦ç»æ°æ®' |
| | | } |
| | | return typeMap[searchForm.reportType] || 'æ¥è¡¨è¯¦ç»æ°æ®' |
| | | } |
| | | |
| | | // æ¥è¡¨ç±»åæ¹å |
| | | const handleReportTypeChange = () => { |
| | | page.current = 1 |
| | | reportData.value = { |
| | | const reportData = ref({ |
| | | summary: null, |
| | | chartData: null, |
| | | tableData: [] |
| | | } |
| | | } |
| | | tableData: [], |
| | | }); |
| | | |
| | | // æ¥è¯¢æ°æ® |
| | | const handleQuery = async () => { |
| | | if (!validateSearchForm()) { |
| | | return |
| | | } |
| | | |
| | | tableLoading.value = true |
| | | try { |
| | | const baseParams = getQueryParams() |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | }); |
| | | |
| | | const total = ref(0); |
| | | |
| | | const stockRecordTypeOptions = ref([]); |
| | | |
| | | const getRecordType = recordType => { |
| | | return ( |
| | | stockRecordTypeOptions.value.find(item => item.value === recordType) |
| | | ?.label || "" |
| | | ); |
| | | }; |
| | | |
| | | // è·åæ¥æºç±»åé项 |
| | | const fetchStockRecordTypeOptions = () => { |
| | | findAllQualifiedStockInRecordTypeOptions().then(res => { |
| | | stockRecordTypeOptions.value = res.data; |
| | | findAllUnQualifiedStockInRecordTypeOptions().then(res => { |
| | | stockRecordTypeOptions.value = [ |
| | | ...stockRecordTypeOptions.value, |
| | | ...res.data, |
| | | ]; |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | // è·åè¡¨æ ¼æ é¢ |
| | | const getTableTitle = () => { |
| | | const typeMap = { |
| | | daily: "æ¥æ¥è¯¦ç»æ°æ®", |
| | | monthly: "ææ¥è¯¦ç»æ°æ®", |
| | | inout: "è¿åºåæ¥è¡¨è¯¦ç»æ°æ®", |
| | | }; |
| | | return typeMap[searchForm.reportType] || "æ¥è¡¨è¯¦ç»æ°æ®"; |
| | | }; |
| | | |
| | | // æ¥è¡¨ç±»åæ¹å |
| | | const handleReportTypeChange = () => { |
| | | page.current = 1; |
| | | reportData.value = { |
| | | summary: null, |
| | | chartData: null, |
| | | tableData: [], |
| | | }; |
| | | }; |
| | | |
| | | // æ¥è¯¢æ°æ® |
| | | const handleQuery = async () => { |
| | | if (!validateSearchForm()) { |
| | | return; |
| | | } |
| | | |
| | | tableLoading.value = true; |
| | | try { |
| | | const baseParams = getQueryParams(); |
| | | const params = { |
| | | ...baseParams, |
| | | current: page.current, |
| | | size: page.size, |
| | | }; |
| | | let response; |
| | | |
| | | if (searchForm.reportType === "inout") { |
| | | response = await getStockInventoryInAndOutReportList(params); |
| | | } else { |
| | | response = await getStockInventoryReportList(params); |
| | | } |
| | | if (response.code === 200) { |
| | | reportData.value.tableData = response.data.records || []; |
| | | total.value = response.data.total || 0; |
| | | // reportData.value.summary = response.data.summary |
| | | // reportData.value.chartData = response.data.chartData |
| | | // nextTick(() => { |
| | | // initCharts() |
| | | // }) |
| | | } |
| | | } catch (error) { |
| | | ElMessage.error("æ¥è¯¢å¤±è´¥ï¼" + error.message); |
| | | } finally { |
| | | tableLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // æ¥è¯¢æé®ï¼éç½®å°ç¬¬ä¸é¡µå¹¶æ¥è¯¢ |
| | | const onSearch = () => { |
| | | page.current = 1; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // å页åå |
| | | const paginationChange = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | handleQuery(); |
| | | }; |
| | | // // çæåæ°æ® |
| | | // const generateMockData = () => { |
| | | // // çæç»è®¡å¡çåæ°æ® |
| | | // const summary = { |
| | | // totalIn: 1000, |
| | | // totalOut: 600, |
| | | // currentStock: 400, |
| | | // turnoverRate: 30 |
| | | // } |
| | | |
| | | // // çæå¾è¡¨åæ°æ® |
| | | // const trendDates = ['2025-09-15', '2025-09-16', '2025-09-17', '2025-09-18', '2025-09-19'] |
| | | // const trendValues = [300, 350, 400, 380, 420] |
| | | // const comparisonDates = ['2025-09-15', '2025-09-16', '2025-09-17'] |
| | | // const inValues = [100, 150, 200] |
| | | // const outValues = [80, 120, 100] |
| | | |
| | | // const chartData = { |
| | | // trendDates, |
| | | // trendValues, |
| | | // comparisonDates, |
| | | // inValues, |
| | | // outValues |
| | | // } |
| | | |
| | | // reportData.value = { |
| | | // summary, |
| | | // chartData, |
| | | // tableData: [] |
| | | // } |
| | | // } |
| | | // éªè¯æç´¢è¡¨å |
| | | const validateSearchForm = () => { |
| | | if (searchForm.reportType === "daily") { |
| | | if (!searchForm.singleDate) { |
| | | ElMessage.warning("è¯·éæ©æ¥æ"); |
| | | return false; |
| | | } |
| | | } else if (searchForm.reportType === "inout") { |
| | | if (!searchForm.dateRange || searchForm.dateRange.length !== 2) { |
| | | ElMessage.warning("è¯·éæ©æ¥æèå´"); |
| | | return false; |
| | | } |
| | | } else if (searchForm.reportType === "monthly") { |
| | | if (!searchForm.monthRange || searchForm.monthRange.length !== 2) { |
| | | ElMessage.warning("è¯·éæ©æä»½èå´"); |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | }; |
| | | |
| | | // è·åæ¥è¯¢åæ° |
| | | const getQueryParams = () => { |
| | | const params = { |
| | | ...baseParams, |
| | | current: page.current, |
| | | size: page.size, |
| | | } |
| | | let response |
| | | reportType: searchForm.reportType, |
| | | reportDate: "", |
| | | startMonth: "", |
| | | endMonth: "", |
| | | startDate: "", |
| | | endDate: "", |
| | | }; |
| | | |
| | | if (searchForm.reportType === 'inout') { |
| | | response = await getStockInventoryInAndOutReportList(params) |
| | | if (searchForm.reportType === "daily") { |
| | | params.reportDate = searchForm.singleDate; |
| | | } else if (searchForm.reportType === "monthly") { |
| | | params.startMonth = searchForm.monthRange[0]; |
| | | params.endMonth = searchForm.monthRange[1]; |
| | | } else { |
| | | response = await getStockInventoryReportList(params) |
| | | params.startDate = searchForm.dateRange[0]; |
| | | params.endDate = searchForm.dateRange[1]; |
| | | } |
| | | if (response.code === 200) { |
| | | reportData.value.tableData = response.data.records || [] |
| | | total.value = response.data.total || 0 |
| | | // reportData.value.summary = response.data.summary |
| | | // reportData.value.chartData = response.data.chartData |
| | | // nextTick(() => { |
| | | // initCharts() |
| | | // }) |
| | | |
| | | |
| | | return params; |
| | | }; |
| | | |
| | | // éç½®æç´¢ |
| | | const handleReset = () => { |
| | | searchForm.reportType = "daily"; |
| | | searchForm.singleDate = ""; |
| | | searchForm.dateRange = []; |
| | | searchForm.monthRange = []; |
| | | reportData.value = { |
| | | summary: null, |
| | | chartData: null, |
| | | tableData: [], |
| | | }; |
| | | }; |
| | | |
| | | // å¯¼åºæ¥è¡¨ |
| | | const handleExport = async () => { |
| | | if (!validateSearchForm()) { |
| | | return; |
| | | } |
| | | } catch (error) { |
| | | ElMessage.error('æ¥è¯¢å¤±è´¥ï¼' + error.message) |
| | | } finally { |
| | | tableLoading.value = false |
| | | } |
| | | } |
| | | |
| | | // æ¥è¯¢æé®ï¼éç½®å°ç¬¬ä¸é¡µå¹¶æ¥è¯¢ |
| | | const onSearch = () => { |
| | | page.current = 1 |
| | | handleQuery() |
| | | } |
| | | try { |
| | | const params = getQueryParams(); |
| | | // const response = await exportStockReport(params) |
| | | proxy.download("/stockin/exportCopy", params, "åºåæ¥è¡¨.xlsx"); |
| | | // å建ä¸è½½é¾æ¥ |
| | | // const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) |
| | | // const url = window.URL.createObjectURL(blob) |
| | | // const link = document.createElement('a') |
| | | // link.href = url |
| | | // link.download = `${getTableTitle()}_${new Date().getTime()}.xlsx` |
| | | // document.body.appendChild(link) |
| | | // link.click() |
| | | // document.body.removeChild(link) |
| | | // window.URL.revokeObjectURL(url) |
| | | |
| | | // å页åå |
| | | const paginationChange = (obj) => { |
| | | page.current = obj.page |
| | | page.size = obj.limit |
| | | handleQuery() |
| | | } |
| | | // // çæåæ°æ® |
| | | // const generateMockData = () => { |
| | | // // çæç»è®¡å¡çåæ°æ® |
| | | // const summary = { |
| | | // totalIn: 1000, |
| | | // totalOut: 600, |
| | | // currentStock: 400, |
| | | // turnoverRate: 30 |
| | | // } |
| | | |
| | | // // çæå¾è¡¨åæ°æ® |
| | | // const trendDates = ['2025-09-15', '2025-09-16', '2025-09-17', '2025-09-18', '2025-09-19'] |
| | | // const trendValues = [300, 350, 400, 380, 420] |
| | | // const comparisonDates = ['2025-09-15', '2025-09-16', '2025-09-17'] |
| | | // const inValues = [100, 150, 200] |
| | | // const outValues = [80, 120, 100] |
| | | |
| | | // const chartData = { |
| | | // trendDates, |
| | | // trendValues, |
| | | // comparisonDates, |
| | | // inValues, |
| | | // outValues |
| | | // } |
| | | |
| | | // reportData.value = { |
| | | // summary, |
| | | // chartData, |
| | | // tableData: [] |
| | | // } |
| | | // } |
| | | // éªè¯æç´¢è¡¨å |
| | | const validateSearchForm = () => { |
| | | if (searchForm.reportType === 'daily') { |
| | | if (!searchForm.singleDate) { |
| | | ElMessage.warning('è¯·éæ©æ¥æ') |
| | | return false |
| | | // ElMessage.success('å¯¼åºæå') |
| | | } catch (error) { |
| | | ElMessage.error("导åºå¤±è´¥ï¼" + error.message); |
| | | } |
| | | } else if (searchForm.reportType === 'inout') { |
| | | if (!searchForm.dateRange || searchForm.dateRange.length !== 2) { |
| | | ElMessage.warning('è¯·éæ©æ¥æèå´') |
| | | return false |
| | | } |
| | | } else if (searchForm.reportType === 'monthly') { |
| | | if (!searchForm.monthRange || searchForm.monthRange.length !== 2) { |
| | | ElMessage.warning('è¯·éæ©æä»½èå´') |
| | | return false |
| | | } |
| | | } |
| | | return true |
| | | } |
| | | }; |
| | | |
| | | // è·åæ¥è¯¢åæ° |
| | | const getQueryParams = () => { |
| | | const params = { |
| | | reportType: searchForm.reportType, |
| | | reportDate: "", |
| | | startMonth: "", |
| | | endMonth: "", |
| | | startDate: "", |
| | | endDate: "" |
| | | } |
| | | |
| | | if (searchForm.reportType === 'daily') { |
| | | params.reportDate = searchForm.singleDate |
| | | } else if (searchForm.reportType === 'monthly') { |
| | | params.startMonth = searchForm.monthRange[0] |
| | | params.endMonth = searchForm.monthRange[1] |
| | | } else { |
| | | params.startDate = searchForm.dateRange[0] |
| | | params.endDate = searchForm.dateRange[1] |
| | | } |
| | | |
| | | return params |
| | | } |
| | | // åå§åå¾è¡¨ |
| | | const initCharts = () => { |
| | | if (!reportData.value.chartData) return; |
| | | |
| | | // éç½®æç´¢ |
| | | const handleReset = () => { |
| | | searchForm.reportType = 'daily' |
| | | searchForm.singleDate = '' |
| | | searchForm.dateRange = [] |
| | | searchForm.monthRange = [] |
| | | reportData.value = { |
| | | summary: null, |
| | | chartData: null, |
| | | tableData: [] |
| | | } |
| | | } |
| | | initTrendChart(); |
| | | initComparisonChart(); |
| | | }; |
| | | |
| | | // å¯¼åºæ¥è¡¨ |
| | | const handleExport = async () => { |
| | | if (!validateSearchForm()) { |
| | | return |
| | | } |
| | | |
| | | try { |
| | | const params = getQueryParams() |
| | | // const response = await exportStockReport(params) |
| | | proxy.download("/stockin/exportCopy", params, 'åºåæ¥è¡¨.xlsx') |
| | | // å建ä¸è½½é¾æ¥ |
| | | // const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) |
| | | // const url = window.URL.createObjectURL(blob) |
| | | // const link = document.createElement('a') |
| | | // link.href = url |
| | | // link.download = `${getTableTitle()}_${new Date().getTime()}.xlsx` |
| | | // document.body.appendChild(link) |
| | | // link.click() |
| | | // document.body.removeChild(link) |
| | | // window.URL.revokeObjectURL(url) |
| | | |
| | | // ElMessage.success('å¯¼åºæå') |
| | | } catch (error) { |
| | | ElMessage.error('导åºå¤±è´¥ï¼' + error.message) |
| | | } |
| | | } |
| | | // åå§åè¶å¿å¾ |
| | | const initTrendChart = () => { |
| | | if (!trendChart.value) return; |
| | | |
| | | // åå§åå¾è¡¨ |
| | | const initCharts = () => { |
| | | if (!reportData.value.chartData) return |
| | | |
| | | initTrendChart() |
| | | initComparisonChart() |
| | | } |
| | | |
| | | // åå§åè¶å¿å¾ |
| | | const initTrendChart = () => { |
| | | if (!trendChart.value) return |
| | | |
| | | const chart = echarts.init(trendChart.value) |
| | | const option = { |
| | | title: { |
| | | text: 'åºåååè¶å¿', |
| | | left: 'center' |
| | | }, |
| | | tooltip: { |
| | | trigger: 'axis' |
| | | }, |
| | | legend: { |
| | | data: ['åºåé'], |
| | | top: 30 |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: reportData.value.chartData.trendDates || [] |
| | | }, |
| | | yAxis: { |
| | | type: 'value' |
| | | }, |
| | | series: [{ |
| | | name: 'åºåé', |
| | | type: 'line', |
| | | data: reportData.value.chartData.trendValues || [], |
| | | smooth: true, |
| | | itemStyle: { |
| | | color: '#409EFF' |
| | | } |
| | | }] |
| | | } |
| | | chart.setOption(option) |
| | | } |
| | | |
| | | // åå§å对æ¯å¾ |
| | | const initComparisonChart = () => { |
| | | if (!comparisonChart.value) return |
| | | |
| | | const chart = echarts.init(comparisonChart.value) |
| | | const option = { |
| | | title: { |
| | | text: 'è¿åºåºå¯¹æ¯', |
| | | left: 'center' |
| | | }, |
| | | tooltip: { |
| | | trigger: 'axis' |
| | | }, |
| | | legend: { |
| | | data: ['å
¥åº', 'åºåº'], |
| | | top: 30 |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: reportData.value.chartData.comparisonDates || [] |
| | | }, |
| | | yAxis: { |
| | | type: 'value' |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'å
¥åº', |
| | | type: 'bar', |
| | | data: reportData.value.chartData.inValues || [], |
| | | itemStyle: { |
| | | color: '#67C23A' |
| | | } |
| | | const chart = echarts.init(trendChart.value); |
| | | const option = { |
| | | title: { |
| | | text: "åºåååè¶å¿", |
| | | left: "center", |
| | | }, |
| | | { |
| | | name: 'åºåº', |
| | | type: 'bar', |
| | | data: reportData.value.chartData.outValues || [], |
| | | itemStyle: { |
| | | color: '#F56C6C' |
| | | } |
| | | } |
| | | ] |
| | | } |
| | | chart.setOption(option) |
| | | } |
| | | tooltip: { |
| | | trigger: "axis", |
| | | }, |
| | | legend: { |
| | | data: ["åºåé"], |
| | | top: 30, |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: reportData.value.chartData.trendDates || [], |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "åºåé", |
| | | type: "line", |
| | | data: reportData.value.chartData.trendValues || [], |
| | | smooth: true, |
| | | itemStyle: { |
| | | color: "#409EFF", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | chart.setOption(option); |
| | | }; |
| | | |
| | | // ç»ä»¶æè½½æ¶è®¾ç½®é»è®¤æ¶é´ |
| | | onMounted(() => { |
| | | const today = new Date() |
| | | searchForm.singleDate = today.toISOString().split('T')[0] |
| | | |
| | | const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000) |
| | | searchForm.dateRange = [ |
| | | yesterday.toISOString().split('T')[0], |
| | | today.toISOString().split('T')[0] |
| | | ] |
| | | // åå§å对æ¯å¾ |
| | | const initComparisonChart = () => { |
| | | if (!comparisonChart.value) return; |
| | | |
| | | fetchStockRecordTypeOptions() |
| | | // åå§åå è½½ä¸æ¬¡æ°æ® |
| | | handleQuery() |
| | | }) |
| | | const chart = echarts.init(comparisonChart.value); |
| | | const option = { |
| | | title: { |
| | | text: "è¿åºåºå¯¹æ¯", |
| | | left: "center", |
| | | }, |
| | | tooltip: { |
| | | trigger: "axis", |
| | | }, |
| | | legend: { |
| | | data: ["å
¥åº", "åºåº"], |
| | | top: 30, |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: reportData.value.chartData.comparisonDates || [], |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "å
¥åº", |
| | | type: "bar", |
| | | data: reportData.value.chartData.inValues || [], |
| | | itemStyle: { |
| | | color: "#67C23A", |
| | | }, |
| | | }, |
| | | { |
| | | name: "åºåº", |
| | | type: "bar", |
| | | data: reportData.value.chartData.outValues || [], |
| | | itemStyle: { |
| | | color: "#F56C6C", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | chart.setOption(option); |
| | | }; |
| | | |
| | | // ç»ä»¶æè½½æ¶è®¾ç½®é»è®¤æ¶é´ |
| | | onMounted(() => { |
| | | const today = new Date(); |
| | | searchForm.singleDate = today.toISOString().split("T")[0]; |
| | | |
| | | const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000); |
| | | searchForm.dateRange = [ |
| | | yesterday.toISOString().split("T")[0], |
| | | today.toISOString().split("T")[0], |
| | | ]; |
| | | |
| | | fetchStockRecordTypeOptions(); |
| | | // åå§åå è½½ä¸æ¬¡æ°æ® |
| | | handleQuery(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .app-container { |
| | | padding: 20px; |
| | | } |
| | | .app-container { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .search_form { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | padding: 20px; |
| | | background: #fff; |
| | | border-radius: 4px; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | .search_form { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | padding: 20px; |
| | | background: #fff; |
| | | border-radius: 4px; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .search_left { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | .search_left { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .search_title { |
| | | font-weight: 500; |
| | | color: #333; |
| | | margin-right: 8px; |
| | | } |
| | | .search_title { |
| | | font-weight: 500; |
| | | color: #333; |
| | | margin-right: 8px; |
| | | } |
| | | |
| | | .ml10 { |
| | | margin-left: 10px; |
| | | } |
| | | .ml10 { |
| | | margin-left: 10px; |
| | | } |
| | | |
| | | .stats_cards { |
| | | margin-bottom: 20px; |
| | | } |
| | | .stats_cards { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .stats_card { |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | } |
| | | .stats_card { |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .stats_content { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 10px 0; |
| | | } |
| | | .stats_content { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 10px 0; |
| | | } |
| | | |
| | | .stats_icon { |
| | | width: 50px; |
| | | height: 50px; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 15px; |
| | | font-size: 24px; |
| | | color: #fff; |
| | | } |
| | | .stats_icon { |
| | | width: 50px; |
| | | height: 50px; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 15px; |
| | | font-size: 24px; |
| | | color: #fff; |
| | | } |
| | | |
| | | .stats_icon.in { |
| | | background: linear-gradient(135deg, #67C23A, #85CE61); |
| | | } |
| | | .stats_icon.in { |
| | | background: linear-gradient(135deg, #67c23a, #85ce61); |
| | | } |
| | | |
| | | .stats_icon.out { |
| | | background: linear-gradient(135deg, #F56C6C, #F78989); |
| | | } |
| | | .stats_icon.out { |
| | | background: linear-gradient(135deg, #f56c6c, #f78989); |
| | | } |
| | | |
| | | .stats_icon.stock { |
| | | background: linear-gradient(135deg, #409EFF, #66B1FF); |
| | | } |
| | | .stats_icon.stock { |
| | | background: linear-gradient(135deg, #409eff, #66b1ff); |
| | | } |
| | | |
| | | .stats_icon.turnover { |
| | | background: linear-gradient(135deg, #E6A23C, #EEBE77); |
| | | } |
| | | .stats_icon.turnover { |
| | | background: linear-gradient(135deg, #e6a23c, #eebe77); |
| | | } |
| | | |
| | | .stats_info { |
| | | flex: 1; |
| | | } |
| | | .stats_info { |
| | | flex: 1; |
| | | } |
| | | |
| | | .stats_value { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #333; |
| | | line-height: 1; |
| | | margin-bottom: 5px; |
| | | } |
| | | .stats_value { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #333; |
| | | line-height: 1; |
| | | margin-bottom: 5px; |
| | | } |
| | | |
| | | .stats_label { |
| | | font-size: 14px; |
| | | color: #666; |
| | | } |
| | | .stats_label { |
| | | font-size: 14px; |
| | | color: #666; |
| | | } |
| | | |
| | | .chart_section { |
| | | margin-bottom: 20px; |
| | | } |
| | | .chart_section { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .table_section { |
| | | margin-bottom: 20px; |
| | | } |
| | | .table_section { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | :deep(.el-card__header) { |
| | | background: #f8f9fa; |
| | | border-bottom: 1px solid #e9ecef; |
| | | font-weight: 500; |
| | | } |
| | | :deep(.el-card__header) { |
| | | background: #f8f9fa; |
| | | border-bottom: 1px solid #e9ecef; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | :deep(.el-table .el-table__header-wrapper th) { |
| | | background-color: #F0F1F5 !important; |
| | | color: #333333; |
| | | font-weight: 600; |
| | | } |
| | | :deep(.el-table .el-table__header-wrapper th) { |
| | | background-color: #f0f1f5 !important; |
| | | color: #333333; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | :deep(.el-table .el-table__body-wrapper td) { |
| | | padding: 8px 0; |
| | | } |
| | | :deep(.el-table .el-table__body-wrapper td) { |
| | | padding: 8px 0; |
| | | } |
| | | </style> |
| | |
| | | v-model="form.leaveDate" |
| | | type="date" |
| | | :disabled="operationType === 'edit'" |
| | | :disabled-date="disabledFutureDate" |
| | | placeholder="è¯·éæ©ç¦»èæ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const getTodayDate = () => { |
| | | const now = new Date(); |
| | | const year = now.getFullYear(); |
| | | const month = `${now.getMonth() + 1}`.padStart(2, '0'); |
| | | const day = `${now.getDate()}`.padStart(2, '0'); |
| | | return `${year}-${month}-${day}`; |
| | | }; |
| | | |
| | | const disabledFutureDate = (time) => { |
| | | const todayEnd = new Date(); |
| | | todayEnd.setHours(23, 59, 59, 999); |
| | | return time.getTime() > todayEnd.getTime(); |
| | | }; |
| | | const data = reactive({ |
| | | form: { |
| | | staffOnJobId: undefined, |
| | |
| | | } |
| | | ] |
| | | } else { |
| | | form.value.leaveDate = getTodayDate() |
| | | getList() |
| | | } |
| | | } |
| | |
| | | color: #303133; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | <el-table-column label="å«ç¨åä»·(å
)" prop="taxInclusiveUnitPrice" width="130"> |
| | | <template #default="scope">{{ formatAmount(scope.row.taxInclusiveUnitPrice) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="å«ç¨æ»ä»·(å
)" prop="taxInclusiveTotalPrice" width="130"> |
| | | <el-table-column label="éè´§æ»ä»·(å
)" prop="taxInclusiveTotalPrice" width="130"> |
| | | <template #default="scope">{{ formatAmount(scope.row.taxInclusiveTotalPrice) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="ä¸å«ç¨æ»ä»·(å
)" prop="taxExclusiveTotalPrice" width="140"> |
| | | <el-table-column label="ä¸éè´§æ»ä»·(å
)" prop="taxExclusiveTotalPrice" width="140"> |
| | | <template #default="scope">{{ formatAmount(scope.row.taxExclusiveTotalPrice) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="æ¯å¦è´¨æ£" prop="isChecked" width="100" align="center"> |
| | |
| | | prop: 'returnUserName', |
| | | width: 110, |
| | | }, |
| | | |
| | | |
| | | { |
| | | label: 'æ´åææ£é¢', |
| | | prop: 'totalDiscountAmount', |
| | |
| | | }, |
| | | ], |
| | | }, |
| | | |
| | | |
| | | ]) |
| | | const data = reactive({ |
| | | searchForm: { |
| | |
| | | const payload = res?.data || {} |
| | | detailData.value = payload |
| | | // æ¼æ¥è¿ä¸ªå¯¹è±¡æä¸ä¸ªå¯¹è±¡ï¼æ¹ä¾¿å±ç¤º item å item.salesLedgerProduct éçåæ®µ |
| | | |
| | | |
| | | |
| | | |
| | | detailProducts.value = |
| | | payload.purchaseReturnOrderProductsDetailVoList.map(item => ({ ...item, ...item.salesLedgerProduct })) || |
| | | [] |
| | |
| | | <el-table-column label="åä½" |
| | | prop="unit" |
| | | width="100" /> |
| | | <el-table-column label="计费类å" |
| | | prop="type" |
| | | width="100"> |
| | | <template #default="scope"> |
| | | {{scope.row.type==0 ? "计æ¶" : "计件"}} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æ¯å¦è´¨æ£" |
| | | prop="isQuality" |
| | | width="100"> |
| | |
| | | {{ item.model }} |
| | | <!-- <span v-if="item.unit" class="product-unit">{{ item.unit }}</span> --> |
| | | </div> |
| | | <el-tag class="product-tag" |
| | | :type="item.type == 1 ? 'primary' : 'success'" |
| | | style="margin-left: 8px;">{{ item.type==0?'计æ¶':'计件' }}</el-tag> |
| | | <el-tag type="primary" |
| | | class="product-tag" |
| | | style="margin-left: 8px;" |
| | | v-if="item.isQuality">è´¨æ£</el-tag> |
| | | <el-tag type="primary" |
| | | class="product-tag" |
| | | :style="item.isQuality?'margin-left:8px':''" |
| | | style="margin-left: 8px;" |
| | | v-if="item.isProduction">ç产</el-tag> |
| | | </div> |
| | | <div v-else |
| | |
| | | v-else> |
| | | <span>{{ form.unit }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="计费类å" |
| | | prop="type"> |
| | | <el-radio-group v-model="form.type"> |
| | | <el-radio :label="0">计æ¶</el-radio> |
| | | <el-radio :label="1">计件</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="æ¯å¦è´¨æ£" |
| | | prop="isQuality"> |
| | | <el-switch v-model="form.isQuality" |
| | |
| | | model: "", |
| | | unit: "", |
| | | isQuality: false, |
| | | type: 0, |
| | | isProduction: false, |
| | | }); |
| | | |
| | |
| | | model: row.model || "", |
| | | unit: row.unit || "", |
| | | isQuality: row.isQuality, |
| | | type: row.type || 0, |
| | | isProduction: row.isProduction, |
| | | }; |
| | | dialogVisible.value = true; |
| | |
| | | operationName: getProcessName(form.value.technologyOperationId), |
| | | productModelId: form.value.productModelId, |
| | | isQuality: form.value.isQuality, |
| | | type: form.value.type, |
| | | isProduction: form.value.isProduction, |
| | | dragSort, |
| | | }) |
| | |
| | | technologyOperationId: form.value.technologyOperationId, |
| | | productModelId: form.value.productModelId, |
| | | isQuality: form.value.isQuality, |
| | | type: form.value.type, |
| | | isProduction: form.value.isProduction, |
| | | dragSort, |
| | | }); |
| | |
| | | operationName: getProcessName(form.value.technologyOperationId), |
| | | productModelId: form.value.productModelId, |
| | | isQuality: form.value.isQuality, |
| | | type: form.value.type, |
| | | isProduction: form.value.isProduction, |
| | | }) |
| | | : addOrUpdateProcessRouteItem1({ |
| | |
| | | productModelId: form.value.productModelId, |
| | | id: form.value.id, |
| | | isQuality: form.value.isQuality, |
| | | type: form.value.type, |
| | | isProduction: form.value.isProduction, |
| | | }); |
| | | |
| | |
| | | model: "", |
| | | unit: "", |
| | | isQuality: false, |
| | | type: 0, |
| | | isProduction: false, |
| | | }; |
| | | formRef.value?.resetFields(); |
| | |
| | | processOptions.value.forEach(item => { |
| | | if (item.id == value) { |
| | | form.value.isQuality = item.isQuality; |
| | | form.value.type = item.type || 0; |
| | | form.value.isProduction = item.isProduction; |
| | | } |
| | | }); |
| | |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | label: "å·¥æ¶ï¼hï¼", |
| | | prop: "workHour", |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | label: "ç产æ°é", |
| | | prop: "quantity", |
| | | minWidth: 100, |
| | |
| | | !item.materialName || |
| | | (Number(item.pickQty) > 0 && |
| | | (!item.batchNo || item.batchNo.length === 0)) || |
| | | (item.batchNo && item.batchNo.length > 0 && Number(item.pickQty) <= 0) || |
| | | item.demandedQuantity === null || |
| | | item.demandedQuantity === undefined || |
| | | item.pickQty === null || |
| | | item.pickQty === undefined |
| | | ); |
| | | if (invalidRow) { |
| | | if ( |
| | | invalidRow.batchNo && |
| | | invalidRow.batchNo.length > 0 && |
| | | Number(invalidRow.pickQty) <= 0 |
| | | ) { |
| | | return { valid: false, message: "éæ©äºæ¹å·æ¶ï¼é¢ç¨æ°éå¿
须大äºé¶" }; |
| | | } |
| | | return { valid: false, message: "请å®åå·¥åºãåæãæ¹å·åæ°éååä¿å" }; |
| | | } |
| | | return { valid: true, message: "" }; |
| | |
| | | |
| | | const openBindRouteDialog = async (row, type) => { |
| | | bindForm.orderId = row.id; |
| | | bindForm.routeId = type === "add" ? null : row.processRouteCode; |
| | | bindForm.routeId = type === "add" ? null : row.technologyRoutingId; |
| | | bindRouteDialogVisible.value = true; |
| | | routeOptions.value = []; |
| | | if (!row.productModelId) { |
| | |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button type="primary" @click="handleProcessSubmit">ç¡®å®</el-button> |
| | | <el-button type="primary" |
| | | @click="handleProcessSubmit">ç¡®å®</el-button> |
| | | <el-button @click="processDialogVisible = false">åæ¶</el-button> |
| | | </span> |
| | | </template> |
| | |
| | | </div> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button type="primary" :disabled="!selectedParam" @click="handleParamSubmit">ç¡®å®</el-button> |
| | | <el-button type="primary" |
| | | :disabled="!selectedParam" |
| | | @click="handleParamSubmit">ç¡®å®</el-button> |
| | | <el-button @click="paramDialogVisible = false">åæ¶</el-button> |
| | | </span> |
| | | </template> |
| | |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="editParamDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="handleEditParamSubmit">ç¡®å®</el-button> |
| | | <el-button @click="editParamDialogVisible = false">åæ¶</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "å·¥æ¶ï¼hï¼", |
| | | width: 100, |
| | | prop: "workHour", |
| | | }, |
| | | { |
| | | label: "å·¥åº", |
| | | prop: "process", |
| | | width: 120, |
| | |
| | | {{ row.workOrder.model || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å·¥åº" |
| | | prop="workOrder.operationName" |
| | | align="center" /> |
| | | <el-table-column prop="workOrder.planQuantity" |
| | | label="éæ±æ°é" |
| | | align="center" /> |
| | | <el-table-column prop="workOrder.completeQuantity" |
| | | label="宿æ°é" |
| | | align="center" /> |
| | | <el-table-column prop="workOrder.scrapQty" |
| | | label="æ¥åºæ°é" |
| | | align="center" /> |
| | | <el-table-column prop="workOrder.completionStatus" |
| | | label="宿è¿åº¦" |
| | |
| | | {{ parseTime(row.createTime) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å·¥æ¶ï¼hï¼" |
| | | prop="workHour" |
| | | align="center" /> |
| | | <el-table-column label="äº§åºæ°é" |
| | | prop="quantity" |
| | | align="center" /> |
| | | <el-table-column label="æ¥åºæ°é" |
| | | prop="scrapQty" |
| | | align="center" /> |
| | | <el-table-column label="æä½" |
| | | align="center" |
| | | width="200"> |
| | |
| | | workOrder: row.workOrder || {}, |
| | | reports: (row.reportList || []).map(r => ({ |
| | | ...r.reportMain, |
| | | ...(r.reportOutputList[0] || {}), |
| | | productionOperationParamList: r.reportParamList || [], |
| | | })), |
| | | }; |
| | |
| | | </div> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="assignReporterDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="handleSaveReporters">ç¡®å®</el-button> |
| | | <el-button @click="assignReporterDialogVisible = false">åæ¶</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | |
| | | :value="user.userId" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <!-- å·¥æ¶ --> |
| | | <el-form-item label="å·¥æ¶" |
| | | v-if="currentReportRowData?.type == 0" |
| | | prop="workHour"> |
| | | <el-input v-model.number="reportForm.workHour" |
| | | type="number" |
| | | min="0" |
| | | style="width: 280px" |
| | | placeholder="请è¾å
¥å·¥æ¶" /><span style="margin-left:10px" |
| | | class="param-unit">h</span> |
| | | </el-form-item> |
| | | <div v-if="params.length > 0" |
| | | class="param-grid" |
| | | v-loading="paramLoading"> |
| | |
| | | addProductMain, |
| | | downProductWorkOrder, |
| | | } from "@/api/productionManagement/workOrder.js"; |
| | | import { listMaterialPickingDetail } from "@/api/productionManagement/productionOrder.js"; |
| | | import { findProcessParamListOrder } from "@/api/productionManagement/productProcessRoute.js"; |
| | | import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import { getDicts } from "@/api/system/dict/data"; |
| | |
| | | productMainId: null, |
| | | productionOrderRoutingOperationId: "", |
| | | productionOrderId: "", |
| | | workHour: 0, |
| | | paramGroups: {}, |
| | | }); |
| | | |
| | |
| | | fileDialogVisible.value = true; |
| | | }; |
| | | |
| | | const showReportDialog = row => { |
| | | const showReportDialog = async row => { |
| | | if (row.productionOrderId) { |
| | | try { |
| | | const res = await listMaterialPickingDetail(row.productionOrderId); |
| | | const records = Array.isArray(res.data) |
| | | ? res.data |
| | | : res.data?.records || []; |
| | | if (res.code === 200 && records.length === 0) { |
| | | proxy.$modal.msgError("æªé¢ææ æ³æ¥å·¥"); |
| | | return; |
| | | } |
| | | } catch (error) { |
| | | console.error("æ¥è¯¢é¢æè¯¦æ
失败:", error); |
| | | } |
| | | } |
| | | currentReportRowData.value = row; |
| | | reportForm.planQuantity = row.planQuantity; |
| | | reportForm.quantity = |
| | |
| | | reportForm.productionOrderRoutingOperationId = |
| | | row.productionOrderRoutingOperationId; |
| | | reportForm.productionOrderId = row.productionOrderId; |
| | | if (row.type == 0) { |
| | | reportForm.workHour = row.workHour || 0; |
| | | } else { |
| | | reportForm.workHour = 0; |
| | | } |
| | | nextTick(() => { |
| | | reportFormRef.value?.clearValidate(); |
| | | if (row.productionOrderRoutingOperationId && row.productionOrderId) { |
| | |
| | | productionOrderRoutingOperationId: |
| | | reportForm.productionOrderRoutingOperationId, |
| | | productionOrderId: reportForm.productionOrderId, |
| | | workHour: reportForm.workHour, |
| | | productionOperationParamList: productionOperationParamList, |
| | | }; |
| | | |
| | |
| | | style="width: 160px;" |
| | | @keyup.enter="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="éå®ååå·:" |
| | | prop="salesContractNo"> |
| | | <el-input v-model="searchForm.salesContractNo" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | style="width: 160px;" |
| | | @keyup.enter="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="éæ±æ¥æèå´:" |
| | | prop="dateRange"> |
| | | <el-date-picker v-model="searchForm.dateRange" |
| | |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button type="primary" @click="handleMergeSubmit">ç¡®å®ä¸å</el-button> |
| | | <el-button type="primary" |
| | | @click="handleMergeSubmit">ç¡®å®ä¸å</el-button> |
| | | <el-button @click="isShowNewModal = false">åæ¶</el-button> |
| | | </span> |
| | | </template> |
| | |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button type="primary" @click="handleSubmit">ç¡®å®</el-button> |
| | | <el-button type="primary" |
| | | @click="handleSubmit">ç¡®å®</el-button> |
| | | <el-button @click="dialogVisible = false">åæ¶</el-button> |
| | | </span> |
| | | </template> |
| | |
| | | productionPlanUpdate, |
| | | productionPlanDelete, |
| | | productionPlanCombine, |
| | | exportProductionPlan, |
| | | } from "@/api/productionPlan/productionPlan.js"; |
| | | import { productTreeList, modelListPage } from "@/api/basicData/product.js"; |
| | | import { workshopPage } from "@/api/basicData/workshop.js"; |
| | |
| | | const loadProdData = () => { |
| | | console.log("Mock loadProdData called"); |
| | | return Promise.resolve({ code: 200, msg: "忥æå" }); |
| | | }; |
| | | |
| | | const exportProductionPlan = () => { |
| | | console.log("Mock exportProductionPlan called"); |
| | | return Promise.resolve(); |
| | | }; |
| | | |
| | | // const productionPlanCombine = payload => { |
| | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | mpsNo: "", |
| | | salesContractNo: "", |
| | | productName: "", |
| | | model: "", |
| | | status: "", |
| | |
| | | } |
| | | Object.assign(searchForm.value, { |
| | | mpsNo: "", |
| | | salesContractNo: "", |
| | | productName: "", |
| | | model: "", |
| | | status: "", |
| | |
| | | link |
| | | type="primary" |
| | | :disabled="!isApproved(scope.row.status)" |
| | | @click="openForm('edit', scope.row)">è¡¥å
åè´§ä¿¡æ¯ |
| | | @click="openForm('edit', scope.row)">åè´§ |
| | | </el-button> |
| | | <el-button |
| | | link |
| | |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = async (type, row) => { |
| | | // è¡¥å
åè´§ä¿¡æ¯ï¼ä»
âå®¡æ ¸éè¿âå
许ç¼è¾ |
| | | // åè´§ï¼ä»
âå®¡æ ¸éè¿âå
许ç¼è¾ |
| | | if (type === 'edit' && row && !isApproved(row.status)) { |
| | | proxy.$modal.msgWarning("åªæå®¡æ ¸éè¿çæ°æ®æå¯ä»¥è¡¥å
åè´§ä¿¡æ¯"); |
| | | proxy.$modal.msgWarning("åªæå®¡æ ¸éè¿çæ°æ®æå¯ä»¥åè´§"); |
| | | return; |
| | | } |
| | | |