6e8d5cd0a054d1250a75436738fe5d83858048e3..a5bac22f6e10b1b662e5ae41055714f234b63d37
2025-04-03 wujingjing
GIS系统隐藏
a5bac2 对比 | 目录
2025-04-03 wujingjing
conclusion 也要读
a05faa 对比 | 目录
2025-04-03 wujingjing
sidebarIsShow
446e50 对比 | 目录
2025-04-03 wujingjing
cancelRecording
2cd1e4 对比 | 目录
2025-04-03 wujingjing
语音输入
da7f77 对比 | 目录
2025-04-03 wujingjing
bg-gray-100
62a9d2 对比 | 目录
2025-04-03 wujingjing
工作台
2efa3f 对比 | 目录
2025-04-03 wujingjing
工作台初稿
f15775 对比 | 目录
已添加1个文件
已修改8个文件
769 ■■■■■ 文件已修改
src/components/chat/assistant/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/components/playBar/PlayBar.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/components/playBar/hook/useSpeech.ts 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/hooks/useAssistantContentOpt.ts 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/component/header/Header.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/component/main.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/component/sidebar/GisMenu.vue 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/component/sidebar/Sidebar.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/ch/workspace/situation/index.vue 495 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/assistant/index.vue
@@ -45,7 +45,7 @@
                                        <!-- #endregion -->
                                        <!-- #region ====================== è¿‡ç¨‹è¾“出 ======================-->
                                        <div class="mt-3 step-list max-h-[500px] overflow-y-auto" ref="stepListRef">
                                        <div class="mt-3 step-list max-h-[500px] overflow-y-auto bg-gray-100" ref="stepListRef">
                                            <el-steps v-show="msg?.stepGroup?.[index].isShow" class="mt-3" direction="vertical">
                                                <el-step
                                                    :key="`template-${stepIndex}`"
src/components/chat/components/playBar/PlayBar.vue
@@ -146,6 +146,18 @@
                                </div>
                            </el-tooltip>
                            <el-tooltip v-if="isSupportSpeech" placement="top" :content="recordState.isRecording ? '停止语音输入' : '语音输入'">
                                <div class="cursor-pointer size-[24px] relative !z-10 rounded flex-center hover:bg-[#f2f2f2]" @click="speechClick">
                                    <div v-if="recordState.isRecording" class="cursor-pointer flex items-center space-x-[1px]">
                                        <div class="w-[2px] h-[6px] bg-[#0284ff] animate-[soundWave_1.5s_ease-in-out_infinite]"></div>
                                        <div class="w-[2px] h-[9px] bg-[#0284ff] animate-[soundWave_1.5s_ease-in-out_infinite_0.1s]"></div>
                                        <div class="w-[2px] h-[12px] bg-[#0284ff] animate-[soundWave_1.5s_ease-in-out_infinite_0.2s]"></div>
                                        <div class="w-[2px] h-[9px] bg-[#0284ff] animate-[soundWave_1.5s_ease-in-out_infinite_0.3s]"></div>
                                        <div class="w-[2px] h-[6px] bg-[#0284ff] animate-[soundWave_1.5s_ease-in-out_infinite_0.4s]"></div>
                                    </div>
                                    <span v-else class="ywifont ywicon-maikefeng !text-[20px]"></span>
                                </div>
                            </el-tooltip>
                            <el-tooltip placement="top" content="停止生成" v-if="isTalking">
                                <div class="cursor-pointer !ml-0 size-[38px] bg-[#1d86ff] relative !z-10 rounded-full flex-center" link>
                                    <div
@@ -157,7 +169,7 @@
                                </div>
                            </el-tooltip>
                            <el-tooltip v-else placement="top" content="发送">
                                <div class="size-[38px] rounded-full bg-black flex-center" @click="emits('sendClick')">
                                <div class="size-[38px] rounded-full bg-black flex-center" @click="sendClick">
                                    <img src="/static/images/wave/QueryImg.png" />
                                </div>
                            </el-tooltip>
@@ -207,6 +219,7 @@
import { newChatRoomClick, sidebarIsShow, toggleSidebar } from '/@/stores/chatRoom';
import MetricValuesPreview from './metricValues/MetricValuesPreview.vue';
import { useSpeech } from './hook/useSpeech';
const emits = defineEmits(['sendClick', 'stopGenClick']);
const props = defineProps({
@@ -214,6 +227,13 @@
    isHome: Boolean,
    msgList: Array,
});
const sendClick = () => {
    if (recordState.isRecording) {
        cancelRecording();
    }
    emits('sendClick');
};
const voicePageIsShow = defineModel('voicePageIsShow', {
    type: Boolean,
@@ -246,6 +266,18 @@
    commonPhraseRef.value.updatePhrase();
};
const speechClick = () => {
    if (recordState.isRecording) {
        stopRecording();
    } else {
        startRecording();
    }
};
const { isSupportSpeech, startRecording, stopRecording, recordState, cancelRecording } = useSpeech({
    inputText: inputValue,
});
//#region ====================== å¸¸ç”¨è¯­åŠŸèƒ½ ======================
const commonPhraseRef = useCompRef(CommonPhrases);
// å¸¸ç”¨è¯­åŠŸèƒ½ç‚¹å‡»
src/components/chat/components/playBar/hook/useSpeech.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,181 @@
import axios from 'axios';
import { ElMessage } from 'element-plus';
import type { Ref } from 'vue';
import { reactive, ref } from 'vue';
type UseSpeechProps = {
    inputText: Ref<string>;
};
export const useSpeech = (props: UseSpeechProps) => {
    const { inputText } = props;
    const isSupportSpeech = ref(!!navigator.mediaDevices);
    const baiduSpeechConfig = {
        format: 'wav',
        rate: 16000,
        channel: 1,
        cuid: 'jVNmJvBApXOwDVb5aLKETdTjMK8bm3nI',
        token: '',
        dev_pid: 80001,
        ak: 'aV5EwAw2hb80x8kVel8NUKfF',
        sk: 'TKVhvcIa4rNskak0lfhPLINlxZWaCIUM',
    };
    const recordState = reactive({
        isRecording: false,
        audioContext: null,
        mediaRecorder: null as MediaRecorder | null,
        audioChunks: [],
    });
    const getAccessToken = async () => {
        try {
            const response = await axios.post(
                `https://wi.beng35.com/api/baidu-token/oauth/2.0/token?grant_type=client_credentials&client_id=${baiduSpeechConfig.ak}&client_secret=${baiduSpeechConfig.sk}`
            );
            baiduSpeechConfig.token = response.data.access_token;
            return response.data.access_token;
        } catch (error) {
            console.error('获取 access token å¤±è´¥:', error);
            ElMessage.error('获取语音识别授权失败');
            throw error;
        }
    };
    const startRecording = async () => {
        try {
            // ç¡®ä¿æœ‰ access token
            if (!baiduSpeechConfig.token) {
                await getAccessToken();
            }
            const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
            recordState.audioContext = new AudioContext({
                sampleRate: 16000,
                latencyHint: 'interactive',
            });
            const source = recordState.audioContext.createMediaStreamSource(stream);
            const processor = recordState.audioContext.createScriptProcessor(4096, 1, 1);
            // å­˜å‚¨åŽŸå§‹éŸ³é¢‘æ•°æ®
            const audioData = [];
            processor.onaudioprocess = (e) => {
                const inputData = e.inputBuffer.getChannelData(0);
                // å°† Float32Array è½¬æ¢ä¸º Int16Array
                const int16Data = new Int16Array(inputData.length);
                for (let i = 0; i < inputData.length; i++) {
                    int16Data[i] = inputData[i] * 32767;
                }
                audioData.push(int16Data);
            };
            recordState.mediaRecorder = new MediaRecorder(stream);
            recordState.audioChunks = [];
            recordState.mediaRecorder.ondataavailable = (event) => {
                if (event.data.size > 0) {
                    recordState.audioChunks.push(event.data);
                }
            };
            recordState.mediaRecorder.onstop = async () => {
                // åˆå¹¶æ‰€æœ‰éŸ³é¢‘数据
                const totalLength = audioData.reduce((acc, curr) => acc + curr.length, 0);
                const mergedData = new Int16Array(totalLength);
                let offset = 0;
                audioData.forEach((data) => {
                    mergedData.set(data, offset);
                    offset += data.length;
                });
                // å°† Int16Array è½¬æ¢ä¸º base64
                const buffer = mergedData.buffer;
                let base64Data = '';
                let len = 0;
                // åˆ†æ‰¹å¤„理大型数据
                // ä¸ç„¶ String.fromCharCode å’Œå±•开运算符... ä¼šçˆ†æ ˆ
                const chunkSize = 0.1 * 1024 * 1024; // 0.5MB chunks
                const uint8Array = new Uint8Array(buffer);
                const chunks = [];
                for (let i = 0; i < uint8Array.length; i += chunkSize) {
                    const chunk = uint8Array.slice(i, i + chunkSize);
                    chunks.push(String.fromCharCode.apply(null, chunk));
                }
                base64Data = btoa(chunks.join(''));
                len = mergedData.length * 2;
                try {
                    // è°ƒç”¨ç™¾åº¦è¯­éŸ³è¯†åˆ«API
                    const response = await fetch('https://wi.beng35.com/api/baidu-speech/pro_api', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                        },
                        body: JSON.stringify({
                            format: 'pcm',
                            rate: 16000,
                            channel: 1,
                            cuid: baiduSpeechConfig.cuid,
                            dev_pid: 80001, // æ™®é€šè¯è¯†åˆ«
                            speech: base64Data,
                            len: len, // PCM 16位,每个采样点2字节
                            token: baiduSpeechConfig.token,
                        }),
                    });
                    const result = await response.json();
                    if (result.err_no === 0 && result.result && result.result.length > 0) {
                        inputText.value = result.result[0];
                        // showToast('语音识别成功');
                    } else {
                        ElMessage.error('语音识别失败:' + result.err_msg);
                    }
                } catch (error) {
                    console.error('语音识别请求失败:', error);
                    ElMessage.error('语音识别请求失败');
                }
            };
            recordState.mediaRecorder.start();
            recordState.isRecording = true;
            source.connect(processor);
            processor.connect(recordState.audioContext.destination);
        } catch (error) {
            console.error('录音失败:', error);
            ElMessage.error('录音失败,请检查麦克风权限');
        }
    };
    const stopRecording = () => {
        if (recordState.mediaRecorder && recordState.isRecording) {
            recordState.mediaRecorder.stop();
            recordState.isRecording = false;
            if (recordState.audioContext) {
                recordState.audioContext.close();
            }
        }
    };
    const cancelRecording = () => {
        if (recordState.mediaRecorder && recordState.isRecording) {
            recordState.mediaRecorder.resume();
            recordState.isRecording = false;
            if (recordState.audioContext) {
                recordState.audioContext.close();
            }
        }
    };
    return {
        isSupportSpeech,
        startRecording,
        stopRecording,
        recordState,
        cancelRecording,
    };
};
src/components/chat/hooks/useAssistantContentOpt.ts
@@ -26,11 +26,12 @@
    };
    const checkIsText = (item) => {
        const isText = item?.content?.values?.some((item) => item?.content?.type === AnswerType.Knowledge);
        const isText = item?.content?.values?.some((item) => item?.content?.type === AnswerType.Knowledge)  || item?.conclusion?.length > 0;
        return isText;
    };
    const getPlainText = (item) => {
        let result = '';
        const knowledgeText = item.content.values
            .filter((item) => {
                const type = item?.content?.type;
@@ -49,7 +50,14 @@
                    .join('\n\n');
                return acc + answer;
            }, '');
        return markdownToTxt(knowledgeText);
        const conclusionText =
            item.conclusion
                ?.filter((item) => !!item.report)
                .map((item) => item.report)
                .join('\n\n') ?? '';
        result += knowledgeText + conclusionText;
        return markdownToTxt(result);
    };
    const likeClick = async (item) => {
src/layout/component/header/Header.vue
@@ -1,5 +1,5 @@
<template>
    <div class="top_text flex justify-between px-6 items-center" :class="sidebarIsShow ? 'px-6' : 'pl-[unset] pr-6'">
    <div class="top_text flex justify-between px-6 items-center pl-[unset] pr-6">
        <div class="flex-items-center h-full">
            <div class="nav-menu">
                <router-link :to="firstToPath" class="nav-item" active-class="active">
@@ -10,10 +10,10 @@
                    <i class="icon-park-outline-workbench"></i>
                    ä¸ªäººå·¥ä½œå°
                </router-link>
                <router-link to="/gis/situation" class="nav-item" active-class="active">
                <!-- <router-link to="/gis/situation" class="nav-item" active-class="active">
                    <i class="icon-park-outline-system"></i>
                    GIS系统
                </router-link>
                </router-link> -->
            </div>
        </div>
        <el-dialog
src/layout/component/main.vue
@@ -9,9 +9,9 @@
            wrap-class="layout-main-scroll  flex"
            view-class="layout-main-scroll bg-[var(--color-bg-side)]  flex h100 w-full"
        >
            <WorkSpaceMenu v-show="isWorkSpace" />
            <!-- <WorkSpaceMenu v-show="isWorkSpace" /> -->
            <div v-show="isAskAnswer || isHome">
                <SideBar v-if="!isSharePage && sidebarIsShow" :isShow="sidebarIsShow" @toggleSidebar="toggleSidebar" />
                <SideBar v-if="!isSharePage " :isShow="sidebarIsShow" @toggleSidebar="toggleSidebar" />
                <!-- <SidebarOther v-if="!isSharePage && !sidebarIsShow" :isShow="!sidebarIsShow" @toggleSidebar="toggleSidebar" /> -->
            </div>
            <GisMenu v-show="isGis" />
src/layout/component/sidebar/GisMenu.vue
@@ -1,10 +1,17 @@
<template>
    <div class="pc-chat_aside flex-0 relative" :style="`width:252px;transition: 0.7s ease-in;`">
        <!-- Logo éƒ¨åˆ† -->
        <div class="h-[40px] w-full bg-[#0084ff] flex justify-between items-center text-white px-2">
            <span>GIS系统</span>
            <!-- <span>官网GIS</span> -->
        <div class="aside_top">
            <div class="logo flex-items-center justify-between">
                <div class="flex items-center">
                    <img src="/static/images/logo/logoWithNoName.png" alt="logo" class="layout-logo-medium-img" />
                    <span class="font-extrabold text-xl text-white tracking-wide"> WI水务智能平台</span>
                </div>
            </div>
        </div>
        <!-- Logo éƒ¨åˆ† -->
        <!-- <div class="h-[40px] w-full bg-[#0084ff] flex justify-between items-center text-white px-2">
            <span>GIS系统</span>
        </div> -->
        <!-- èœå•项 -->
        <div class="menu-container">
            <el-menu :default-active="activeId" class="wi-menu !w-full" :unique-opened="true" @select="handleSelect">
src/layout/component/sidebar/Sidebar.vue
@@ -1,5 +1,5 @@
<template>
    <div class="pc-chat_aside flex-0 relative" :style="`width: ${leftBox}px;transition: 0.7s ease-in;`">
    <div  class="pc-chat_aside flex-0 relative" :style="`width: ${leftWidth}px;`">
        <div class="aside_top">
            <div class="logo flex-items-center justify-between">
                <div class="flex items-center">
@@ -8,15 +8,15 @@
                </div>
            </div>
        </div>
        <!-- <div class="hide-sidebar" @click="toggleSidebar" v-if="!isSharePage">
            <i class="text-[#fff] transition-all ywifont ywicon-zuoyoujiantou"></i>
        </div> -->
        <div class="aside_center">
            <ChatRecord />
        </div>
        <div class="aside_bottom">
            <MenuList />
        </div>
        <div class="hide-sidebar" @click="toggleSidebar">
            <i class="text-[#fff] transition-all ywifont" :class="isShow ? 'ywicon-zuoyoujiantou' : 'ywicon-zuoyoujiantou1'"></i>
        </div>
    </div>
</template>
@@ -25,13 +25,11 @@
import { computed } from 'vue';
import ChatRecord from './components/ChatRecord.vue';
import MenuList from './components/MenuList.vue';
import { toggleSidebar } from '/@/stores/chatRoom';
const emit = defineEmits(['toggleSidebar']);
const prop = defineProps(['isShow']);
const leftBox = computed(() => (prop.isShow ? 252 : 198));
const leftWidth= computed(()=>prop.isShow ? 252:0)
</script>
<style scoped lang="scss">
@@ -52,9 +50,9 @@
    box-sizing: border-box;
    background-color: var(--color-bg-side);
    overflow: visible;
    -webkit-transition: width 0.1s ease-in;
    -o-transition: width 0.1s ease-in;
    transition: width 0.1s ease-in;
    // -webkit-transition: width 0.1s ease-in;
    // -o-transition: width 0.1s ease-in;
    // transition: width 0.1s ease-in;
    position: relative;
    display: flex;
    flex-direction: column;
src/views/project/ch/workspace/situation/index.vue
@@ -1,16 +1,491 @@
<template>
    <div class="workspace-situation">
        <div class="workspace-situation-header">
            <div class="workspace-situation-header-title">
                <span>个人工作台概况</span>
            </div>
        </div>
    </div>
    <div class="workspace-container h-full">
        <!-- å·¦ä¾§ä¸»è¦å†…容区 -->
        <div class="flex flex-col h-full overflow-hidden" :style="{ gap: layoutGap }">
            <!-- å·¥ä½œæ¦‚览 -->
            <div class="overview-section flex-0 bg-white border border-solid border-white rounded-lg p-[20px] h-[150px]">
                <div class="section-header mb-4">
                    <h3 class="font-bold">工作概览</h3>
                </div>
                <div class="overview-cards">
                    <!-- ç®¡ç†äººæ•° -->
                    <div class="overview-card bg-[#dee8ff] rounded-lg p-4 flex items-center">
                        <i class="i-carbon:user-multiple text-3xl text-blue-500 mr-4"></i>
                        <div>
                            <div class="text-3xl font-bold text-[#4f85f6]">3</div>
                            <div class="text-gray-600">管理人数</div>
                        </div>
                    </div>
                    <!-- å¾…办事项 -->
                    <div class="overview-card bg-[#faeaed] rounded-lg p-4 flex items-center">
                        <i class="i-carbon:task text-3xl text-red-500 mr-4"></i>
                        <div>
                            <div class="text-3xl font-bold text-red-500">3</div>
                            <div class="text-gray-600">待办事项</div>
                        </div>
                    </div>
                    <!-- é¢„警事项 -->
                    <div class="overview-card bg-[#dbf2f8] rounded-lg p-4 flex items-center">
                        <i class="i-carbon:warning text-3xl text-cyan-500 mr-4"></i>
                        <div>
                            <div class="text-3xl font-bold text-cyan-500">3</div>
                            <div class="text-gray-600">预警事项</div>
                        </div>
                    </div>
                </div>
            </div>
            <div class="flex flex-col flex-auto" :style="{ gap: layoutGap }">
                <!-- å¾…办事项和我发起的表格 -->
                <div class="flex h-1/2" :style="{ gap: layoutGap }">
                    <!-- å¾…办事项 -->
                    <div class="bg-white rounded-lg p-4 shadow-sm flex flex-col w-1/2">
                        <div class="flex justify-between items-center mb-4 flex-0">
                            <div class="text-lg font-bold">待办事项</div>
                            <el-tabs v-model="todoType" class="todo-tabs">
                                <el-tab-pane label="待办事项" name="todo" />
                                <el-tab-pane label="已办事项" name="done" />
                            </el-tabs>
                        </div>
                        <el-table class="flex-auto" :data="todoList" style="width: 100%" size="small">
                            <el-table-column prop="name" label="事件类型" min-width="120">
                                <template #default="{ row }">
                                    <span :class="getEventTypeClass(row.type)">{{ row.name }}</span>
                                </template>
                            </el-table-column>
                            <el-table-column prop="sender" label="发起人" width="100" />
                            <el-table-column prop="time" label="发起时间" width="100" />
                            <el-table-column prop="status" label="状态" width="80">
                                <template #default="{ row }">
                                    <el-tag :type="getStatusType(row.status)" size="small">
                                        {{ row.status }}
                                    </el-tag>
                                </template>
                            </el-table-column>
                            <el-table-column label="操作" width="80" fixed="right">
                                <template #default>
                                    <el-button type="primary" size="small" class="custom-button">办理</el-button>
                                </template>
                            </el-table-column>
                        </el-table>
                    </div>
                    <!-- æˆ‘发起的 -->
                    <div class="bg-white rounded-lg p-4 shadow-sm flex flex-col h-full w-1/2">
                        <div class="flex justify-between items-center mb-4 flex-0">
                            <div class="text-lg font-bold">我发起的</div>
                            <el-tabs v-model="initiatedType" class="todo-tabs">
                                <el-tab-pane label="我发起" name="initiated" />
                                <el-tab-pane label="我收到" name="received" />
                            </el-tabs>
                        </div>
                        <el-table :data="initiatedList" class="flex-auto" style="width: 100%" size="small">
                            <el-table-column prop="type" label="类型" min-width="120">
                                <template #default="{ row }">
                                    <span :class="getEventTypeClass(row.type)">{{ row.type }}</span>
                                </template>
                            </el-table-column>
                            <el-table-column prop="sender" label="发起人" width="100" />
                            <el-table-column prop="time" label="发起时间" width="100" />
                            <el-table-column prop="status" label="状态" width="80">
                                <template #default="{ row }">
                                    <el-tag :type="getStatusType(row.status)" size="small">
                                        {{ row.status }}
                                    </el-tag>
                                </template>
                            </el-table-column>
                            <el-table-column label="操作" width="80" fixed="right">
                                <template #default>
                                    <el-button type="primary" size="small" class="custom-button">详情</el-button>
                                </template>
                            </el-table-column>
                        </el-table>
                    </div>
                </div>
                <!-- é¢„警表格 -->
                <div class="bg-white rounded-lg p-4 shadow-sm h-1/2">
                    <div class="text-lg font-bold mb-4">预警</div>
                    <el-table :data="warningList" style="width: 100%" size="small">
                        <el-table-column prop="studentId" label="学号" min-width="120" />
                        <el-table-column prop="name" label="姓名" width="100" />
                        <el-table-column prop="type" label="学生类型" width="100" />
                        <el-table-column prop="guardianId" label="护照号" min-width="150" />
                        <el-table-column prop="warningType" label="预警类型" width="120">
                            <template #default="{ row }">
                                <el-tag :type="getWarningType(row.warningType)" size="small">
                                    {{ row.warningType }}
                                </el-tag>
                            </template>
                        </el-table-column>
                        <el-table-column label="操作" width="100" fixed="right">
                            <template #default>
                                <el-button type="primary" size="small" class="custom-button">提交预警</el-button>
                            </template>
                        </el-table-column>
                    </el-table>
                </div>
            </div>
        </div>
        <!-- å³ä¾§è¾¹æ  -->
        <div class="sidebar h-full overflow-hidden flex flex-col" :style="{ gap: layoutGap }">
            <!-- ä¸ªäººä¿¡æ¯ -->
            <div class="profile-card p-[20px] bg-white border border-solid border-white rounded-lg h-[150px] flex-0">
                <div class="section-header mb-4">
                    <h3>个人信息</h3>
                </div>
                <div class="profile-header flex gap-4">
                    <!-- <div class="avatar-placeholder">
                        <i class="i-carbon:user text-3xl text-gray-400"></i>
                    </div> -->
                    <div class="profile-info flex flex-col gap-2">
                        <div class="flex items-center" v-for="item in 3" :key="item">
                            <span class="mr-1">用户名:</span>
                            <span>wjj</span>
                        </div>
                    </div>
                    <div class="profile-info flex flex-col gap-2">
                        <div class="flex items-center" v-for="item in 3" :key="item">
                            <span class="mr-1">用户名:</span>
                            <span>wjj</span>
                        </div>
                    </div>
                </div>
            </div>
            <!-- ç»Ÿè®¡å›¾è¡¨ -->
            <div class="chart-container p-[20px] h-1/2 overflow-hidden">
                <div class="chart-header">
                    <h3>统计图表</h3>
                    <span class="subtitle">国别</span>
                </div>
                <div class="chart" ref="chartRef"></div>
            </div>
            <!-- æ—¥åކ -->
            <div class="calendar-container p-[20px] h-1/2 overflow-hidden">
                <div class="calendar-header">
                    <h3>时间</h3>
                    <div class="flex items-center gap-2">
                        <el-date-picker v-model="currentMonth" type="month" format="YYYYå¹´MM月" :placeholder="'选择月份'" size="small" />
                    </div>
                </div>
                <div class="calendar-notice mb-0.5">您今日无待办</div>
                <el-calendar v-model="currentDate">
                    <template #dateCell="{ data }">
                        <div class="custom-calendar-cell">
                            <span class="date-text">{{ data.day.split('-')[2] }}</span>
                            <div v-if="hasEvent(data)" class="event-dot"></div>
                        </div>
                    </template>
                </el-calendar>
            </div>
        </div>
    </div>
</template>
<script setup lang="ts" name="WorkspaceSituation">
import { ref } from 'vue';
<script setup lang="ts">
import * as echarts from 'echarts';
import { computed, onMounted, ref } from 'vue';
const layoutGap = '10px';
interface CalendarData {
    day: string;
    [key: string]: any;
}
// çŠ¶æ€å˜é‡
const todoType = ref('todo');
const initiatedType = ref('initiated');
const currentDate = ref(new Date('2025-04-06'));
const currentMonth = computed(() => {
    const date = currentDate.value;
    return date instanceof Date ? date : new Date(date);
});
const chartRef = ref<HTMLElement>();
// ç¤ºä¾‹æ•°æ® - å¢žåŠ æ›´å¤šæ•°æ®
const todoList = ref([
    { name: '实验室更新', type: 'update', sender: '王美丽', time: '2021.12.06', status: '待确认' },
    { name: '新生指导', type: 'guide', sender: '李明', time: '2021.12.06', status: '未读' },
    { name: '活动报名', type: 'activity', sender: '张三', time: '2021.12.06', status: '已读' },
    { name: '课程变更', type: 'update', sender: '李四', time: '2021.12.06', status: '待确认' },
    { name: '会议通知', type: 'notice', sender: '王五', time: '2021.12.06', status: '未读' },
    { name: '教材订购', type: 'order', sender: '赵六', time: '2021.12.06', status: '已处理' },
    { name: '成绩录入', type: 'grade', sender: '孙七', time: '2021.12.06', status: '待确认' },
    { name: '请假审批', type: 'leave', sender: '周八', time: '2021.12.06', status: '已读' },
    { name: '请假审批', type: 'leave', sender: '周八', time: '2021.12.06', status: '已读' },
    { name: '请假审批', type: 'leave', sender: '周八', time: '2021.12.06', status: '已读' },
    { name: '请假审批', type: 'leave', sender: '周八', time: '2021.12.06', status: '已读' },
    { name: '请假审批', type: 'leave', sender: '周八', time: '2021.12.06', status: '已读' },
]);
const initiatedList = ref([
    { type: '活动', sender: '王美丽', time: '2021.12.06', status: '进行中' },
    { type: '报到注册', sender: '李明', time: '2021.12.06', status: '关闭' },
    { type: '课程变更', sender: '张三', time: '2021.12.06', status: '待审核' },
    { type: '会议通知', sender: '李四', time: '2021.12.06', status: '已完成' },
    { type: '教材订购', sender: '王五', time: '2021.12.06', status: '进行中' },
    { type: '成绩录入', sender: '赵六', time: '2021.12.06', status: '待审核' },
    { type: '请假审批', sender: '孙七', time: '2021.12.06', status: '已拒绝' },
    { type: '活动策划', sender: '周八', time: '2021.12.06', status: '进行中' },
    { type: '活动策划', sender: '周八', time: '2021.12.06', status: '进行中' },
    { type: '活动策划', sender: '周八', time: '2021.12.06', status: '进行中' },
    { type: '活动策划', sender: '周八', time: '2021.12.06', status: '进行中' },
    { type: '活动策划', sender: '周八', time: '2021.12.06', status: '进行中' },
    { type: '活动策划', sender: '周八', time: '2021.12.06', status: '进行中' },
    { type: '活动策划', sender: '周八', time: '2021.12.06', status: '进行中' },
    { type: '活动策划', sender: '周八', time: '2021.12.06', status: '进行中' },
]);
const warningList = ref([
    { studentId: 'N20930498594', name: '王美丽', type: '高中', guardianId: '309209382903943', warningType: '迟到预警' },
    { studentId: 'N20930498595', name: '李明', type: '高中', guardianId: '309209382903944', warningType: '缺勤预警' },
    { studentId: 'N20930498596', name: '张三', type: '初中', guardianId: '309209382903945', warningType: '成绩预警' },
    { studentId: 'N20930498597', name: '李四', type: '高中', guardianId: '309209382903946', warningType: '行为预警' },
    { studentId: 'N20930498598', name: '王五', type: '初中', guardianId: '309209382903947', warningType: '迟到预警' },
]);
// å·¥å…·å‡½æ•°
const getEventTypeClass = (type: string) => {
    const classes = {
        update: 'text-green-500',
        guide: 'text-blue-500',
        activity: 'text-orange-500',
        notice: 'text-purple-500',
        order: 'text-cyan-500',
        grade: 'text-pink-500',
        leave: 'text-indigo-500',
    };
    return classes[type] || '';
};
const getStatusType = (status: string) => {
    const types = {
        å¾…确认: 'warning',
        æœªè¯»: 'danger',
        å·²è¯»: 'info',
        å·²å¤„理: 'success',
        è¿›è¡Œä¸­: 'primary',
        å…³é—­: 'info',
        å¾…审核: 'warning',
        å·²å®Œæˆ: 'success',
        å·²æ‹’绝: 'danger',
    };
    return types[status] || 'default';
};
const getWarningType = (type: string) => {
    const types = {
        è¿Ÿåˆ°é¢„è­¦: 'danger',
        ç¼ºå‹¤é¢„è­¦: 'warning',
        æˆç»©é¢„è­¦: 'info',
        è¡Œä¸ºé¢„è­¦: 'warning',
    };
    return types[type] || 'default';
};
const hasEvent = (date: CalendarData) => {
    // å®žçŽ°åˆ¤æ–­æ—¥æœŸæ˜¯å¦æœ‰äº‹ä»¶çš„é€»è¾‘
    return Math.random() > 0.8;
};
// å›¾è¡¨åˆå§‹åŒ–
onMounted(() => {
    if (chartRef.value) {
        const chart = echarts.init(chartRef.value);
        chart.setOption({
            xAxis: {
                type: 'category',
                data: ['中国', '德国', '法国', '英国', '新西兰', '美国', '瑞士'],
            },
            yAxis: {
                type: 'value',
            },
            series: [
                {
                    data: [80, 50, 100, 30, 70, 80, 60],
                    type: 'bar',
                    barWidth: '30%',
                    itemStyle: {
                        color: '#409EFF',
                    },
                },
            ],
        });
    }
});
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.workspace-container {
    display: grid;
    grid-template-columns: 1fr 300px;
    gap: v-bind(layoutGap);
    padding: 20px;
    background-color: #f5f7fa;
}
.overview-cards {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 20px;
}
// è‡ªå®šä¹‰æŒ‰é’®æ ·å¼
.custom-button {
    --el-button-bg-color: var(--color-btn-base);
    --el-button-border-color: var(--color-btn-base);
    --el-button-hover-bg-color: var(--color-btn-hover);
    --el-button-hover-border-color: var(--color-btn-hover);
}
// è‡ªå®šä¹‰ tabs æ ·å¼
:deep(.todo-tabs) {
    .el-tabs__header {
        margin: 0;
    }
    .el-tabs__nav-wrap::after {
        display: none;
    }
    .el-tabs__item {
        padding: 0 10px;
        height: 32px;
        line-height: 32px;
        font-size: 14px;
        &.is-active {
            color: var(--color-btn-base);
        }
    }
    .el-tabs__active-bar {
        background-color: var(--color-btn-base);
    }
}
// è‡ªå®šä¹‰æ—¥åŽ†æ ·å¼
:deep(.el-calendar) {
    --el-calendar-border: none;
    --el-calendar-header-border-bottom: none;
    background: none;
    .el-calendar__header {
        display: none;
    }
    .el-calendar__body {
        padding: 12px 0;
    }
    .el-calendar-table {
        td {
            border: none;
            padding: 4px;
        }
        .current {
            background: none;
        }
    }
}
.custom-calendar-cell {
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    .date-text {
        font-size: 14px;
    }
    .event-dot {
        position: absolute;
        top: 2px;
        right: 2px;
        width: 6px;
        height: 6px;
        border-radius: 50%;
        background-color: var(--el-color-primary);
    }
}
.calendar-notice {
    background-color: #fff7e6;
    color: #fa8c16;
    padding: 8px 12px;
    border-radius: 4px;
    font-size: 14px;
}
.avatar-placeholder {
    width: 64px;
    height: 64px;
    border-radius: 50%;
    background-color: #f5f7fa;
    display: flex;
    align-items: center;
    justify-content: center;
}
:deep(.el-calendar-table .el-calendar-day) {
    --el-calendar-cell-width: 10px;
}
.chart-container {
    background: white;
    border-radius: 8px;
    .chart {
        height: 300px;
    }
    .chart-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 16px;
        .subtitle {
            color: var(--el-text-color-secondary);
            font-size: 14px;
        }
    }
}
.calendar-container {
    background: white;
    border-radius: 8px;
    .calendar-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 16px;
        h3 {
            font-size: 16px;
            font-weight: 500;
            margin: 0;
        }
    }
}
:deep(.el-date-picker) {
    --el-input-width: 120px;
}
.section-header {
    h3 {
        font-size: 16px;
        // font-weight: 500;
        // margin: 0;
        // color: var(--el-text-color-primary);
    }
}
</style>